package page.image;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.util.*;

import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;

import org.wikiwebserver.handler.http.FormData;
import org.wikiwebserver.handler.http.HTTPException;
import org.wikiwebserver.handler.http.HTTPHandler;
import org.wikiwebserver.handler.http.HTTPHeaders;
import org.wikiwebserver.handler.http.interfaces.*;

import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;

public class ImageResizer implements HTTPResponder {

    public static final String IMAGE_CACHE_EXTENSION = ".wwsj";
    private static final int MAX_SIZE = 1600;
    private static final int DEFAULT_SIZE = 100;
    private static final int CONCURRENCY_LEVEL = 4;
    
    // Simple semaphore implementation since Java Semaphore kept throwing exception:
    // Could not initialize class java.util.concurrent.locks.LockSupport randomly
    private static Object lock = new Object();
    private static volatile int counter = CONCURRENCY_LEVEL;  
    public static void release() {
    	synchronized (lock) {
	        if (counter == 0) lock.notify();
	        counter ++;
    	}
    }
    public static void acquire() throws InterruptedException {
    	synchronized (lock) {
	        while (counter == 0) lock.wait();
	        counter --;
    	}
    }

    
    public Object respond(HTTPHandler conn) throws IOException {
        
        FormData formData = conn.getRequest().getFormData();        
        if (formData == null) {        
            throw new HTTPException(500, "Path required");
        }
        
        // Read posted data
        Object path = locatePath(formData.getFirst("path"));
        
        boolean showErrors = formData.get("errors") != null;
        
        int targetSize = (int) readValue(formData, "s", DEFAULT_SIZE);
        if (targetSize > MAX_SIZE) targetSize = MAX_SIZE;            
        
        long sourceFileModified = System.currentTimeMillis();
        
        File cacheFile = null;
        
        if (path instanceof File) {
            
            sourceFileModified = ((File)path).lastModified();
            cacheFile = new File(path.toString() + ".resized." + 
                                     targetSize + IMAGE_CACHE_EXTENSION);     
            
            boolean modified = sourceFileModified > cacheFile.lastModified();
            if (cacheFile.exists() && cacheFile.length() > 0 && !modified) {
                prepairStream(conn, sourceFileModified);
                return cacheFile;
            }
        }
        
        BufferedImage sourceImage = null;
        Graphics2D g2d = null;
        BufferedImage resizedImage = null;        
        try {
        	// Ensure only CONCURRENCY_LEVEL threads continue concurrently
            acquire(); 

            int maxDim = getDimension(path, true);
            
            if (maxDim == 0) {
                throw new HTTPException(500, "Invalid image");
            }   
            
            int factor = maxDim / targetSize;
            if (factor < 1) factor = 1;
            
            sourceImage = getImage(path, factor);
            
            int w = sourceImage.getWidth();
            int h = sourceImage.getHeight();
            float r = (float)w/h;
            
            int width, height;
            if (r > 1) {
                width = Math.min(targetSize, w);
                height = (int) (width/r);
            } else {
                height = Math.min(targetSize, h);
                width = (int) (height*r);
            }
            if (width == 0) width = 1;
            if (height == 0) height = 1;
            
            resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            g2d = resizedImage.createGraphics();      
            
            g2d.drawImage(sourceImage, 0, 0, width, height, Color.white, null);  
            if (true && width > 300) {
                String copyright = " © WikiWebServer.org ";
                FontMetrics fm = g2d.getFontMetrics();
                int fh = fm.getHeight();
                int fd = fm.getDescent();
                int fw = fm.stringWidth(copyright);
                g2d.setColor(new Color(0, 0, 0, 128));
                g2d.fillRect(width-fw, height-fh, width, fh);
                g2d.setColor(Color.white);
                g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 
                                     RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
                g2d.drawString(copyright, width-fw, height-fd);
            }
            
        } 
        catch (Throwable t) {
            if (resizedImage != null) resizedImage.flush();
            if (showErrors) {
                throw new HTTPException(500, "Failed to resize image", t);
            } else {
            	cacheFile = null; // Don't cache error response
                resizedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
                resizedImage.setRGB(0, 0, 0xFFFFFFFF);
            }
            
        } 
        finally {
            release();
            if (g2d != null) g2d.dispose();   
            if (sourceImage != null) sourceImage.flush();
        }
        

        if (cacheFile != null) {
            try {
                saveImage(resizedImage, cacheFile);
                prepairStream(conn, sourceFileModified);
                return cacheFile;
            } catch (Exception ex) {  }
        }
        
        prepairStream(conn, sourceFileModified);
        return resizedImage;
    }  
    
