package org.forkalsrud.album.web;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.forkalsrud.album.exif.Dimension;
import org.forkalsrud.album.exif.Thumbnail;

public class PictureScaler {

    private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PictureScaler.class);

	ExecutorService executor;
	BlockingQueue<PictureRequest> queue;
    HashMap<String, PictureRequest> outstandingRequests;


    public PictureScaler() {

		queue = new PriorityBlockingQueue<>(20, (o1, o2) -> Long.signum(o1.priority - o2.priority));
		executor = Executors.newFixedThreadPool(4);
		outstandingRequests = new HashMap<>();
    }


    synchronized PictureRequest getPictureRequest(String key, File file, Thumbnail thumbnail, String size) {
        PictureRequest req = outstandingRequests.get(key); 
        if (req == null) {
        	req = new PictureRequest(key, file, thumbnail, size);
        	outstandingRequests.put(key, req);
        	Future<CachedImage> future = executor.submit(req);
        	req.setFuture(future);
        }
        return req;
    }

    synchronized void completePictureRequest(PictureRequest req) {

    	outstandingRequests.remove(req.getKey());
    }


    class PictureRequest implements Callable<CachedImage> {

    	long priority;
    	String key;
    	File file;
    	Thumbnail thumbnail;
    	String size;
    	Future<CachedImage> future;

    	public PictureRequest(String key, File file, Thumbnail thumbnail, String size) {
    		this.key = key;
    		this.file = file;
    		this.thumbnail = thumbnail;
    		this.size = size;
    		this.priority = System.currentTimeMillis();
    	}

    	public String getKey() {
    		return key;
    	}


    	@Override
        public CachedImage call() throws Exception {
			try {
				if (file.getName().endsWith(".pdf")) {
					return renderPdf(file, thumbnail, size);
				}
    	        return scalePictureReally(file, thumbnail, size);
    	    } catch (Exception e) {
    	        log.error("sadness: " + file.getAbsolutePath(), e);
    	        return new CachedImage();
    	    }
		}

    	public void setFuture(Future<CachedImage> future) {
    		this.future = future;
    	}

    	public CachedImage waitForIt(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException {

    		return future.get(/*timeout, unit*/);
    	}

    	public void cancel() {
    		future.cancel(false);
    	}
    }


	public CachedImage scalePicture(File file, Thumbnail thumbnail, String size) {
        String key = file.getPath() + ":" + size;
        return scalePicture(key, file, thumbnail, size);
    }
    public CachedImage scalePicture(String key, File file, Thumbnail thumbnail, String size) {
    	PictureRequest req = getPictureRequest(key, file, thumbnail, size);
    	try {
    		return req.waitForIt(30, TimeUnit.SECONDS);
//    	} catch (TimeoutException toe) {
//    		req.cancel();
//    		toe.printStackTrace();
//    		throw new TimeoutException("timed out");
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    		return null;
        } catch (OutOfMemoryError e) {
            log.error(file.getPath(), e);
            return null;
    	} catch (ExecutionException e) {
    		throw new RuntimeException(e);
    	} finally {
    		completePictureRequest(req);
    	}
    }


    /**
	 * @param file
	 * @param thumbnail
	 * @param size
	 * @throws IOException
	 */
	CachedImage scalePictureReally(File file, Thumbnail thumbnail, String size) throws IOException {

		Dimension orig = thumbnail.getSize();
		Dimension outd = orig.scale(size);

		/* In order to make the quality as good as possible we follow the advice from
		 * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
		 * and scale the image by a factor of 2 in intermediate steps, thereby getting a smoother
		 * end result.  The first scale will get to a size which is a multiple of the result size.
		 * the first scaling operation will also take care of any rotation that needs to happen.
		 */
		Dimension intermediate = new Dimension(outd);
		int targetWidth = orig.getWidth() / 2;
		while (intermediate.getWidth() < targetWidth) {
			intermediate = intermediate.doubled();
		}

		BufferedImage img = ImageIO.read(file);
		if (img == null) {
			CachedImage cimg = new CachedImage();
			cimg.lastModified = file.lastModified();
			cimg.mimeType = "image/jpeg";
			cimg.bits = new byte[0];
			return cimg;
		}

        // The orientation is about flipping and rotating.  Here is what an 'F' looks like
        // on pictures with each orientation.
        //
        //   1        2       3      4         5            6           7          8
        //
        // 888888  888888      88  88      8888888888  88                  88  8888888888
        // 88          88      88  88      88  88      88  88          88  88      88  88
        // 8888      8888    8888  8888    88          8888888888  8888888888          88
        // 88          88      88  88
        // 88          88  888888  888888
        //
        // The first four are obtained with only flipping X and/or Y
        // The last four are obtained by rotating 90 degrees and then flipping X and/or Y.
        //

        double[] flipX = new double[]{ +1, -1, -1, +1, -1, +1, +1, -1 };
        double[] flipY = new double[]{ +1, +1, -1, -1, +1, +1, -1, -1 };
        int idx = thumbnail.getOrientation() - 1;

        // Recalculate scale after sub-sampling was applied
        double scale;
        AffineTransform xform = new AffineTransform();
        boolean rotate = idx >= 4;

        xform.translate(intermediate.getWidth() / 2d, intermediate.getHeight() / 2d);
        if (rotate) {
            xform.rotate(Math.PI / 2);
            scale = (double) intermediate.getHeight() / img.getWidth();
        } else {
            scale = (double) intermediate.getWidth() / img.getWidth();
        }
        xform.scale(scale, scale);
        xform.scale(flipX[idx], flipY[idx]);
        xform.translate(-img.getWidth() / 2d, -img.getHeight() / 2d);

        int imgType = img.getType() > 0 ? img.getType() : BufferedImage.TYPE_INT_RGB;
        BufferedImage buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), imgType);
        Graphics2D g2 = buf2.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

        g2.transform(xform);
        g2.drawImage(img, 0, 0, null);
        img = null;

        while (intermediate.getWidth() > outd.getWidth()) {

            BufferedImage buf3 = buf2;
            intermediate = intermediate.halved();
            buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), imgType);
            g2 = buf2.createGraphics();
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2.drawImage(buf3, 0, 0, intermediate.getWidth(), intermediate.getHeight(), null);
        }
        return encoded(buf2, file.lastModified());
    }


    private CachedImage encoded(BufferedImage buf2, long lastModifiedTs) throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
		ImageWriter writer = writers.next();
		ImageWriteParam writeParam = writer.getDefaultWriteParam();
		writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
		writeParam.setCompressionQuality(0.95f);
	
		ByteArrayOutputStream bits = new ByteArrayOutputStream();
	
		ImageOutputStream ios = ImageIO.createImageOutputStream(bits);
		writer.setOutput(ios);

		IIOImage image = new IIOImage(buf2, null, null);
		writer.write(null, image, writeParam);
		writer.dispose();
		ios.flush();
	
		CachedImage cimg = new CachedImage();
		cimg.lastModified = lastModifiedTs;
		cimg.mimeType = "image/jpeg";
		cimg.bits = bits.toByteArray();
		return cimg;
	}


    CachedImage renderPdf(File file, Thumbnail thumbnail, String size) throws IOException {


		Dimension orig = thumbnail.getSize();
		Dimension dim = orig.scale(size);



		try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(file))) {

			PDPage page = document.getPage(0);
			PDRectangle rect = page.getMediaBox();


			float width = rect.getWidth() / 72;  // inches
			float targetWidth = dim.getWidth(); // pixels

			PDFRenderer renderer = new PDFRenderer(document);
			BufferedImage image = renderer.renderImageWithDPI(0, targetWidth / width);


			return encoded(image, file.lastModified());
		}
	}


}
