package com.ss.editor.config;

import static com.ss.editor.config.DefaultSettingsProvider.Defaults.*;
import static com.ss.editor.config.DefaultSettingsProvider.Preferences.*;
import static com.ss.rlib.util.ClassUtils.unsafeCast;
import static com.ss.rlib.util.ObjectUtils.notNull;
import com.jme3.asset.AssetEventListener;
import com.jme3.asset.AssetKey;
import com.jme3.asset.TextureKey;
import com.jme3.math.Vector3f;
import com.jme3.system.AppSettings;
import com.jme3x.jfx.injfx.JmeToJFXIntegrator;
import com.ss.editor.JmeApplication;
import com.ss.editor.annotation.FromAnyThread;
import com.ss.editor.util.EditorUtil;
import com.ss.rlib.logging.Logger;
import com.ss.rlib.logging.LoggerManager;
import com.ss.rlib.util.Utils;
import com.ss.rlib.util.dictionary.ConcurrentObjectDictionary;
import com.ss.rlib.util.dictionary.DictionaryFactory;
import com.ss.rlib.util.dictionary.DictionaryUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.io.Serializable;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

/**
 * The user configuration of this editor.
 *
 * @author JavaSaBr
 */
public final class EditorConfig implements AssetEventListener {

    @NotNull
    private static final Logger LOGGER = LoggerManager.getLogger(EditorConfig.class);

    private static final String SCREEN_ALIAS = "screen";
    private static final String ASSET_ALIAS = "asset";
    private static final String OTHER_ALIAS = "other";

    private static final String PREF_SCREEN_WIDTH = SCREEN_ALIAS + "." + "width";
    private static final String PREF_SCREEN_HEIGHT = SCREEN_ALIAS + "." + "height";
    private static final String PREF_SCREEN_MAXIMIZED = SCREEN_ALIAS + "." + "maximized";

    private static final String PREF_ASSET_CURRENT_ASSET = ASSET_ALIAS + "." + "current";
    private static final String PREF_ASSET_LAST_OPENED_ASSETS = ASSET_ALIAS + "." + "lastOpened";

    private static final String PREF_OTHER_ANALYTICS_QUESTION = OTHER_ALIAS + "." + "analyticsQuestion" + Config.STRING_VERSION;

    private static final String PREF_OTHER_GLOBAL_LEFT_TOOL_WIDTH = OTHER_ALIAS + "." + "global.leftTool.width";
    private static final String PREF_OTHER_GLOBAL_LEFT_TOOL_COLLAPSED = OTHER_ALIAS + "." + "global.leftTool.collapsed";
    private static final String PREF_OTHER_GLOBAL_BOTTOM_TOOL_WIDTH = OTHER_ALIAS + "." + "global.bottomTool.height";
    private static final String PREF_OTHER_GLOBAL_BOTTOM_TOOL_COLLAPSED = OTHER_ALIAS + "." + "global.bottomTool.collapsed";

    @Nullable
    private static volatile EditorConfig instance;

    @FromAnyThread
    public static @NotNull EditorConfig getInstance() {
        if (instance == null) {
            synchronized (EditorConfig.class) {
                if (instance == null) {
                    final EditorConfig config = new EditorConfig();
                    instance = config;
                }
            }
        }
        return instance;
    }

    /**
     * The list of last opened asset folders.
     */
    @NotNull
    private final List<String> lastOpenedAssets;

    /**
     * The all settings.
     */
    @NotNull
    private final ConcurrentObjectDictionary<String, Object> settings;

    /**
     * The current asset folder.
     */
    @Nullable
    private volatile Path currentAsset;

    /**
     * The width of this screen.
     */
    private volatile int screenWidth;

    /**
     * The height of this screen.
     */
    private volatile int screenHeight;

    /**
     * The global left tool width.
     */
    private volatile int globalLeftToolWidth;

    /**
     * The global bottom tool width.
     */
    private volatile int globalBottomToolHeight;

    /**
     * Flag is for collapsing the global left tool.
     */
    private volatile boolean globalLeftToolCollapsed;

