package nl.tno.stormcv.operation;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.sun.jersey.api.container.httpserver.HttpServerFactory;
import com.sun.jersey.api.core.ApplicationAdapter;
import com.sun.net.httpserver.HttpServer;

import backtype.storm.task.TopologyContext;
import backtype.storm.utils.Utils;
import nl.tno.stormcv.model.*;
import nl.tno.stormcv.model.serializer.*;

/**
 * A BatchInputBolt primarily used for testing that creates its own simple webservice used to view results MJPEG streams. The webservice 
 * supports a number of calls different calls:
 * <ol>
 * <li>http://IP:PORT/streaming/streams : lists the available JPG and MJPEG urls</li>
 * <li>http://IP:PORT/streaming/picture/{streamid}.jpg : url to grab jpg pictures </li>
 * <li>http://IP:PORT/streaming/tiles : provides a visual overview of all the streams available at this service. Clicking an image will open the mjpeg stream</li>
 * <li>http://IP:PORT/streaming/mjpeg/{streamid}.mjpeg : provides a possibly never ending mjpeg formatted stream</li>
 * </ol> 
 * 
 * The service runs on port 8558 by default but this can be changed by using the port(int) method.
 * 
 * @author Corne Versloot
 *
 */

@SuppressWarnings("restriction")
@Path("/streaming")
public class MjpegStreamingOp extends Application implements IBatchOperation<Frame> {

	private static final long serialVersionUID = -4558017042873627826L;
	private static Cache<String, BufferedImage> images = null;
	private Logger logger = LoggerFactory.getLogger(getClass());
	private HttpServer server;
	private int port = 8558;
	private int frameRate = 2;
	
	private static Cache<String, BufferedImage> getImages(){
		if(images == null){
			images = CacheBuilder.newBuilder()
					.expireAfterWrite(20, TimeUnit.SECONDS) 
					.build();
		}
		return images;
	}

	public MjpegStreamingOp port(int nr){
		this.port = nr;
		return this;
	}
	
	public MjpegStreamingOp framerate(int nr){
		this.frameRate = nr;
		return this;
	}
	
	/**
	 * Sets the classes to be used as resources for this application
	 */
	public Set<Class<?>> getClasses() {
        Set<Class<?>> s = new HashSet<Class<?>>();
        s.add(MjpegStreamingOp.class);
        return s;
    }
	
	@SuppressWarnings("rawtypes")
	@Override
	public void prepare(Map stormConf, TopologyContext context)	throws Exception {
		images = MjpegStreamingOp.getImages();
		
		ApplicationAdapter connector = new ApplicationAdapter(new MjpegStreamingOp());
		server = HttpServerFactory.create ("http://localhost:"+port+"/", connector);
		server.start();		
	}

	@Override
	public void deactivate() {
		server.stop(0);
		images.invalidateAll();
		images.cleanUp();
	}

	@Override
	public CVParticleSerializer<Frame> getSerializer() {
		return new FrameSerializer();
	}

	@Override
	public List<Frame> execute(List<CVParticle> input) throws Exception {
		List<Frame> result = new ArrayList<Frame>();
		for(int i=0; i<input.size(); i++){
			CVParticle s = input.get(i);
			if(!(s instanceof Frame)) continue;
			Frame frame = (Frame)s;
			result.add(frame);
			if(frame.getImage() == null) continue;
			//BufferedImage prevImage = images.getIfPresent(frame.getStreamId());
			images.put(frame.getStreamId(), frame.getImage());
			/*
			if(prevImage != null) synchronized (prevImage){ prevImage.notifyAll(); }// notify all listeners that they can grab a new image
			//System.err.println("Add frame ["+frame.getSequenceNr()+"] after "+(System.currentTimeMillis() - prevAdd)+" ms");
			prevAdd = System.currentTimeMillis();
			*/
			break;
		}
		return result;
	}
	
	@GET @Path("/streams")
	@Produces("text/plain")
	public String getStreamIds() throws IOException{
		String result = new String();
		for(String id : images.asMap().keySet()){
			result += "/streaming/picture/"+id+".jpeg\r\n";
		}
		System.out.println("\r\n");
		for(String id : images.asMap().keySet()){
			result += "/streaming/mjpeg/"+id+".mjpeg\r\n";
		}
		return result;
	}
	
	@GET @Path("/picture/{streamid}.jpeg")
	@Produces("image/jpg")
	public Response jpeg(@PathParam("streamid") final String streamId){
		BufferedImage image = null;
		if((image = images.getIfPresent(streamId)) != null){
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			try {
				ImageIO.write(image, "jpg", baos);
				byte[] imageData = baos.toByteArray();
				return Response.ok(imageData).build(); // non streaming
				// return Response.ok(new ByteArrayInputStream(imageDAta)).build(); // streaming
			} catch (IOException ioe) {
				logger.warn("Unable to write image to output", ioe);
				return Response.serverError().build();
			}
		}else{
			return Response.noContent().build();
		}
	}
	
