package seng302.Model;


import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.collections.ObservableList;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.transform.Rotate;
import seng302.GraphCoordinate;
import seng302.Mock.StreamedCourse;
import seng302.RaceDataSource;
import seng302.RaceMap;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;

import java.util.*;

/**
 * This creates a JavaFX Canvas that is fills it's parent.
 * Cannot be downsized.
 * Created by fwy13 on 17/03/17.
 */
public class ResizableRaceCanvas extends ResizableCanvas {
    private RaceMap map;
    private List<Boat> boats;
    private List<Marker> boatMarkers;
    private boolean raceAnno = true;
    private boolean annoName = true;
    private boolean annoAbbrev = true;
    private boolean annoSpeed = true;
    private boolean annoPath = true;
    private boolean annoTimeSinceLastMark = true;
    private List<Color> colours;
    private final List<Marker> markers;
    private final RaceDataSource raceData;
    private Map<Integer, Color> boatColours = new HashMap<>();
    private Node arrow;

    private RaceClock raceClock;

    public ResizableRaceCanvas(RaceDataSource raceData) {
        super();

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

        setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight()));

        this.markers = raceData.getMarkers();
        makeColours();
        this.raceData = raceData;
    }

    /**
     * Sets the boats that are to be displayed in this race.
     *
     * @param boats in race
     */
    public void setBoats(List<Boat> boats) {
        this.boats = boats;
        mapBoatColours();
    }


    /**
     * Sets the boat markers that are to be displayed in this race.
     *
     * @param boatMarkers in race
     */
    public void setBoatMarkers(ObservableList<Marker> boatMarkers) {
        this.boatMarkers = boatMarkers;
    }

    /**
     * Sets the RaceMap that the RaceCanvas is to be displaying for.
     *
     * @param map race map
     */
    private void setMap(RaceMap map) {
        this.map = map;
    }

    private void displayBoat(Boat boat, double angle, Color colour) {
        if (boat.getCurrentPosition() != null) {
            GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());

            double[] x = {pos.getX() - 6, pos.getX(), pos.getX() + 6};
            double[] y = {pos.getY() + 12, pos.getY() - 12, pos.getY() + 12};
            gc.setFill(colour);

            gc.save();
            rotate(angle, pos.getX(), pos.getY());
            gc.fillPolygon(x, y, 3);
            gc.restore();
        }
    }

    /**
     * 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.
     * @see GraphCoordinate
     * @see Color
     * @see Paint
     */
    private void displayLine(GraphCoordinate graphCoordinateA, GraphCoordinate graphCoordinateB, Paint paint) {
        gc.setStroke(paint);
        gc.setFill(paint);
        gc.fillOval(graphCoordinateA.getX() - 3, graphCoordinateA.getY() - 3, 6, 6);
        gc.fillOval(graphCoordinateB.getX() - 3, graphCoordinateB.getY() - 3, 6, 6);
        gc.strokeLine(graphCoordinateA.getX(), graphCoordinateA.getY(), graphCoordinateB.getX(), graphCoordinateB.getY());
    }

    /**
     * Display a point on the Canvas
     *
     * @param graphCoordinate Coordinate that the point is to be displayed at.
     * @param paint           Colour that the boat is to be coloured.
     * @see GraphCoordinate
     * @see Paint
     * @see Color
     */
    private void displayPoint(GraphCoordinate graphCoordinate, Paint paint) {
        gc.setFill(paint);
        gc.fillOval(graphCoordinate.getX(), graphCoordinate.getY(), 10, 10);
    }


    /**
     * Displays an arrow on the Canvas
     *
     * @param coordinate Coordinate that the arrow is to be displayed at.
     * @param angle      Angle that the arrow is to be facing in degrees 0 degrees = North (Up).
     * @see GraphCoordinate
     */
    private void displayArrow(GraphCoordinate coordinate, double angle) {
        gc.save();
        rotate(angle, coordinate.getX(), coordinate.getY());
        gc.setFill(Color.BLACK);
        gc.fillPolygon(new double[]{coordinate.getX() - 12, coordinate.getX() - 6, coordinate.getX(), coordinate.getX() - 4, coordinate.getX() - 4, coordinate.getX() - 8, coordinate.getX() - 8},
                new double[]{coordinate.getY() - 5, coordinate.getY() - 20, coordinate.getY() - 5, coordinate.getY() - 5, coordinate.getY() + 20, coordinate.getY() + 20, coordinate.getY() - 5},
                7);
        gc.restore();
    }

    private void displayFancyArrow(GraphCoordinate coordinate, double angle) {
        angle = angle % 360;
        if (arrow != null && arrow.getRotate() != angle) {
            arrow.setRotate(angle);
        }
    }

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

    /**
     * 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 timeSinceLastMark time since the last mark was passed
     */
    private void displayText(String name, String abbrev, double speed, GraphCoordinate coordinate, ZonedDateTime timeSinceLastMark) {
        String text = "";
        //Check name toggle value
        if (annoName){
            text += String.format("%s ", name);
        }
        //Check abbreviation toggle value
        if (annoAbbrev){
            text += String.format("%s ", abbrev);
        }
        //Check speed toggle value
        if (annoSpeed){
            text += String.format("%.2fkn ", speed);
        }
        //Check time since last mark toggle value
        if(annoTimeSinceLastMark){
            Duration timeSince = Duration.between(timeSinceLastMark, raceClock.getTime());
            text += String.format("%d",  timeSince.getSeconds());
        }
        //String text = String.format("%s, %2$.2fkn", name, speed);
        long xCoord = coordinate.getX() + 20;
        long yCoord = coordinate.getY();
        if (xCoord + (text.length() * 7) >= getWidth()) {
            xCoord -= text.length() * 7;
        }
        if (yCoord - (text.length() * 2) <= 0) {
            yCoord += 30;
        }
        gc.fillText(text, xCoord, yCoord);
    }

    /**
     * Draws race map with up to date data.
     */
    public void update() {
        this.draw();
        this.updateBoats();
    }

    /**
     * Draw race markers
     */
    private void drawMarkers() {
        for(Marker marker: markers) {
            GraphCoordinate mark1 = this.map.convertGPS(marker.getMark1());
            // removed drawing of lines between the marks as only
            // the start and finish line should have a line drawn
            if(marker.isCompoundMark()) {
                GraphCoordinate mark2 = this.map.convertGPS(marker.getMark2());
                displayPoint(mark1, Color.LIMEGREEN);
                displayPoint(mark2, Color.LIMEGREEN);
            } else {
                displayPoint(mark1, Color.GREEN);
            }
        }
    }

    /**
     * Draws the Race Map
     */
    public void draw() {

        double width = getWidth();
        double height = getHeight();

        gc.clearRect(0, 0, width, height);

        if (map == null) {
            return;//TODO this should return a exception in the future
        }
        this.map.setHeight((int) height);
        this.map.setWidth((int) width);

        gc.setLineWidth(2);
        updateBoats();
        drawMarkers();

        //display wind direction arrow - specify origin point and angle - angle now set to random angle
        if (raceData instanceof StreamedCourse) {
            displayFancyArrow(new GraphCoordinate((int) getWidth() - 40, 40), ((StreamedCourse) raceData).getWindDirection());
        } else {
            displayFancyArrow(new GraphCoordinate((int) getWidth() - 40, 40), 150);
        }
    }

    /**
     * Toggle the raceAnno value
     */
    public void toggleAnnotations() {
        raceAnno = !raceAnno;
    }

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

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

    /**
     * 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;
    }

    /**
     * Draws boats while race in progress, when leg heading is set.
     */
    private void updateBoats() {
        if (boats != null) {
            if (boatColours.size() < boats.size()) mapBoatColours();
            for (Boat boat : boats) {
                boolean finished = boat.getCurrentLeg().getName().equals("Finish") || boat.getCurrentLeg().getName().equals("DNF");
                boolean isStart = boat.isStarted();
                int sourceID = boat.getSourceID();
                if (!finished && isStart) {
                    displayBoat(boat, boat.getHeading(), boatColours.get(sourceID));
                    GraphCoordinate wakeFrom = this.map.convertGPS(boat.getCurrentPosition());
                    GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake());
                    displayLine(wakeFrom, wakeTo, boatColours.get(sourceID));
                } else if (!isStart) {
                    displayBoat(boat, boat.getHeading(), boatColours.get(sourceID));
                } else {
                    displayBoat(boat, 0, boatColours.get(sourceID));
                }

                if (raceAnno)
                    if (Duration.between(boat.getTimeSinceLastMark(), raceClock.getTime()).getSeconds() < 0){
                        boat.setTimeSinceLastMark(raceClock.getTime());
                    }
                    displayText(boat.toString(), boat.getAbbrev(), boat.getVelocity(), this.map.convertGPS(boat.getCurrentPosition()), boat.getTimeSinceLastMark());
                //TODO this needs to be fixed.
                drawTrack(boat, boatColours.get(sourceID));
            }
        }
    }

    /**
     * Draws all track points for a given boat. Colour is set by boat, opacity by track point.
     * @param boat whose track is displayed
     * @param colour The color to use for the track.
     * @see seng302.Model.TrackPoint
     */
    private void drawTrack(Boat boat, Color colour) {
        if (annoPath && raceAnno) {
            for (TrackPoint point : boat.getTrack()) {
                GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate());
                gc.setFill(new Color(colour.getRed(), colour.getGreen(), colour.getBlue(), point.getAlpha()));
                gc.fillOval(scaledCoordinate.getX(), scaledCoordinate.getY(), 5, 5);
            }
        }
    }

    private void makeColours() {
        colours = new ArrayList<>(Arrays.asList(
                Color.BLUEVIOLET,
                Color.BLACK,
                Color.RED,
                Color.ORANGE,
                Color.DARKOLIVEGREEN,
                Color.LIMEGREEN,
                Color.PURPLE,
                Color.DARKGRAY,
                Color.YELLOW
        ));
    }

    public void setArrow(Node arrow) {
        this.arrow = arrow;
    }

    public void setRaceClock(RaceClock raceClock) {
        this.raceClock = raceClock;
    }

    private void mapBoatColours() {
        int currentColour = 0;
        for (Boat boat : boats) {
            if (!boatColours.containsKey(boat.getSourceID())) {
                boatColours.put(boat.getSourceID(), colours.get(currentColour));
            }
            currentColour = (currentColour + 1) % colours.size();
        }
    }

}