    private void prepairStream(HTTPHandler conn, long modified) {
        HTTPHeaders headers = conn.getResponse().getHeaders();
        headers.set("Content-Type", "image/jpeg");
        headers.setDate("Last-Modified", modified);
        long expires = System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000;
        headers.setDate("Expires", expires);
        headers.set("Pragma", "cache");
        headers.set("Cache-Control", "public");            
    } 
    
    private void saveImage(BufferedImage image, File file) throws IOException {
        FileOutputStream fout = null;
        try {
            fout = new FileOutputStream(file);
            JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(fout);
            JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(image);
            param.setQuality(0.95f, false);
            encoder.setJPEGEncodeParam(param);
            encoder.encode(image);
        } finally {
            fout.close(); 
            image.flush();
        }
    }
    
    private float readValue(FormData data, String key, float def) {
        float value = def;
        try {
            value = Integer.parseInt(data.getFirst(key));
        } catch (NumberFormatException ex) {}
        return value;
    }   
    
    public static int getMaxDimension(String imageUrl) {
        return getDimension(imageUrl, true);
    }
    
    public static int getMinDimension(String imageUrl) {
         return getDimension(imageUrl, false);
    }    
    
    private static int getDimension(String path, boolean max) {

        try { 
            return getDimension(locatePath(path), max);
        }
        catch (Exception ex) {
            ex.printStackTrace();
            return 0;
        }
    }
    
    private static Object locatePath(String path) throws IOException, HTTPException {
        if (path == null || path.length() <= 1) {
            throw new HTTPException(500, "Invalid path");
        }        
        else if (path.contains("://")) {
            return new URL(path);
        }
        else {
            // Relative path
            File file = new File(path.substring(1));
            if (file.exists()) return file;
            
            // Absolute
            file = new File(path);
            if (file.exists()) return file;
            
            throw new HTTPException(500, "Path not found");
        }
    }    
    
    private static BufferedImage getImage(Object obj, int subsample) throws IOException {

        ImageInputStream iis = null;
        try {
            if (obj instanceof URL) {
                obj = ((URL)obj).openStream();
            }            
            iis = ImageIO.createImageInputStream(obj);
            Iterator<ImageReader> iter = ImageIO.getImageReaders(iis);
            ImageReader reader = iter.next();
            reader.setInput(iis);
            ImageReadParam param = reader.getDefaultReadParam();
            param.setSourceSubsampling(subsample, subsample, 0, 0);
            
            return reader.read(0, param);  
        }
        catch (IOException ex) {
            throw ex;
        }
        finally {
            if (iis != null) iis.close();
        }
    }
    
    private static int getDimension(Object obj, boolean max) throws IOException {

        ImageInputStream iis = null;
        try {
            if (obj instanceof URL) {
                obj = ((URL)obj).openStream();
            }
            iis = ImageIO.createImageInputStream(obj);
            if (iis == null) {
                throw new FileNotFoundException(obj + " not found");
            }
            Iterator<ImageReader> iter = ImageIO.getImageReaders(iis);
            ImageReader reader = iter.next();
            reader.setInput(iis);
            
            int w = reader.getWidth(0);
            int h = reader.getHeight(0);
            
            return max ? (w > h ? w : h) : (w < h ? w : h);
        }
        catch (IOException ex) {
            throw ex;
        }
        finally {
            if (iis != null) iis.close();
        }
    }    
}
