package de.renew.propertymanagement.prop;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import javax.swing.KeyStroke;

import org.apache.log4j.Logger;

import de.renew.plugin.PluginManager;


/**
 * The {@link ConfigurablePropertyPersister} class is responsible for saving and loading the current set of
 * configurable properties to and from a file, either in the user's home directory or in
 * the application's configuration directory.
 */
class ConfigurablePropertyPersister implements IConfigurablePropertyPersister {
    /**
     * Logger for logging messages related to {@link ConfigurablePropertyPersister}.
     */
    private static final Logger LOGGER = Logger.getLogger(ConfigurablePropertyPersister.class);

    private static final String USER_HOME_PATH =
        System.getProperty("user.home") + File.separator + ".renew.properties";
    private static final String CONFIG_PATH = PluginManager.getLoaderLocation().getPath()
        + File.separator + "config" + File.separator + "renew.properties";

    /**
     * Constructs a new {@link ConfigurablePropertyPersister}.
     */
    ConfigurablePropertyPersister() {}

    @Override
    public void saveProperties(boolean useHomeDir, Map<String, ConfigurableProperty> propertyMap)
        throws PropertySaveFailedException
    {
        //Get file; either homeDir/.renew.properties or dist/config/renew.properties
        File file;
        try {
            file = getOrCreateFile(useHomeDir ? USER_HOME_PATH : CONFIG_PATH);
        } catch (IOException e) {
            String errorMessage = "An Error occurred while creating the properties file: "
                + (useHomeDir ? USER_HOME_PATH : CONFIG_PATH);
            LOGGER.error(errorMessage);
            throw new PropertySaveFailedException(errorMessage);
        }

        // Get the properties from file so that properties that have not been registered yet are not lost on saving.
        Map<String, ConfigurableProperty> propertiesFromFile = new HashMap<>();
        Optional<Properties> properties = getPropertiesFromFile(file);
        properties.ifPresent(
            properties1 -> properties1.forEach(
                (key, value) -> propertiesFromFile
                    .put((String) key, new ConfigurableProperty((String) key, (String) value))));

        Properties allProps = mergeProperties(propertyMap, propertiesFromFile);

        writePropertiesToFile(allProps, file);
    }

    @Override
    public void loadProperties(boolean useHomeDir, Map<String, ConfigurableProperty> propertyMap)
        throws PropertyLoadFailedException
    {

        File file = new File(useHomeDir ? USER_HOME_PATH : CONFIG_PATH);

        Optional<Properties> propertiesFromFile = getPropertiesFromFile(file);

        if (propertiesFromFile.isEmpty()) {
            String errorMessage =
                "An Error occurred while reading the properties from: " + file.getAbsolutePath();
            LOGGER.error(errorMessage);
            throw new PropertyLoadFailedException(errorMessage);
        }

        for (Map.Entry<String, ConfigurableProperty> property : propertyMap.entrySet()) {
            String value = propertiesFromFile.get().getProperty(property.getKey());
            if (value != null) {
                KeyStroke ks = KeyStroke.getKeyStroke(value);
                if (ks != null
                    && property.getValue() instanceof ConfigurableHotkeyProperty hotkeyProperty) {
                    hotkeyProperty.setKeyStroke(ks);
                    continue;
                }
                property.getValue().setCurrentValue(value);
            }
        }
    }

    /**
     * Merges the registered properties and the properties from the
     * file into one property's object.
     *
     * @param propertyMap The map of properties to set.
     * @param propertiesFromFile The properties how they are in the file at the moment of calling this method.
     *
     * @return allProps The properties object to set the properties in.
     */
    private Properties mergeProperties(
        Map<String, ConfigurableProperty> propertyMap,
        Map<String, ConfigurableProperty> propertiesFromFile)
    {
        Properties allProps = new Properties();
        for (Map.Entry<String, ConfigurableProperty> property : propertiesFromFile.entrySet()) {
            if (!propertyMap.containsKey(property.getKey())) {
                allProps.setProperty(property.getKey(), property.getValue().getCurrentValue());
            }
        }

        for (Map.Entry<String, ConfigurableProperty> property : propertyMap.entrySet()) {
            if (property.getValue().getCurrentValue() != null) {
                allProps.setProperty(property.getKey(), property.getValue().getCurrentValue());
            } else {
                allProps.setProperty(property.getKey(), property.getValue().getDefaultValue());
            }
        }
        return allProps;
    }

    /**
     * Writes the properties to a file.
     *
     * @param allProps The properties to write to the file.
     * @param file The file to write the properties to.
     * @throws PropertySaveFailedException If the properties could not be saved to the file.
     */
    private void writePropertiesToFile(Properties allProps, File file)
        throws PropertySaveFailedException
    {
        try (OutputStream outputStream = new FileOutputStream(file)) {
            allProps.store(outputStream, null);
        } catch (IOException e) {
            String errorMessage = "Could not save properties to file: " + file.getAbsolutePath();
            LOGGER.error(errorMessage);
            throw new PropertySaveFailedException(errorMessage);
        }
    }

    /**
     * Either creates or loads a file from a given path.
     *
     * @param path The path to the file.
     * @throws IOException If the file could not be created.
     * @return The file at the given path.
     */
    private File getOrCreateFile(String path) throws IOException {
        File file = new File(path);
        if (file.createNewFile()) {
            LOGGER.warn("Creating new file: " + path);
        }
        return file;
    }

    /**
     * Retrieves the properties from a file.
     *
     * @param file The file to retrieve the properties from.
     * @return An {@code Optional} containing the properties
     * from the given file or an empty {@code Optional}, if there
     * was an error loading the properties.
     */
    private Optional<Properties> getPropertiesFromFile(File file) {
        Properties allProps = new Properties();
        if (file != null && file.exists()) {
            try (InputStream inputStream = new FileInputStream(file)) {
                allProps.load(inputStream);
            } catch (IOException e) {
                return Optional.empty();
            }
        }
        return Optional.of(allProps);
    }
}
