package visualiser.model;


import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.transform.Rotate;
import network.Messages.Enums.BoatStatusEnum;
import shared.dataInput.RaceDataSource;
import shared.enums.RoundingType;
import shared.model.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * This JavaFX Canvas is used to update and display details for a
 * {@link RaceMap} via the
 * {@link visualiser.Controllers.RaceController}.<br>
 * It fills it's parent and cannot be downsized. <br>
 * Details displayed include:
 *  {@link VisualiserBoat}s (and their
 *  {@link TrackPoint}s),
 *  {@link shared.model.Mark}s, a
 *  {@link RaceClock}, a wind direction arrow and
 *  various user selected {@link Annotations}.
 */
public class ResizableRaceCanvas extends ResizableCanvas {

    /**
     * The RaceMap used for converting GPSCoordinates to GraphCoordinates.
     */
    private RaceMap map;

    /**
     * The race we read data from and draw.
     */
    private VisualiserRaceEvent visualiserRace;


    private boolean annoName = true;
    private boolean annoAbbrev = true;
    private boolean annoSpeed = true;
    private boolean annoPath = true;
    private boolean annoEstTime = true;
    private boolean annoTimeSinceLastMark = true;
    private boolean annoGuideLine = false;



    /**
     * Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRaceEvent}.
     * @param visualiserRace The race that data is read from in order to be drawn.
     */
    public ResizableRaceCanvas(VisualiserRaceEvent visualiserRace) {
        super();

        this.visualiserRace = visualiserRace;

        RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource();

        double lat1 = raceData.getMapTopLeft().getLatitude();
        double long1 = raceData.getMapTopLeft().getLongitude();
        double lat2 = raceData.getMapBottomRight().getLatitude();
        double long2 = raceData.getMapBottomRight().getLongitude();

        this.map = new RaceMap(
                lat1, long1, lat2, long2,
                (int) getWidth(), (int) getHeight() );


    }



    /**
     * Toggle name display in annotation
     */
    public void toggleAnnoName() {
        annoName = !annoName;
    }

    /**
     * Toggle boat path display in annotation
     */
    public void toggleBoatPath() {
        annoPath = !annoPath;
    }

    public void toggleAnnoEstTime() {
        annoEstTime = !annoEstTime;
    }

    /**
     * Toggle boat time display in annotation
     */
    public void toggleAnnoTime() {
        annoTimeSinceLastMark = !annoTimeSinceLastMark;
    }

    /**
     * Toggle abbreviation display in annotation
     */
    public void toggleAnnoAbbrev() {
        annoAbbrev = !annoAbbrev;
    }

    /**
     * Toggle speed display in annotation
     */
    public void toggleAnnoSpeed() {
        annoSpeed = !annoSpeed;
    }

    /**
     * Toggle the guideline annotation
     */
    public void toggleGuideLine() {
        annoGuideLine = !annoGuideLine;
    }




