package de.renew.propertymanagement.prop;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import javax.swing.KeyStroke;

import org.apache.log4j.Logger;


/**
 * The {@link ConfigurablePropertyManager} class is responsible for managing a collection of configurable properties.
 * It follows the Singleton pattern, allowing only one instance to exist throughout the application.
 * Clients can add listeners to be notified of changes to the configurable properties.
 */
public class ConfigurablePropertyManager {
    /**
     * Logger for logging messages related to ConfigurablePropertyManager.
     */
    private static final Logger LOGGER = Logger.getLogger(ConfigurablePropertyManager.class);
    private static ConfigurablePropertyManager _instance;
    private final ConfigurablePropertyPersister _persister;

    /**
     * Contains all properties added to the ConfigurablePropertyManager via {@link #registerProperty}.
     */
    private final Map<String, ConfigurableProperty> _properties;

    private final List<PropertyChangeListener> _listeners;

    private ConfigurablePropertyManager() {
        _properties = new HashMap<>();
        _listeners = new ArrayList<>();
        _persister = new ConfigurablePropertyPersister();
    }

    /**
     * Retrieves the singleton instance of ConfigurablePropertyManager.
     *
     * @return The ConfigurablePropertyManager instance.
     */
    public static ConfigurablePropertyManager getInstance() {
        if (_instance == null) {
            _instance = new ConfigurablePropertyManager();
        }
        return _instance;
    }

