package io.xpipe.app.core;

import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.platform.ColorHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.OsType;

import javafx.application.Application;
import javafx.application.ColorScheme;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.MapChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import atlantafx.base.theme.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.SneakyThrows;

import java.lang.ref.WeakReference;
import java.nio.file.Files;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class AppTheme {

    private static final PseudoClass LIGHT = PseudoClass.getPseudoClass("light");
    private static final PseudoClass DARK = PseudoClass.getPseudoClass("dark");
    private static final PseudoClass PRETTY = PseudoClass.getPseudoClass("pretty");
    private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance");
    private static boolean init;

    public static void initThemeHandlers(Stage stage) {
        if (stage.getScene() == null) {
            return;
        }

        if (AppPrefs.get() == null) {
            var root = stage.getScene().getRoot();
            applyClasses(root, Theme.getDefaultLightTheme(), false);
            return;
        }

        stage.getScene().rootProperty().subscribe(parent -> {
            applyClasses(
                    parent,
                    AppPrefs.get().theme().getValue(),
                    AppPrefs.get().performanceMode().getValue());
        });

        // Allow for GC
        var ref = new WeakReference<>(stage);

        AppPrefs.get().theme().subscribe(t -> {
            var val = ref.get();
            if (val != null) {
                var scene = val.getScene();
                if (scene != null) {
                    var r = scene.getRoot();
                    applyClasses(r, t, AppPrefs.get().performanceMode().get());
                }
            }
        });

        AppPrefs.get().performanceMode().subscribe(pm -> {
            var val = ref.get();
            if (val != null) {
                var scene = val.getScene();
                if (scene != null) {
                    var r = scene.getRoot();
                    applyClasses(r, AppPrefs.get().theme().getValue(), pm);
                }
            }
        });
    }

    private static void applyClasses(Node r, Theme t, boolean perf) {
        if (r == null) {
            return;
        }

        r.pseudoClassStateChanged(PseudoClass.getPseudoClass(OsType.ofLocal().getId()), true);

        if (t != null) {
            Theme.ALL.forEach(theme -> {
                r.pseudoClassStateChanged(
                        PseudoClass.getPseudoClass(theme.getCssId()),
                        theme.getCssId().equals(t.getCssId()));
            });
        }

        if (t != null) {
            r.pseudoClassStateChanged(LIGHT, !t.isDark());
            r.pseudoClassStateChanged(DARK, t.isDark());
        }

        r.pseudoClassStateChanged(PRETTY, !perf);
        r.pseudoClassStateChanged(PERFORMANCE, perf);
    }

    public static void init() {
        if (init) {
            TrackEvent.trace("Theme init requested again");
            return;
        }

        if (AppPrefs.get() == null) {
            TrackEvent.trace("Theme init prior to prefs init, setting theme to default");
            Theme.getDefaultLightTheme().apply();
            return;
        }

        try {
            var lastSystemDark = AppCache.getBoolean("lastDarkTheme", false);
            var nowDark = isDarkMode();
            AppCache.update("lastDarkTheme", nowDark);
            if (AppPrefs.get().theme().getValue() == null || lastSystemDark != nowDark) {
                TrackEvent.trace("Updating theme to system theme");
                setDefault();
            }

            Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
                TrackEvent.withTrace("Platform preference changed")
                        .tag("change", change.toString())
                        .handle();
            });

            Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
                if (change.getKey().equals("GTK.theme_name")) {
                    Platform.runLater(() -> {
                        updateThemeToThemeName(change.getValueRemoved(), change.getValueAdded());
                    });
                }
            });

            Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> {
                Platform.runLater(() -> {
                    updateThemeToColorScheme(t1);
                });
            });
        } catch (IllegalStateException ex) {
            // The platform preferences are sometimes not initialized yet
            ErrorEventFactory.fromThrowable(ex).expected().omit().handle();
        } catch (Throwable t) {
            ErrorEventFactory.fromThrowable(t).omit().handle();
        }

        var t = AppPrefs.get().theme().getValue();
        t.apply();
        TrackEvent.debug("Set theme " + t.getId() + " for scene");

        AppPrefs.get().theme().addListener((c, o, n) -> {
            changeTheme(n);
        });

        init = true;
    }

    private static void updateThemeToThemeName(Object oldName, Object newName) {
        if (OsType.ofLocal() == OsType.LINUX && newName != null) {
            var toDark = (oldName == null || !oldName.toString().contains("-dark"))
                    && newName.toString().contains("-dark");
            var toLight = (oldName == null || oldName.toString().contains("-dark"))
                    && !newName.toString().contains("-dark");
            if (toDark) {
                updateThemeToColorScheme(ColorScheme.DARK);
            } else if (toLight) {
                updateThemeToColorScheme(ColorScheme.LIGHT);
            }
        }
    }

    private static boolean isDarkMode() {
        var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
        if (nowDark) {
            return true;
        }

        var gtkTheme = Platform.getPreferences().get("GTK.theme_name");
        return gtkTheme != null && gtkTheme.toString().contains("-dark");
    }

    private static void updateThemeToColorScheme(ColorScheme colorScheme) {
        if (colorScheme == null) {
            return;
        }

        if (colorScheme == ColorScheme.DARK
                && !AppPrefs.get().theme().getValue().isDark()) {
            AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
        }

        if (colorScheme != ColorScheme.DARK && AppPrefs.get().theme().getValue().isDark()) {
            AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
        }
    }

    public static void reset() {
        if (!init) {
            return;
        }

        PlatformThread.runLaterIfNeededBlocking(() -> {
            var nowDark = isDarkMode();
            AppCache.update("lastDarkTheme", nowDark);
        });
    }

    private static void setDefault() {
        try {
            var colorScheme = Platform.getPreferences().getColorScheme();
            if (colorScheme == ColorScheme.DARK) {
                AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
            } else {
                AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
            }
        } catch (IllegalStateException ex) {
            // The platform preferences are sometimes not initialized yet
            ErrorEventFactory.fromThrowable(ex).expected().omit().handle();
        } catch (Exception ex) {
            // The color scheme query can fail if the toolkit is not initialized properly
            AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
        }
    }

    private static void changeTheme(Theme newTheme) {
        if (newTheme == null) {
            return;
        }

        PlatformThread.runLaterIfNeeded(() -> {
            var window = AppMainWindow.get();
            if (window == null) {
                return;
            }

            TrackEvent.debug("Setting theme " + newTheme.getId() + " for scene");

            // Don't animate anything for performance reasons
            newTheme.apply();
        });
    }

    public static class DerivedTheme extends Theme {

        private final String name;
        private final int skipLines;

        public DerivedTheme(
                String id,
                String cssId,
                String name,
                atlantafx.base.theme.Theme theme,
                Supplier<AppFontSizes> sizes,
                Color baseColor,
                Color borderColor,
                Supplier<Color> contextMenuColor,
                Supplier<Color> emphasisColor,
                int displayBorderRadius,
                int skipLines) {
            super(
                    id,
                    cssId,
                    theme,
                    sizes,
                    baseColor,
                    borderColor,
                    contextMenuColor,
                    emphasisColor,
                    displayBorderRadius);
            this.name = name;
            this.skipLines = skipLines;
        }

        @Override
        @SneakyThrows
        public void apply() {
            var builder = new StringBuilder();
            AppResources.with(AppResources.MAIN_MODULE, "theme/" + id + ".css", path -> {
                var content = Files.readString(path);
                builder.append(content);
            });

            // Watch out for the leading slash
            AppResources.with("atlantafx.base", theme.getUserAgentStylesheet().substring(1), path -> {
                var baseStyleContent = Files.readString(path);
                builder.append("\n")
                        .append(baseStyleContent.lines().skip(skipLines).collect(Collectors.joining("\n")));
            });

            Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString()));
        }

        @Override
        public ObservableValue<String> toTranslatedString() {
            return new SimpleStringProperty(name);
        }
    }

    @AllArgsConstructor
    public static class Theme implements PrefsChoiceValue {

        public static final Theme PRIMER_LIGHT = new Theme(
                "light",
                "primer",
                new PrimerLight(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.WHITE,
                Color.web("#24292f"),
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences()
                                .getAccentColor()
                                .darker()
                                .desaturate()
                                .brighter(),
                        0.3),
                () -> Platform.getPreferences().getAccentColor(),
                4);
        public static final Theme PRIMER_DARK = new Theme(
                "dark",
                "primer",
                new PrimerDark(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.web("#0d1117"),
                Color.web("#c9d1d9"),
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences()
                                .getAccentColor()
                                .desaturate()
                                .desaturate()
                                .darker(),
                        0.2),
                () -> Platform.getPreferences().getAccentColor(),
                4);
        public static final Theme NORD_LIGHT = new Theme(
                "nordLight",
                "nord",
                new NordLight(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.web("#dadadc"),
                Color.web("#2E3440"),
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences()
                                .getAccentColor()
                                .darker()
                                .desaturate()
                                .brighter(),
                        0.3),
                () -> Platform.getPreferences().getAccentColor(),
                0);
        public static final Theme NORD_DARK = new Theme(
                "nordDark",
                "nord",
                new NordDark(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.web("#2d3137"),
                Color.web("#24292f"),
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences()
                                .getAccentColor()
                                .desaturate()
                                .desaturate()
                                .darker(),
                        0.2),
                () -> Platform.getPreferences().getAccentColor(),
                0);
        public static final Theme CUPERTINO_LIGHT = new Theme(
                "cupertinoLight",
                "cupertino",
                new CupertinoLight(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.WHITE,
                Color.BLACK,
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences()
                                .getAccentColor()
                                .darker()
                                .desaturate()
                                .brighter(),
                        0.3),
                () -> Platform.getPreferences().getAccentColor(),
                4);
        public static final Theme CUPERTINO_DARK = new Theme(
                "cupertinoDark",
                "cupertino",
                new CupertinoDark(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.BLACK,
                Color.WHITE,
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences()
                                .getAccentColor()
                                .desaturate()
                                .desaturate()
                                .darker(),
                        0.2),
                () -> Platform.getPreferences().getAccentColor(),
                4);
        public static final Theme DRACULA = new Theme(
                "dracula",
                "dracula",
                new Dracula(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.web("#383f49"),
                Color.web("#9580ff"),
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences()
                                .getAccentColor()
                                .desaturate()
                                .desaturate()
                                .darker(),
                        0.2),
                () -> Platform.getPreferences().getAccentColor(),
                6);
        public static final Theme MOCHA = new DerivedTheme(
                "mocha",
                "mocha",
                "Mocha",
                new PrimerDark(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.web("#2E2E4EFF"),
                Color.web("#CDD6F4FF"),
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences()
                                .getAccentColor()
                                .desaturate()
                                .desaturate()
                                .darker(),
                        0.2),
                () -> Platform.getPreferences().getAccentColor(),
                4,
                91);

        // Adjust this to create your own theme
        @SuppressWarnings("unused")
        public static final Theme CUSTOM = new DerivedTheme(
                "custom",
                "primer",
                "Custom",
                new PrimerDark(),
                () -> AppFontSizes.forOs(AppFontSizes.BASE_10_5, AppFontSizes.BASE_10_5, AppFontSizes.BASE_11),
                Color.web("#0d1117"),
                Color.web("#24292f"),
                () -> ColorHelper.withOpacity(
                        Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2),
                () -> Platform.getPreferences().getAccentColor(),
                4,
                91);

        // Also include your custom theme here
        public static final List<Theme> ALL = List.of(
                PRIMER_LIGHT, PRIMER_DARK, NORD_LIGHT, NORD_DARK, CUPERTINO_LIGHT, CUPERTINO_DARK, DRACULA, MOCHA);
        protected final String id;

        @Getter
        protected final String cssId;

        protected final atlantafx.base.theme.Theme theme;

        @Getter
        protected final Supplier<AppFontSizes> fontSizes;

        @Getter
        protected final Color baseColor;

        @Getter
        protected final Color borderColor;

        @Getter
        protected final Supplier<Color> contextMenuColor;

        @Getter
        protected final Supplier<Color> emphasisColor;

        @Getter
        protected final int displayBorderRadius;

        static Theme getDefaultLightTheme() {
            return switch (OsType.ofLocal()) {
                case OsType.Windows ignored -> PRIMER_LIGHT;
                case OsType.Linux ignored -> PRIMER_LIGHT;
                case OsType.MacOs ignored -> CUPERTINO_LIGHT;
            };
        }

        static Theme getDefaultDarkTheme() {
            return switch (OsType.ofLocal()) {
                case OsType.Windows ignored -> PRIMER_DARK;
                case OsType.Linux ignored -> PRIMER_DARK;
                case OsType.MacOs ignored -> CUPERTINO_DARK;
            };
        }

        public boolean isDark() {
            return theme.isDarkMode();
        }

        public void apply() {
            Application.setUserAgentStylesheet(theme.getUserAgentStylesheetBSS());
        }

        protected String getPlatformPreferencesStylesheet() {
            var s = """
                    * {
                        -color-context-menu: %s;
                        -color-accent-fg: %s;
                        -color-accent-emphasis: %s;
                        -color-accent-muted: %s;
                        -color-accent-subtle: %s;
                    }
                    """.formatted(
                            ColorHelper.toWeb(contextMenuColor.get()),
                            ColorHelper.toWeb(emphasisColor.get()),
                            ColorHelper.toWeb(emphasisColor.get().darker()),
                            ColorHelper.toWeb(emphasisColor.get().desaturate()),
                            ColorHelper.toWeb(ColorHelper.withOpacity(
                                    emphasisColor.get().darker().desaturate().desaturate(), 0.2)));
            return s;
        }

        @Override
        public ObservableValue<String> toTranslatedString() {
            return new SimpleStringProperty(theme.getName());
        }

        @Override
        public String getId() {
            return id;
        }
    }
}
