package ecoparasite.svg;

import ecoparasite.representation.ValeursXY;
import ecoparasite.svg.elements.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

public class SVGBuilder {

    final int SIZE_TICK_TEXT = ElementsFactory.AXES_TEXT_SIZE - 3;

    private ArrayList<Double> pointsX;
    private ArrayList<Double> pointsY;
    private Double offsetX;
    private Double offsetY;
    private SVGResizing resizer;

    private Double minPointsX;
    private Double minPointsY;
    private Double maxPointsX;
    private Double maxPointsY;

    public SVGBuilder(HashMap<String, ArrayList<Double>> axesPoints ) throws IncorrectAxesPointsException {

        if( axesPoints.get("AxeX") == null ||  axesPoints.get("AxeY") == null || axesPoints.get("OffsetX") == null || axesPoints.get("OffsetY") == null ){
            throw new IncorrectAxesPointsException();
        }

        this.pointsX = axesPoints.get("AxeX");
        this.pointsY = axesPoints.get("AxeY");
        this.offsetX = axesPoints.get("OffsetX").getFirst();
        this.offsetY = axesPoints.get("OffsetY").getFirst();

        this.minPointsX = this.pointsX.getFirst();
        this.minPointsY = this.pointsY.getFirst();
        this.maxPointsX = this.pointsX.getLast();
        this.maxPointsY = this.pointsY.getLast();

    }

    public ArrayList<Double> getPointsX() {
        return pointsX;
    }

    public ArrayList<Double> getPointsY() {
        return pointsY;
    }

    public Double getOffsetX() {
        return offsetX;
    }

    public Double getOffsetY() {
        return offsetY;
    }

    public SVGResizing getResizer() {
        if( this.resizer == null ){
            this.resizer = new SVGResizing( this.minPointsX, this.minPointsY, this.maxPointsX, this.maxPointsY );
        }
        return resizer;
    }

    public void setResizer(SVGResizing resizer) {
        this.resizer = resizer;
    }

    public ArrayList<Element> buildAll(String XLabel, String YLabel, HashSet<ValeursXY> points, double A, double B){

        ArrayList<Element> elements = new ArrayList<>();

        elements.addAll(buildAxes(XLabel, YLabel));
        elements.addAll(buildXTicks());
        elements.addAll(buildYTicks());
        elements.addAll(buildPoints(points));
        elements.addAll(buildRegression(A,B));

        return elements;
    }

    public ArrayList<Element> buildAxes(String XLabel, String YLabel){

        final int OFFSET_TEXT_AXISX_X = -20;
        final int OFFSET_TEXT_AXISX_Y = -10;
        final int OFFSET_TEXT_AXISY_X = +5;
        final int OFFSET_TEXT_AXISY_Y = +10;

        ArrayList<Element> elements = new ArrayList<>();

        double beginAxeX = getBeginAxeX();
        double beginAxeY = getBeginAxeY();

        Coordonnees bottom = getResizer().resize( beginAxeX, minPointsY );
        Coordonnees top = getResizer().resize( beginAxeX, maxPointsY );
        Coordonnees left = getResizer().resize( minPointsX, beginAxeY );
        Coordonnees right = getResizer().resize( maxPointsX, beginAxeY );

        // Axes
        elements.add( new Line( bottom, top, ElementsFactory.COLOR_WHITE, 2 ) );
        elements.add( new Line( left, right, ElementsFactory.COLOR_WHITE, 2 ) );

        // Labels.
        elements.add( new Text(
           new Coordonnees( right.getX() + OFFSET_TEXT_AXISX_X, right.getY() + OFFSET_TEXT_AXISX_Y ),
           XLabel, ElementsFactory.COLOR_WHITE, ElementsFactory.AXES_TEXT_SIZE
        ) );
        elements.add( new Text(
                new Coordonnees( top.getX() + OFFSET_TEXT_AXISY_X, top.getY() + OFFSET_TEXT_AXISY_Y ),
                YLabel, ElementsFactory.COLOR_WHITE, ElementsFactory.AXES_TEXT_SIZE
        ));

        return elements;

    }

    public ArrayList<Element> buildXTicks(){

        final int OFFSET_TICK = -5;
        final int OFFSET_TEXT_X = -10;
        final int OFFSET_TEXT_Y = +15;

        ArrayList<Element> elements = new ArrayList<>();

        double beginAxeY = getBeginAxeY();
        for( Double X : this.pointsX ){
            Coordonnees coords = getResizer().resize( X, beginAxeY );

            elements.add(new Line(
                    coords,
                    new Coordonnees( coords.getX(), coords.getY() + OFFSET_TICK ),
                    ElementsFactory.COLOR_WHITE, 1
            ));
            elements.add(new Text(
               new Coordonnees( coords.getX() + OFFSET_TEXT_X, coords.getY() + OFFSET_TEXT_Y ),
                X.toString(),
                ElementsFactory.COLOR_WHITE,
                SIZE_TICK_TEXT
            ));
        }

        return elements;
    }

    public ArrayList<Element> buildYTicks(){

        final int OFFSET_TICK = +5;
        final int OFFSET_TEXT_X = -35;
        final int OFFSET_TEXT_Y = +5;

        ArrayList<Element> elements = new ArrayList<>();

        double beginAxeX = getBeginAxeX();
        for( Double Y : this.pointsY ){
            Coordonnees coords = getResizer().resize( beginAxeX, Y );

            elements.add(new Line(
                    new Coordonnees(coords.getX() + OFFSET_TICK, coords.getY() ),
                    coords,
                    ElementsFactory.COLOR_WHITE, 1
            ));
            elements.add(new Text(
                    new Coordonnees( coords.getX() + OFFSET_TEXT_X, coords.getY() + OFFSET_TEXT_Y ),
                    Y.toString(),
                    ElementsFactory.COLOR_WHITE,
                    SIZE_TICK_TEXT
            ));
        }

        return elements;
    }

