package com.googlecode.mp4parser.authoring.tracks.webvtt;

import com.coremedia.iso.boxes.Box;
import com.coremedia.iso.boxes.SampleDescriptionBox;
import com.googlecode.mp4parser.authoring.AbstractTrack;
import com.googlecode.mp4parser.authoring.Sample;
import com.googlecode.mp4parser.authoring.TrackMetaData;
import com.googlecode.mp4parser.authoring.tracks.webvtt.sampleboxes.CuePayloadBox;
import com.googlecode.mp4parser.authoring.tracks.webvtt.sampleboxes.CueSettingsBox;
import com.googlecode.mp4parser.authoring.tracks.webvtt.sampleboxes.VTTCueBox;
import com.googlecode.mp4parser.authoring.tracks.webvtt.sampleboxes.VTTEmptyCueBox;
import com.googlecode.mp4parser.util.ByteBufferByteChannel;
import com.googlecode.mp4parser.util.Mp4Arrays;
import com.mp4parser.iso14496.part30.WebVTTConfigurationBox;
import com.mp4parser.iso14496.part30.WebVTTSampleEntry;
import com.mp4parser.iso14496.part30.WebVTTSourceLabelBox;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.googlecode.mp4parser.util.CastUtils.l2i;

public class WebVttTrack extends AbstractTrack {
    TrackMetaData trackMetaData = new TrackMetaData();
    SampleDescriptionBox stsd;

    private static final String WEBVTT_FILE_HEADER_STRING = "^\uFEFF?WEBVTT((\\u0020|\u0009).*)?$";
    private static final Pattern WEBVTT_FILE_HEADER =
            Pattern.compile(WEBVTT_FILE_HEADER_STRING);

    private static final String WEBVTT_METADATA_HEADER_STRING = "\\S*[:=]\\S*";
    private static final Pattern WEBVTT_METADATA_HEADER =
            Pattern.compile(WEBVTT_METADATA_HEADER_STRING);

    private static final String WEBVTT_CUE_IDENTIFIER_STRING = "^(?!.*(-->)).*$";
    private static final Pattern WEBVTT_CUE_IDENTIFIER =
            Pattern.compile(WEBVTT_CUE_IDENTIFIER_STRING);

