package nl.eveoh.scheduleviewer.export;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import net.fortuna.ical4j.data.AbstractOutputter;
import net.fortuna.ical4j.model.*;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.model.property.*;

import edu.emory.mathcs.backport.java.util.Arrays;

/**
 * XCAL output for iCal4j. Generates an XCAL file (according to
 * draft-daboo-et-al-icalendar-in-xml-07) from an iCal4j object model. This
 * class is still beta and not fully tested.
 * 
 * @author Mike Noordermeer
 * 
 */
public class XmlOutputter extends AbstractOutputter {

    public XmlOutputter() {
        super();
    }

    public XmlOutputter(final boolean validating) {
        super(validating);
    }

    public XmlOutputter(final boolean validating, final int foldLength) {
        super(validating, foldLength);
    }

    /**
     * Outputs an iCalender string to the specified output stream.
     * 
     * @param calendar
     *            calendar to write to ouput stream
     * @param out
     *            an output stream
     * @throws IOException
     *             thrown when unable to write to output stream
     * @throws ValidationException
     *             where calendar validation fails
     */
    public final void output(final Calendar calendar, final OutputStream out) throws IOException, ValidationException {
        output(calendar, new OutputStreamWriter(out, DEFAULT_CHARSET));
    }

    /**
     * Outputs an iCalender string to the specified writer.
     * 
     * @param calendar
     *            calendar to write to writer
     * @param out
     *            a writer
     * @throws IOException
     *             thrown when unable to write to writer
     * @throws ValidationException
     *             where calendar validation fails
     */
    public final void output(final Calendar calendar, final Writer out) throws IOException, ValidationException {
        if (isValidating()) {
            calendar.validate();
        }

        XMLStreamWriter xmlWriter = null;

        try {
            xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(out);
            xmlWriter.writeStartDocument();

            xmlWriter.writeStartElement("icalendar");
            xmlWriter.writeDefaultNamespace("urn:ietf:params:xml:ns:icalendar-2.0");

            writeCalendar(xmlWriter, calendar);

            xmlWriter.writeEndElement();

            xmlWriter.writeEndDocument();
            xmlWriter.flush();
            xmlWriter.close();
        } catch (XMLStreamException ex) {
            throw new IOException("Could not write XML stream", ex);
        } finally {
            if (xmlWriter != null)
                try {
                    xmlWriter.close();
                } catch (XMLStreamException ex) {
                }
        }

    }

    private void writeCalendar(XMLStreamWriter xmlWriter, Calendar calendar) throws XMLStreamException {
        xmlWriter.writeStartElement("vcalendar");

        writeProperties(xmlWriter, calendar.getProperties());
        writeComponents(xmlWriter, calendar.getComponents());

        xmlWriter.writeEndElement();
    }

    private void writeComponents(XMLStreamWriter xmlWriter, ComponentList components) throws XMLStreamException {
        if (components.size() > 0) {
            xmlWriter.writeStartElement("components");

            for (Object o : components) {
                Component c = (Component) o;

                writeComponent(xmlWriter, c);
            }

            xmlWriter.writeEndElement();
        }
    }

    private void writeComponent(XMLStreamWriter xmlWriter, Component c) throws XMLStreamException {
        xmlWriter.writeStartElement(c.getName().toLowerCase());

        writeProperties(xmlWriter, c.getProperties());

        if (c instanceof VTimeZone) {
            writeComponents(xmlWriter, ((VTimeZone) c).getObservances());
        }

        xmlWriter.writeEndElement();
    }

    private void writeProperties(XMLStreamWriter xmlWriter, PropertyList properties) throws XMLStreamException {
        if (properties.size() > 0) {
            xmlWriter.writeStartElement("properties");

            for (Object o : properties) {
                Property p = (Property) o;

                writeProperty(xmlWriter, p);
            }

            xmlWriter.writeEndElement();
        }
    }

