/*
 * Copyright (C) 2025  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.species;

import org.aglais.zms.AglaisZMS;
import org.aglais.zms.db.core.model.AncestryData;
import org.aglais.zms.db.core.model.ModifiedStateTrackerList;
import org.aglais.zms.ui.component.*;
import org.aglais.zms.ui.component.text.NullableDateFormatter;
import org.aglais.zms.ui.dialog.MapCoordinateChooser;
import org.aglais.zms.ui.map.RadiusWaypoint;
import org.aglais.zms.ui.map.RadiusWaypointRenderer;
import org.aglais.zms.ui.tab.ISavableTab;
import org.aglais.zms.ui.tab.SubTab;
import org.aglais.zms.ui.tab.SpeciesTab;
import org.aglais.zms.ui.util.GeoURLImporter;
import org.aglais.zms.ui.util.MnemonicUtil;
import org.aglais.zms.util.IntUtil;
import org.jetbrains.annotations.NotNull;
import org.jxmapviewer.viewer.GeoPosition;
import org.jxmapviewer.viewer.WaypointPainter;

import javax.swing.*;
import javax.swing.text.DefaultFormatterFactory;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;

/**
 * Represents the Species Ancestry tab in the application.
 * This tab allows users to visually manage and interact with the ancestry data of species,
 * including adding, deleting, and selecting nodes within the ancestry diagram.
 * <p>
 * The class extends {@link SubTab} and provides a user interface to handle
 * ancestry-related data operations, reflecting changes dynamically on the diagram widget.
 */
public class SpeciesAncestryTab extends SubTab<SpeciesTab> implements ISavableTab {

    private static final String DATE_FORMAT = "dd.MM.yyyy";

    private JPanel contentPane;
    private AncestryDiagram ancestryDiagram;
    private JButton saveButton;
    private JButton addNodeButton;
    private DangerButton deleteNodeButton;
    private JSplitPane splitPane;
    private JTabbedPane ancestryDataDetailTabbedPane;
    private JSpinner dateStartSpinner;
    private JSpinner dateEndSpinner;
    private JTextField generationTextField;
    private JTextArea notesTextArea;
    private MapPanel originMap;
    private JButton chooseOriginButton;
    private DangerButton clearOriginButton;
    private JPanel originPane;
    private JButton importOriginURLButton;
    private final List<AncestryData> selectedData = new ArrayList<>();
    private ModifiedStateTrackerList<AncestryData> modifiedStateTracker;
    private final WaypointPainter<RadiusWaypoint> waypointPainter = new WaypointPainter<org.aglais.zms.ui.map.RadiusWaypoint>();

