package seng302.Mock;

import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import seng302.GPSCoordinate;
import seng302.Model.Leg;
import seng302.Model.Marker;
import seng302.XMLReader;

import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * Created by jjg64 on 21/04/17.
 */
public class StreamedCourseXMLReader extends XMLReader {
    private static final double COORDINATEPADDING = 0.000;
    private GPSCoordinate mapTopLeft, mapBottomRight;
    private final List<GPSCoordinate> boundary = new ArrayList<>();
    private final Map<Integer,Element> compoundMarks = new HashMap<>();
    private final Map<Integer, StreamedBoat> participants = new HashMap<>();
    private final List<Leg> legs = new ArrayList<>();
    private final List<Marker> markers = new ArrayList<>();
    private ZonedDateTime creationTimeDate;
    private ZonedDateTime raceStartTime;
    private int raceID;
    private String raceType;
    private boolean postpone;

    /**
     * Constructor for Streamed Race XML
     * @param filePath file path to read
     * @param read whether or not to read and store the files straight away.
     * @throws IOException error
     * @throws SAXException error
     * @throws ParserConfigurationException error
     * @throws StreamedCourseXMLException error
     */
    public StreamedCourseXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException, StreamedCourseXMLException {
        super(filePath);
        if (read) {
            read();
        }
    }

    /**
     * Constructor for Streamed Race XML
     * @param xmlString string to read
     * @throws IOException error
     * @throws SAXException error
     * @throws ParserConfigurationException error
     * @throws StreamedCourseXMLException error
     */
    public StreamedCourseXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException, StreamedCourseXMLException {
        super(xmlString);
        read();
    }

    /**
     * reads
     * @throws StreamedCourseXMLException error
     */
    private void read() throws StreamedCourseXMLException {
        readRace();
        readParticipants();
        readCourse();
    }

    /**
     * reads a race
     */
    private void readRace() {
        DateTimeFormatter dateFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
        Element settings = (Element) doc.getElementsByTagName("Race").item(0);
        NamedNodeMap raceTimeTag = doc.getElementsByTagName("RaceStartTime").item(0).getAttributes();

        if (raceTimeTag.getNamedItem("Time") != null) dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");


        raceID = Integer.parseInt(getTextValueOfNode(settings, "RaceID"));
        raceType = getTextValueOfNode(settings, "RaceType");

        creationTimeDate = ZonedDateTime.parse(getTextValueOfNode(settings, "CreationTimeDate"), dateFormat);

        if (raceTimeTag.getNamedItem("Time") != null) raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Time").getTextContent(), dateFormat);
        else raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Start").getTextContent(), dateFormat);

        postpone = Boolean.parseBoolean(raceTimeTag.getNamedItem("Postpone").getTextContent());
    }

    private void readParticipants() {
        Element nParticipants = (Element) doc.getElementsByTagName("Participants").item(0);
        nParticipants.getChildNodes().getLength();
        for (int i = 0; i < nParticipants.getChildNodes().getLength(); i++) {
            int sourceID;
            Node yacht = nParticipants.getChildNodes().item(i);
            if (yacht.getNodeName().equals("Yacht")) {
                if (exists(yacht, "SourceID")) {
                    sourceID = Integer.parseInt(yacht.getAttributes().getNamedItem("SourceID").getTextContent());
                    participants.put(sourceID, new StreamedBoat(sourceID));
                }
            }
        }
    }

    /**
     * reads a course
     * @throws StreamedCourseXMLException error
     */
    private void readCourse() throws StreamedCourseXMLException {
        readCompoundMarks();
        readCompoundMarkSequence();
        readCourseLimit();
    }

    /**
     * Indexes CompoundMark elements by their ID for use in generating the course, and populates list of Markers.
     * @throws StreamedCourseXMLException if a CompoundMark element contains an unhandled number of compoundMarks.
     * @see seng302.Model.Marker
     */
    private void readCompoundMarks() throws StreamedCourseXMLException {
        Element nCourse = (Element) doc.getElementsByTagName("Course").item(0);
        for(int i = 0; i < nCourse.getChildNodes().getLength(); i++) {
            Node compoundMark = nCourse.getChildNodes().item(i);
            if(compoundMark.getNodeName().equals("CompoundMark")) {
                int compoundMarkID = getCompoundMarkID((Element) compoundMark);
                compoundMarks.put(compoundMarkID, (Element)compoundMark);
                markers.add(getMarker(compoundMarkID));
            }
        }
    }