    /**
     * Adds a PropertyChangeListener to the list of listeners.
     *
     * @param listener The listener to be added.
     */
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        _listeners.add(listener);
    }

    /**
     * Removes a PropertyChangeListener from the list of listeners.
     *
     * @param listener The listener to be removed.
     */
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        _listeners.remove(listener);
    }

    /**
     * Adds a new configurable property with the specified key and default value.
     * Notifies all listeners about the addition.
     *
     * @param key The key of the property.
     * @param defaultValue The default value of the property.
     * @param pluginName The name of the plugin this property belongs to
     * @param displayName A human-readable name for display. (nullable)
     * @param selectionChoices The possible options for the property (nullable).
     */
    public void addConfigurableProperty(
        String key, String defaultValue, String pluginName, String displayName,
        String[] selectionChoices)
    {
        ConfigurableProperty configurableProperty =
            new ConfigurableProperty(key, defaultValue, pluginName, displayName, selectionChoices);
        registerProperty(configurableProperty);
    }

    /**
     * Adds a new configurable property with the specified key and default value.
     * Notifies all listeners about the addition.
     *
     * @param key The key of the property.
     * @param keyStroke The default hotkey of the property.
     * @param pluginName The name of the plugin this property belongs to
     * @param displayName A human-readable name for display. (nullable)
     */
    public void addConfigurableHotkeyProperty(
        String key, KeyStroke keyStroke, String pluginName, String displayName)
    {
        ConfigurableHotkeyProperty property =
            new ConfigurableHotkeyProperty(key, keyStroke, pluginName, displayName);
        registerProperty(property);
    }

    /**
     * Adds a previously constructed property to the map of properties
     * @param configurableProperty the new property to be registered
     */
    private void registerProperty(ConfigurableProperty configurableProperty) {
        _properties.put(configurableProperty.getKey(), configurableProperty);
        PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(
            this, configurableProperty.getKey(), configurableProperty.getDefaultValue());
        notifyListeners(propertyChangeEvent, PropertyChangeType.PROPERTY_ADDED);
    }

    /**
     * Removes a configurable property with the specified key.
     * Notifies all listeners about the removal.
     *
     * @param key The key of the property to be removed.
     */
    public void removeConfigurableProperty(String key) {
        ConfigurableProperty oldValue = _properties.get(key);
        if (oldValue == null) {
            LOGGER.warn(
                "PropertyManager: Attempting to remove a property that was not added before.");
        } else {
            _properties.remove(key);
            PropertyChangeEvent propertyChangeEvent =
                new PropertyChangeEvent(this, key, oldValue.getCurrentValue(), null);
            notifyListeners(propertyChangeEvent, PropertyChangeType.PROPERTY_REMOVED);
        }
    }

    /**
     * Changes the value of a configurable property with the specified key.
     * If the property does not exist, it is added, and listeners are notified of the addition of a new Property.
     * Notifies then all listeners about the property change.
     * A listener will therefore be called twice, when this method is used to change a non-existing property;
     * the listeners can differentiate between the two events by the methods called:
     * {@link PropertyChangeListener#propertyAdded(PropertyChangeEvent) propertyAdded} or
     * {@link PropertyChangeListener#propertyChanged(PropertyChangeEvent) propertyChanged}
     *
     * @param key The key of the property to be changed.
     * @param newValue The new value of the property.
     */
    public void changeProperty(String key, String newValue) {
        PropertyChangeEvent propertyChangeEvent;
        if (_properties.containsKey(key)) {
            ConfigurableProperty configurableProperty = _properties.get(key);
            configurableProperty.setCurrentValue(newValue);
            propertyChangeEvent = new PropertyChangeEvent(
                this, key, configurableProperty.getCurrentValue(), newValue);
        } else {
            LOGGER.warn(
                "PropertyManager: Changing a property that was not added before. Adding it now");
            propertyChangeEvent = new PropertyChangeEvent(this, key, newValue);
            ConfigurableProperty configurableProperty = new ConfigurableProperty(key, newValue);
            registerProperty(configurableProperty);
        }
        notifyListeners(propertyChangeEvent, PropertyChangeType.PROPERTY_CHANGED);
    }

    /**
     * Changes the value of a configurable property with the specified key.
     * If the property does not exist, it is added, and listeners are notified of the adding of a new Property.
     * Notifies then all listeners about the property change.
     * A listener will therefore be called twice, when this method is used to change a non-existing property;
     * the listeners can differentiate between the two events by the methods called:
     * {@link PropertyChangeListener#propertyAdded(PropertyChangeEvent) propertyAdded} or
     * {@link PropertyChangeListener#propertyChanged(PropertyChangeEvent) propertyChanged}
     *
     * @param key The key of the property to be changed.
     * @param newKeyStroke The new hotkey of the property.
     */
    public void changeHotkeyProperty(String key, KeyStroke newKeyStroke) {
        ConfigurableProperty property = _properties.get(key);
        if (property == null) {
            LOGGER.warn(
                "PropertyManager: Changing a property that was not added before. Adding it now");
            ConfigurableHotkeyProperty hotkeyProperty =
                new ConfigurableHotkeyProperty(key, newKeyStroke, null, null);
            registerProperty(hotkeyProperty);
        } else if (property instanceof ConfigurableHotkeyProperty hotkeyProperty) {
            hotkeyProperty.setKeyStroke(newKeyStroke);
            PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(
                this, key, hotkeyProperty.getCurrentValue(), newKeyStroke.toString());
            notifyListeners(propertyChangeEvent, PropertyChangeType.PROPERTY_CHANGED);
        } else {
            LOGGER.error("PropertyManager: Property is not a HotkeyProperty, ignoring");
        }
    }

    /**
     * Saves the properties to a file.
     *
     * @param useHomeDir If {@code true}, the properties are saved in the home directory.
     * Otherwise, the properties are saved in the current working directory.
     *
     * @throws PropertySaveFailedException If the properties could not be saved.
     */
    public void saveProperties(boolean useHomeDir) throws PropertySaveFailedException {
        _persister.saveProperties(useHomeDir, _properties);
    }

    /**
     * Loads the properties from a file.
     *
     * @param useHomeDir If {@code true}, the properties are loaded from the home directory.
     * Otherwise, the properties are loaded from the current working directory.
     *
     * @throws PropertyLoadFailedException If the properties could not be loaded.
     */
    public void loadProperties(boolean useHomeDir) throws PropertyLoadFailedException {
        _persister.loadProperties(useHomeDir, _properties);
    }

    /**
     * Notifies all listeners depending on the notification type
     *
     * @param event The PropertyChangeEvent send to the listeners
     * @param notificationType Type of the notification represented as a string
     */
    private void notifyListeners(PropertyChangeEvent event, PropertyChangeType notificationType) {
        switch (notificationType) {
            case PROPERTY_ADDED:
                for (PropertyChangeListener listener : _listeners) {
                    listener.propertyAdded(event);
                }
                break;
            case PROPERTY_CHANGED:
                for (PropertyChangeListener listener : _listeners) {
                    listener.propertyChanged(event);
                }
                break;
            case PROPERTY_REMOVED:
                for (PropertyChangeListener listener : _listeners) {
                    listener.propertyRemoved(event);
                }
                break;
            default:
                LOGGER.error("Tried to notify listeners for an unknown notification type.");
                break;
        }
    }

    /**
     * Retrieves the display name associated with a specified configurable property key.
     *
     * @param key The key identifying the configurable property.
     * @return an {@code Optional} containing the display name for the property key
     * or {@code Optional.empty()} if the property has no display name.
     * @throws NoSuchElementException if the key is not known
     */
    public Optional<String> getDisplayNameForProperty(String key) throws NoSuchElementException {
        return Optional
            .ofNullable(Optional.ofNullable(_properties.get(key)).orElseThrow().getDisplayName());
    }

    /**
     * Retrieves the plugin name associated with a specified configurable property key.
     *
     * @param key The key identifying the configurable property.
     * @return an {@code Optional} containing the plugin name for the property key
     * or {@code Optional.empty()} if the property is not related to a plugin.
     * @throws NoSuchElementException if the key is not known
     */
    public Optional<String> getPluginForProperty(String key) throws NoSuchElementException {
        return Optional
            .ofNullable(Optional.ofNullable(_properties.get(key)).orElseThrow().getPluginName());
    }

    /**
     * Retrieves the current value associated with a specified configurable property key.
     *
     * @param key The key identifying the configurable property.
     * @return an {@code Optional} containing the current value for the property key
     * or {@code Optional.empty()} if the property has no current value set.
     * @throws NoSuchElementException if the key is not known
     */
    public Optional<String> getCurrentValueForProperty(String key) throws NoSuchElementException {
        return Optional
            .ofNullable(Optional.ofNullable(_properties.get(key)).orElseThrow().getCurrentValue());
    }

    /**
     * Returns all configurable properties.
     *
     * @return configurable properties as a Map.
     */
    public Map<String, ConfigurableProperty> getConfigurableProperties() {
        return Collections.unmodifiableMap(_properties);
    }
}