    /**
     * Rotates things on the canvas Note: this must be called in between gc.save() and gc.restore() else they will rotate everything
     *
     * @param degrees Bearing degrees to rotate.
     * @param px    Pivot point x of rotation.
     * @param py    Pivot point y of rotation.
     */
    private void rotate(double degrees, double px, double py) {
        Rotate r = new Rotate(degrees, px, py);
        gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy());
    }



    /**
     * Draws a circle with a given diameter, centred on a given graph coordinate.
     * @param center The center coordinate of the circle.
     * @param diameter The diameter of the circle.
     */
    private void drawCircle(GraphCoordinate center, double diameter) {

        //The graphCoordinates are for the center of the point, so we offset them to get the corner coordinate.
        gc.fillOval(
                center.getX() - (diameter / 2),
                center.getY() - (diameter / 2),
                diameter, diameter    );

    }

    /**
     * Displays a line on the map with rectangles on the starting and ending point of the line.
     *
     * @param graphCoordinateA Starting Point of the line in GraphCoordinate.
     * @param graphCoordinateB End Point of the line in GraphCoordinate.
     * @param paint            Colour the line is to coloured.
     */
    private void drawLine(GraphCoordinate graphCoordinateA, GraphCoordinate graphCoordinateB, Paint paint) {

        gc.setStroke(paint);
        gc.setFill(paint);


        double endPointDiameter = 6;

        //Draw first end-point.
        drawCircle(graphCoordinateA, endPointDiameter);

        //Draw second end-point.
        drawCircle(graphCoordinateB, endPointDiameter);

        //Draw line between them.
        gc.strokeLine(
                graphCoordinateA.getX(),
                graphCoordinateA.getY(),
                graphCoordinateB.getX(),
                graphCoordinateB.getY() );


    }

    /**
     * Display a point on the Canvas. It has a diameter of 10 pixels.
     *
     * @param graphCoordinate Coordinate that the point is to be displayed at.
     * @param paint           Paint to use for the point.
     */
    private void drawPoint(GraphCoordinate graphCoordinate, Paint paint) {

        //Set paint.
        gc.setFill(paint);

        double pointDiameter = 10;

        //Draw the point.
        drawCircle(graphCoordinate, pointDiameter);
    }


    /**
     * Display given name and speed of boat at a graph coordinate
     *
     * @param name       name of the boat
     * @param abbrev     abbreviation of the boat name
     * @param speed      speed of the boat
     * @param coordinate coordinate the text appears
     * @param timeToNextMark The time until the boat reaches the next mark.
     * @param timeSinceLastMark The time since the boat passed the last mark.
     */
    private void drawText(String name, String abbrev, double speed, GraphCoordinate coordinate, String timeToNextMark, String timeSinceLastMark) {

        //The text to draw. Built during the function.
        String text = "";


        //Draw name if annotation is enabled.
        if (annoName) {
            text += String.format("%s ", name);
        }

        //Draw abbreviation/country if annotation is enabled.
        if (annoAbbrev) {
            text += String.format("%s ", abbrev);
        }

        //Draw speed if annotation is enabled.
        if (annoSpeed){
            text += String.format("%.2fkn ", speed);
        }

        //Draw time to reach next mark if annotation is enabled.
        if (annoEstTime) {
            text += timeToNextMark;
        }

        //Draw time since last mark if annotation is enabled.
        if(annoTimeSinceLastMark) {
            text += timeSinceLastMark;
        }


        //Offset by 20 pixels horizontally.
        long xCoord = coordinate.getX() + 20;
        long yCoord = coordinate.getY();

        //If the text would extend out of the canvas (to the right), move it left.
        if (xCoord + (text.length() * 7) >= getWidth()) {
            xCoord -= text.length() * 7;
        }


        if (yCoord - (text.length() * 2) <= 0) {
            yCoord += 30;
        }

        //Draw text.
        gc.fillText(text, xCoord, yCoord);
    }


    /**
     * Draws the label for a given boat. Includes name, abbreviation, speed, time since mark, and time to next mark.
     * @param boat The boat to draw text for.
     */
    private void drawBoatText(VisualiserBoat boat) {

        drawText(
                boat.getName(),
                boat.getCountry(),
                boat.getCurrentSpeed(),
                this.map.convertGPS(boat.getCurrentPosition()),
                boat.getTimeToNextMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()),
                boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()) );

    }




    /**
     * Draws all of the boats on the canvas.
     */
    private void drawBoats() {

        for (VisualiserBoat boat : new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoats())) {

            //Draw the boat.
            drawBoat(boat);

            //Only draw wake if they are currently racing.
            if (boat.getStatus() == BoatStatusEnum.RACING) {
                drawWake(boat);
            }


            //If the race hasn't started, we set the time since last mark to the current time, to ensure we don't start counting until the race actually starts.
            if ((boat.getStatus() != BoatStatusEnum.RACING) && (boat.getStatus() == BoatStatusEnum.FINISHED)) {
                boat.setTimeAtLastMark(visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime());
            }

            //Draw boat label.
            drawBoatText(boat);

            //Draw track.
            drawTrack(boat);

        }

    }



    /**
     * Draws a given boat on the canvas.
     * @param boat The boat to draw.
     */
    private void drawBoat(VisualiserBoat boat) {

        if (boat.isClientBoat()) {
            drawClientBoat(boat);
        }

        //Convert position to graph coordinate.
        GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());

        //The x coordinates of each vertex of the boat.
        double[] x = {
                pos.getX() - 6,
                pos.getX(),
                pos.getX() + 6  };

        //The y coordinates of each vertex of the boat.
        double[] y = {
                pos.getY() + 12,
                pos.getY() - 12,
                pos.getY() + 12 };

        //The above shape is essentially a triangle 12px wide, and 24px long.

        //Draw the boat.
        gc.setFill(boat.getColor());

        gc.save();
        rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
        gc.fillPolygon(x, y, 3);
        gc.restore();


    }

    /**
     * Draws extra decorations to show which boat has been assigned to the client.
     * @param boat The client's boat.
     */
    private void drawClientBoat(VisualiserBoat boat) {

        //Convert position to graph coordinate.
        GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());

        //The x coordinates of each vertex of the boat.
        double[] x = {
                pos.getX() - 9,
                pos.getX(),
                pos.getX() + 9  };

        //The y coordinates of each vertex of the boat.
        double[] y = {
                pos.getY() + 15,
                pos.getY() - 15,
                pos.getY() + 15 };

        //The above shape is essentially a triangle 24px wide, and 48 long.

        //Draw the boat.
        gc.setFill(Color.BLACK);

        gc.save();
        rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
        gc.fillPolygon(x, y, 3);
        gc.restore();

    }


    /**
     * Draws the wake for a given boat.
     * @param boat Boat to draw wake for.
     */
    private void drawWake(VisualiserBoat boat) {

        //Calculate either end of wake line.
        GraphCoordinate wakeFrom = this.map.convertGPS(boat.getCurrentPosition());
        GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake());

        //Draw.
        drawLine(wakeFrom, wakeTo, boat.getColor());

    }



    /**
     * Draws all of the {@link Mark}s on the canvas.
     */
    private void drawMarks() {

        for (Mark mark : new ArrayList<>(visualiserRace.getVisualiserRaceState().getMarks())) {
            drawMark(mark);
        }
    }


    /**
     * Draws a given mark on the canvas.
     * @param mark The mark to draw.
     */
    private void drawMark(Mark mark) {

        //Calculate screen position.
        GraphCoordinate mark1 = this.map.convertGPS(mark.getPosition());

        //Draw.
        drawPoint(mark1, Color.LIMEGREEN);

    }



    /**
     * Draws the Race Map.
     * Called when the canvas is resized.
     */
    public void draw() {

        //Clear canvas.
        clear();

        //Update our RaceMap using new canvas size.
        this.map.setWidth((int) getWidth());
        this.map.setHeight((int) getHeight());

        //Draw the race.
        drawRace();

    }


    /**
     * Clears the canvas.
     */
    private void clear() {
        gc.clearRect(0, 0, getWidth(), getHeight());
    }


    /**
     * Draws the race boundary.
     */
    private void drawBoundary() {

        //Prepare to draw.
        gc.setLineWidth(1);
        gc.setFill(Color.AQUA);


        //Calculate the screen coordinates of the boundary.
        List<GPSCoordinate> boundary = new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoundary());
        double[] xpoints = new double[boundary.size()];
        double[] ypoints = new double[boundary.size()];

        //For each boundary coordinate.
        for (int i = 0; i < boundary.size(); i++) {
            //Convert.
            GraphCoordinate coord = map.convertGPS(boundary.get(i));
            //Use.
            xpoints[i] = coord.getX();
            ypoints[i] = coord.getY();
        }

        //Draw the boundary.
        gc.fillPolygon(xpoints, ypoints, xpoints.length);

    }

    /**
     * Draws the race.
     * Called once per frame, and on canvas resize.
     */
    public void drawRace() {

        //Update RaceMap with new GPS values of race.
        this.map.setGPSTopLeft(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapTopLeft());
        this.map.setGPSBotRight(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapBottomRight());

        gc.setLineWidth(2);

        clear();

        //Race boundary.
        drawBoundary();

        //Guiding Line
        if (annoGuideLine){
            drawRaceLine();
        }

        //Boats.
        drawBoats();

        //Marks.
        drawMarks();

    }

    /**
     * draws a transparent line around the course that shows the paths boats must travel
     */
    public void drawRaceLine(){
        List<Leg> legs = this.visualiserRace.getVisualiserRaceState().getLegs();
        GPSCoordinate legStartPoint = legs.get(0).getStartCompoundMark().getAverageGPSCoordinate();
        GPSCoordinate nextStartPoint;
        for (int i = 0; i < legs.size() -1; i++) {
            nextStartPoint = drawLineRounding(legs, i, legStartPoint);
            legStartPoint = nextStartPoint;
        }
    }

    /**
     * Draws a line around a course that shows where boats need to go. This method
     * draws the line leg by leg
     * @param legs the legs of a race
     * @param index the index of the current leg to use
     * @param legStartPoint The position the current leg.
     * @return the end point of the current leg that has been drawn
     */
    private GPSCoordinate drawLineRounding(List<Leg> legs, int index, GPSCoordinate legStartPoint){
        GPSCoordinate startDirectionLinePoint;
        GPSCoordinate endDirectionLinePoint;
        Bearing bearingOfDirectionLine;

        GPSCoordinate startNextDirectionLinePoint;
        GPSCoordinate endNextDirectionLinePoint;
        Bearing bearingOfNextDirectionLine;

        //finds the direction of the current leg as a bearing
        startDirectionLinePoint = legStartPoint;
        GPSCoordinate tempEndDirectionLinePoint = legs.get(index).getEndCompoundMark().getAverageGPSCoordinate();

        bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, tempEndDirectionLinePoint);

        //after finding the initial bearing pick the mark used for rounding
        endDirectionLinePoint = legs.get(index).getEndCompoundMark().getMarkForRounding(bearingOfDirectionLine).getPosition();
        bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, endDirectionLinePoint);

        //finds the direction of the next leg as a bearing
        if (index < legs.size() -2){ // not last leg
            startNextDirectionLinePoint = legs.get(index + 1).getStartCompoundMark().getMark1Position();
            endNextDirectionLinePoint = legs.get(index + 1).getEndCompoundMark().getMark1Position();
            bearingOfNextDirectionLine = GPSCoordinate.calculateBearing(startNextDirectionLinePoint, endNextDirectionLinePoint);

            double degreesToAdd;
            //find which side is need to be used
            if (legs.get(index).getEndCompoundMark().getRoundingType() == RoundingType.Port ||
                    legs.get(index).getEndCompoundMark().getRoundingType() == RoundingType.SP){
                degreesToAdd = 90;
            }else{
                degreesToAdd = -90;
            }

            //use the direction line to find a point parallel to it by the mark
            GPSCoordinate pointToStartCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint,
                    100, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+degreesToAdd));

            //use the direction line to find a point to curve too
            GPSCoordinate pointToEndCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint,
                    100, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees()+degreesToAdd));

            //use the curve points to find the two control points for the bezier curve
            GPSCoordinate controlPoint;
            GPSCoordinate controlPoint2;
            Bearing bearingOfCurveLine = GPSCoordinate.calculateBearing(pointToStartCurve, pointToEndCurve);
            if ((bearingOfDirectionLine.degrees() - bearingOfNextDirectionLine.degrees() +360)%360< 145){
                //small turn
                controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve,
                        50, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+(degreesToAdd/2)));
                controlPoint2 = controlPoint;
            }else{
                //large turn
                controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve,
                        150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd));
                controlPoint2 = GPSCoordinate.calculateNewPosition(pointToEndCurve,
                        150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd));
            }


            //change all gps into graph coordinate
            GraphCoordinate startPath = this.map.convertGPS(startDirectionLinePoint);
            GraphCoordinate curvePoint = this.map.convertGPS(pointToStartCurve);
            GraphCoordinate curvePointEnd = this.map.convertGPS(pointToEndCurve);
            GraphCoordinate c1 = this.map.convertGPS(controlPoint);
            GraphCoordinate c2 = this.map.convertGPS(controlPoint2);

            gc.setLineWidth(2);
            gc.setStroke(Color.MEDIUMAQUAMARINE);

            gc.beginPath();
            gc.moveTo(startPath.getX(), startPath.getY());
            gc.lineTo(curvePoint.getX(), curvePoint.getY());
            drawArrowHead(startDirectionLinePoint, pointToStartCurve);
            gc.bezierCurveTo(c1.getX(), c1.getY(), c2.getX(), c2.getY(), curvePointEnd.getX(), curvePointEnd.getY());
            gc.stroke();
            gc.closePath();
            gc.save();

            return pointToEndCurve;
        }else{//last leg so no curve
            GraphCoordinate startPath = this.map.convertGPS(legStartPoint);
            GraphCoordinate endPath = this.map.convertGPS(legs.get(index).getEndCompoundMark().getAverageGPSCoordinate());

            gc.beginPath();
            gc.moveTo(startPath.getX(), startPath.getY());
            gc.lineTo(endPath.getX(), endPath.getY());
            gc.stroke();
            gc.closePath();
            gc.save();
            drawArrowHead(legStartPoint, legs.get(index).getEndCompoundMark().getAverageGPSCoordinate());
            return null;
        }
    }

    private void drawArrowHead(GPSCoordinate start, GPSCoordinate end){

        GraphCoordinate lineStart = this.map.convertGPS(start);
        GraphCoordinate lineEnd = this.map.convertGPS(end);

        double arrowAngle = Math.toRadians(45.0);
        double arrowLength = 10.0;
        double dx = lineStart.getX() - lineEnd.getX();
        double dy = lineStart.getY() - lineEnd.getY();
        double angle = Math.atan2(dy, dx);
        double x1 = Math.cos(angle + arrowAngle) * arrowLength + lineEnd.getX();
        double y1 = Math.sin(angle + arrowAngle) * arrowLength + lineEnd.getY();

        double x2 = Math.cos(angle - arrowAngle) * arrowLength + lineEnd.getX();
        double y2 = Math.sin(angle - arrowAngle) * arrowLength + lineEnd.getY();
        gc.strokeLine(lineEnd.getX(), lineEnd.getY(), x1, y1);
        gc.strokeLine(lineEnd.getX(), lineEnd.getY(), x2, y2);
    }




    /**
     * Draws all track points for a given boat. Colour is set by boat, opacity by track point.
     * This checks if {@link #annoPath} is enabled.
     * @param boat The boat to draw tracks for.
     * @see TrackPoint
     */
    private void drawTrack(VisualiserBoat boat) {
        //Check that track points are enabled.
        if (this.annoPath) {

            //Apply the boat color.
            gc.setFill(boat.getColor());

            double[] xPoints = new double[boat.getTrack().size()];
            double[] yPoints = new double[boat.getTrack().size()];
            int index = 0;

            //Copy trackpoint locations to x/y arrays.
            for (TrackPoint point : new ArrayList<>(boat.getTrack())) {

                //Convert the GPSCoordinate to a screen coordinate.
                GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate());

                xPoints[index] = ((double)scaledCoordinate.getX());
                yPoints[index] = ((double)scaledCoordinate.getY());

                index++;
            }

            gc.strokePolyline(xPoints, yPoints, xPoints.length);
        }

    }



}
