package mock.xml;

import org.xml.sax.SAXException;
import shared.dataInput.RaceXMLReader;
import shared.enums.XMLFileType;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.XMLReaderException;
import shared.model.CompoundMark;
import shared.model.Constants;
import shared.model.GPSCoordinate;
import shared.xml.Race.XMLCompoundMark;
import shared.xml.Race.XMLLimit;
import shared.xml.Race.XMLMark;
import shared.xml.Race.XMLRace;
import shared.xml.XMLUtilities;

import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Helper Class for creating a Race XML
 */
public class RaceXMLCreator {


    /**
     * get the windward gate in a race
     * @param reader reads in the mark
     * @return the windward gate.
     */
    public static CompoundMark getWindwardGate(RaceXMLReader reader){
        for (CompoundMark mark: reader.getCompoundMarks()){
            if (mark.getName().equals("Windward Gate")) return mark;
        }
        return null;
    }

    /**
     * get the leeward gate in a race
     * @param reader reads in the mark
     * @return the leeward gate.
     */
    public static CompoundMark getLeewardGate(RaceXMLReader reader){
        for (CompoundMark mark: reader.getCompoundMarks()){
            if (mark.getName().equals("Leeward Gate")) return mark;
        }
        return null;
    }

    /**
     * Rotates the race in a specified direction.
     * @param s xml file name or contents.
     * @param fileType Whether s is a file name or contents.
     * @param degrees degrees to rotate
     * @param tutorial Whether we wish to run the tutorial - this changes the race start time.
     * @return the new xml file as a string
     * @throws XMLReaderException if the xml is not readable
     * @throws InvalidRaceDataException if the race is invalid
     */
    public static String alterRaceToWind(String s, XMLFileType fileType, double degrees, boolean tutorial) throws XMLReaderException, InvalidRaceDataException {

        RaceXMLReader reader = new RaceXMLReader(s, fileType);

        try {

            XMLRace race = XMLUtilities.xmlToClass(
                    s,
                    RaceXMLCreator.class.getClassLoader().getResource("mock/mockXML/schema/raceSchema.xsd"),
                    XMLRace.class);

            if(tutorial){
                setRaceXMLAtCurrentTimeToNow(race, 1000l, 5000l);
            } else {
                setRaceXMLAtCurrentTimeToNow(race);
            }


            CompoundMark leewardGate = getLeewardGate(reader);
            CompoundMark windwardGate = getWindwardGate(reader);

            double raceOriginalBearing = 0;

            /*if (leewardGate != null && windwardGate != null) {
                raceOriginalBearing = getLineAngle(
                        leewardGate.getMark1Position(),
                        windwardGate.getMark1Position() );
            }*/

            double degreesToRotate = degrees - raceOriginalBearing;

            if (degrees >= 0) {
                alterRaceRotation(race, degreesToRotate);
            }

            return XMLUtilities.classToXML(race);

        } catch (ParserConfigurationException | IOException | SAXException | JAXBException e) {
            throw new InvalidRaceDataException("Could not parse or marshall race data file.", e);
        }
    }


    /**
     * Rotate the features in a race such as the boundary, and the marks.
     * @param race the race to alter
     * @param degrees the degrees to rotate by.
     */
    public static void alterRaceRotation(XMLRace race, double degrees){
        GPSCoordinate center = getCenter(race);
        for(XMLLimit limit: race.getCourseLimit().getLimit()){
            GPSCoordinate rotatedLim = rotate(center, limitToGPSCoordinate(limit), degrees);
            limit.setLat(rotatedLim.getLatitude());
            limit.setLon(rotatedLim.getLongitude());
        }

        for(XMLCompoundMark compoundMark: race.getCourse().getCompoundMark()){
            for (XMLMark mark: compoundMark.getMark()){
                GPSCoordinate rotatedMark = rotate(center, markToGPSCoordinate(mark), degrees);
                mark.setTargetLat(rotatedMark.getLatitude());
                mark.setTargetLng(rotatedMark.getLongitude());
            }
        }
    }

    /**
     * Converts a Race.CourseLimit.Limit to a GPS coordinate
     * @param limit limit to convert
     * @return gps coordinate corresponding to the limit
     */
    public static GPSCoordinate limitToGPSCoordinate(XMLLimit limit){
        return new GPSCoordinate(limit.getLat(), limit.getLon());
    }

    /**
     * get new gps coordinate after rotating
     * @param pivot center point to rotating from.
     * @param point point to rotate
     * @param degrees number of degress to rotate by
     * @return the new GPSCoordinate of the transformed point.
     */
    public static GPSCoordinate rotate(GPSCoordinate pivot, GPSCoordinate point, double degrees){
        double radDeg = Math.toRadians(degrees);
        double deltaLat = (point.getLatitude() - pivot.getLatitude());
        double deltaLon = (point.getLongitude() - pivot.getLongitude());
        //map to (0,1) vector and use vector maths to rotate.
        double resLat = deltaLat * Math.cos(radDeg) - deltaLon * Math.sin(radDeg) + pivot.getLatitude();
        double resLon = deltaLat * Math.sin(radDeg) + deltaLon * Math.cos(radDeg) + pivot.getLongitude();
        return new GPSCoordinate(resLat, resLon);
    }

    /**
     * obtains the GPSCoordinates of a mark
     * @param mark mark to obtain the GPSCoordinates of
     * @return the GPSCOordinatess of a mark
     */
    public static GPSCoordinate markToGPSCoordinate(XMLMark mark){
        return new GPSCoordinate(mark.getTargetLat(), mark.getTargetLng());
    }

    /**
     * get the center of a race
     * @param race race to get the center of
     * @return GPSCoordinates of the center
     */
    public static GPSCoordinate getCenter(XMLRace race){
        double avgLat = 0;
        double avgLng = 0;
        for (XMLLimit limit: race.getCourseLimit().getLimit()){
            avgLat += limit.getLat();
            avgLng += limit.getLon();
        }
        avgLat = avgLat/race.getCourseLimit().getLimit().size();
        avgLng = avgLng/race.getCourseLimit().getLimit().size();
        return new GPSCoordinate(avgLat, avgLng);
    }

    /**
     * gets the angle of a line
     * @param coord1 point a of the line
     * @param coord2 point b of the line
     * @return the angle in degrees that the bearing of the line is [-180, 180]
     */
    public static double getLineAngle(GPSCoordinate coord1, GPSCoordinate coord2){
        double dx = coord1.getLongitude() - coord2.getLongitude();
        double dy = coord1.getLatitude() - coord2.getLatitude();
        return Math.atan2(dy, dx)/Math.PI * 180;
    }




    public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML, long racePrestartTime, long racePreparatoryTime){
        //The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute.
        long millisecondsToAdd = racePrestartTime + racePreparatoryTime;
        long secondsToAdd = millisecondsToAdd / 1000;

        DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
        ZonedDateTime creationTime = ZonedDateTime.now();
        raceXML.setCreationTimeDate(dateFormat.format(creationTime));
        raceXML.getRaceStartTime().setTime(dateFormat.format(creationTime.plusSeconds(secondsToAdd)));
    }

    /**
     * Sets the xml description of the race to show the race was created now, and starts in 4 minutes
     * @param raceXML The race.xml contents.
     */
    public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML) {
        setRaceXMLAtCurrentTimeToNow(raceXML, Constants.RacePreStartTime, Constants.RacePreparatoryTime);

    }

}
