/*
 * Copyright 2015 52°North Initiative for Geospatial Open Source
 * Software GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.n52.iceland.config.json;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.n52.iceland.config.FileSettingsConfiguration;
import org.n52.iceland.exception.ConfigurationError;
import org.n52.iceland.lifecycle.Constructable;
import org.n52.iceland.lifecycle.Destroyable;
import org.n52.iceland.service.ConfigLocationProvider;
import org.n52.iceland.util.Debouncer;
import org.n52.iceland.util.JSONUtils;
import org.n52.iceland.util.Producer;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.MoreObjects;

/**
 * Class to encapsulate writes and reads to a JSON file.
 *
 * @since 1.0.0
 *
 * @author Christian Autermann
 */
public class JsonConfiguration implements Constructable,
                                          Destroyable,
                                          Producer<ObjectNode>,
                                          FileSettingsConfiguration {

    private static final Logger LOG = LoggerFactory
            .getLogger(JsonConfiguration.class);

    public static final String DEFAULT_FILE_NAME = "configuration.json";
    public static final int DEFAULT_WRITE_TIMEOUT = 1000;

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final JsonNodeFactory nodeFactory = JSONUtils.nodeFactory();
    private String fileName = DEFAULT_FILE_NAME;
    private int writeTimeout = DEFAULT_WRITE_TIMEOUT;
    private ObjectNode configuration;
    private File file;
    private ConfigLocationProvider configLocationProvider;
    private Debouncer debouncer;

    /**
     * Initializes this configuration by initializing a {@link Debouncer} for
     * writes and reading the configuration file.
     */
    @Override
    public void init() {
        writeLock().lock();
        try {
            this.debouncer = new Debouncer(this.writeTimeout, this::persist);
            File directory = new File(this.configLocationProvider.get());
            this.file = new File(directory, this.fileName);
            this.refresh();
        } finally {
            writeLock().unlock();
        }
    }

    /**
     * Refreshes the configuration from disk.
     */
    @Override
    public void refresh() {
        writeLock().lock();
        try {
            this.configuration = readConfiguration(this.file)
                    .orElseGet(nodeFactory::objectNode);
        } finally {
            writeLock().unlock();
        }
    }

    /**
     * Destroys this configuration by executing any pending write.
     *
     * @see Debouncer#finish()
     */
    @Override
    public void destroy() {
        LOG.info("Destroying {}", System.identityHashCode(this));
        this.debouncer.finish();
    }

    /**
     * Deletes the configuration file if it exists.
     */
    public void delete() {
        writeLock().lock();
        try {
            if (this.file.exists() && this.file.isFile()) {
                if (!this.file.delete()) {
                    throw new ConfigurationError("Can not delete configuration file %s", file
                                                 .getAbsolutePath());
                }
            }
        } finally {
            writeLock().unlock();
        }
    }

    /**
     * Gets the (possibly empty) configuration JSON node.
     *
     * @return the node (never {@code null})
     */
    @Override
    public ObjectNode get() {
        return this.configuration;
    }

    /**
     * Sets the time to wait for additional changes before the configuration is
     * persisted.
     *
     * @param writeTimeout the time out in milliseconds
     */
    public void setWriteTimeout(int writeTimeout) {
        this.writeTimeout = writeTimeout;
    }

    /**
     * Sets the file name of this configuration.
     *
     * @param fileName the file name
     */
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    /**
     * Sets the configuration location provider.
     *
     * @param configLocationProvider the provider
     */
    @Inject
    public void setConfigLocationProvider(
            ConfigLocationProvider configLocationProvider) {
        this.configLocationProvider = configLocationProvider;
    }

    /**
     * Gets the read lock for this configuration.
     *
     * @return the (reentrant) read lock
     */
    public Lock readLock() {
        return this.lock.readLock();
    }

    /**
     * Gets the write lock for this configuration.
     *
     * @return the (reentrant) write lock
     */
    public Lock writeLock() {
        return this.lock.writeLock();
    }

    /**
     * Schedule a write. This will be executed after the configured
     * {@link #setWriteTimeout timeout} if no further writes are scheduled, else
     * it will postponed until their are no further requests for {@code timeout}
     * milliseconds.
     *
     * @see Debouncer#call()
     */
    public void scheduleWrite() {
//        LOG.debug("Scheduling write");
        this.debouncer.call();
    }
    /**
     * Do not wait, but persist the settings right now.
     */
    public void writeNow() {
        this.persist();
    }

    /**
     * Actually persists the configuration.
     */
    private synchronized void persist() {
        readLock().lock();
        try {
            LOG.debug("Writing configuration file");
            try (FileOutputStream fos = new FileOutputStream(this.file)) {
                JSONUtils.print(fos, this.configuration);
            }
        } catch (IOException e) {
            throw new ConfigurationError("Could not persist configuration", e);
        } finally {
            readLock().unlock();
        }
    }

    /**
     * Reads the configuration, if the file exists.
     *
     * @param file the file holding the configuration
     *
     * @return the decoded JSON object
     */
    private Optional<ObjectNode> readConfiguration(File file) {
        if (!file.exists()) {
            return Optional.empty();
        }
        if (!file.isFile()) {
            throw new ConfigurationError("%s is not a file", file
                                         .getAbsolutePath());
        }
        if (!file.canRead()) {
            throw new ConfigurationError("%s is not a readable file", file
                                         .getAbsolutePath());
        }
        try {
            JsonNode node = JSONUtils.loadFile(file);
            if (!node.isObject()) {
                throw new ConfigurationError("%s does not contain a JSON object", file
                                             .getAbsolutePath());
            }
            return Optional.of((ObjectNode) node);
        } catch (IOException ex) {
            throw new ConfigurationError("Could not read " + file
                                         .getAbsolutePath(), ex);
        }
    }

    @Override
    public Path getPath() {
        return this.file.toPath();
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).omitNullValues()
                .add("file", file).toString();
    }


}

@@ -965,37 +965,8 @@
 k;%0A%0A
-import javax.inject.Inject;%0A%0A
 impo
@@ -1133,56 +1133,8 @@
 or;%0A
-import org.n52.iceland.lifecycle.Constructable;%0A
 impo
@@ -1718,65 +1718,8 @@
 nts 
-Constructable,%0A                                          
 Dest
@@ -2635,38 +2635,24 @@
 le.%0A     */%0A
-    @Override%0A
     public v
@@ -4942,20 +4942,8 @@
  */%0A
-    @Inject%0A
     

remove constructable from JsonConfiguration