    /**
     * Creates a new species ancestry tab.
     *
     * @param zms    The main application instance
     * @param parent The parent species tab
     */
    public SpeciesAncestryTab(AglaisZMS zms, SpeciesTab parent) {
        super(zms, parent);

        addNodeButton.addActionListener((event) -> {
            int temporaryId = Math.abs((int) System.currentTimeMillis()) * -1;
            List<Integer> parents = new ArrayList<>();
            for (AncestryData selected : selectedData)
                parents.add(selected.getID());
            List<AncestryData> data = modifiedStateTracker.getData();
            AncestryData newData = new AncestryData(temporaryId, null, null,
                    "", "", IntUtil.toArray(parents), null, null, null);
            data.add(newData);
            updateDiagram();
            updateModifiedState();
            updateSelectedState();
        });
        deleteNodeButton.addActionListener((event) -> {
            List<AncestryData> data = modifiedStateTracker.getData();
            for (AncestryData selected : selectedData) {
                for (AncestryData ancestor : data) {
                    List<Integer> parents = IntUtil.toList(ancestor.getParents());

                    if (parents.contains(selected.getID())) {
                        parents.remove(Integer.valueOf(selected.getID()));
                        parents.addAll(IntUtil.toList(selected.getParents()));
                        ancestor.setParents(IntUtil.toArray(parents));
                    }
                }
            }
            data.removeAll(selectedData);
            selectedData.clear();
            updateDiagram();
            updateModifiedState();
            updateSelectedState();
        });
        saveButton.addActionListener((event) -> saveChangesWithUI());

        // Editable text fields
        dateStartSpinner.setModel(new NullableSpinnerDateModel());
        JSpinner.DateEditor dateStartEditor = new JSpinner.DateEditor(dateStartSpinner, DATE_FORMAT);
        dateStartEditor.getTextField().setFormatterFactory(new DefaultFormatterFactory(
                new NullableDateFormatter(dateStartEditor.getFormat())
        ));
        dateStartSpinner.setEditor(dateStartEditor);
        dateStartSpinner.addChangeListener((event) -> {
            AncestryData selected = selectedData.getFirst();
            selected.setDateStart((Date) dateStartSpinner.getValue());

            updateModifiedState();
            updateDiagram();
        });

        dateEndSpinner.setModel(new NullableSpinnerDateModel());
        JSpinner.DateEditor dateEndEditor = new JSpinner.DateEditor(dateEndSpinner, DATE_FORMAT);
        dateEndEditor.getTextField().setFormatterFactory(new DefaultFormatterFactory(
                new NullableDateFormatter(dateEndEditor.getFormat())
        ));
        dateEndSpinner.setEditor(dateEndEditor);
        dateEndSpinner.addChangeListener((event) -> {
            AncestryData selected = selectedData.getFirst();
            selected.setDateEnd((Date) dateEndSpinner.getValue());

            updateModifiedState();
            updateDiagram();
        });

        generationTextField.addCaretListener((event) -> {
            AncestryData selected = selectedData.getFirst();
            selected.setGeneration(generationTextField.getText());

            updateModifiedState();
            updateDiagram();
        });

        notesTextArea.addCaretListener((event) -> {
            AncestryData selected = selectedData.getFirst();
            selected.setNotes(notesTextArea.getText());

            updateModifiedState();
            updateDiagram();
        });

        waypointPainter.setRenderer(new RadiusWaypointRenderer());
        originMap.addOverlayPainter(waypointPainter);
        chooseOriginButton.addActionListener((event) -> {
            MapCoordinateChooser mapCoordinateChooser = new MapCoordinateChooser(getContentPane());

            AncestryData selected = selectedData.getFirst();
            Integer radius = selected.getOriginRadius();
            GeoPosition selectedPosition = null;
            if (selected.getOriginLatitude() != null && selected.getOriginLongitude() != null)
                selectedPosition = new GeoPosition(selected.getOriginLatitude(), selected.getOriginLongitude());

            mapCoordinateChooser.setSelectedPosition(selectedPosition, radius != null ? radius : 0);
            mapCoordinateChooser.zoomToSelectedPosition();

            int result = mapCoordinateChooser.showDialog(contentPane);
            if (result == MapCoordinateChooser.APPROVE_OPTION) {
                selectedPosition = mapCoordinateChooser.getSelectedPosition();

                selected.setOriginLatitude(selectedPosition.getLatitude());
                selected.setOriginLongitude(selectedPosition.getLongitude());
                selected.setOriginRadius(mapCoordinateChooser.getSelectedRadius());
                updateModifiedState();
                updateOriginMap();
            }
        });
        clearOriginButton.addActionListener((event) -> {
            AncestryData selected = selectedData.getFirst();

            selected.setOriginLatitude(null);
            selected.setOriginLongitude(null);
            selected.setOriginRadius(null);
            updateModifiedState();
            updateOriginMap();
        });
        importOriginURLButton.addActionListener((event) -> {
            ResourceBundle uiBundle = ResourceBundle.getBundle("ui");
            String url = JOptionPane.showInputDialog(contentPane, null,
                    uiBundle.getString("import_origin_from_url"), JOptionPane.PLAIN_MESSAGE);
            if (url != null && !url.trim().isEmpty()) {
                GeoPosition position = GeoURLImporter.importFrom(url);

                if (position != null) {
                    AncestryData selected = selectedData.getFirst();

                    selected.setOriginLatitude(position.getLatitude());
                    selected.setOriginLongitude(position.getLongitude());
                    selected.setOriginRadius(null);
                    updateModifiedState();
                    updateOriginMap();
                } else {
                    JOptionPane.showMessageDialog(contentPane, uiBundle.getString("import_origin_from_url_failed"),
                            uiBundle.getString("error"), JOptionPane.ERROR_MESSAGE);
                }
            }
        });
    }

    /**
     * Updates the enabled state of the save button based on whether the species ancestry has been modified.
     */
    private void updateModifiedState() {
        saveButton.setEnabled(modifiedStateTracker.isModified());
    }

