package mock.model;

import network.AckSequencer;
import network.Messages.*;
import network.Messages.Enums.BoatLocationDeviceEnum;
import network.Messages.Enums.YachtEventEnum;
import network.Messages.Enums.XMLMessageType;
import shared.model.Bearing;
import shared.model.CompoundMark;
import shared.model.Mark;
import shared.xml.Race.RaceDataSourceToXML;
import shared.xml.boats.BoatDataSourceToXML;
import shared.xml.regatta.RegattaDataSourceToXML;

import javax.xml.bind.JAXBException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created by connortaylorbrown on 2/08/17.
 */
public class RaceServer {
    private MockRace race;
    private LatestMessages latestMessages;
    private List<YachtEvent> collisionEvents = new ArrayList<>();


    /**
     * The sequence number of the latest BoatLocation message sent or received.
     */
    private int boatLocationSequenceNumber = 1;

    /**
     * The sequence number of race XML messages.
     */
    private int raceXMLSeqNumber = -1;

    /**
     * The sequence number of boat XML messages.
     */
    private int boatXMLSeqNumber = -1;

    /**
     * The sequence number of regatta XML messages.
     */
    private int regattaXMLSeqNumber = -1;


    public RaceServer(MockRace race, LatestMessages latestMessages) {
        this.race = race;
        this.latestMessages = latestMessages;
    }


    /**
     * Parses the race to create a snapshot, and places it in latestMessages.
     */
    public void parseSnapshot() {

        List<AC35Data> snapshotMessages = new ArrayList<>();

        //Parse the boat locations.
        snapshotMessages.addAll(parseBoatLocations());

        //Parse the marks.
        snapshotMessages.addAll(parseMarks());

        //Parse the race status.
        snapshotMessages.add(parseRaceStatus());

        //Parse collisions
        if(collisionEvents.size()>0){
            snapshotMessages.addAll(collisionEvents);
        }

        latestMessages.setSnapshot(snapshotMessages);

        updateXMLFiles();

        //Reset collision list
        collisionEvents.clear();
        //System.out.println(collisionEvents.size());

    }

    /**
     * Checks if the race/boat/regatta data sources have changed, and if they have, update their xml representations.
     */
    private void updateXMLFiles() {
        updateRaceXMLFile();
        updateBoatXMLFile();
        updateRegattaXMLFile();
    }

    /**
     * Checks if the race data source has changed, and if it has, updates LatestMessages' race xml message.
     */
    private void updateRaceXMLFile() {
        if (raceXMLSeqNumber != race.getRaceDataSource().getSequenceNumber()) {

            raceXMLSeqNumber = race.getRaceDataSource().getSequenceNumber();
            try {
                String raceXMLString = RaceDataSourceToXML.toString(race.getRaceDataSource());
                XMLMessage message = createXMLMessage(raceXMLString, XMLMessageType.RACE);
                latestMessages.setXMLMessage(message);

            } catch (JAXBException e) {
                Logger.getGlobal().log(Level.WARNING, "Could not serialise: " + race.getRaceDataSource(), e);
            }

        }
    }

    /**
     * Checks if the boat data source has changed, and if it has, updates LatestMessages' boat xml message.
     */
    private void updateBoatXMLFile() {
        if (boatXMLSeqNumber != race.getBoatDataSource().getSequenceNumber()) {

            boatXMLSeqNumber = race.getBoatDataSource().getSequenceNumber();
            try {
                String boatXMLString = BoatDataSourceToXML.toString(race.getBoatDataSource());
                XMLMessage message = createXMLMessage(boatXMLString, XMLMessageType.BOAT);
                latestMessages.setXMLMessage(message);

            } catch (JAXBException e) {
                Logger.getGlobal().log(Level.WARNING, "Could not serialise: " + race.getBoatDataSource(), e);
            }

        }
    }

    /**
     * Checks if the regatta data source has changed, and if it has, updates LatestMessages' regatta xml message.
     */
    private void updateRegattaXMLFile() {
        if (regattaXMLSeqNumber != race.getRegattaDataSource().getSequenceNumber()) {

            regattaXMLSeqNumber = race.getRegattaDataSource().getSequenceNumber();

            try {
                String regattaXMLString = RegattaDataSourceToXML.toString(race.getRegattaDataSource());
                XMLMessage message = createXMLMessage(regattaXMLString, XMLMessageType.REGATTA);
                latestMessages.setXMLMessage(message);

            } catch (JAXBException e) {
                Logger.getGlobal().log(Level.WARNING, "Could not serialise: " + race.getRegattaDataSource(), e);
            }

        }
    }

    /**
     * Parses an individual marker boat, and returns it.
     * @param mark The marker boat to parse.
     * @return The BoatLocation message.
     */
    private BoatLocation parseIndividualMark(Mark mark) {
        //Create message.
        BoatLocation boatLocation = new BoatLocation(
                mark.getSourceID(),
                mark.getPosition().getLatitude(),
                mark.getPosition().getLongitude(),
                this.boatLocationSequenceNumber,
                BoatLocationDeviceEnum.Mark,
                Bearing.fromDegrees(0),
                0,
                race.getRaceClock().getCurrentTimeMilli());

        //Iterates the sequence number.
        this.boatLocationSequenceNumber++;

        return boatLocation;
    }

