/*
    This file is part of RouteConverter.

    RouteConverter is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    RouteConverter is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with RouteConverter; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    Copyright (C) 2007 Christian Pesch. All Rights Reserved.
*/

package slash.navigation.mm;

import slash.navigation.*;
import slash.common.io.CompactCalendar;
import slash.common.io.Transfer;

import javax.xml.bind.JAXBException;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
import javax.xml.stream.events.*;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

/**
 * Reads and writes MagicMaps Project (.ikt) files.
 *
 * @author Christian Pesch
 */

public class MagicMapsIktFormat extends XmlNavigationFormat<MagicMapsIktRoute> implements MultipleRoutesFormat<MagicMapsIktRoute> {
    private static final Logger log = Logger.getLogger(MagicMapsIktFormat.class.getName());
    private static final String ROOT_ELEMENT = "Root";
    private static final String FILE_FORMAT_ATTRIBUTE = "FileFormat";
    private static final String FILE_FORMAT = "IK3D-Project-File";
    private static final String HEADER_ELEMENT = "Header";
    private static final String PROJECT_NAME_ELEMENT = "Projectname";
    private static final String DESCRIPTION_ELEMENT = "description";
    private static final String CONTENT_ELEMENT = "Content";
    private static final String GEO_OBJECTS_ELEMENT = "MMGeoObjects";
    private static final String COUNT_ELEMENT = "count";
    private static final String GEO_OBJECT_ELEMENT = "MMGeoObject";
    private static final String GEO_OBJECT_TYPE_ELEMENT = "GeoObjectType";
    private static final String NAME_ELEMENT = "Name";
    private static final String PATH_DRAW_TYPE_ELEMENT = "PathDrawType";
    private static final String PATH_POINTS_ELEMENT = "PathPoints";
    private static final String POINT_ELEMENT = "Point";
    private static final String GEO_POSITION_ELEMENT = "GeoPosition";
    private static final String X_ATTRIBUTE = "X";
    private static final String Y_ATTRIBUTE = "Y";


    public String getExtension() {
        return ".ikt";
    }

    public String getName() {
        return "MagicMaps Project (*" + getExtension() + ")";
    }

    public int getMaximumPositionCount() {
        return UNLIMITED_MAXIMUM_POSITION_COUNT;
    }

    public boolean isSupportsMultipleRoutes() {
        return true;
    }

    public <P extends BaseNavigationPosition> MagicMapsIktRoute createRoute(RouteCharacteristics characteristics, String name, List<P> positions) {
        return new MagicMapsIktRoute(name, null, (List<Wgs84Position>) positions);
    }

    private Double extractValue(StartElement startElement, String attributeName) {
        Attribute attribute = startElement.getAttributeByName(new QName(attributeName));
        if (attribute == null)
            return null;
        return Transfer.parseDouble(attribute.getValue());
    }

    private String extractValue(Characters chars, String value) {
        String append = Transfer.trim(chars.getData());
        if (append == null)
            return value;
        if (value == null)
            return append;
        return value + append;
    }

    private Wgs84Position processPosition(StartElement startElement) {
        return new Wgs84Position(extractValue(startElement, X_ATTRIBUTE), extractValue(startElement, Y_ATTRIBUTE),
                null, null, null, null);
    }

