/*
 * Copyright (C) 2025-2026  Tim Zimmermann <tim@aglais.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.aglais.zms.ui.tab;

import org.aglais.zms.AglaisZMS;
import org.aglais.zms.i18n.InterpolatingResourceBundle;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;
import java.util.ResourceBundle;
import java.util.concurrent.Future;

/**
 * Abstract base class for tabs in the application UI.
 * Provides common functionality for tab registration and showing/hiding behavior.
 */
public abstract class Tab {

    /**
     * Custom SingleSelectionModel that supports vetoing tab selection changes.
     * Extends DefaultSingleSelectionModel to add the ability to cancel tab changes
     * if they are vetoed by a VetoableChangeListener.
     */
    private static class VetoableSingleSelectionModel extends DefaultSingleSelectionModel {

        private final VetoableChangeSupport vetoableChangeSupport = new VetoableChangeSupport(this);

        /**
         * Sets the selected index and notifies listeners, allowing the change to be vetoed.
         * If the change is vetoed via PropertyVetoException, the selection change is cancelled.
         *
         * @param index The index to select
         */
        @Override
        public void setSelectedIndex(int index) {
            // Don't fire events if selection isn't changing
            if (getSelectedIndex() == index)
                return;

            try {
                // Notify vetoable listeners and allow them to cancel the change
                vetoableChangeSupport.fireVetoableChange("selectedIndex", getSelectedIndex(), index);
            } catch (PropertyVetoException e) {
                // Change was vetoed, keep the current selection
                return;
            }

            // Change was approved, update selection
            super.setSelectedIndex(index);
        }

        /**
         * Adds a vetoable change listener to the model. This listener will be notified
         * of changes to the property values and be able to veto changes by
         * throwing a {@code PropertyVetoException}.
         *
         * @param listener The {@link VetoableChangeListener} to be added. The listener
         *                 will receive {@link java.beans.PropertyChangeEvent}s related
         *                 to the vetoable property changes.
         */
        public void addVetoableChangeListener(VetoableChangeListener listener) {
            vetoableChangeSupport.addVetoableChangeListener(listener);
        }
    }

    protected final AglaisZMS zms;
    private Future<?> onShowFuture;
    private Future<?> onHideFuture;

    /**
     * Creates a new tab instance
     *
     * @param zms The application instance
     */
    public Tab(AglaisZMS zms) {
        this.zms = zms;
    }

    /**
     * Gets the tab's internal name.
     * A matching tab_<name> key must exist in the {@link ResourceBundle} unless {@link #isNameTranslatable} returns false.
     * The default implementation converts the class name to lowercase snake case and cuts off "Tab" suffix.
     *
     * @return The tab's name
     */
    public String getName() {
        return this.getClass().getSimpleName().replaceFirst("(?i)tab$", "")
                .replaceAll("([a-z])([A-Z])", "$1_$2")
                .replaceAll("__+", "_")
                .toLowerCase();
    }

    /**
     * Check if this tab's name should be translated using {@link ResourceBundle#getString} using tab_<name>.
     *
     * @return True if the name should be translated, false if it should be used as-is
     */
    public boolean isNameTranslatable() {
        return true;
    }

    /**
     * Called when this tab becomes visible/selected
     * <p>
     * Do not call this directly, use {@link #show}
     */
    public void onShow() {
    }

    /**
     * Called from a background thread when this tab becomes visible/selected
     * <p>
     * Do not call this directly, use {@link #show}
     */
    public void onShowAsync() {
    }

    /**
     * Trigger the tab's {@link #onShow} and {@link #onShowAsync}
     */
    public final void show() {
        SwingUtilities.invokeLater(this::onShow);
        if (onShowFuture != null)
            onShowFuture.cancel(true);
        onShowFuture = zms.getBackgroundExecutor().submit(this::onShowAsync);
    }

    /**
     * Called when this tab is no longer visible
     * <p>
     * Do not call this directly, use {@link #hide}
     */
    public void onHide() {
    }