    /**
     * Flag is for collapsing the global bottom tool.
     */
    private volatile boolean globalBottomToolCollapsed;

    /**
     * Flag is for maximizing a window.
     */
    private volatile boolean maximized;

    /**
     * Flag is of showing analytics question.
     */
    private volatile boolean analyticsQuestion;

    public EditorConfig() {
        this.lastOpenedAssets = new ArrayList<>();
        this.settings = DictionaryFactory.newConcurrentAtomicObjectDictionary();
        init();
    }

    /**
     * Get the generic method to get generic values from settings.
     *
     * @param id   the setting id.
     * @param type the type.
     * @param def  the default value.
     * @param <T>  the setting's type.
     * @return the setting's value or default.
     */
    @FromAnyThread
    private <T> @Nullable T get(@NotNull final String id, @NotNull final Class<T> type, @Nullable T def) {

        final Object value = DictionaryUtils.getInReadLock(settings, id, (objects, s) -> objects.get(id));
        if (value == null) {
            return def;
        } else if (type.isInstance(value)) {
            return unsafeCast(value);
        }

        T result = null;

        if (type == Boolean.class) {
            if (value instanceof String) {
                result = unsafeCast(Boolean.valueOf(value.toString()));
            }
        } else if (type == Integer.class) {
            if (value instanceof String) {
                result = unsafeCast(Integer.valueOf(value.toString()));
            }
        } else if (type == Vector3f.class) {
            if (value instanceof String) {
                final String[] values = value.toString().split(",");
                final float x = Float.parseFloat(values[0]);
                final float y = Float.parseFloat(values[1]);
                final float z = Float.parseFloat(values[2]);
                result = unsafeCast(new Vector3f(x, y, z));
            }
        } else if (Enum.class.isAssignableFrom(type)) {
            final Class<Enum> enumType = unsafeCast(type);
            if (value instanceof String) {
                final Enum enumValue = Enum.valueOf(enumType, value.toString());
                result = unsafeCast(enumValue);
            }
        } else if (Path.class.isAssignableFrom(type)) {
            if (value instanceof String) {
                final URI uri = Utils.get(value.toString(), URI::new);
                result = unsafeCast(Paths.get(uri));
            }
        }

        if (result != null) {
            set(id, result);
            return result;
        }

        throw new IllegalArgumentException("Can't convert the value " + value + " to the type " + type);
    }

    /**
     * Set the new value of the setting by the id.
     *
     * @param id    the setting's id.
     * @param value the setting's value.
     */
    @FromAnyThread
    public void set(@NotNull final String id, @Nullable final Object value) {
        final long stamp = settings.writeLock();
        try {

            if (value == null) {
                settings.remove(id);
            } else {
                settings.put(id, value);
            }

        } finally {
            settings.writeUnlock(stamp);
        }
    }

    @FromAnyThread
    public boolean getBoolean(@NotNull final String id, final boolean def) {
        return Boolean.TRUE.equals(get(id, Boolean.class, def));
    }

    @FromAnyThread
    public @Nullable Boolean getBoolean(@NotNull final String id) {
        return get(id, Boolean.class, null);
    }

    @FromAnyThread
    public int getInteger(@NotNull final String id, final int def) {
        return notNull(get(id, Integer.class, def));
    }

    @FromAnyThread
    public @Nullable Integer getInteger(@NotNull final String id) {
        return get(id, Integer.class, null);
    }

    @FromAnyThread
    public <T extends Enum<T>> @Nullable T getEnum(@NotNull final String id, @NotNull final Class<T> type) {
        return get(id, type, null);
    }

    @FromAnyThread
    public <T extends Enum<T>> @NotNull T getEnum(@NotNull final String id, @NotNull final T def) {
        return notNull(get(id, unsafeCast(def.getClass()), def));
    }

    @FromAnyThread
    public @NotNull Vector3f getVector3f(@NotNull final String id, @NotNull final Vector3f def) {
        return notNull(get(id, Vector3f.class, def));
    }