    private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}";
    private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING);

    private static final String WEBVTT_CUE_SETTING_STRING = "\\S*:\\S*";
    private static final Pattern WEBVTT_CUE_SETTING = Pattern.compile(WEBVTT_CUE_SETTING_STRING);


    private static final Sample EMPTY_SAMPLE = new Sample() {
        ByteBuffer vtte;

        {
            VTTEmptyCueBox vttEmptyCueBox = new VTTEmptyCueBox();
            vtte = ByteBuffer.allocate(l2i(vttEmptyCueBox.getSize()));
            try {
                vttEmptyCueBox.getBox(new ByteBufferByteChannel(vtte));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            vtte.rewind();
        }


        public void writeTo(WritableByteChannel channel) throws java.io.IOException {
            channel.write(vtte.duplicate());
        }

        public long getSize() {
            return vtte.remaining();
        }

        public ByteBuffer asByteBuffer() {
            return vtte.duplicate();
        }
    };

    private static class BoxBearingSample implements Sample {
        List<Box> boxes;

        public BoxBearingSample(List<Box> boxes) {
            this.boxes = boxes;
        }

        public void writeTo(WritableByteChannel channel) throws java.io.IOException {
            for (Box box : boxes) {
                box.getBox(channel);
            }
        }

        public long getSize() {
            long l = 0;
            for (Box box : boxes) {
                l += box.getSize();
            }
            return l;
        }

        public ByteBuffer asByteBuffer() {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try {
                writeTo(Channels.newChannel(baos));
            } catch (java.io.IOException e) {
                throw new RuntimeException(e);
            }

            return ByteBuffer.wrap(baos.toByteArray());
        }
    }

    List<Sample> samples = new ArrayList<Sample>();
    long[] sampleDurations = new long[0];
    WebVTTSampleEntry sampleEntry;


    public WebVttTrack(InputStream is, String trackName, Locale locale) throws IOException {
        super(trackName);
        trackMetaData.setTimescale(1000);
        trackMetaData.setLanguage(locale.getISO3Language());
        long mediaTimestampUs = 0;

        stsd = new SampleDescriptionBox();

        sampleEntry = new WebVTTSampleEntry();
        stsd.addBox(sampleEntry);
        WebVTTConfigurationBox webVttConf = new WebVTTConfigurationBox();
        sampleEntry.addBox(webVttConf);
        sampleEntry.addBox(new WebVTTSourceLabelBox());

        BufferedReader webvttData = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        String line;

        // file should start with "WEBVTT"
        line = webvttData.readLine();
        if (line == null || !WEBVTT_FILE_HEADER.matcher(line).matches()) {
            throw new IOException("Expected WEBVTT. Got " + line);
        }
        webVttConf.setConfig(webVttConf.getConfig() + "\n" + line);
        while (true) {
            line = webvttData.readLine();
            if (line == null) {
                // we reached EOF before finishing the header
                throw new IOException("Expected an empty line after webvtt header");
            } else if (line.isEmpty()) {
                // we've read the newline that separates the header from the body
                break;
            }

            Matcher matcher = WEBVTT_METADATA_HEADER.matcher(line);
            if (!matcher.find()) {
                throw new IOException("Expected WebVTT metadata header. Got " + line);
            }
            webVttConf.setConfig(webVttConf.getConfig() + "\n" + line);
        }


        // process the cues and text
        while ((line = webvttData.readLine()) != null) {
            if ("".equals(line.trim())) {
                continue;
            }
            // parse the cue identifier (if present) {
            Matcher matcher = WEBVTT_CUE_IDENTIFIER.matcher(line);
            if (matcher.find()) {
                // ignore the identifier (we currently don't use it) and read the next line
                line = webvttData.readLine();
            }

            long startTime;
            long endTime;

            // parse the cue timestamps
            matcher = WEBVTT_TIMESTAMP.matcher(line);

            // parse start timestamp
            if (!matcher.find()) {
                throw new IOException("Expected cue start time: " + line);
            } else {
                startTime = parseTimestampUs(matcher.group());
            }

            // parse end timestamp
            String endTimeString;
            if (!matcher.find()) {
                throw new IOException("Expected cue end time: " + line);
            } else {
                endTimeString = matcher.group();
                endTime = parseTimestampUs(endTimeString);
            }

            // parse the (optional) cue setting list
            line = line.substring(line.indexOf(endTimeString) + endTimeString.length());
            matcher = WEBVTT_CUE_SETTING.matcher(line);
            String settings = null;
            while (matcher.find()) {
                settings = matcher.group();
            }
            StringBuilder payload = new StringBuilder();
            while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
                if (payload.length() > 0) {
                    payload.append("\n");
                }
                payload.append(line.trim());
            }

            if (startTime != mediaTimestampUs) {
                //System.err.println("" + mediaTimestampUs + " - " + startTime + " Add empty sample");
                sampleDurations = Mp4Arrays.copyOfAndAppend(sampleDurations, startTime - mediaTimestampUs);
                samples.add(EMPTY_SAMPLE);
            }
            sampleDurations = Mp4Arrays.copyOfAndAppend(sampleDurations, endTime - startTime);
            VTTCueBox vttCueBox = new VTTCueBox();
            if (settings != null) {
                CueSettingsBox csb = new CueSettingsBox();
                csb.setContent(settings);
                vttCueBox.setCueSettingsBox(csb);
            }
            CuePayloadBox cuePayloadBox = new CuePayloadBox();
            cuePayloadBox.setContent(payload.toString());
            vttCueBox.setCuePayloadBox(cuePayloadBox);

            samples.add(new BoxBearingSample(Collections.<Box>singletonList(vttCueBox)));


            mediaTimestampUs = endTime;
            // samples.add();
        }


    }

    private static long parseTimestampUs(String s) throws NumberFormatException {
        if (!s.matches(WEBVTT_TIMESTAMP_STRING)) {
            throw new NumberFormatException("has invalid format");
        }

        String[] parts = s.split("\\.", 2);
        long value = 0;
        for (String group : parts[0].split(":")) {
            value = value * 60 + Long.parseLong(group);
        }
        return (value * 1000 + Long.parseLong(parts[1]));
    }


    public SampleDescriptionBox getSampleDescriptionBox() {
        return stsd;
    }


    public long[] getSampleDurations() {
        long[] adoptedSampleDuration = new long[sampleDurations.length];
        for (int i = 0; i < adoptedSampleDuration.length; i++) {
            adoptedSampleDuration[i] = sampleDurations[i] * trackMetaData.getTimescale() / 1000;
        }
        return adoptedSampleDuration;

    }

    public TrackMetaData getTrackMetaData() {
        return trackMetaData;
    }

    public String getHandler() {
        return "text";
    }

    public List<Sample> getSamples() {
        return samples;
    }

    public void close() throws java.io.IOException {

    }
}
