/*
 *
 *  Copyright (C) 2024 Andrew Gegg
 *
 * 	This file is part of the Gardeners Notebook application
 *
 *  The Gardeners Notebook application is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU 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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/gpl.html>.
 *
 *
 */

 /*
    Change log
    3.2.1   Support selection by Location
 */

package uk.co.gardennotebook;

import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.SVGPath;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
 import uk.co.gardennotebook.fxbean.*;
import uk.co.gardennotebook.spi.GNDBException;
import uk.co.gardennotebook.util.StoryLineTree;

import java.io.IOException;
import java.time.LocalDate;
import java.time.Month;
import java.time.format.TextStyle;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
 *	Controller class for Lifecycle Analysis
 *
 *	@author Andy Gegg
 *	@version	3.2.1
 *	@since	3.2.0
 */
final public class LifecycleAnalysisTab extends GridPane implements INotebookLoadable
{
    private static final Logger LOGGER = LogManager.getLogger();

    private final static double SCALE = 3;
    private final static double ICONSIZE = 20;
    private final static double V_PAD = 5;
    private final static double H_PAD = 5;
    private double textHeight;
    private double yearTextWidth;

    List<LifecycleAnalysisBean> analysisBeans = Collections.EMPTY_LIST;
    List<List<LifecycleAnalysisBean>> splitBeans;   //Better as a sequenced map in Java 21

    @FXML
    private PlantSpeciesCombo parentPlantSpecies;
    @FXML
    private PlantVarietyCombo parentPlantVariety;
    @FXML
    private SafeDatePicker dtpFrom;
    @FXML
    private SafeDatePicker dtpTo;
    @FXML
    private LocationCombo parentLocation;
    @FXML
    private GridPane subPane;
    @FXML
    private ScrollPane viewPort;
    @FXML
    private ScrollPane columnHeadersPane;
    @FXML
    private ScrollPane rowHeadersPane;

    @FXML
    private ResourceBundle resources;

    private int[] indexedMonths;
    private int[] requiredYears;

    private PlantSpeciesBean plantSpeciesBean = null;
    private PlantVarietyBean plantVarietyBean = null;
    private LocationBean locationBean = null;

    private Group colGroup;

    private Consumer<Node> loadSplit;
    private Consumer<Node> clearSplit;
    private BiConsumer<String, Node> loadTab;
    private Consumer<Node> clearTab;

    private static final SVGPath afflictionGraphic = DiaryIcons.afflictionGraphic;
    private static final SVGPath groundworkGraphic = DiaryIcons.groundworkGraphic;
    private static final SVGPath husbandryGraphic = DiaryIcons.husbandryGraphic;

    LifecycleAnalysisTab(PlantSpeciesBean plantSpecies, PlantVarietyBean plantVariety, LocationBean location)
    {
        LOGGER.traceEntry("LifecycleAnalysisTab constructor: plantSpecies: {}, plantVariety: {}, location:{}", plantSpecies, plantVariety, location);

        plantSpeciesBean = plantSpecies;
        plantVarietyBean = plantVariety;
        locationBean = location;

        loadGUI();
    }

    private void loadGUI()
    {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/LifecycleAnalysisTab.fxml"),
                ResourceBundle.getBundle("notebook"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);
        try
        {
            fxmlLoader.load();    // NB initialize is called from in here
        } catch (IOException exception)
        {
            throw new RuntimeException(exception);
        }
    }

    @FXML
    private void initialize()
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("initialize()");

        if (plantSpeciesBean != null)
        {
            parentPlantSpecies.setValue(plantSpeciesBean);
        }
        if (plantVarietyBean != null)
        {
            if (plantSpeciesBean == null)
            {
                parentPlantSpecies.setValue(plantVarietyBean.getPlantSpecies());

            }
            parentPlantVariety.setValue(plantVarietyBean);
        }
//        }

        if (locationBean != null)
        {
            parentLocation.setValue(locationBean);
        }

        loadData();

        parentPlantVariety.plantSpeciesProperty().bind(parentPlantSpecies.valueProperty());