    @SuppressWarnings("unchecked")
    private void writeProperty(XMLStreamWriter xmlWriter, Property p) throws XMLStreamException {
        xmlWriter.writeStartElement(p.getName().toLowerCase());

        Class<?> propertyClass = p.getClass();

        Parameter valueParam = p.getParameter(Parameter.VALUE);

        List<Class<?>> calAddressClasses = Arrays.asList(new Class[] { Attendee.class, Organizer.class });
        List<Class<?>> dateTimeClasses = Arrays.asList(new Class[] { LastModified.class, DtStamp.class, DtStart.class,
                DtEnd.class, Created.class, Completed.class });
        List<Class<?>> durationClasses = Arrays.asList(new Class[] { Duration.class, FreeBusy.class });
        List<Class<?>> integerClasses = Arrays.asList(new Class[] { PercentComplete.class, Priority.class,
                Sequence.class, Repeat.class });
        List<Class<?>> textClasses = Arrays.asList(new Class[] { BusyType.class, CalScale.class, Comment.class,
                ExtendedAddress.class, Locality.class, Location.class, LocationType.class, Method.class, Name.class,
                Postalcode.class, ProdId.class, Clazz.class, Description.class, Country.class, Resources.class,
                StreetAddress.class, Status.class, Summary.class, Version.class, XProperty.class, Uid.class,
                TzName.class, TzId.class, Transp.class, Tel.class, RequestStatus.class, RelatedTo.class, Region.class,
                Action.class, Contact.class });
        List<Class<?>> uriClasses = Arrays.asList(new Class[] { TzUrl.class, Url.class });
        List<Class<?>> utcOffsetClasses = Arrays.asList(new Class[] { TzOffsetTo.class, TzOffsetFrom.class });

        if (calAddressClasses.contains(propertyClass)) {
            writeCharacters(xmlWriter, "cal-address", p.getValue());
        } else if (p instanceof Categories) {
            writeTextList(xmlWriter, ((Categories) p).getCategories());
        } else if (p instanceof ExDate) {
            if (Value.DATE.equals(valueParam)) {
                writeList(xmlWriter, "date", ((ExDate) p).getDates());
            } else {
                writeList(xmlWriter, "date-time", ((ExDate) p).getDates());
            }
        } else if (p instanceof RDate) {
            if (((RDate) p).getPeriods() != null) {
                writeList(xmlWriter, "period", ((RDate) p).getPeriods());
            } else if (Value.DATE.equals(valueParam)) {
                writeList(xmlWriter, "date", ((RDate) p).getDates());
            } else {
                writeList(xmlWriter, "date-time", ((RDate) p).getDates());
            }
        } else if (Value.DATE.equals(valueParam)) {
            writeCharacters(xmlWriter, "date", p.getValue());
        } else if (Value.DATE_TIME.equals(valueParam) || dateTimeClasses.contains(propertyClass)) {
            writeCharacters(xmlWriter, "date-time", p.getValue());
        } else if (durationClasses.contains(propertyClass)) {
            writeCharacters(xmlWriter, "duration", p.getValue());
        } else if (integerClasses.contains(propertyClass)) {
            writeCharacters(xmlWriter, "integer", p.getValue());
        } else if (Value.PERIOD.equals(valueParam)) {
            writeCharacters(xmlWriter, "period", p.getValue());
        } else if (textClasses.contains(propertyClass)) {
            writeCharacters(xmlWriter, "text", p.getValue());
        } else if (uriClasses.contains(propertyClass)) {
            writeCharacters(xmlWriter, "uri", p.getValue());
        } else if (utcOffsetClasses.contains(propertyClass)) {
            writeCharacters(xmlWriter, "utc-offset", p.getValue());
        } else if (p instanceof Geo) {
            xmlWriter.writeStartElement("value");
            writeCharacters(xmlWriter, "latitude", String.valueOf(((Geo) p).getLatitude()));
            writeCharacters(xmlWriter, "longitude", String.valueOf(((Geo) p).getLongitude()));
            xmlWriter.writeEndElement();
        } else if (p instanceof Attach) {
            if (Value.BINARY.equals(valueParam)) {
                writeCharacters(xmlWriter, "binary", p.getValue());
            } else {
                writeCharacters(xmlWriter, "uri", p.getValue());
            }
        } else if (p instanceof Trigger) {
            if (Value.DATE_TIME.equals(valueParam)) {
                writeCharacters(xmlWriter, "date-time", p.getValue());
            } else {
                writeCharacters(xmlWriter, "duration", p.getValue());
            }
        } else if (p instanceof RRule || p instanceof ExRule) {
            xmlWriter.writeStartElement("recur");

            Recur r = null;

            if (p instanceof RRule) {
                r = ((RRule) p).getRecur();
            } else if (p instanceof ExRule) {
                r = ((ExRule) p).getRecur();
            }

            writeCharacters(xmlWriter, "freq", r.getFrequency());
            if (r.getWeekStartDay() != null) {
                writeCharacters(xmlWriter, "wkst", r.getWeekStartDay());
            }
            if (r.getUntil() != null) {
                writeCharacters(xmlWriter, "until", r.getUntil().toString());
            }
            if (r.getCount() >= 1) {
                writeCharacters(xmlWriter, "count", String.valueOf(r.getCount()));
            }
            if (r.getInterval() >= 1) {
                writeCharacters(xmlWriter, "interval", String.valueOf(r.getInterval()));
            }
            if (!r.getMonthList().isEmpty()) {
                writeList(xmlWriter, "bymonth", r.getMonthList());
            }
            if (!r.getWeekNoList().isEmpty()) {
                writeList(xmlWriter, "byweekno", r.getWeekNoList());
            }
            if (!r.getYearDayList().isEmpty()) {
                writeList(xmlWriter, "byyearday", r.getYearDayList());
            }
            if (!r.getMonthDayList().isEmpty()) {
                writeList(xmlWriter, "bymonthday", r.getMonthDayList());
            }
            if (!r.getDayList().isEmpty()) {
                writeList(xmlWriter, "byday", r.getDayList());
            }
            if (!r.getHourList().isEmpty()) {
                writeList(xmlWriter, "byhour", r.getHourList());
            }
            if (!r.getMinuteList().isEmpty()) {
                writeList(xmlWriter, "byminute", r.getMinuteList());
            }
            if (!r.getSecondList().isEmpty()) {
                writeList(xmlWriter, "bysecond", r.getSecondList());
            }
            if (!r.getSetPosList().isEmpty()) {
                writeList(xmlWriter, "bysetpos", r.getSetPosList());
            }
            xmlWriter.writeEndElement();
        } else {
            // Last resort
            writeCharacters(xmlWriter, "text", p.getValue());
        }

        writeParameters(xmlWriter, p.getParameters());

        xmlWriter.writeEndElement();
    }

