package eu.crushedpixel.replaymod.video;

import com.google.common.base.Preconditions;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import org.monte.media.*;
import org.monte.media.FormatKeys.MediaType;
import org.monte.media.math.Rational;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class VideoWriter {

    private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss";
    private static final SimpleDateFormat FILE_FORMAT = new SimpleDateFormat(DATE_FORMAT);

    private static final String VIDEO_EXTENSION = ".avi";

    private final File file;
    private final MovieWriter out;
    private final Buffer buf;
    private final int track;

    private volatile boolean active = true;
    private volatile boolean cancelled = false;

    private int queueLimit;
    private final Queue<BufferedImage> toWrite;
    private final Thread writerThread;

    private final Lock lock = new ReentrantLock();
    private final Condition emptyCondition = lock.newCondition();
    private final Condition noLongerEmptyCondition = lock.newCondition();
    private final Condition noLongerFullCondition = lock.newCondition();

    public VideoWriter(int width, int height, int fps, float quality) throws IOException {
        this(width, height, fps, quality, Integer.MAX_VALUE);
    }

    public VideoWriter(int width, int height, int fps, float quality, int queueLimit) throws IOException {
        this.queueLimit = queueLimit;
        this.toWrite = new LinkedList<BufferedImage>();

        File folder = ReplayFileIO.getRenderFolder();
        String fileName = FILE_FORMAT.format(Calendar.getInstance().getTime());
        file = new File(folder, fileName + VIDEO_EXTENSION);
        Files.createFile(file.toPath());

        out = Registry.getInstance().getWriter(file);
        Format format = new Format(FormatKeys.MediaTypeKey, MediaType.VIDEO,
                FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG,
                FormatKeys.FrameRateKey, new Rational(fps, 1),
                VideoFormatKeys.WidthKey, width,
                VideoFormatKeys.HeightKey, height,
                VideoFormatKeys.DepthKey, 24,
                VideoFormatKeys.QualityKey, quality);

        track = out.addTrack(format);

        buf = new Buffer();
        buf.format = new Format(VideoFormatKeys.DataClassKey, BufferedImage.class);
        buf.sampleDuration = out.getFormat(track).get(VideoFormatKeys.FrameRateKey).inverse();

        writerThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while(!cancelled && (active || !toWrite.isEmpty())) {
                    try {
                        lock.lockInterruptibly();
                        BufferedImage img;
                        try {
                            img = toWrite.poll();
                            if (img == null) {
                                noLongerEmptyCondition.await();
                                img = toWrite.poll();
                            }
                            noLongerFullCondition.signal();
                            if (toWrite.isEmpty()) {
                                emptyCondition.signalAll();
                            }
                        } finally {
                            lock.unlock();
                        }
                        buf.data = img;
                        try {
                            out.write(track, buf);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    } catch (InterruptedException ignored) {
                    }
                }
                try {
                    toWrite.clear();
                    out.close();
                    if (cancelled) {
                        Files.delete(file.toPath());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, "replaymod-video-writer");
        writerThread.start();
    }

    public void setQueueLimit(int limit) {
        this.queueLimit = limit;
    }

    /**
     * Add the image to the writer queue.
     * @param image The image
     * @param waitIfFull Whether to wait if the queue is full or to return immediately
     * @return {@code} true if the image was added, {@code false} if it could not be added due to the queue being full
     *         and {@code waitIfFull} being {@code false}
     */
    public boolean writeImage(BufferedImage image, boolean waitIfFull) {
        Preconditions.checkState(active, "This VideoWriter has already been closed.");

        lock.lock();
        try {
            while (toWrite.size() >= queueLimit) {
                if (waitIfFull) {
                    noLongerFullCondition.await();
                } else {
                    return false;
                }
            }
            toWrite.offer(image);
            noLongerEmptyCondition.signal();
            return true;
        } catch (InterruptedException ignored) {
        } finally {
            lock.unlock();
        }
        return false;
    }

    public void endRecording() {
        active = false;
        writerThread.interrupt();
    }

    public void abortRecording() {
        cancelled = true;
        active = false;
        writerThread.interrupt();
    }

    public void waitTillQueueEmpty() {
        lock.lock();
        try {
            if (!toWrite.isEmpty()) {
                emptyCondition.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void waitForFinish() throws InterruptedException {
        Preconditions.checkState(!active, "Video writer still active.");
        writerThread.join();
    }
}