    /**
     * Generates a Marker from the CompoundMark element with given ID.
     * @param compoundMarkID index of required CompoundMark element
     * @return generated Marker
     * @throws StreamedCourseXMLException if CompoundMark element contains unhandled number of compoundMarks
     * @see seng302.Model.Marker
     */
    private Marker getMarker(int compoundMarkID) throws StreamedCourseXMLException {
        Element compoundMark = compoundMarks.get(compoundMarkID);
        NodeList nMarks = compoundMark.getElementsByTagName("Mark");
        Marker marker;

        switch(nMarks.getLength()) {
            case 1: marker = new Marker(getCoordinate((Element)nMarks.item(0)),getSourceId((Element)nMarks.item(0))); break;
            case 2: marker = new Marker(getCoordinate((Element)nMarks.item(0)), getCoordinate((Element)nMarks.item(1)),
                    getSourceId((Element)nMarks.item(0)), getSourceId((Element)nMarks.item(1))); break;
            default: throw new StreamedCourseXMLException();
        }

        return marker;
    }

    private GPSCoordinate getCoordinate(Element mark) {
        double lat = Double.parseDouble(mark.getAttribute("TargetLat"));
        double lon = Double.parseDouble(mark.getAttribute("TargetLng"));
        return new GPSCoordinate(lat,lon);
    }

    private int getSourceId(Element mark) {
        String sourceId = mark.getAttribute("SourceID");
        if (sourceId.isEmpty()){
            return 0;
        }
        return Integer.parseInt(sourceId);
    }

    /**
     * Reads "compoundMarkID" attribute of CompoundMark or Corner element
     * @param element with "compoundMarkID" attribute
     * @return value of "compoundMarkID" attribute
     */
    private int getCompoundMarkID(Element element) {
        return Integer.parseInt(element.getAttribute("CompoundMarkID"));
    }

    /**
     * Reads "name" attribute of CompoundMark element with corresponding CompoundMarkID
     * @param compoundMarkID unique ID for CompoundMark element
     * @return value of "name" attribute
     */
    private String getCompoundMarkName(int compoundMarkID) {
        return compoundMarks.get(compoundMarkID).getAttribute("Name");
    }

    /**
     * Populates list of legs given CompoundMarkSequence element and referenced CompoundMark elements.
     * @throws StreamedCourseXMLException if markers cannot be resolved from CompoundMark
     */
    private void readCompoundMarkSequence() throws StreamedCourseXMLException {
        Element nCompoundMarkSequence = (Element) doc.getElementsByTagName("CompoundMarkSequence").item(0);
        NodeList nCorners = nCompoundMarkSequence.getElementsByTagName("Corner");
        Element markXML = (Element)nCorners.item(0);
        Marker lastMarker = getMarker(getCompoundMarkID(markXML));
        String legName = getCompoundMarkName(getCompoundMarkID(markXML));
        for(int i = 1; i < nCorners.getLength(); i++) {
            markXML = (Element)nCorners.item(i);
            Marker currentMarker = getMarker(getCompoundMarkID(markXML));
            legs.add(new Leg(legName, lastMarker, currentMarker, i-1));
            lastMarker = currentMarker;
            legName = getCompoundMarkName(getCompoundMarkID(markXML));
        }
    }

    private void readCourseLimit() {
        Element nCourseLimit = (Element) doc.getElementsByTagName("CourseLimit").item(0);
        for(int i = 0; i < nCourseLimit.getChildNodes().getLength(); i++) {
            Node limit = nCourseLimit.getChildNodes().item(i);
            if (limit.getNodeName().equals("Limit")) {
                double lat = Double.parseDouble(limit.getAttributes().getNamedItem("Lat").getTextContent());
                double lon = Double.parseDouble(limit.getAttributes().getNamedItem("Lon").getTextContent());
                boundary.add(new GPSCoordinate(lat, lon));
            }
        }


        double maxLatitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude() + COORDINATEPADDING;
        double maxLongitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude() + COORDINATEPADDING;
        double minLatitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude() + COORDINATEPADDING;
        double minLongitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude() + COORDINATEPADDING;

        mapTopLeft = new GPSCoordinate(minLatitude, minLongitude);
        mapBottomRight = new GPSCoordinate(maxLatitude, maxLongitude);
    }

    public List<GPSCoordinate> getBoundary() {
        return boundary;
    }

    public GPSCoordinate getMapTopLeft() {
        return mapTopLeft;
    }

    public GPSCoordinate getMapBottomRight() {
        return mapBottomRight;
    }

    public List<Leg> getLegs() {
        return legs;
    }

    public List<Marker> getMarkers() { return markers; }

    public Double getPadding() {
        return COORDINATEPADDING;
    }

    public ZonedDateTime getRaceStartTime() {
        return raceStartTime;
    }

    public int getRaceID() {
        return raceID;
    }

    public String getRaceType() {
        return raceType;
    }

    public boolean isPostpone() {
        return postpone;
    }

    public Map<Integer, StreamedBoat> getParticipants() {
        return participants;
    }
}
