package com.jdblabs.timestamper.core;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.ParseException;
import java.util.Date;
import java.util.Scanner;
import java.util.UUID;

/**
 *
 * @author Jonathan Bernard ({@literal jdbernard@gmail.com})
 */
public class StreamBasedTimelineSource extends TimelineSource {

    private static final int lineWrap = 78;

    private static enum ReadingState {
        NewMarker,
        StartMark,
        ReadMark,
        StartNotes,
        ReadNotes,
        EndMarker };


    private InputStream in;
    private OutputStream out;
    private ByteArrayOutputStream comments;

    public StreamBasedTimelineSource(InputStream inStream,
    OutputStream outStream) {
        super(null);
        this.in = inStream;
        this.out = outStream;
        this.comments = new ByteArrayOutputStream(); }

    /** {@inheritDoc } */
    public Timeline read() throws IOException {
        return readFromStream(in, comments); }

    /** {@inheritDoc } */
    public void persist(Timeline t) throws IOException {
        writeToStream(out, t); }

    public void authenticate() throws AuthenticationException { }
    public boolean isAuthenticated() { return true; }

    /**
     * Allows a user to extract the comments from the last parsed file.
     * @return a <code>byte[]</code> representing the portions of the file
     * which were comments.
     */
    public byte[] getCommentBytes() { return comments.toByteArray(); }

    /**
    * Write the a representation of the timeline to a stream. This method
    * flushes the stream after it finishes writing but does not close the
    * stream.
    * @param stream An open stream to write the timeline representation to.
    * @param timeline The timeline to write.
    * @throws java.io.IOException
    */
    public static void writeToStream(OutputStream stream, Timeline timeline)
    throws IOException {
        Writer out = new OutputStreamWriter(stream);
        for (TimelineMarker tm : timeline) {

            // write timestamp and UUID
            out.write(Timeline.longFormat.format(tm.getTimestamp()) + "," +
                tm.getUuid().toString() + "\n");

            // write mark
            String mark = tm.getMark().replace('\n', '\u0000');
            if (mark.length() < lineWrap) out.write(mark + "\n");
            else {
                // wrap lines if neccessary
                int i;
                for (i = 0; (i + lineWrap) < mark.length(); i+=lineWrap)
                    out.write(mark.substring(i, i+lineWrap) + "\\\n");
                if (i < mark.length())
                    out.write(mark.substring(i, mark.length()) + "\n"); }

            // write notes
            String notes = tm.getNotes().replace('\n', '\u0000');
            if (notes.length() < lineWrap) out.write(notes + "\n");
            else {
                // wrap lines if neccessary
                int i;
                for (i = 0; (i + lineWrap) < notes.length(); i+=lineWrap)
                    out.write(notes.substring(i, i+lineWrap) + "\\\n");
                if (i < notes.length())
                    out.write(notes.substring(i, notes.length()) + "\n"); }
            out.write("\n"); }
        out.flush(); }


    /**
    * Create a <b>Timeline</b> instance from a given stream.
    * @param stream  The stream to read from.
    * @return A new <b>Timeline</b> instance specified by the input stream.
    * @throws java.io.IOException
    * @throws java.io.FileNotFoundException
    */
    public static Timeline readFromStream(InputStream stream)
    throws IOException {
        return readFromStream(stream, null); }

    /**
    * Create a <b>Timeline</b> instance from a given stream.
    * @param stream  The stream to read from.
    * @param commentStream A stream to write comments found in the file. This
    * parameter may be <b>null</b>, in which case comments are ignored.
    * @return A new <b>Timeline</b> instance specified by the input stream.
    * @throws java.io.IOException
    * @throws java.io.FileNotFoundException
    */
    public static Timeline readFromStream(InputStream stream,
    OutputStream commentStream) throws IOException {
        Scanner in = new Scanner(stream);
        Timeline timeline = new Timeline();
        PrintWriter commentsWriter = commentStream == null ?
            null : new PrintWriter(commentStream);

        ReadingState readingState = ReadingState.NewMarker;
        Date date = null;
        UUID uuid = null;
        StringBuilder mark = null;
        StringBuilder notes = null;
        String line;
        int lineNumber = 0;

        while (in.hasNextLine()) {
            line = in.nextLine();
            lineNumber++;

            // line is a comment
            if (line.startsWith("#")) {
                if (commentsWriter != null) commentsWriter.println(line);
                // don't parse this line as part of the timeline
                continue;   }

            switch (readingState) {

            case NewMarker:
                try {
                    String[] parts = line.split(",");
                    date = Timeline.longFormat.parse(parts[0]);

                    // If there is no UUID, we will ignore it. This allows us
                    // to support timeline files generated by 1.x versions of
                    // this library.
                    // TODO: Remove this check in version 3.x
                    if (parts.length > 1) uuid = UUID.fromString(parts[1]);
                    else uuid = null; }
                catch (ParseException pe) {
                    throw new IOException("Error parsing timeline file at line "
                        + lineNumber + ": expected a new marker date but could not parse"
                        + " the date. Error: " + pe.getLocalizedMessage()); }
                catch (IllegalArgumentException iae) {
                    throw new IOException("Error parsing timeline file at line "
                        + lineNumber + ": Expected a UUID but could not parse "
                        + "the value. Details: " + iae.getLocalizedMessage()); }
                readingState = ReadingState.StartMark;
                break;

            case StartMark:
                mark = new StringBuilder();
                // fall through to ReadMark
            case ReadMark:
                if (line.endsWith("\\")) {
                    readingState = ReadingState.ReadMark;
                    line = line.substring(0, line.length() - 1); }
                else readingState = ReadingState.StartNotes;
                mark.append(line);
                break;

            case StartNotes:
                notes = new StringBuilder();
                // fall through to ReadNotes
            case ReadNotes:
                if (line.endsWith("\\")) {
                    readingState = ReadingState.ReadNotes;
                    line = line.substring(0, line.length() - 1); }
                else readingState = ReadingState.EndMarker;
                notes.append(line);
                break;

            case EndMarker:
                String sMark = mark.toString().replace('\u0000', '\n');
                String sNotes = notes.toString().replace('\u0000', '\n');
                TimelineMarker marker;

                // Support a missing UUID until version 3.x
                if (uuid == null)
                    marker = new TimelineMarker(date, sMark, sNotes);
                else marker = new TimelineMarker(date, sMark, sNotes, uuid);
                timeline.addMarker(marker);
                readingState = ReadingState.NewMarker; } }

        return timeline; }
}