    /**
     * Parse the compound marker boats, and returns a list of BoatLocation messages.
     * @return BoatLocation messages for each mark.
     */
    private List<BoatLocation> parseMarks() {

        List<BoatLocation> markLocations = new ArrayList<>(race.getCompoundMarks().size());

        for (CompoundMark compoundMark : race.getCompoundMarks()) {

            //Get the individual marks from the compound mark.
            Mark mark1 = compoundMark.getMark1();
            Mark mark2 = compoundMark.getMark2();

            //If they aren't null, parse them (some compound marks only have one mark).
            if (mark1 != null) {
                markLocations.add(this.parseIndividualMark(mark1));
            }

            if (mark2 != null) {
                markLocations.add(this.parseIndividualMark(mark2));
            }
        }

        return markLocations;
    }


    /**
     * Parse the boats in the race, and returns all of their BoatLocation messages.
     * @return List of BoatLocation messages, for each boat.
     */
    private List<BoatLocation> parseBoatLocations() {

        List<BoatLocation> boatLocations = new ArrayList<>(race.getBoats().size());

        //Parse each boat.
        for (MockBoat boat : race.getBoats()) {
            boatLocations.add(this.parseIndividualBoatLocation(boat));
        }

        return boatLocations;
    }

    /**
     * Parses an individual boat, and returns it.
     * @param boat The boat to parse.
     * @return The BoatLocation message.
     */
    private BoatLocation parseIndividualBoatLocation(MockBoat boat) {

        BoatLocation boatLocation = new BoatLocation(
                boat.getSourceID(),
                boat.getPosition().getLatitude(),
                boat.getPosition().getLongitude(),
                this.boatLocationSequenceNumber,
                BoatLocationDeviceEnum.RacingYacht,
                boat.getBearing(),
                boat.getCurrentSpeed(),
                race.getRaceClock().getCurrentTimeMilli());

        //Iterates the sequence number.
        this.boatLocationSequenceNumber++;

        return boatLocation;

    }



    /**
     * Parses the race status, and returns it.
     * @return The race status message.
     */
    private RaceStatus parseRaceStatus() {

        //A race status message contains a list of boat statuses.
        List<BoatStatus> boatStatuses = new ArrayList<>();

        //Add each boat status to the status list.
        for (MockBoat boat : race.getBoats()) {

            BoatStatus boatStatus = new BoatStatus(
                    boat.getSourceID(),
                    boat.getStatus(),
                    boat.getCurrentLeg().getLegNumber(),
                    boat.getEstimatedTimeAtNextMark().toInstant().toEpochMilli() );

            boatStatuses.add(boatStatus);
        }



        //Create race status object, and send it.
        RaceStatus raceStatus = new RaceStatus(
                RaceStatus.currentMessageVersionNumber,
                System.currentTimeMillis(),
                race.getRaceId(),
                race.getRaceStatusEnum(),
                race.getRaceClock().getStartingTimeMilli(),
                race.getWindDirection(),
                race.getWindSpeed(),
                race.getRaceType(),
                boatStatuses);

        return raceStatus;
    }




    /**
     * Creates an XMLMessage of a specified subtype using the xml contents string.
     * @param xmlString The contents of the xml file.
     * @param messageType The subtype of xml message (race, regatta, boat).
     * @return The created XMLMessage object.
     */
    private XMLMessage createXMLMessage(String xmlString, XMLMessageType messageType) {

        //Get the correct sequence number to use.
        int sequenceNumber = 0;
        if (messageType == XMLMessageType.RACE) {
            sequenceNumber = this.raceXMLSeqNumber;

        } else if (messageType == XMLMessageType.BOAT) {
            sequenceNumber = this.boatXMLSeqNumber;

        } else if (messageType == XMLMessageType.REGATTA) {
            sequenceNumber = this.regattaXMLSeqNumber;

        }

        //Create the message.
        XMLMessage message = new XMLMessage(
                XMLMessage.currentVersionNumber,
                AckSequencer.getNextAckNum(),
                System.currentTimeMillis(),
                messageType,
                sequenceNumber,
                xmlString);


        return message;
    }

    /**
     * Parse the yacht event and return it
     * @param boat yacht with event
     * @param event event that happened
     * @return yacht event
     */
    private YachtEvent parseYachtEvent(MockBoat boat, YachtEventEnum event){
        YachtEvent yachtEvent = new YachtEvent(
                System.currentTimeMillis(),
                this.boatLocationSequenceNumber,
                race.getRaceId(),
                boat.getSourceID(),
                1337,
                event);
        return yachtEvent;
    }

    public void parseBoatCollisions(ArrayList<MockBoat> boats){
        for (MockBoat boat : boats){
            collisionEvents.add(parseYachtEvent(boat, YachtEventEnum.COLLISION));
        }
    }

}