    private void writeParameters(XMLStreamWriter xmlWriter, ParameterList parameters) throws XMLStreamException {
        boolean startWritten = false;

        @SuppressWarnings("unchecked")
        Iterator<Parameter> i = parameters.iterator();

        while (i.hasNext()) {
            Parameter p = i.next();

            // Skip value parameters, they are indicated differently in XCAL
            if (Parameter.VALUE.equals(p)) {
                continue;
            }

            if (!startWritten) {
                xmlWriter.writeStartElement("parameters");
                startWritten = true;
            }

            writeParameter(xmlWriter, p);
        }

        if (startWritten)
            xmlWriter.writeEndElement();
    }

    private void writeParameter(XMLStreamWriter xmlWriter, Parameter p) throws XMLStreamException {
        writeCharacters(xmlWriter, p.getName().toLowerCase(), p.getValue());
    }

    private void writeTextList(XMLStreamWriter xmlWriter, TextList list) throws XMLStreamException {
        @SuppressWarnings("unchecked")
        Iterator<String> i = list.iterator();
        while (i.hasNext()) {
            writeCharacters(xmlWriter, "text", i.next());
        }
    }

    private void writeList(XMLStreamWriter xmlWriter, String element, Collection<?> list) throws XMLStreamException {
        for (Object o : list) {
            writeCharacters(xmlWriter, element, o.toString());
        }
    }

    private void writeCharacters(XMLStreamWriter xmlWriter, String element, String characters)
            throws XMLStreamException {
        xmlWriter.writeStartElement(element);
        xmlWriter.writeCharacters(characters);
        xmlWriter.writeEndElement();
    }
}