    private List<MagicMapsIktRoute> process(XMLEventReader eventReader) throws XMLStreamException {
        boolean hasValidRoot = false, nextIsProjectName = false, nextIsDescription = false, nextIsName = false;
        String projectName = null, description = null, name = null;
        List<MagicMapsIktRoute> routes = new ArrayList<MagicMapsIktRoute>();
        List<Wgs84Position> positions = null;

        while (eventReader.hasNext()) {
            XMLEvent event = eventReader.nextEvent();
            if (event.getEventType() == XMLStreamConstants.CHARACTERS) {
                Characters chars = event.asCharacters();

                if (nextIsProjectName)
                    projectName = extractValue(chars, projectName);
                if (nextIsDescription)
                    description = extractValue(chars, description);
                if (nextIsName)
                    name = extractValue(chars, name);
            }
            if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
                StartElement startElement = event.asStartElement();
                String elementName = startElement.getName().getLocalPart();

                // detect by having first element to be <Root FileFormat="IK3D-Project-File">
                if (!hasValidRoot) {
                    if (!(ROOT_ELEMENT.equals(elementName)))
                        return null;
                    Attribute fileFormat = startElement.getAttributeByName(new QName(FILE_FORMAT_ATTRIBUTE));
                    if (fileFormat == null || !FILE_FORMAT.equals(fileFormat.getValue()))
                        return null;

                    hasValidRoot = true;

                } else if (elementName.startsWith(GEO_OBJECT_ELEMENT)) {
                    name = null;
                    positions = new ArrayList<Wgs84Position>();
                } else if (elementName.startsWith(GEO_POSITION_ELEMENT)) {
                    positions.add(processPosition(startElement));
                }

                nextIsProjectName = PROJECT_NAME_ELEMENT.equals(elementName);
                nextIsDescription = DESCRIPTION_ELEMENT.equals(elementName);
                nextIsName = NAME_ELEMENT.equals(elementName);
            }
            if (event.getEventType() == XMLStreamConstants.END_ELEMENT) {
                EndElement endElement = event.asEndElement();
                String elementName = endElement.getName().getLocalPart();

                if (ROOT_ELEMENT.equals(elementName)) {
                    return routes;
                } else if (elementName.startsWith(GEO_OBJECTS_ELEMENT)) {
                    // ignore these
                } else if (elementName.startsWith(GEO_OBJECT_ELEMENT)) {
                    if (name == null)
                        name = projectName;
                    routes.add(new MagicMapsIktRoute(this, name, asDescription(description), positions));
                }
            }
        }
        return null;
    }

    private List<MagicMapsIktRoute> unmarshal(InputStream in) throws JAXBException {
        try {
            XMLInputFactory factory = XMLInputFactory.newInstance();
            XMLEventReader eventReader = factory.createXMLEventReader(in, UTF8_ENCODING);
            return process(eventReader);
        } catch (XMLStreamException e) {
            throw new JAXBException("Parse error from " + in + ": " + e.getMessage());
        }
    }

    public List<MagicMapsIktRoute> read(InputStream source, CompactCalendar startDate) throws IOException {
        try {
            List<MagicMapsIktRoute> routes = unmarshal(source);
            if (routes != null && routes.size() > 0)
                return routes;
        } catch (JAXBException e) {
            log.fine("Error reading " + source + ": " + e.getMessage());
        }
        return null;
    }

    private void writeHeader(String name, String description, XMLEventWriter writer, XMLEventFactory eventFactory) throws XMLStreamException {
        writer.add(eventFactory.createStartDocument(UTF8_ENCODING, "1.0", true));
        List<Attribute> rootAttributes = new ArrayList<Attribute>();
        rootAttributes.add(eventFactory.createAttribute(FILE_FORMAT_ATTRIBUTE, FILE_FORMAT));
        writer.add(eventFactory.createStartElement(new QName(ROOT_ELEMENT), rootAttributes.iterator(), null));

        writer.add(eventFactory.createStartElement(new QName(HEADER_ELEMENT), null, null));
        writer.add(eventFactory.createStartElement(new QName(PROJECT_NAME_ELEMENT), null, null));
        writer.add(eventFactory.createCharacters(name));
        writer.add(eventFactory.createEndElement(new QName(PROJECT_NAME_ELEMENT), null));
        writer.add(eventFactory.createStartElement(new QName(DESCRIPTION_ELEMENT), null, null));
        writer.add(eventFactory.createCData(description));
        writer.add(eventFactory.createEndElement(new QName(DESCRIPTION_ELEMENT), null));
        writer.add(eventFactory.createEndElement(new QName(HEADER_ELEMENT), null));

        writer.add(eventFactory.createStartElement(new QName(CONTENT_ELEMENT), null, null));
        writer.add(eventFactory.createStartElement(new QName(GEO_OBJECTS_ELEMENT), null, null));
    }

    private void writePosition(Wgs84Position position, int index, XMLEventWriter writer, XMLEventFactory eventFactory) throws XMLStreamException {
        writer.add(eventFactory.createStartElement(new QName(POINT_ELEMENT + "_" + index), null, null));

        List<Attribute> attributes = new ArrayList<Attribute>();
        attributes.add(eventFactory.createAttribute(X_ATTRIBUTE, Transfer.formatPositionAsString(position.getLongitude())));
        attributes.add(eventFactory.createAttribute(Y_ATTRIBUTE, Transfer.formatPositionAsString(position.getLatitude())));
        writer.add(eventFactory.createStartElement(new QName(GEO_POSITION_ELEMENT), attributes.iterator(), null));
        writer.add(eventFactory.createEndElement(new QName(GEO_POSITION_ELEMENT), null));

        writer.add(eventFactory.createEndElement(new QName(POINT_ELEMENT + "_" + index), null));
    }

    private void writeRoute(MagicMapsIktRoute route, int index, XMLEventWriter writer, XMLEventFactory eventFactory) throws XMLStreamException {
        writer.add(eventFactory.createStartElement(new QName(GEO_OBJECT_ELEMENT + "_" + index), null, null));

        writer.add(eventFactory.createStartElement(new QName(GEO_OBJECT_TYPE_ELEMENT), null, null));
        writer.add(eventFactory.createCharacters("1"));
        writer.add(eventFactory.createEndElement(new QName(GEO_OBJECT_TYPE_ELEMENT), null));

        writer.add(eventFactory.createStartElement(new QName(NAME_ELEMENT), null, null));
        writer.add(eventFactory.createCharacters(route.getName()));
        writer.add(eventFactory.createEndElement(new QName(NAME_ELEMENT), null));

        writer.add(eventFactory.createStartElement(new QName(PATH_DRAW_TYPE_ELEMENT), null, null));
        writer.add(eventFactory.createCharacters("5"));
        writer.add(eventFactory.createEndElement(new QName(PATH_DRAW_TYPE_ELEMENT), null));

        writer.add(eventFactory.createStartElement(new QName(PATH_POINTS_ELEMENT), null, null));

        List<Wgs84Position> positions = route.getPositions();
        writer.add(eventFactory.createStartElement(new QName(COUNT_ELEMENT), null, null));
        writer.add(eventFactory.createCharacters(Transfer.formatIntAsString(positions.size())));
        writer.add(eventFactory.createEndElement(new QName(COUNT_ELEMENT), null));

        for (int i = 0; i < positions.size(); i++) {
            writePosition(positions.get(i), i, writer, eventFactory);
        }

        writer.add(eventFactory.createEndElement(new QName(PATH_POINTS_ELEMENT), null));
        writer.add(eventFactory.createEndElement(new QName(GEO_OBJECT_ELEMENT + "_" + index), null));
    }

    private void writeFooter(XMLEventWriter writer, XMLEventFactory eventFactory) throws XMLStreamException {
        writer.add(eventFactory.createEndElement(new QName(GEO_OBJECTS_ELEMENT), null));
        writer.add(eventFactory.createEndElement(new QName(CONTENT_ELEMENT), null));
        writer.add(eventFactory.createEndElement(new QName(ROOT_ELEMENT), null));
        writer.add(eventFactory.createEndDocument());
    }

    public void write(MagicMapsIktRoute route, OutputStream target, int startIndex, int endIndex) throws IOException {
        write(Arrays.asList(route), target);
    }

    private String getProjectName(List<MagicMapsIktRoute> routes) {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < routes.size(); i++) {
            MagicMapsIktRoute route = routes.get(i);
            buffer.append(route.getName());
            if (i < routes.size() - 1)
                buffer.append(", ");
        }
        return buffer.toString();
    }

    private String getDescription(List<MagicMapsIktRoute> routes) {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < routes.size(); i++) {
            MagicMapsIktRoute route = routes.get(i);
            List<String> descriptions = route.getDescription();
            if (descriptions == null)
                continue;
            for (int j = 0; j < descriptions.size(); j++) {
                String description = descriptions.get(j);
                buffer.append(description);
                if (j < descriptions.size() - 1)
                    buffer.append(", ");
            }
            if (i < routes.size() - 1)
                buffer.append("; ");
        }
        return buffer.toString();
    }

    public void write(List<MagicMapsIktRoute> routes, OutputStream target) throws IOException {
        try {
            XMLEventFactory eventFactory = XMLEventFactory.newInstance();
            XMLOutputFactory output = XMLOutputFactory.newInstance();
            XMLEventWriter writer = output.createXMLEventWriter(target, UTF8_ENCODING);

            try {
                writeHeader(getProjectName(routes), getDescription(routes), writer, eventFactory);

                writer.add(eventFactory.createStartElement(new QName(COUNT_ELEMENT), null, null));
                writer.add(eventFactory.createCharacters(Transfer.formatIntAsString(routes.size())));
                writer.add(eventFactory.createEndElement(new QName(COUNT_ELEMENT), null));

                for (int i = 0; i < routes.size(); i++) {
                    writeRoute(routes.get(i), i, writer, eventFactory);
                }

                writeFooter(writer, eventFactory);
            }
            finally {
                writer.flush();
                writer.close();
                target.flush();
                target.close();
            }
        } catch (XMLStreamException e) {
            throw new IOException("Error while marshalling: " + e.getMessage());
        }
    }
}