    @FromAnyThread
    public @Nullable Path getFile(@NotNull final String id) {
        return get(id, Path.class, null);
    }

    @FromAnyThread
    public @Nullable String getString(@NotNull final String id) {
        return get(id, String.class, null);
    }

    /**
     * Get the last opened assets.
     *
     * @return The list of last opened asset folders.
     */
    @FromAnyThread
    public synchronized @NotNull List<String> getLastOpenedAssets() {
        return lastOpenedAssets;
    }

    /**
     * Add the new last opened asset folder.
     *
     * @param currentAsset the current asset
     */
    @FromAnyThread
    public synchronized void addOpenedAsset(@NotNull final Path currentAsset) {

        final String filePath = currentAsset.toString();
        final List<String> lastOpenedAssets = getLastOpenedAssets();
        lastOpenedAssets.remove(filePath);
        lastOpenedAssets.add(0, filePath);

        if (lastOpenedAssets.size() > 10) {
            lastOpenedAssets.remove(lastOpenedAssets.size() - 1);
        }
    }

    @Override
    public void assetDependencyNotFound(@Nullable final AssetKey parentKey, @Nullable final AssetKey dependentAssetKey) {
    }

    @Override
    public void assetLoaded(@NotNull final AssetKey key) {
    }

    @Override
    public void assetRequested(@NotNull final AssetKey key) {
        if (key instanceof TextureKey) {
            ((TextureKey) key).setAnisotropy(getInteger(PREF_ANISOTROPY, PREF_DEFAULT_ANISOTROPY));
        }
    }

    /**
     * Get the current asset folder.
     *
     * @return the current asset folder.
     */
    @FromAnyThread
    public @Nullable Path getCurrentAsset() {
        return currentAsset;
    }

    /**
     * Set the current asset folder.
     *
     * @param currentAsset the current asset folder.
     */
    @FromAnyThread
    public void setCurrentAsset(@Nullable final Path currentAsset) {
        this.currentAsset = currentAsset;
    }

    /**
     * Set the screen height.
     *
     * @param screenHeight the height of this screen.
     */
    @FromAnyThread
    public void setScreenHeight(final int screenHeight) {
        this.screenHeight = screenHeight;
    }

    /**
     * Set the screen width.
     *
     * @param screenWidth the width of this screen.
     */
    @FromAnyThread
    public void setScreenWidth(final int screenWidth) {
        this.screenWidth = screenWidth;
    }

    /**
     * Get the screen height.
     *
     * @return the height of this screen.
     */
    @FromAnyThread
    public int getScreenHeight() {
        return screenHeight;
    }

    /**
     * Get the screen width.
     *
     * @return the width of this screen.
     */
    @FromAnyThread
    public int getScreenWidth() {
        return screenWidth;
    }

    /**
     * Return the maximized state of editor's window.
     *
     * @return true is the editor's window is maximized.
     */
    @FromAnyThread
    public boolean isMaximized() {
        return maximized;
    }

    /**
     * Set the maximized state of editor's window.
     *
     * @param maximized true is the editor's window is maximized.
     */
    @FromAnyThread
    public void setMaximized(final boolean maximized) {
        this.maximized = maximized;
    }

    /**
     * Get the global left tool width.
     *
     * @return the global left tool width.
     */
    @FromAnyThread
    public int getGlobalLeftToolWidth() {
        return globalLeftToolWidth;
    }

    /**
     * Get the global bottom tool height.
     *
     * @return the global bottom tool height.
     */
    @FromAnyThread
    public int getGlobalBottomToolHeight() {
        return globalBottomToolHeight;
    }

    /**
     * Set the global left tool width.
     *
     * @param globalLeftToolWidth the global left tool width.
     */
    @FromAnyThread
    public void setGlobalLeftToolWidth(final int globalLeftToolWidth) {
        this.globalLeftToolWidth = globalLeftToolWidth;
    }

    /**
     * Set the global bottom tool height.
     *
     * @param globalBottomToolHeight the global bottom tool height.
     */
    @FromAnyThread
    public void setGlobalBottomToolHeight(final int globalBottomToolHeight) {
        this.globalBottomToolHeight = globalBottomToolHeight;
    }

