/* ----------------------------------------
 * Origin  : GFI INFORMATIQUE 
 * Project : CDPP Propagation Tool 
 * ----------------------------------------
 * History 
 * Version : 01/05/2015 : SCA : V2.1
 * Version : 07/04/2016 : SCA : V2.2 (PSWS-H2020)
 *  Add comets
 *  Move Radial End computation on server side (WS)  
 * Version : 12/09/2023 : LBE : V2.8
 * DM-ID : Redmine#10629 : 12/09/2023 : SEP plot improvments
 * Version : 05/08/2024 : LBE : V2.11
 * DM-ID : Redmine#l0763 : 05/08/2024 : PFSS grid lines
 * Version : V2.13
 * DM-ID : redmine#12279 : 10/09/2025 : LBE : Add field lines in 3d panel
 * ----------------------------------------
 */
package fr.tlse.irap.pt.client.controller.tools.radial;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.tlse.irap.pt.client.component.CustomComponent;
import fr.tlse.irap.pt.client.component.ICustomComponent;
import fr.tlse.irap.pt.client.controller.RootController;
import fr.tlse.irap.pt.client.controller.common.FlareCollection;
import fr.tlse.irap.pt.client.controller.common.SpaceElementCollection;
import fr.tlse.irap.pt.client.controller.dialogbox.DialogBox;
import fr.tlse.irap.pt.client.controller.drawingPlane.DrawHelper.DrawingCmeType;
import fr.tlse.irap.pt.client.controller.eclipticPlane.EclipticPlaneController;
import fr.tlse.irap.pt.client.controller.timestamp.TimestampController;
import fr.tlse.irap.pt.client.controller.tools.IToolController.JMapTool;
import fr.tlse.irap.pt.client.controller.tools.IToolController.Tool;
import fr.tlse.irap.pt.client.controller.tools.carrington.CarringtonDashboardController;
import fr.tlse.irap.pt.client.controller.tools.plot.FluxPlotComponent;
import fr.tlse.irap.pt.client.controller.tools.plot.SpectroPlotComponent;
import fr.tlse.irap.pt.client.controller.tools.plot.VPlotComponent;
import fr.tlse.irap.spaceElement.SpaceElement;
import fr.tlse.irap.spaceElement.Sun;
import fr.tlse.irap.tool.radial.CMEType;
import fr.tlse.irap.tool.radial.Radial;
import fr.tlse.irap.tool.radial.RadialEnd;
import fr.tlse.irap.utils.CoordConverter;
import fr.tlse.irap.utils.ToolHelper;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.scene.Cursor;
import javafx.scene.layout.GridPane;

public class RadialDashboardController extends GridPane implements ICustomComponent {

    protected static final Logger log = LoggerFactory.getLogger(RadialDashboardController.class);

    private BooleanProperty hasChanged = new SimpleBooleanProperty();
    private ObjectProperty<Date> startDate;

    public Radial radial = new Radial();
    public RadialEnd radialEnd = new RadialEnd();

    @FXML
    private RadialDashboardController rootRadialDashboard;
    @FXML
    protected CarringtonDashboardController carringtonDashboard;
    @FXML
    protected VPlotComponent vPlotComponent;
    @FXML
    protected FluxPlotComponent fluxPlotComponent;
    @FXML
    protected SpectroPlotComponent spectroPlotComponent;
    @FXML
    public RadialStartEndController radialStartEndController;

    protected EclipticPlaneController eclipticPlaneComponent;
    protected TimestampController timestampComponent;

    private int NB_STEP = 100;
    private Date resultDate = new Date();
    private boolean initDefaultView = true;