    public ArrayList<Element> buildPoints( HashSet<ValeursXY> points ){

        ArrayList<Element> elements = new ArrayList<>();
        for( ValeursXY point : points ){
            Coordonnees coords = getResizer().resize( point.getX(), point.getY() );
            elements.add(new Circle(coords,3,ElementsFactory.COLOR_BLUE) );
        }

        return elements;
    }

    public ArrayList<Element> buildRegression( double A, double B ){
        ArrayList<Element> elements = new ArrayList<>();

        double y1 = A * minPointsX + B;
        double y2 = A * maxPointsX + B;

        Coordonnees coords1 = getResizer().resize(minPointsX, y1);
        Coordonnees coords2 = getResizer().resize(maxPointsX, y2);

        elements.add( new Line( coords1, coords2, ElementsFactory.COLOR_RED, 2 ) );
        return elements;
    }

    private double getBeginAxeX(){
        return ( minPointsX > 0 ) ? minPointsX : ( maxPointsX < 0 ? maxPointsX : 0 );
    }

    private double getBeginAxeY(){
        return ( minPointsY > 0 ) ? minPointsY : ( maxPointsY < 0 ? maxPointsY : 0 );
    }

    /**
     * Permet de renvoyer des valeurs "clean" pour l'affichage des axes
     * @param h Contient les Coordonnées de chacun des points de nos données
     * @return une HashMap de String et de Hashset de Double.
     *         Avec la String "AxeX", un Hashset de Double contenant les valeurs des gradations de l'axe X
     *         Avec la String "AxeY", un Hashset de Double contenant les valeurs des gragations de l'axe Y
     *         Avec la String "OffsetX", un Hashset de Double contenant uniquement la valeur de l'offset des points par rapport à l'axe X
     *         Avec la String "OffsetY", un Hashset de Double contenant uniquement la valeur de l'offset des points par rapport à l'axe Y
     */
    public static HashMap< String ,ArrayList<Double>> calcPointAxes(HashSet<ValeursXY> h){

        HashMap< String, ArrayList<Double> > map = new HashMap<>();

        //Définition des min et max
        double max_x = Double.MIN_VALUE;
        double min_x = Double.MAX_VALUE;
        double max_y = Double.MIN_VALUE;
        double min_y = Double.MAX_VALUE;

        //Trouvé les min et max
        for (ValeursXY var : h) {

            if (max_x < var.getX()){
                max_x = var.getX();
            }
            if (min_x > var.getX()){
                min_x = var.getX();
            }

            if (max_y < var.getY()){
                max_y = var.getY();
            }
            if (min_y > var.getY()){
                min_y = var.getY();
            }

        }

        double range_x = max_x-min_x;
        double range_y = max_y-min_y;

        int target = 10; // Ideal Number of Gradation

        double step_x = niceStep(range_x,target);
        double step_y = niceStep(range_y,target);

        double nicemin_x = roundMin(min_x,step_x);
        double nicemax_x = roundMax(max_x,step_x);
        double nicemin_y = roundMin(min_y,step_y);
        double nicemax_y = roundMax(max_y,step_y);

        // Compléter un Hashset de Double pour X et pour Y et Offset X et Y. TODO
        ArrayList<Double> axeX = new ArrayList<>();
        ArrayList<Double> axeY = new ArrayList<>();
        ArrayList<Double> OffsetX = new ArrayList<>();
        ArrayList<Double> OffsetY = new ArrayList<>();

        Double ix = nicemin_x;
        while ( ix <= nicemax_x ) {
            axeX.add(ix);
            ix+=step_x;
        };
        map.put("AxeX", axeX);

        Double iy = nicemin_y;
        while ( iy <= nicemax_y ) {
            axeY.add(iy);
            iy+=step_y;
        }
        map.put("AxeY",axeY);

        double offsetX = min_x - nicemin_x;
        double offsetY = min_y - nicemin_y;

        ArrayList<Double> offsetXHash = new ArrayList<>();
        offsetXHash.add(offsetX);
        ArrayList<Double> offsetYHash = new ArrayList<>();
        offsetYHash.add(offsetY);

        map.put("OffsetX", offsetXHash);
        map.put("OffsetY", offsetYHash);

        return map;
    }

    /**
     * Fonction de calcul d'un step rond
     * Cette fonction est basé sur une idée demandée à ChatGPT
     * @param range écart entre la plus petite et la plus grande valeur
     * @param targetTicks nombre de gradation ideal
     * @return
     */
    public static double niceStep(double range, int targetTicks) {

        double rawStep = range / targetTicks;

        double exponent = Math.floor(Math.log10(rawStep));
        double fraction = rawStep / Math.pow(10, exponent);

        double niceFraction;

        if (fraction < 1.5)
            niceFraction = 1;
        else if (fraction < 3)
            niceFraction = 2;
        else if (fraction < 7)
            niceFraction = 5;
        else
            niceFraction = 10;

        return niceFraction * Math.pow(10, exponent);
    }

    /**
     * retourne une valeur arrondi "joli" adapter à un graphique
     * @param value
     * @param step
     * @return
     */
    public static double roundMin(double value, double step) {
        return Math.floor(value / step) * step;
    }

    public static double roundMax(double value, double step) {
        return Math.ceil(value / step) * step;
    }
}