    /**
     * Updates the origin map by creating and setting waypoints based on the selected ancestry data.
     * For each selected data, it retrieves the origin latitude, longitude, and radius, and creates a
     * RadiusWaypoint if the latitude and longitude are available. The radius defaults to 0 if not specified.
     * Finally, it updates the waypoint painter with the new waypoints and repaints the origin map.
     */
    private void updateOriginMap() {
        HashSet<RadiusWaypoint> waypoints = new HashSet<>();

        for (AncestryData selected : selectedData) {
            Integer radius = selected.getOriginRadius();
            GeoPosition selectedPosition = null;
            if (selected.getOriginLatitude() != null && selected.getOriginLongitude() != null)
                selectedPosition = new GeoPosition(selected.getOriginLatitude(), selected.getOriginLongitude());

            if (selectedPosition != null)
                waypoints.add(new RadiusWaypoint(selectedPosition, radius != null ? radius : 0));
        }

        waypointPainter.setWaypoints(waypoints);
        originMap.repaint();
        originMap.zoomTo(waypoints.toArray(new RadiusWaypoint[0]));
    }

    /**
     * Updates the enabled state of the "delete node" button based on the current selection.
     * The button is enabled only when there is a selected data node, and it exists in the list of ancestry data.
     */
    private void updateSelectedState() {
        ResourceBundle uiBundle = ResourceBundle.getBundle("ui");
        boolean selected = !selectedData.isEmpty();

        deleteNodeButton.setEnabled(selected);
        MnemonicUtil.loadButtonText(addNodeButton, selected ? uiBundle.getString("button_add_generation")
                : uiBundle.getString("button_add_population"));

        // Show the detail pane only if exactly one data node is selected
        ancestryDataDetailTabbedPane.setVisible(selectedData.size() == 1);
        if (ancestryDataDetailTabbedPane.isVisible()) {
            AncestryData data = selectedData.getFirst();
            dateStartSpinner.setValue(data.getDateStart());
            dateEndSpinner.setValue(data.getDateEnd());
            generationTextField.setText(data.getGeneration());
            notesTextArea.setText(data.getNotes());

            if (data.getParents().length == 0) {
                ancestryDataDetailTabbedPane.add(originPane, uiBundle.getString("origin"));
                updateOriginMap();
            } else {
                ancestryDataDetailTabbedPane.remove(originPane);
            }
        }
    }

    /**
     * Updates the ancestry diagram with the current data.
     * Converts the list of {@link AncestryData} objects into an array and sets it
     * to the ancestry diagram to refresh its displayed content.
     */
    private void updateDiagram() {
        List<AncestryData> data = modifiedStateTracker.getData();
        ancestryDiagram.setData(data.toArray(new AncestryData[0]));
        if (!selectedData.isEmpty()) {
            for (AncestryDiagramPoint point : ancestryDiagram.getPoints()) {
                point.setSelected(selectedData.contains(point.getData()));
            }
        }
    }

    @Override
    public void onShow() {
        contentPane.getRootPane().setDefaultButton(saveButton);
        splitPane.setDividerLocation(0.5);

        modifiedStateTracker = new ModifiedStateTrackerList<>(zms.getClient()
                .getSpeciesAncestry(parent.getSelectedSpecies()));

        selectedData.clear();
        updateDiagram();
        ancestryDiagram.setPointMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                AncestryDiagramPoint point = (AncestryDiagramPoint) e.getSource();
                boolean isSelected = selectedData.contains(point.getData());

                // Only clear the previous selection if neither shift nor ctrl is pressed.
                if ((e.getModifiersEx() & (MouseEvent.SHIFT_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK)) == 0) {
                    selectedData.clear();

                    for (AncestryDiagramPoint p : ancestryDiagram.getPoints())
                        p.setSelected(false);
                }

                if (isSelected) {
                    selectedData.remove(point.getData());
                    point.setSelected(false);
                } else {
                    selectedData.add(point.getData());
                    point.setSelected(true);
                }

                updateSelectedState();
            }
        });
        updateModifiedState();
        updateSelectedState();
    }

    @Override
    public @NotNull JComponent getContentPane() {
        return contentPane;
    }

    @Override
    public String getSaveTitle() {
        return parent.getSelectedSpecies().getScientificName();
    }

    @Override
    public boolean hasUnsavedChanges() {
        return modifiedStateTracker.isModified();
    }

    @Override
    public boolean saveChanges() {
        List<AncestryData> data = modifiedStateTracker.getData();

        if (zms.getClient().updateSpeciesAncestry(parent.getSelectedSpecies(), data)) {
            modifiedStateTracker.applyChanges();
            updateModifiedState();

            return true;
        }

        return false;
    }

    @Override
    public void discardChanges() {
        modifiedStateTracker.discardChanges();
    }
}