    public RadialDashboardController() {
        super();
        CustomComponent.initCustomComponent(this);

        // Change start field listener
        radialStartEndController.getHasChanged().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observedBoolean, Boolean oldValue, Boolean newValue) {
                if (newValue) {
                    // update Radial model with start fields model
                    radialStartEndController.getStartModel().update(radial);
                    // update carrington view
                    updateCarringtonFromRadial();
                    // notify this change
                    notifyChanged();
                }
            }
        });

        // Change Carrington CME listener
        carringtonDashboard.getHasChanged().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observedBoolean, Boolean oldValue, Boolean newValue) {
                if (!newValue) {
                    // update Carrington CME 
                    carringtonDashboard.updateCME();

                    if (carringtonDashboard.isValidCME()) {
                        // update Radial Model
                        updateRadialFromCarrington();
                        // update start fields
                        updateStartView();
                    }
                }
            }
        });
        
        // Change Carrington GridFieldlines listener
        carringtonDashboard.getHasGridFieldLinesChanged().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observedBoolean, Boolean oldValue, Boolean newValue) {
                
                log.debug("************************ CarringtonDashBoard Grid Field lines changed old="+oldValue+" new="+newValue);
                
                if (newValue) {
                    log.debug("***CarringtonDashBoard Grid Field lines changed");
                    eclipticPlaneComponent.updateGridFieldLinesWith(
                            carringtonDashboard.getGridFieldLines().getFieldLines(),
                            carringtonDashboard.getGridFieldLines().getFlTimes()
                    );
                }
            }
        });

        // Change VPlot listener
        vPlotComponent.getHasChanged().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observedBoolean, Boolean oldValue, Boolean newValue) {
                if (!newValue) {
                    if (vPlotComponent.getVPlotModel().isValid()) {
                        // update Radial Model from Vplot
                        log.debug("===> VPLOT set radial speed solar wind: "+vPlotComponent.getVPlotModel().getSpeed());
                        radial.setSpeedSolarWind(vPlotComponent.getVPlotModel().getSpeed());
                        // update start date 
                        startDate.set(vPlotComponent.getVPlotModel().getDate());
                    } else {
                        if (radial.isInSitu()) {
                            //log.debug("Set default speed");
                            radial.setSpeedSolarWind(Radial.getDEFAULT_SPEED());
                        }
                    }
                    //log.debug("Init Radial Start");
                    initRadialStart();
                }
            }
        });
        // Change SpectroPlot listener
        spectroPlotComponent.getHasChanged().addListener(
                new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observedBoolean, Boolean oldValue, Boolean newValue) {
                if (!newValue) {
                    log.debug("SpectroPlot time changed!");
                    if (spectroPlotComponent.getSpectroPlotModel().isValid()) {
                        // update start date 
                        startDate.set(spectroPlotComponent.getSpectroPlotModel().getDate());
                    }
                    log.debug("Init Radial Start");
                    initRadialStart();
                }
            }
        });
    }

    /**
     * Dynamic component initialization
     */
    public void initComponentWith(EclipticPlaneController eclipticPlaneComponent, TimestampController timestampComponent) {

        if (this.startDate == null) {
            this.timestampComponent = timestampComponent;
            this.eclipticPlaneComponent = eclipticPlaneComponent;
            // Init dynamic part of components
            timestampComponent.initTimeScrollBar(0, NB_STEP);
            carringtonDashboard.initComponent(DrawingCmeType.CME);
            if(radialEnd!=null) {
                carringtonDashboard.setEndTime(radialEnd.getEndTime().getTime());
            }
            // bind start date properties
            bindStartDate();
            // bind start/end properties
            bindStartEndSource();
            // bind time slider properties
            this.timestampComponent.getResultTScrollBar().valueProperty().addListener(new ChangeListener<Number>() {
                @Override
                public void changed(ObservableValue<? extends Number> arg0, Number oldValue, Number newValue) {
                    //log.debug("Time slider has changed => new step = " + newValue.intValue());
                    updateResultTime(newValue.intValue());
                    carringtonDashboard.setCurrentTime(resultDate);
                    eclipticPlaneComponent.setCurrentTime(resultDate);
                }
            });
        }
    }

    /**
     * Bind start date Listen to start date changes
     */
    private void bindStartDate() {
        // bind Radial start time to startDate
        radialStartEndController.getStartModel().getStartTime().bind(timestampComponent.getStartTimeProperty());
        // bind Radial current date to timestamp selected date
        this.startDate = timestampComponent.selectedDateProperty();
        if (startDate.get() != null) {
            // Send startDate to controller which need it
            carringtonDashboard.setStartDate(startDate);
            vPlotComponent.setStartDate(startDate);
            fluxPlotComponent.setStartDate(startDate);
            spectroPlotComponent.setStartDate(startDate);
        }
    }

    /**
     * Bind start/end source Listen to start source changes
     */
    private void bindStartEndSource() {
        // Set startSource to controller which need it
        vPlotComponent.setStartSource(radialStartEndController.getStartModel().getStartSource());
        spectroPlotComponent.setStartSource(radialStartEndController.getStartModel().getStartSource());
        // Set endSource to controller which need it
        carringtonDashboard.setEndSource(radialStartEndController.getEndSource(), radialStartEndController.getEndVisible());
        // init start/end source menu
        radialStartEndController.loadMenu();
        // Listen for space elements list change
        SpaceElementCollection.getHasChanged().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observedBoolean, Boolean oldValue, Boolean newValue) {
                if (!newValue) {
                    // init start/end source menu
                    radialStartEndController.loadMenu();
                    // test if start and end elements are in the menu (probe/comet)
//					radialStartEndController.reset
                    initDefaultView = false;
                    // notify change for refreshing timetable
                    notifyChanged();
                }
            }
        });
        // Listen for start source change
        radialStartEndController.getStartModel().getStartSource().addListener(new ChangeListener<SpaceElement>() {
            @Override
            public void changed(ObservableValue<? extends SpaceElement> observable, SpaceElement oldValue, SpaceElement newValue) {
                radial.setStartElement(newValue);
            }
        });
        // first init
        radial.setStartElement(new Sun());
    }

    
  
    /**
     * Configure views according to JMap Tool
     *
     * @param jMapTool
     */
    public void setJMapTool(JMapTool jMapTool) {
        // change j-map tool and set start=SUN
        radialStartEndController.setJMapTool(jMapTool);
        carringtonDashboard.setJMapTool(jMapTool);
        // reset default values
        radial.setLinkcatMode(false);
        if (!radial.getCmeType().equals(CMEType.FIXED_POINT)) {
            // set default CME type 
            radial.setCmeType(CMEType.FIXED_POINT);
            // notify this change
            notifyChanged();
        }
        updateLinkcatMode();
    }

    /**
     * Initialize Radial Start fields (set default values and compute HAE central axis)
     */
    public void initRadialStart() {
        final Task<Object> initRadialStartWorker = initRadialStartWorker();

        initRadialStartWorker.stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
                if (newState == Worker.State.SUCCEEDED) {
                    // thread is done with success
                    endCursor();
                    // update carrington view
                    carringtonDashboard.updateView(radial, radialEnd);
                    // update start fields
                    updateStartView();
                    // Reset default In Situ mode 
                    radial.setInSitu(true);

                } else if (newState == Worker.State.FAILED || newState == Worker.State.CANCELLED) {
                    // thread is failed or cancelled
                    endCursor();
                    String source = radialStartEndController.getStartModel().getStartSource().get().getName();
                    radialStartEndController.runStartPoint(new Sun());
                    DialogBox.warn("Position of " + source + " at start time has not be found.\nDefault start has been reset to SUN");
                    initRadialStartWorker.getException().printStackTrace();
                }
            }
        });

        new Thread(initRadialStartWorker).start();
        waitCursor();
    }

    private Task<Object> initRadialStartWorker() {
        return new Task<Object>() {
            @Override
            protected Object call() throws Exception {
                if (radial.isInSitu()) {
                    // compute Radial start
                    radialStartEndController.getStartModel().compute(radial, startDate.get());
                } else {
                    // compute Carrington source from tStart and longStart
                    radialStartEndController.getStartModel().computeSource(radial, startDate.get());
                }
                return true;
            }
        };
    }

    /**
     * Compute Radial end fields + impact
     *
     * @return
     * @throws Exception
     */
    public boolean computeRadial() throws Exception {
        radialEnd = radialStartEndController.compute(radial);
        return true;
    }

    /**
     * Update view called after computing Radial
     */
    public void updateView() {
        if (radial.getEndElement() != null) {
            radialStartEndController.setUpViewWith(radialEnd);
        } else {
            radialStartEndController.clearView();
        }

        // update only the active tool
        if (eclipticPlaneComponent.getTool() == Tool.RADIAL) {
            if (!initDefaultView) {
                initDefaultView = true;
            } else {
                // set default radius (1 AU)
                double rs = radial.getRadialDistanceStart();
                rs = (rs == 0 ? rs : rs + ToolHelper.getRadiusSun());
                //double re = radialStartEndController.getRadialEnd().getDistEnd();
                double re = radialEnd.getDistEnd();
                re = (re == 0 ? re : re + ToolHelper.getRadiusSun());
                double radius = Math.min(CoordConverter.toKm(1.0), Math.max(rs, re));
                // set End source
                eclipticPlaneComponent.setEndSource(radialEnd.getEndSource());
                // set result time from default radius
                int step;
                if (radial.isDragModel()) {
                    long ts = startDate.get().getTime() / 1000;
                    long te = radialEnd.getEndTime().getTimeInMillis() / 1000;
                    if (te > ts) {
                        long t = ts + radial.getDragTime(radius) / 1000;
                        step = (int) ((t - ts) / ((te - ts) / NB_STEP));
                    } else {
                        // default step 
                        step = 1;
                    }
                } else {
                    step = (int) ((radius - rs) / ((re - rs) / NB_STEP));
                }
                timestampComponent.getResultTScrollBar().setValue(step);

                log.debug("Update view with step = " + step);
                setResultTime(step);
                // set result date property from result time => ask ecliptic plane to be refreshed
                timestampComponent.updateResultDate();
                // update ecliptic CME
                updateCmeWith(radius, step);
                // enable slider
                timestampComponent.getResultTScrollBar().setDisable(false);
            }
        }
    }

    /**
     * Reset End source after computing Radial on error
     */
    public void resetEndSource() {
        radialStartEndController.getEndSource().set(radialEnd.getEndSource());
    }

    private void updateCmeWith(double radius, int step) {
        double t1, t2;
        if (radial.isDragModel()) {
            t1 = radial.getDragTime(radius / 3);
            t2 = radial.getDragTime(2 * radius / 3);
        } else {
            t1 = getResultTime(step / 3) - startDate.get().getTime();
            t2 = getResultTime(2 * step / 3) - startDate.get().getTime();
        }
        double t3 = resultDate.getTime() - startDate.get().getTime();
        if (radial.getCmeType().equals(CMEType.SSE)) {
            //eclipticPlaneComponent.updateCmeRadius(radius, radialStartEndController.getRadialEnd().getLongSep());
            eclipticPlaneComponent.updateCmeRadius(radius, radialEnd.getLongSep());
        } else {
            eclipticPlaneComponent.updateCmeRadius(radius, t1, t2, t3);
        }
    }

    /**
     * update result time from current step
     */
    public void updateResultTime() {
        int step = (int) this.timestampComponent.getResultTScrollBar().valueProperty().get();
        updateResultTime(step);
        // update result date => ask ecliptic to be refreshed
        timestampComponent.updateResultDate();
        // enable slider
        timestampComponent.getResultTScrollBar().setDisable(false);
    }

    private void updateResultTime(int step) {
        // update only the active tool
        if (eclipticPlaneComponent.getTool() == Tool.RADIAL) {
            long startTime = startDate.get().getTime();
            long endTime = radialEnd.getEndTime().getTimeInMillis();
            //if end = Sun, end time before start, reverse time slider
            if (endTime < startTime) {
                step = NB_STEP - step;
            }

            //log.debug("Update result time from step = " + step);
            double radius = 0;
            setResultTime(step);
            // update CME radius
            if (radial.isDragModel()) {
                double time = resultDate.getTime() - startDate.get().getTime();
                radius = radial.getDragRadius(time);
            } else {
                double rs = radial.getRadialDistanceStart();
                rs = (rs == 0 ? rs : rs + ToolHelper.getRadiusSun());
                double re = radialEnd.getDistEnd();
                re = (re == 0 ? re : re + ToolHelper.getRadiusSun());
                radius = rs + ((re - rs) / NB_STEP) * step;
            }
            updateCmeWith(radius, step);
        }
    }

    /**
     *
     * @param step
     */
    private void setResultTime(int step) {
        resultDate.setTime(getResultTime(step));
        timestampComponent.setResultTime(resultDate);
    }

    /**
     * Get result time from step between start time and end time
     *
     * @param step
     * @return
     */
    private long getResultTime(int step) {
        long startTime = startDate.get().getTime();
        long endTime = radialEnd.getEndTime().getTimeInMillis();
        return startTime + ((endTime - startTime) / NB_STEP) * step;
    }

    /**
     * Update Linkcat mode (linkcat flares and flux + title)
     */
    public void updateLinkcatMode() {
        // set flares properties
        FlareCollection.isLinkcat = radial.isLinkcatMode();
        FlareCollection.cmeType = radial.getCmeType();
        FlareCollection.obs = radial.getObs();
        // refresh title 
        if (radial.isLinkcatMode()) {
            RootController.stage.setTitle("Propagation Tool - LINKCAT");
        } else {
            RootController.stage.setTitle("Propagation Tool");
        }
    }

    /**
     * Update start view and notify the current change
     */
    public void updateStartView() {
        // init drag parameter
        radial.setDragParam(0.0);
        // update start view
        radialStartEndController.getStartModel().setUpViewWith(radial);
        // notify this change
        notifyChanged();
    }

    public void updateDragView() {
        // update computed Drag param
        radialStartEndController.getStartModel().setUpViewWith(radial);
    }

    /**
     * update Ecliptic plane from Radial (CME, Start, End)
     */
    public void updateEcliptic() {
        // update only the active tool
        if (eclipticPlaneComponent.getTool() == Tool.RADIAL) {
            // set Start/End source
            eclipticPlaneComponent.setStartSource(radial.getStartElement());
            eclipticPlaneComponent.setEndSource(radial.getEndElement());
            // update CME
            eclipticPlaneComponent.updateCmeWith(radial.getCmeType(), radial.getLongStartT(), radial.getExtentCME());
            eclipticPlaneComponent.updateFieldLinesWith(null,null);
            if (radialEnd != null && radialEnd.getEndTime() != null) {
                carringtonDashboard.setEndTime(radialEnd.getEndTime().getTime());
            }
        }
    }

    /**
     * Update Radial Model from Carrington CME
     */
    private void updateRadialFromCarrington() {
        Double longitude = carringtonDashboard.getLongSource();
        if (longitude != null) {
            radial.setLongSource(longitude);
        }
        longitude = carringtonDashboard.getLongExtent();
        if (longitude != null) {
            radial.setExtentCME(longitude);
        }
        // compute HAE long start from Carrington source
        radialStartEndController.getStartModel().computeFromSource(radial);
    }

    /**
     * Update Carrington CME from Radial Model
     */
    public void updateCarringtonFromRadial() {
        // compute Carrington source 
        radialStartEndController.getStartModel().computeSource(radial);
        // update carrington CME 
        carringtonDashboard.updateCMEWith(radial.getLongSource(), radial.getExtentCME());
    }

    public BooleanProperty getHasChanged() {
        return hasChanged;
    }

    public void setHasChanged(BooleanProperty hasChanged) {
        this.hasChanged = hasChanged;
    }

    public void notifyChanged() {
        getHasChanged().set(true);
        getHasChanged().set(false);
    }

    public ObjectProperty<Date> getStartDate() {
        return startDate;
    }

    public void waitCursor() {
        radialStartEndController.setDisable(true);
        carringtonDashboard.setDisable(true);
        vPlotComponent.setDisable(true);
        fluxPlotComponent.setDisable(true);
        spectroPlotComponent.setDisable(true);
        rootRadialDashboard.setCursor(Cursor.WAIT);
    }

    public void endCursor() {
        radialStartEndController.setDisable(false);
        carringtonDashboard.setDisable(false);
        vPlotComponent.setDisable(false);
        fluxPlotComponent.setDisable(false);
        spectroPlotComponent.setDisable(false);
        rootRadialDashboard.setCursor(null);
    }

}