    /**
     * Called from a background thread when this tab is no longer visible
     * <p>
     * Do not call this directly, use {@link Tab#hide}
     */
    public void onHideAsync() {
    }

    /**
     * Call the tab's {@link #onHide} and {@link #onHideAsync}
     */
    public final void hide() {
        SwingUtilities.invokeLater(this::onHide);
        if (onHideFuture != null)
            onHideFuture.cancel(true);
        onHideFuture = zms.getBackgroundExecutor().submit(this::onHideAsync);
    }

    /**
     * Retrieves the content pane associated with this tab.
     *
     * @return The JComponent representing the main content area of the tab, must not be null
     */
    public abstract @NotNull JComponent getContentPane();

    /**
     * Checks if there are any unsaved changes that need to be handled before proceeding
     *
     * @return true if changes are saved and code can proceed, false if the user pressed cancel
     * and the operation should be aborted
     */
    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public final boolean ensureNoUnsavedChanges() {
        if (this instanceof ISavableTab tab) {
            InterpolatingResourceBundle uiBundle = (InterpolatingResourceBundle) ResourceBundle.getBundle("ui");

            if (tab.hasUnsavedChanges()) {
                int result = JOptionPane.showConfirmDialog(getContentPane(),
                        uiBundle.getString("confirm_unsaved_changes"),
                        uiBundle.getString("dialog_confirm_unsaved_changes_title"),
                        JOptionPane.YES_NO_CANCEL_OPTION);

                if (result == JOptionPane.CANCEL_OPTION)
                    return false;

                if (result == JOptionPane.YES_OPTION) {
                    if (!saveChangesWithUI()) {
                        tab.discardChanges();
                        return false;
                    }
                } else
                    tab.discardChanges();
            }

            return true;
        }

        return true;
    }

    /**
     * Attempts to save any pending changes in the tab with user interaction in case of failure.
     * If the current instance implements {@code ISavableTab}, invokes its {@code saveChanges} method and
     * displays an error message to the user via a dialog if the save operation fails.
     * This uses the localized strings from the "ui" resource bundle to display error messages.
     *
     * @return true if changes were successfully saved or if the current instance does not implement {@code ISavableTab};
     * false if the save operation failed and an error message was displayed.
     */
    public final boolean saveChangesWithUI() {
        if (this instanceof ISavableTab tab) {
            InterpolatingResourceBundle uiBundle = (InterpolatingResourceBundle) ResourceBundle.getBundle("ui");

            if (!tab.saveChanges()) {
                JOptionPane.showMessageDialog(getContentPane(), uiBundle.getString("error_save_failed",
                                tab.getSaveTitle()), uiBundle.getString("error"),
                        JOptionPane.ERROR_MESSAGE);

                return false;
            }
        }

        return true;
    }

    /**
     * Registers multiple tabs and sets up a change listener to handle tab selection
     *
     * @param tabbedPane The JTabbedPane to register tabs in
     * @param bundle     ResourceBundle containing localized strings
     * @param tabs       The Tab instances to register
     */
    public static void registerAll(JTabbedPane tabbedPane, ResourceBundle bundle, Tab... tabs) {
        VetoableSingleSelectionModel selectionModel = new VetoableSingleSelectionModel();

        tabbedPane.setModel(selectionModel);
        selectionModel.addVetoableChangeListener(changeEvent -> {
            int index = (int) changeEvent.getNewValue();
            int oldIndex = (int) changeEvent.getOldValue();

            if (oldIndex >= 0 && oldIndex < tabs.length) {
                if (!tabs[oldIndex].ensureNoUnsavedChanges()) {
                    tabbedPane.setSelectedIndex(oldIndex);
                    throw new PropertyVetoException("Tab changes not saved", changeEvent);
                }
                tabs[oldIndex].hide();
            }

            if (index >= 0 && index < tabs.length) {
                tabs[index].show();
            }
        });

        for (Tab tab : tabs) {
            String tabName = tab.isNameTranslatable() ? bundle.getString("tab_" + tab.getName()) : tab.getName();

            tabbedPane.addTab(tabName, tab.getContentPane());
        }
    }

}
