/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.app.state;

import android.graphics.Bitmap;
import com.jme3.app.Application;
import com.jme3.post.SceneProcessor;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.system.JmeSystem;
import com.jme3.system.Timer;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.util.AndroidScreenshots;
import com.jme3.util.BufferUtils;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A Video recording AppState that records the screen output into an AVI file with
 * M-JPEG content. The file should be playable on any OS in any video player.<br/>
 * The video recording starts when the state is attached and stops when it is detached
 * or the application is quit. You can set the fileName of the file to be written when the
 * state is detached, else the old file will be overwritten. If you specify no file
 * the AppState will attempt to write a file into the user home directory, made unique
 * by a timestamp.
 * @author normenhansen, Robert McIntyre, entrusC
 */
public class VideoRecorderAppState extends AbstractAppState {
    private static final Logger logger = Logger.getLogger(VideoRecorderAppState.class.getName());

    private int numFrames = 0;
    private int framerate = 30;
    private VideoProcessor processor;
    private File file;
    private Application app;
    private ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactory() {

        public Thread newThread(Runnable r) {
            Thread th = new Thread(r);
            th.setName("jME3 Video Processor");
            th.setDaemon(true);
            return th;
        }
    });
    private int numCpus = Runtime.getRuntime().availableProcessors();
    private ViewPort lastViewPort;
    private float quality;
    private Timer oldTimer;

    /**
     * Using this constructor the video files will be written sequentially to the user's home directory with
     * a quality of 0.8 and a framerate of 30fps.
     */
    public VideoRecorderAppState() {
        this(null, 0.8f);
    }

    /**
     * Using this constructor the video files will be written sequentially to the user's home directory.
     * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file)
     */
    public VideoRecorderAppState(float quality) {
        this(null, quality);
    }

    /**
     * Using this constructor the video files will be written sequentially to the user's home directory.
     * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file)
     * @param framerate the frame rate of the resulting video, the application will be locked to this framerate
     */
    public VideoRecorderAppState(float quality, int framerate) {
        this(null, quality, framerate);
    }

    /**
     * This constructor allows you to specify the output file of the video. The quality is set
     * to 0.8 and framerate to 30 fps.
     * @param file the video file
     */
    public VideoRecorderAppState(File file) {
        this(file, 0.8f);
    }

    /**
     * This constructor allows you to specify the output file of the video as well as the quality
     * @param file the video file
     * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file)
     * @param framerate the frame rate of the resulting video, the application will be locked to this framerate
     */
    public VideoRecorderAppState(File file, float quality) {
        this.file = file;
        this.quality = quality;
        Logger.getLogger(this.getClass().getName()).log(Level.FINE, "JME3 VideoRecorder running on {0} CPU's", numCpus);
    }

    /**
     * This constructor allows you to specify the output file of the video as well as the quality
     * @param file the video file
     * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file)
     */
    public VideoRecorderAppState(File file, float quality, int framerate) {
        this.file = file;
        this.quality = quality;
        this.framerate = framerate;
        Logger.getLogger(this.getClass().getName()).log(Level.FINE, "JME3 VideoRecorder running on {0} CPU's", numCpus);
    }

    public File getFile() {
        return file;
    }

    public void setFile(File file) {
        if (isInitialized()) {
            throw new IllegalStateException("Cannot set file while attached!");
        }
        this.file = file;
    }

    /**
     * Get the quality used to compress the video images.
     * @return the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file)
     */
    public float getQuality() {
        return quality;
    }

    /**
     * Set the video image quality from 0(worst/smallest) to 1(best/largest).
     * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file)
     */
    public void setQuality(float quality) {
        this.quality = quality;
    }

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.app = app;
        this.oldTimer = app.getTimer();
        app.setTimer(new IsoTimer(framerate));
        if (file == null) {
            String filename = JmeSystem.getStorageFolder(JmeSystem.StorageFolderType.External) + File.separator + "jMonkey-" + System.currentTimeMillis() / 1000 + ".avi";
            logger.log(Level.INFO, "fileName: {0}", filename);
            file = new File(filename);
        }
        processor = new VideoProcessor();
        List<ViewPort> vps = app.getRenderManager().getPostViews();

        for (int i = vps.size() - 1; i >= 0; i-- ) {
            lastViewPort = vps.get(i);
            if (lastViewPort.isEnabled()) {
                break;
            }
        }
        lastViewPort.addProcessor(processor);
    }

    @Override
    public void cleanup() {
        logger.log(Level.INFO, "removing processor");
        lastViewPort.removeProcessor(processor);
        app.setTimer(oldTimer);
        initialized = false;
        file = null;
        super.cleanup();
    }

    private class WorkItem {

        ByteBuffer buffer;
        Bitmap image;
        byte[] data;

        public WorkItem(int width, int height) {
            image = Bitmap.createBitmap(width, height,
                    Bitmap.Config.ARGB_8888);
            buffer = BufferUtils.createByteBuffer(width * height * 4);
        }
    }

    private class VideoProcessor implements SceneProcessor {

        private Camera camera;
        private int width;
        private int height;
        private RenderManager renderManager;
        private boolean isInitilized = false;
        private LinkedBlockingQueue<WorkItem> freeItems;
        private LinkedBlockingQueue<WorkItem> usedItems = new LinkedBlockingQueue<WorkItem>();
        private MjpegFileWriter writer;
        private boolean fastMode = true;

        public void addImage(Renderer renderer, FrameBuffer out) {
            if (freeItems == null) {
                return;
            }
            try {
                final WorkItem item = freeItems.take();
                usedItems.add(item);
                item.buffer.clear();
                renderer.readFrameBufferWithFormat(out, item.buffer, Image.Format.BGRA8);
                executor.submit(new Callable<Void>() {

                    public Void call() throws Exception {
                        if (fastMode) {
                            item.data = item.buffer.array();
                        } else {
                            AndroidScreenshots.convertScreenShot(item.buffer, item.image);
                            item.data = writer.writeImageToBytes(item.image, quality);
                        }
                        while (usedItems.peek() != item) {
                            Thread.sleep(1);
                        }
                        writer.addImage(item.data);
                        usedItems.poll();
                        freeItems.add(item);
                        return null;
                    }
                });
            } catch (InterruptedException ex) {
                Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        public void initialize(RenderManager rm, ViewPort viewPort) {
            logger.log(Level.INFO, "initialize in VideoProcessor");
            this.camera = viewPort.getCamera();
            this.width = camera.getWidth();
            this.height = camera.getHeight();
            this.renderManager = rm;
            this.isInitilized = true;
            if (freeItems == null) {
                freeItems = new LinkedBlockingQueue<WorkItem>();
                for (int i = 0; i < numCpus; i++) {
                    freeItems.add(new WorkItem(width, height));
                }
            }
        }

        public void reshape(ViewPort vp, int w, int h) {
        }

        public boolean isInitialized() {
            return this.isInitilized;
        }

        public void preFrame(float tpf) {
            if (null == writer) {
                try {
                    writer = new MjpegFileWriter(file, width, height, framerate);
                } catch (Exception ex) {
                    Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error creating file writer: {0}", ex);
                }
            }
        }

        public void postQueue(RenderQueue rq) {
        }

        public void postFrame(FrameBuffer out) {
            numFrames++;
            addImage(renderManager.getRenderer(), out);
        }

        public void cleanup() {
            logger.log(Level.INFO, "cleanup in VideoProcessor");
            logger.log(Level.INFO, "VideoProcessor numFrames: {0}", numFrames);
            try {
                while (freeItems.size() < numCpus) {
                    Thread.sleep(10);
                }
                logger.log(Level.INFO, "finishAVI in VideoProcessor");
                writer.finishAVI();
            } catch (Exception ex) {
                Logger.getLogger(VideoRecorderAppState.class.getName()).log(Level.SEVERE, "Error closing video: {0}", ex);
            }
            writer = null;
        }
    }

    public static final class IsoTimer extends com.jme3.system.Timer {

        private float framerate;
        private int ticks;
        private long lastTime = 0;

        public IsoTimer(float framerate) {
            this.framerate = framerate;
            this.ticks = 0;
        }

        public long getTime() {
            return (long) (this.ticks * (1.0f / this.framerate) * 1000f);
        }

        public long getResolution() {
            return 1000L;
        }

        public float getFrameRate() {
            return this.framerate;
        }

        public float getTimePerFrame() {
            return (float) (1.0f / this.framerate);
        }

        public void update() {
            long time = System.currentTimeMillis();
            long difference = time - lastTime;
            lastTime = time;
            if (difference < (1.0f / this.framerate) * 1000.0f) {
                try {
                    Thread.sleep(difference);
                } catch (InterruptedException ex) {
                }
            } else {
                logger.log(Level.INFO, "actual tpf(ms): {0}, 1/framerate(ms): {1}",
                        new Object[]{difference, (1.0f / this.framerate) * 1000.0f});
            }
            this.ticks++;
        }

        public void reset() {
            this.ticks = 0;
        }
    }
}