    /**
     * Set the global left tool collapsed.
     *
     * @param globalLeftToolCollapsed flag is for collapsing the global left tool.
     */
    @FromAnyThread
    public void setGlobalLeftToolCollapsed(final boolean globalLeftToolCollapsed) {
        this.globalLeftToolCollapsed = globalLeftToolCollapsed;
    }

    /**
     * Set the global bottom tool collapsed.
     *
     * @param globalBottomToolCollapsed flag is for collapsing the global bottom tool.
     */
    @FromAnyThread
    public void setGlobalBottomToolCollapsed(final boolean globalBottomToolCollapsed) {
        this.globalBottomToolCollapsed = globalBottomToolCollapsed;
    }

    /**
     * Is global left tool collapsed boolean.
     *
     * @return true if the global left tool is collapsed.
     */
    @FromAnyThread
    public boolean isGlobalLeftToolCollapsed() {
        return globalLeftToolCollapsed;
    }

    /**
     * Is global bottom tool collapsed boolean.
     *
     * @return true if the global bottom tool is collapsed.
     */
    @FromAnyThread
    public boolean isGlobalBottomToolCollapsed() {
        return globalBottomToolCollapsed;
    }

    /**
     * Is analytics question boolean.
     *
     * @return true if the question was showed.
     */
    public boolean isAnalyticsQuestion() {
        return analyticsQuestion;
    }

    /**
     * Sets analytics question.
     *
     * @param analyticsQuestion true if the question was showed.
     */
    public void setAnalyticsQuestion(final boolean analyticsQuestion) {
        this.analyticsQuestion = analyticsQuestion;
    }

    /**
     * Get the jME settings.
     *
     * @return the the jME settings.
     */
    @FromAnyThread
    public AppSettings getSettings() {

        final GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
        final GraphicsDevice device = graphicsEnvironment.getDefaultScreenDevice();
        final DisplayMode displayMode = device.getDisplayMode();

        final AppSettings settings = new AppSettings(true);
        settings.setFrequency(displayMode.getRefreshRate());
        settings.setGammaCorrection(getBoolean(PREF_GAMMA_CORRECTION, PREF_DEFAULT_GAMMA_CORRECTION));
        settings.setResizable(true);
        // settings.putBoolean("GraphicsDebug", true);

        JmeToJFXIntegrator.prepareSettings(settings, getInteger(PREF_FRAME_RATE, PREF_DEFAULT_FRAME_RATE));

        return settings;
    }

    /**
     * Load user settings.
     */
    private void init() {

        final Preferences prefs = Preferences.userNodeForPackage(JmeApplication.class);
        final long stamp = settings.writeLock();
        try {

            for (final String key : prefs.keys()) {
                settings.put(key, prefs.get(key, null));
            }

        } catch (final BackingStoreException e) {
            throw new RuntimeException(e);
        } finally {
            settings.writeUnlock(stamp);
        }

        this.maximized = prefs.getBoolean(PREF_SCREEN_MAXIMIZED, false);
        this.screenHeight = prefs.getInt(PREF_SCREEN_HEIGHT, 800);
        this.screenWidth = prefs.getInt(PREF_SCREEN_WIDTH, 1200);
        this.globalLeftToolWidth = prefs.getInt(PREF_OTHER_GLOBAL_LEFT_TOOL_WIDTH, 300);
        this.globalLeftToolCollapsed = prefs.getBoolean(PREF_OTHER_GLOBAL_LEFT_TOOL_COLLAPSED, false);
        this.globalBottomToolHeight = prefs.getInt(PREF_OTHER_GLOBAL_BOTTOM_TOOL_WIDTH, 300);
        this.globalBottomToolCollapsed = prefs.getBoolean(PREF_OTHER_GLOBAL_BOTTOM_TOOL_COLLAPSED, true);
        this.analyticsQuestion = prefs.getBoolean(PREF_OTHER_ANALYTICS_QUESTION, false);

        final String currentAssetUri = prefs.get(PREF_ASSET_CURRENT_ASSET, null);

        if (currentAssetUri != null) {
            this.currentAsset = Utils.get(currentAssetUri, uri -> Paths.get(new URI(uri)));
        }

        if (currentAsset != null && !Files.exists(currentAsset)) {
            this.currentAsset = null;
        }

        final int cameraAngle = getInteger(PREF_CAMERA_ANGLE, PREF_DEFAULT_CAMERA_ANGLE);

        System.setProperty("jfx.frame.transfer.camera.angle", String.valueOf(cameraAngle));

        final byte[] byteArray = prefs.getByteArray(PREF_ASSET_LAST_OPENED_ASSETS, null);
        if (byteArray == null) {
            return;
        }

        final List<String> lastOpenedAssets = getLastOpenedAssets();
        try {

            lastOpenedAssets.addAll(EditorUtil.deserialize(byteArray));

            for (Iterator<String> iterator = lastOpenedAssets.iterator(); iterator.hasNext(); ) {

                final String assetUrl = iterator.next();
                final Path assetPath = Utils.get(assetUrl, uri -> Paths.get(uri));

                if (!Files.exists(assetPath)) {
                    iterator.remove();
                }
            }

        } catch (final RuntimeException e) {
            LOGGER.warning(e);
        }
    }

