package seng302.Data;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import seng302.DataInput.RaceDataSource;
import seng302.Exceptions.InvalidRaceDataException;
import seng302.Model.Boat;
import seng302.Model.GPSCoordinate;
import seng302.Model.Marker;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Created by esa46 on 21/04/17.
 */
public class RaceData {

    private RaceDataSource dataSource;
    private Document doc;
    private Element rootElement;
    private ZonedDateTime creationTimeDate;

    public RaceData(RaceDataSource dataSource) {
        this.dataSource = dataSource;
        creationTimeDate = ZonedDateTime.now();
    }

    /**
     * Creates an AC35 officially formatted xml description of a race.
     *
     * @return String containing xml-formatted race description
     */
    public String createXML() {

        try {

            //create base xml document
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
            doc = docBuilder.newDocument();

            //create root element (In this case, "Race")
            rootElement = doc.createElement("Race");
            doc.appendChild(rootElement);

            appendChildElements();

            // write the content into xml file
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            DOMSource source = new DOMSource(doc);

            //Serialize document.
            StringWriter stringWriter = new StringWriter();
            StreamResult result = new StreamResult(stringWriter);
            transformer.transform(source, result);

            return stringWriter.toString();

        } catch (ParserConfigurationException pce) {
            throw new InvalidRaceDataException();
        } catch (TransformerException tfe) {
            throw new InvalidRaceDataException();
        }
    }

    /**
     * Creates all necessary child elements and appends them to the xml doc
     */
    private void appendChildElements() {
        appendRaceId();
        appendRaceType();
        appendCreationTimeDate();
        appendRaceStartTime();
        appendParticipants();
        appendCourse();
        appendCourseLimit();
    }

    /**
     * Creates and appends race id element
     */
    private void appendRaceId() {
        Element raceIdElement = doc.createElement("RaceID");
        raceIdElement.appendChild(doc.createTextNode(Integer.toString(dataSource.getRaceId())));
        rootElement.appendChild(raceIdElement);
    }

    /**
     * Creates and appends race type element
     */
    private void appendRaceType() {
        Element raceTypeElement = doc.createElement("RaceType");
        raceTypeElement.appendChild(doc.createTextNode(dataSource.getRaceType()));
        rootElement.appendChild(raceTypeElement);
    }

    /**
     * Creates and appends creation time date element
     */
    private void appendCreationTimeDate() {
        Element creationTimeElement = doc.createElement("CreationTimeDate");


        creationTimeElement.appendChild(doc.createTextNode(toTruncatedString(creationTimeDate)));
        rootElement.appendChild(creationTimeElement);
    }

    /**
     * Creates and appends race start time element, which is 3 minutes after the race was created by default
     */
    private void appendRaceStartTime() {
        Element startTimeElement = doc.createElement("RaceStartTime");
        startTimeElement.setAttribute("Time", (toTruncatedString(creationTimeDate.plusMinutes(0))));
        startTimeElement.setAttribute("Postpone", "false");
        rootElement.appendChild(startTimeElement);
    }

    /**
     * Creates and appends participants element
     */
    private void appendParticipants() {
        Element participantsElement = doc.createElement("Participants");

        for (Boat boat : dataSource.getBoats()) {
            Element yachtElement = doc.createElement("Yacht");
            yachtElement.setAttribute("SourceID", boat.getSourceID() + "");
            participantsElement.appendChild(yachtElement);
        }
        rootElement.appendChild(participantsElement);
    }

    /**
     * Creates and appends course elements
     */
    private void appendCourse() {
        Element courseElement = doc.createElement("Course");
        Element compoundMarkSeqElement = doc.createElement("CompoundMarkSequence");

        int i = 1;
        for (Marker marker : dataSource.getMarkers()) {

            courseElement.appendChild(createCompoundMarker(marker, i));
            compoundMarkSeqElement.appendChild(createCornerElement(i));
            i++;
        }

        rootElement.appendChild(compoundMarkSeqElement);
        rootElement.appendChild(courseElement);
    }


    /**
     * Creates a mark element for insertion in a coumpound mark element
     *
     * @param marker GPS coordinates of the mark
     * @return Element mark element
     */
    private Element createMark(GPSCoordinate marker) {
        Element mark = doc.createElement("Mark");
        mark.setAttribute("TargetLat", marker.getLatitude() + "");
        mark.setAttribute("TargetLng", marker.getLongitude() + "");
        return mark;
    }

    /**
     * Creates a compound marker holding one or two marks,and a sequence number
     *
     * @param marker marker
     * @param i      sequence number
     * @return Element compound mark element
     */
    private Element createCompoundMarker(Marker marker, int i) {
        Element compoundMarkElement = doc.createElement("CompoundMark");
        compoundMarkElement.setAttribute("CompoundMarkID", i + "");
        compoundMarkElement.setAttribute("Name", marker.getName());

        compoundMarkElement.appendChild(createMark(marker.getMark1()));

        if (!(marker.getMark1().equals(marker.getMark2()))) {
            compoundMarkElement.appendChild(createMark(marker.getMark2()));
        }

        return compoundMarkElement;
    }

    /**
     * Creates a corner element
     *
     * @param i sequence number
     * @return Element corner element
     */
    private Element createCornerElement(int i) {
        Element cornerElement = doc.createElement("Corner");
        cornerElement.setAttribute("SeqID", i + "");
        cornerElement.setAttribute("CompoundMarkID", i + "");

        return cornerElement;
    }

    /**
     * Creates and appends course limits element (boundaries)
     */
    private void appendCourseLimit() {
        int j = 1;

        Element courseLimitElement = doc.createElement("CourseLimit");
        for (GPSCoordinate coordinate : dataSource.getBoundary()) {
            Element limitElement = doc.createElement("Limit");
            limitElement.setAttribute("SeqID", j + "");
            limitElement.setAttribute("Lat", coordinate.getLatitude() + "");
            limitElement.setAttribute("Lon", coordinate.getLongitude() + "");

            courseLimitElement.appendChild(limitElement);

            j++;
        }

        rootElement.appendChild(courseLimitElement);
    }

    /**
     * Format time data and return it.
     * @param time time data.
     * @return formatted time data.
     */
    private String toTruncatedString(ZonedDateTime time) {

        DateTimeFormatter dateFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
        dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
        String text = dateFormat.format(time);
        return text;


    }

}