        setupDataDisplay();
    }

    /*
     * The events passed in are in a single list, ordered by ancestorId.  We need them in
     * separate lists for each ancestor.
     * NB any events which are NOT in a storyline - i.e. stand-alone events (perhaps a sowing which failed and was never
     * recorded as failed) - have no ancestor and no entries in StoryLine so ancestorId is 0 or -1 and ancestorType is null.
     */
    private void splitEvents()
    {
        splitBeans = new ArrayList<>();
        LifecycleAnalysisBean root = analysisBeans.get(0);
        List<LifecycleAnalysisBean> subList = new ArrayList<>();
        subList.add(root);
        splitBeans.add(subList);
        for (int ix = 1; ix < analysisBeans.size();  ix++)
        {
            LOGGER.debug("outer loop: ix: {}", ix);
            var item = analysisBeans.get(ix);
//            LOGGER.debug("item: {}", item);
            if (root.hasSameAncestor(item))
            {
//                LOGGER.debug("adding item to current sublist");
                subList.add(item);
                continue;
            }
            LOGGER.debug("new subList: previous list: {}", subList);
            root = item;
            subList = new ArrayList<>();
            splitBeans.add(subList);
            subList.add(item);
        }

        //  The list returned from the database is necessarily sorted with ancestorId as primary key.  These will normally
        //  be in ascending date order, but only by accident.  Therefore, sort on the date of the sublist header to ensure
        //  increasing date sequence.
        splitBeans.sort(Comparator.comparing(l -> l.get(0).getDate()));
        LOGGER.debug("last subList: {}", subList);
    }

    private void loadData()
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("loadData(): plantSpeciesBean: {}, plantVarietyBean: {}, locationBean: {}", plantSpeciesBean, plantVarietyBean, locationBean);

        try
        {
            analysisBeans = LifecycleAnalysisBean.fetch(plantSpeciesBean, plantVarietyBean, locationBean, dtpFrom.getValue(), dtpTo.getValue());
        }
        catch (GNDBException ex)
        {
            LOGGER.debug("/n+++++++++++++++++++++++++++++++++++++++++++++++\nGNDBException thrown!\n+++++++++++++++++++++++++++++++++++++++++");
            PanicHandler.panic(ex);
        }
        if (!analysisBeans.isEmpty())
        {
            splitEvents();
        }

    }

    private void setupDataDisplay()
    {
        if (analysisBeans.isEmpty())
        {
//            viewPort.setContent(new Label("No data to display!"));
            viewPort.setContent(new Label(resources.getString("text.lifecycle.nodata")));
            columnHeadersPane.setContent(null);
            rowHeadersPane.setContent(null);
            return;
        }

        Text text = new Text("X");
        textHeight = Math.max(text.getLayoutBounds().getHeight(), ICONSIZE);
        text.setText(Integer.toString(LocalDate.now().getYear()));
        yearTextWidth = Math.max(text.getLayoutBounds().getWidth(), 30);

        buildIndexedMonths();

        ColumnHeaders ch = new ColumnHeaders(indexedMonths);
        columnHeadersPane.setContent(ch);

        buildYears();

        RowHeaders rh = new RowHeaders(requiredYears);
        rowHeadersPane.setMinWidth(yearTextWidth);
        rowHeadersPane.setContent(rh);

        DataArea dataArea = new DataArea(splitBeans, ch, rh);
        viewPort.setContent(dataArea);

        columnHeadersPane.hvalueProperty().bindBidirectional(viewPort.hvalueProperty());
        rowHeadersPane.vvalueProperty().bindBidirectional(viewPort.vvalueProperty());

    }

    private void buildIndexedMonths()
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("buildIndexedMonths()");

        int minMonth = 99999;
        int maxMonth = -1;
        for (var outer : splitBeans)
        {
            int yearBase = outer.get(0).getYear();
            int yearIndex = 0;
            for (var inner : outer)
            {
                int thisYear = inner.getYear();
                if (thisYear > yearBase)
                {
                    yearIndex += 12;
                    yearBase = thisYear;
                }
                int month = inner.getMonthNumber() + yearIndex;
                inner.setIndexedMonth(month);
                minMonth = Math.min(month, minMonth);
                maxMonth = Math.max(month, maxMonth);
            }
        }
        LOGGER.debug("minMonth: {}, maxMonth: {}", minMonth, maxMonth);

        indexedMonths = new int[maxMonth - minMonth + 1];
        for (int jx = 0, ix = minMonth; ix <= maxMonth; ix++, jx++)
        {
            indexedMonths[jx] = ix;
        }
        LOGGER.debug("indexedMonths: {}", indexedMonths);

    }

    private void buildYears()
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("buildYears()");

        requiredYears = new int[splitBeans.size()];

        int ix = 0;
        for (var outer : splitBeans)
        {
            int yearBase = outer.get(0).getYear();
            requiredYears[ix++] = yearBase;
        }
        LOGGER.debug("requiredYears: {}", requiredYears);
    }

    @Override
    public void clearUpOnClose(Event event)
    {
        INotebookLoadable.super.clearUpOnClose(event);
    }

    @Override
    public void setLoadSplit(Consumer<Node> code)
    {
        loadSplit = code;
    }

    @Override
    public void setClearSplit(Consumer<Node> code)
    {
        clearSplit = code;
    }

    @Override
    public void setLoadTab(BiConsumer<String, Node> code)
    {
        loadTab = code;
    }

    @Override
    public void setClearTab(Consumer<Node> code)
    {
        clearTab = code;
    }

    private  Node setGraphicForItem(SVGPath shape, String colour)
    {
        Region svgRegion = new Region();
        svgRegion.setShape(shape);
        svgRegion.setMinSize(ICONSIZE, ICONSIZE);
        svgRegion.setMaxSize(ICONSIZE, ICONSIZE);
        svgRegion.setPrefSize(ICONSIZE, ICONSIZE);
        svgRegion.setStyle("-fx-background-color: "+colour+";");
        return svgRegion;
//        SVGPath copy = new SVGPath();
//        copy.setContent(shape.getContent());
//        copy.setFill(Color.web(colour));
//        // thanks to https://stackoverflow.com/questions/38953921/how-to-set-the-size-of-a-svgpath-in-javafx ItachiUchiha
//        double originalWidth = shape.prefWidth(-1);
//        double originalHeight = shape.prefHeight(originalWidth);
//        //  Scaling happens about the centre of the node.  Layout puts the left of the SVGPath in the right place
//        //  but the size for the graphic added to the label (at layout)is taken as the original size, not the scaled size
//        //  so the svg has to be moved left to the right place. The y co-ord is fine.
//        copy.setTranslateX(-(originalWidth - ICONSIZE)/2);
//        copy.setScaleX(ICONSIZE/originalWidth);
//        copy.setScaleY(ICONSIZE/originalHeight);
//        return copy;
    }

    private Node getItemGraphic(LifecycleAnalysisBean item)
    {
        return switch (item.getChildType())
        {
            case AFFLICTIONEVENT -> setGraphicForItem(afflictionGraphic, "red");
            case GROUNDWORK -> setGraphicForItem(groundworkGraphic, "black");
            case HUSBANDRY -> setGraphicForItem(husbandryGraphic, "green");
            default -> new Circle(10);
        };
    }

    @FXML
    private void btnFetchOnAction(ActionEvent event)
    {
        EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnFetchOnAction()");

        plantSpeciesBean = null;
        plantVarietyBean = null;
        locationBean = null;

        /*
         *  Emulate the behaviour of the plant catalogue tree - if a plant variety is selected, ignore the plant species
         *  and just use the PV - this is what you'd expect the behavior to be.
         *  If the PS is passed to the lister, events for ALL PVs for that PS are read in.
         */
        if (parentPlantVariety.valueProperty().isNotNull().get())
        {
            plantVarietyBean = parentPlantVariety.getValue();
        }
        else if (parentPlantSpecies.valueProperty().isNotNull().get())
        {
            plantSpeciesBean = parentPlantSpecies.getValue();
//            if (parentPlantVariety.valueProperty().isNotNull().get())
//            {
//                plantVarietyBean = parentPlantVariety.getValue();
//            }
        }

        if (parentLocation.valueProperty().isNotNull().get())
        {
            locationBean = parentLocation.getValue();
        }


        loadData();
        setupDataDisplay();
    }

    private static class ColumnHeaders extends Canvas
    {
        private final double[] colCoords;
        private final int[] storedIndices;

        ColumnHeaders(int[] indexedMonths)
        {
            EntryMessage log4jEntryMsg = LOGGER.traceEntry("ColumnHeaders: constructor: indexedMonths: {}", indexedMonths);

            storedIndices = Arrays.copyOf(indexedMonths, indexedMonths.length);
            LOGGER.debug("storedIndices: {}", storedIndices);

            GraphicsContext gc = this.getGraphicsContext2D();
            gc.getCanvas().setWidth(31*SCALE*indexedMonths.length+50);
            gc.getCanvas().setHeight(50);

            gc.setTextBaseline(VPos.TOP);
            gc.setTextAlign(TextAlignment.CENTER);

            colCoords = new double[indexedMonths.length];

            double offset = 5;
            int ix = 0;
            for (int mn : indexedMonths)
            {
                colCoords[ix++] = offset;
                int yearMonth = mn%12;
                if (yearMonth == 0) yearMonth = 12;    //  December%12 -> 0 which is not a valid month number
//                Month month = Month.of(mn%12);
                Month month = Month.of(yearMonth);
                int daysInMonth = month.maxLength();
                gc.fillText(month.getDisplayName(TextStyle.SHORT_STANDALONE, Locale.getDefault()), offset +(daysInMonth*SCALE/2.0), 2);
                gc.strokeLine(offset, 50, offset, 30);
                int week = 7;
                while (week < daysInMonth)
                {
                    gc.strokeLine((offset + SCALE * week), 50, (offset + SCALE * week), 40);
                    week += 7;
                }
                offset += daysInMonth * SCALE;
            }
            gc.strokeLine(offset, 50, offset, 30);
            LOGGER.debug("setting canvas width: {}", offset+SCALE);
            gc.getCanvas().setWidth(offset+SCALE);

            LOGGER.debug("colCoords: {}, width: {}", colCoords, getWidth());
        }

        double getOffsetForDate(LocalDate date, int indexedMonth)
        {
            EntryMessage log4jEntryMsg = LOGGER.traceEntry("getOffsetForDate(): indexedMonth: {}", indexedMonth);

            for (int ix = 0; ix < storedIndices.length; ix++)
            {
                if (storedIndices[ix] == indexedMonth)
                {
                    int day = date.getDayOfMonth();
                    return colCoords[ix] + SCALE*day;
                }
            }
            LOGGER.debug("Invalid indexedMonth!");
            throw new IllegalArgumentException("Invalid indexedMonth! " + indexedMonth);
//            return 99.0;
        }
    }

    private class RowHeaders extends Canvas
    {
        private final double[] rowCoords;

        RowHeaders(int[] requiredYears)
        {
            EntryMessage log4jEntryMsg = LOGGER.traceEntry("RowHeaders: constructor: requiredYears: {}", requiredYears);

            GraphicsContext gc = this.getGraphicsContext2D();

            gc.getCanvas().setWidth(yearTextWidth + 2*H_PAD);
            gc.getCanvas().setHeight(requiredYears.length * (textHeight + 2*V_PAD));

            gc.setTextBaseline(VPos.TOP);
            gc.setTextAlign(TextAlignment.LEFT);

            rowCoords = new double[requiredYears.length];

            double offset = 0.0;
            int ix = 0;
            for (int yr : requiredYears)
            {
                String year = Integer.toString(yr);
                offset += V_PAD;
                gc.fillText(year, H_PAD, offset);
                rowCoords[ix++] = offset;
                offset += textHeight + V_PAD;
            }
            gc.getCanvas().setHeight(offset);
        }

        double getOffsetForRow(int rowNo)
        {
            return rowCoords[rowNo];
        }

    }

    private class DataArea extends Region
    {
        private record DisplayItem(Node item, double x, double y){}
//        private record RowItem(/*Node rect, */Paint fill, double x, double y, double width, double height){}
        private final List<DisplayItem> displayItems = new ArrayList<>();
//        private final List<RowItem> rowItems = new ArrayList<>();

        DataArea(List<List<LifecycleAnalysisBean>> beans, ColumnHeaders ch, RowHeaders rh)
        {
            EntryMessage log4jEntryMsg = LOGGER.traceEntry("DataArea: constructor");

            setWidth(ch.getWidth());
            LOGGER.debug("setting width: {}, actual: {}", ch.getWidth(), getWidth());
            setHeight(rh.getHeight());

            // force DataArea to correct width
            Rectangle rect = new Rectangle(0, 0, ch.getWidth(), ICONSIZE);
            rect.setFill(Color.TRANSPARENT);
            getChildren().add(rect);

            for (int rowNo = 0; rowNo < beans.size(); rowNo++)
            {
                for (var inner : beans.get(rowNo))
                {
                    LOGGER.debug("adding item: {}", inner);
                    double x = ch.getOffsetForDate(inner.getDate(), inner.getIndexedMonth());
                    double y = rh.getOffsetForRow(rowNo);

                    Label itemLabel = new Label("", getItemGraphic(inner));
                    itemLabel.getStyleClass().add("notebook-analysis-icon");
                    if (inner.isTargetLocation())
                    {
                        itemLabel.setBorder(new Border(new BorderStroke(Paint.valueOf("brown"), BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(1))));
                    }
                    Tooltip.install(itemLabel, new LifecycleAnalysisTooltip(inner));
                    itemLabel.setContextMenu(getContextMenu(inner));
                    getChildren().add(itemLabel);

                    DisplayItem item = new DisplayItem(itemLabel, x, y);
                    displayItems.add(item);
                    LOGGER.debug("added item: {}", item);
                }
            }
        }

        protected void layoutChildren()
        {
            LOGGER.debug("DataArea: layoutChildren(): width: {}", getWidth());
            for (var item : displayItems)
            {
                layoutInArea(item.item, item.x, item.y, ICONSIZE, ICONSIZE, 0, HPos.CENTER, VPos.TOP);
            }
        }

        private ContextMenu getContextMenu(LifecycleAnalysisBean bean)
        {
            LOGGER.debug("getContextMenu(): bean: {}", bean);
            ContextMenu topMenu = new ContextMenu();
            MenuItem details = new MenuItem(resources.getString("menu.analysis.detail"));
            details.setOnAction(_ -> {
                LOGGER.debug("LifecycleAnalysis: context menu: details: bean: {}", bean);
                switch (bean.getChildType())
                {
                    case HUSBANDRY -> {
                        HusbandryEditor tabCon = new HusbandryEditor(bean.getHusbandry());
                        loadTab.accept(resources.getString("tab.husbandry"), tabCon);
                    }
                    case GROUNDWORK -> {
                        GroundworkEditor tabCon = new GroundworkEditor(bean.getGroundwork());
                        loadTab.accept(resources.getString("tab.groundwork"), tabCon);
                    }
                    case AFFLICTIONEVENT -> {
                        AfflictionEventEditor tabCon = new AfflictionEventEditor(bean.getAfflictionEvent());
                        loadTab.accept(resources.getString("tab.affliction"), tabCon);
                    }
                    default -> throw new RuntimeException("unknown item type:" + bean.getChildType());
                }
            });
            MenuItem prior = new MenuItem(resources.getString("menu.ancestors"));
            prior.setOnAction(_ -> {
                StoryLineTree<DiaryBean> history = null;
                try
                {
                    history = DiaryBean.from(bean.getChild()).getAncestors();
                }
                catch (GNDBException ex)
                {
                    throw new RuntimeException(ex);
                }
                StoryLineTab tabCon = new StoryLineTab();
                loadTab.accept(resources.getString("tab.ancestors"), tabCon);
                tabCon.setHistory(history);
                });

            MenuItem later = new MenuItem(resources.getString("menu.descendants"));
            later.setOnAction(_ -> {
                StoryLineTree<DiaryBean> history = null;
                try
                {
                    history = DiaryBean.from(bean.getChild()).getDescendants();
                }
                catch (GNDBException ex)
                {
                    throw new RuntimeException(ex);
                }
                StoryLineTab tabCon = new StoryLineTab();
                loadTab.accept(resources.getString("tab.descendants"), tabCon);
                tabCon.setHistory(history);
            });

            topMenu.getItems().setAll(details, prior, later);
            topMenu.setOnShowing(_ -> {
                prior.setDisable(!bean.hasAncestor());
                later.setDisable(!bean.hasDescendant());
            });

            return topMenu;
        }
    }

}