	@GET @Path("/playmultiple")
	@Produces("text/html")
	public String showPlayers( @DefaultValue("3") @QueryParam("cols") int cols,
			 @DefaultValue("0") @QueryParam("offset") int offset,
			 @DefaultValue("6") @QueryParam("number") int number) throws IOException{
		//number = Math.min(6, number);
		String result = "<html><head><title>Mjpeg stream players</title></head><body bgcolor=\"#3C3C3C\">";
		result += "<font style=\"color:#CCC;\">Streams: "+images.size()+" (showing "+offset+" - "+Math.min(images.size(), offset+number)+")</font><br/>";
		result += "<table style=\"border-spacing:0; border-collapse: collapse;\"><tr>";
		int videoNr = 0;
		for(String id : images.asMap().keySet()){
			if(videoNr < offset ){
				videoNr++;
				continue;
			}
			if(videoNr-offset > 0 &&(videoNr-offset) % cols == 0){
				result+="</tr><tr>";
			}
			result += "<td><video poster=\"mjpeg/"+id+".mjpeg\">"+
					"Your browser does not support the video tag.</video></td>";
			//result += "<td><img src=\"http://"+InetAddress.getLocalHost().getHostAddress()+":"+port+"/streaming/mjpeg/"+id+".mjpeg\"></td>";
			if(videoNr > offset + number) break;
			videoNr++;
		}
		result += "</tr></table></body></html>";
		return result;
	}
	
	@GET @Path("/play")
	@Produces("text/html")
	public String showPlayers( @QueryParam("streamid") String streamId) throws IOException{
		String result = "<html><head><title>Mjpeg stream: "+streamId+"</title></head><body bgcolor=\"#3C3C3C\">";
		result += "<font style=\"color:#CCC;\"><a href=\"tiles\">Back</a></font><br/>";
		result += "<table style=\"border-spacing:0; border-collapse: collapse;\"><tr>";
		result += "<video poster=\"mjpeg/"+streamId+".mjpeg\">"+
					"Your browser does not support the video tag.</video>";
		return result;
	}
	
	@GET @Path("/tiles")
	@Produces("text/html")
	public String showTiles( @DefaultValue("3") @QueryParam("cols") int cols,
			@DefaultValue("-1") @QueryParam("width") float width) throws IOException{
		String result = "<html><head><title>Mjpeg stream players</title>";
		result += "</head><body bgcolor=\"#3C3C3C\">";
		
		result += "<table style=\"border-spacing:0; border-collapse: collapse;\"><tr>";
		int videoNr = 0;
		for(String id : images.asMap().keySet()){
			if(videoNr > 0 && videoNr % cols == 0){
				result+="</tr><tr>";
			}
			result += "<td><a href=\"play?streamid="+id+"\"><img src=\"picture/"+id+".jpeg\" "+(width > 0 ? "width=\""+width+"\"" : "")+"/></a>";
			videoNr++;
		}
		result += "</tr></table></body></html>";
		return result;
	}
	
	@GET @Path("/mjpeg/{streamid}.mjpeg")
	@Produces("multipart/x-mixed-replace; boundary=--BoundaryString\r\n")
	public Response mjpeg(@PathParam("streamid") final String streamId){
		StreamingOutput output = new StreamingOutput() {
			
			private BufferedImage prevImage = null;
			private int sleep = 1000/frameRate;
			
			@Override
			public void write(OutputStream outputStream) throws IOException, WebApplicationException {
				BufferedImage image = null;
				try{
					while((image = images.getIfPresent(streamId)) != null) /*synchronized(image)*/ {
						if(prevImage == null || !image.equals(prevImage)){
							ByteArrayOutputStream baos = new ByteArrayOutputStream();
							ImageIO.write(image, "jpg", baos);
							byte[] imageData = baos.toByteArray();
							 outputStream.write((
								        "--BoundaryString\r\n" +
								        "Content-type: image/jpeg\r\n" +
								        "Content-Length: "+imageData.length+"\r\n\r\n").getBytes());
							outputStream.write(imageData);
							outputStream.write("\r\n\r\n".getBytes());
							outputStream.flush();
						}
						Utils.sleep(sleep);
						/*
						try {
							image.notifyAll();
							image.wait();
						} catch (InterruptedException e) {
							// just read the next image
						}
						*/
					}
					outputStream.flush();
					outputStream.close();
				}catch(IOException ioe){
					logger.info("Steam for ["+streamId+"] closed by client!");
				}
			}
		};
		return Response.ok(output)
				.header("Connection", "close")
				.header("Max-Age", "0")
				.header("Expires", "0")
				.header("Cache-Control", "no-cache, private")
				.header("Pragma", "no-cache")
				.build();
	}

}