    /**
     * Save these settings.
     */
    @FromAnyThread
    public synchronized void save() {

        final Preferences prefs = Preferences.userNodeForPackage(JmeApplication.class);

        final long stamp = settings.readLock();
        try {

            settings.forEach((key, value) -> {
                if (value instanceof Boolean) {
                    prefs.putBoolean(key, (Boolean) value);
                } else if (value instanceof Integer) {
                    prefs.putInt(key, (Integer) value);
                }  else if (value instanceof Enum<?>) {
                    prefs.put(key, (((Enum) value).name()));
                } else if (value instanceof Path) {
                    prefs.put(key, ((Path) value).toUri().toString());
                } else if (value instanceof Vector3f) {
                    final Vector3f vector = (Vector3f) value;
                    prefs.put(key, vector.getX() + "," + vector.getY() + "," + vector.getZ());
                } else {
                    prefs.put(key, value.toString());
                }
            });

        } finally {
            settings.readUnlock(stamp);
        }

        prefs.putInt(PREF_SCREEN_HEIGHT, getScreenHeight());
        prefs.putInt(PREF_SCREEN_WIDTH, getScreenWidth());
        prefs.putBoolean(PREF_SCREEN_MAXIMIZED, isMaximized());
        prefs.putInt(PREF_OTHER_GLOBAL_LEFT_TOOL_WIDTH, getGlobalLeftToolWidth());
        prefs.putBoolean(PREF_OTHER_GLOBAL_LEFT_TOOL_COLLAPSED, isGlobalLeftToolCollapsed());
        prefs.putInt(PREF_OTHER_GLOBAL_BOTTOM_TOOL_WIDTH, getGlobalBottomToolHeight());
        prefs.putBoolean(PREF_OTHER_GLOBAL_BOTTOM_TOOL_COLLAPSED, isGlobalBottomToolCollapsed());
        prefs.putBoolean(PREF_OTHER_ANALYTICS_QUESTION, isAnalyticsQuestion());

        if (currentAsset != null && !Files.exists(currentAsset)) {
            currentAsset = null;
        }

        if (currentAsset != null) {
            prefs.put(PREF_ASSET_CURRENT_ASSET, currentAsset.toUri().toString());
        } else {
            prefs.remove(PREF_ASSET_CURRENT_ASSET);
        }

        final List<String> lastOpenedAssets = getLastOpenedAssets();

        prefs.putByteArray(PREF_ASSET_LAST_OPENED_ASSETS, EditorUtil.serialize((Serializable) lastOpenedAssets));
        try {
            prefs.flush();
        } catch (final BackingStoreException e) {
            throw new RuntimeException(e);
        }
    }
}
