/*
  JSmooth: a VM wrapper toolkit for Windows
  Copyright (C) 2003 Rodrigo Reyes <reyes@charabia.net>
 
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.
 
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
 
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
 */

package net.charabia.jsmoothgen.application;

import net.charabia.jsmoothgen.skeleton.*;
import net.charabia.jsmoothgen.pe.*;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;
import java.awt.*;
import java.awt.image.*;
import java.lang.reflect.*;
import net.charabia.util.codec.*;

import javax.imageio.ImageIO;

public class ExeCompiler
{
    private java.util.Vector m_errors = new java.util.Vector();
    private Vector m_listeners = new Vector();
	
    public interface StepListener
    {
	public void setNewState(int percentComplete, String state);
	public void failed();
	public void complete();
    }
	
    public void addListener(ExeCompiler.StepListener listener)
    {
	m_listeners.add(listener);
		
    }
	
    public void cleanErrors()
    {
	m_errors.removeAllElements();
    }
	
    public java.util.Vector getErrors()
    {
	return m_errors;
    }
	
    public class CompilerRunner implements Runnable
    {
	private File m_skelroot;
	private SkeletonBean m_skel;
	private JSmoothModelBean m_data;
	private File m_out;
	private File m_basedir;

	public CompilerRunner(File skelroot, SkeletonBean skel, File basedir, JSmoothModelBean data, File out)
	{
	    m_skelroot = skelroot;
	    m_skel = skel;
	    m_data = data;
	    m_out = out;
	    m_basedir = basedir;
	}
		
	public void run()
	{
	    try
		{
		    compile(m_skelroot, m_skel, m_basedir, m_data, m_out);
		} catch (Exception exc)
		    {
			exc.printStackTrace();
		    }
	}
	
	public ExeCompiler getCompiler()
	{
	    return ExeCompiler.this;
	}
    }
	
    public ExeCompiler.CompilerRunner getRunnable(File skelroot, SkeletonBean skel, File basedir, JSmoothModelBean data, File out)
    {
	return new CompilerRunner(skelroot, skel, basedir, data, out);
    }
	
    public void compileAsync(File skelroot, SkeletonBean skel, File basedir, JSmoothModelBean data, File out)
    {
	Thread t = new Thread(new CompilerRunner(skelroot, skel, basedir, data, out));
	t.start();
    }
	
    public boolean compile(File skelroot, SkeletonBean skel, File basedir ,JSmoothModelBean data, File out) throws Exception
    {
	try {
	    fireStepChange(0, "Starting compilation");
	    
	    File pattern = new File(skelroot, skel.getExecutableName());
	    if (pattern.exists() == false)
		{
		    m_errors.add("Error: Can't find any skeleton at " + skelroot);
		    fireFailedChange();
		    return false;
		}
			
	    fireStepChange(10, "Scanning skeleton...");
	    PEFile pe = new PEFile(pattern);
	    pe.open();
	    PEResourceDirectory resdir = pe.getResourceDirectory();

	    boolean resb = false;
		
	    //
	    // Adds the jar only if the user selected one
	    //
	    if (data.getEmbeddedJar() == true)
		{
		    if (data.getJarLocation() == null)
			{
			    m_errors.add("Error: Jar is not specified!");
			    fireFailedChange();
			    return false;
			}
			
		    fireStepChange(40, "Loading Jar...");
		    File jarloc = concFile(basedir, new File(data.getJarLocation()));
		    if (jarloc.exists() == false)
			{
			    m_errors.add("Error: Can't find jar at " + jarloc);
			    fireFailedChange();
			    return false;
			}

		    ByteBuffer jardata = load(jarloc);

		    fireStepChange(60, "Adding Jar to Resources...");
		    resb = resdir.replaceResource(skel.getResourceCategory(), skel.getResourceJarId(), 1033, jardata);
		    if (resb == false)
			{
			    m_errors.add("Error: Can't replace jar resource! It is probably missing from the skeleton.");
			    fireFailedChange();
			    return false;
			}
		}
	    
	    fireStepChange(70, "Adding Properties to Resources...");
	    String props = PropertiesBuilder.makeProperties(basedir, data);
	    ByteBuffer propdata = convert(props);
	    resb = resdir.replaceResource(skel.getResourceCategory(), skel.getResourcePropsId(), 1033, propdata);

	    if (data.getIconLocation() != null)
		{
		    fireStepChange(80, "Loading icon...");
		    String iconpath;
		    if (new java.io.File(data.getIconLocation()).isAbsolute())
			iconpath = data.getIconLocation();
		    else
			iconpath = new java.io.File(basedir, data.getIconLocation()).getAbsolutePath();

		    Image img = getScaledImage(iconpath, 32, 32);
		    //Hashtable set = calculateColorCount(img);
		    //		    System.out.println("COLORS TOTAL 4: " + set.size());

		    if (img != null)
			{
			    net.charabia.jsmoothgen.pe.res.ResIcon32 resicon = new net.charabia.jsmoothgen.pe.res.ResIcon32(img);
			    pe.replaceDefaultIcon(resicon);
			}
		}

	    fireStepChange(90, "Saving exe...");
	    pe.dumpTo(out);
			
	    //		System.out.println("PROPERTIES:\n" + props);
			
	    fireCompleteChange();
	    return true;
	} catch (Exception exc)
	    {
		m_errors.add("Error: " + exc.getMessage());
		exc.printStackTrace();
		fireFailedChange();
		return false;
	    }
    }
	
    public Image[] loadImages(String path)
    {
	File f = new File(path);

	if (path.toUpperCase().endsWith(".ICO"))
	    {
		//
		// Try to load with our ico codec...
		//
		try {
		    java.awt.Image[] images = net.charabia.util.codec.IcoCodec.loadImages(f);
		    if ((images != null) && (images.length>0))
			{
			    return images;
			}
		} catch (java.io.IOException exc)
		    {
			exc.printStackTrace();
		    }
	    }

	// 
	// defaults to the standard java loading process
	//
        BufferedImage bufferedImage;
        try {
            bufferedImage = ImageIO.read(f);
            javax.swing.ImageIcon icon = new javax.swing.ImageIcon(bufferedImage, "default icon");
            java.awt.Image[] imgs = new java.awt.Image[1];
            imgs[0] = icon.getImage();
            return imgs;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;        
    }

    public void checkImageLoaded(Image img)
    {
	MediaTracker mtrack = new MediaTracker(new Canvas());
	mtrack.addImage(img, 1);
	try {
	    mtrack.waitForAll();
	} catch (InterruptedException e) {
	}
    }

    private Hashtable calculateColorCount(Image img)
    {
	int width = img.getWidth(null);
	int height = img.getHeight(null);
	int[] pixels = new int[width*height];
	PixelGrabber grabber = new PixelGrabber(img, 0, 0, width, height, pixels, 0, width);
	try
	    {
		grabber.grabPixels();
	    } catch (InterruptedException e)
		{
		    System.err.println("interrupted waiting for pixels!");
		    //		    throw new Exception("Can't load the image provided",e);
		}
		


	Hashtable result = new Hashtable();
	int colorindex = 0;
	for (int i=0; i<pixels.length; i++)
	    {
		int pix = pixels[i];
		if (((pix>>24)&0xFF) > 0)
		    {
			pix &= 0x00FFFFFF;
			Integer pixi = new Integer(pix);
			Object o = result.get(pixi);
			if (o == null)
			    {
				result.put(pixi, new Integer(colorindex++));
			    }
			//			if (colorindex > 256)
			//			    return result;
		    }
	    }
	return result;
    }

    public BufferedImage getQuantizedImage(Image img)
    {
        // 32 bit ico file already loaded as BufferedImage
        if (img instanceof BufferedImage) {
        return (BufferedImage) img;
        } else {
    int width = img.getWidth(null);
	int height = img.getHeight(null);
	int[][] data = new int[width][height];
	
	int[] pixelbuffer = new int[width*height];
	PixelGrabber grabber = new PixelGrabber(img, 0, 0, width, height, pixelbuffer, 0, width);
	try
	    {
		grabber.grabPixels();
	    } catch (InterruptedException e)
		{
		    System.err.println("interrupted waiting for pixels!");
		    throw new RuntimeException("Can't load the image provided",e);
		}
	for (int i=0; i<pixelbuffer.length; i++)
	    {
		data[i%width][i/width] = pixelbuffer[i];
	    }
	
	int[][] savedata = new int[width][height];	

	for(int y=0;y<height;y++)
	    for (int x=0;x<width;x++)
		savedata[x][y] = data[x][y];
	
	int[] palette = net.charabia.util.codec.Quantize.quantizeImage(data, 255);
	byte[] cmap = new byte[256*4];

	for (int i=0; i<palette.length; i++)
	    {
		//		System.out.println(" i= " + (i));
		cmap[(i*4)] = (byte)((palette[i] >> 16) & 0xFF);
		cmap[(i*4)+1] = (byte)((palette[i] >> 8) & 0xFF);
		cmap[(i*4)+2] = (byte) (palette[i] & 0xFF);
		cmap[(i*4)+3] = (byte) 0xFF;
	    }

	IndexColorModel colmodel = new IndexColorModel(8, palette.length, cmap, 0, true, 0);
	BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
	//
	// The normal manner of quantizing would be to run
	// result.setRGB(0,0, width, height, pixelbuffer, 0, width);
	// where result is a BufferedImage of
	// BufferedImage.TYPE_BYTE_INDEXED type. Unfortunately, I
	// couldn't make it work. So, here is a work-around that
	// should work similarly.
	//
	java.util.Hashtable set = new java.util.Hashtable();
	for (int y=0; y<height; y++)
	    {
		for (int x=0; x<width; x++)
		    {
			int alpha = (savedata[x][y]>>24)&0xFF;
			if (alpha == 0)
			    {
				result.setRGB(x,y, 0);
				// 				System.out.print(".");
			    }
			else
			    {
				int rgb = colmodel.getRGB(data[x][y]);
				rgb |= 0xFF000000;
				set.put(new Integer(rgb), new Integer(rgb));
				result.setRGB(x,y,rgb);
				// 				System.out.print("*");
			    }
		    }
		//		System.out.println("");
	    }


	return result;
        }
    }

    public Image checkImageSize(Image img, int width, int height)
    {
	int w = img.getWidth(null);
	int h = img.getHeight(null);
	if ((w == width) && (h == height))
	    return img;
	return null;
    }

    public Image getScaledImage(String path, int width, int height)
    {
	Image[] orgimages = loadImages(path);
	
	if ((orgimages == null) || (orgimages.length == 0))
	    return null;

	for (int i=0; i<orgimages.length; i++)
	    checkImageLoaded(orgimages[i]);

	//	System.out.println("Loaded " + orgimages.length + " images");
	for (int i=0; (i<orgimages.length); i++)
	    {
		int w = orgimages[i].getWidth(null);
		int h = orgimages[i].getHeight(null);
		//		System.out.println("Size of " + i + " = " + w + "," + h);
	    }	

	//
	// We prefer 32x32 pictures, then 64x64, then 16x16...
	//
	Image selected = null;
	for (int i=0; (i<orgimages.length) && (selected==null); i++)
	    selected = checkImageSize(orgimages[i], 32, 32);
	for (int i=0; (i<orgimages.length) && (selected==null); i++)
	    selected = checkImageSize(orgimages[i], 64, 64);
	for (int i=0; (i<orgimages.length) && (selected==null); i++)
	    selected = checkImageSize(orgimages[i], 16, 16);

	if (selected != null)
	    {
		return getQuantizedImage(selected);
	    }

	//
	// If there is no 32x32, 64x64, nor 16x16, then we scale the
	// biggest image to be 32x32... This should happen mainly when
	// loading an image from a png of gif file, and in most case
	// there is only one image on the array.
	//
	int maxsize = 0;
	Image biggest = null;
	for (int i=0; (i<orgimages.length) && (selected==null); i++)
	    {
		int size = orgimages[i].getWidth(null) * orgimages[i].getHeight(null);
		if (size>maxsize)
		    {
			maxsize = size;
			biggest = orgimages[i];
		    }
	    }

	if (biggest != null)
	    {
		BufferedImage result = getScaledInstance((BufferedImage)biggest, 32, 32, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
		checkImageLoaded(result);
		return getQuantizedImage(result);
	    }
	//
	// Here, we have failed and return null
	//
	return null;
    }
    
    /**
     * Convenience method that returns a scaled instance of the
     * provided {@code BufferedImage}.
     *
     * @param img the original image to be scaled
     * @param targetWidth the desired width of the scaled instance,
     *    in pixels
     * @param targetHeight the desired height of the scaled instance,
     *    in pixels
     * @param hint one of the rendering hints that corresponds to
     *    {@code RenderingHints.KEY_INTERPOLATION} (e.g.
     *    {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
     *    {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},
     *    {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})
     * @param higherQuality if true, this method will use a multi-step
     *    scaling technique that provides higher quality than the usual
     *    one-step technique (only useful in downscaling cases, where
     *    {@code targetWidth} or {@code targetHeight} is
     *    smaller than the original dimensions, and generally only when
     *    the {@code BILINEAR} hint is specified)
     * @return a scaled version of the original {@code BufferedImage}
     */
    public BufferedImage getScaledInstance(BufferedImage img,
                                           int targetWidth,
                                           int targetHeight,
                                           Object hint,
                                           boolean higherQuality)
    {
        int type = (img.getTransparency() == Transparency.OPAQUE) ?
            BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
        BufferedImage ret = (BufferedImage)img;
        int w, h;
        if (higherQuality) {
            // Use multi-step technique: start with original size, then
            // scale down in multiple passes with drawImage()
            // until the target size is reached
            w = img.getWidth();
            h = img.getHeight();
        } else {
            // Use one-step technique: scale directly from original
            // size to target size with a single drawImage() call
            w = targetWidth;
            h = targetHeight;
        }
        
        do {
            if (higherQuality && w > targetWidth) {
                w /= 2;
                if (w < targetWidth) {
                    w = targetWidth;
                }
            }

            if (higherQuality && h > targetHeight) {
                h /= 2;
                if (h < targetHeight) {
                    h = targetHeight;
                }
            }

            BufferedImage tmp = new BufferedImage(w, h, type);
            Graphics2D g2 = tmp.createGraphics();
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
            g2.drawImage(ret, 0, 0, w, h, null);
            g2.dispose();

            ret = tmp;
        } while (w != targetWidth || h != targetHeight);

        return ret;
    }



    private ByteBuffer load(File in) throws Exception
    {
	FileInputStream fis = new FileInputStream(in);
	ByteBuffer data = ByteBuffer.allocate((int)in.length());
	data.order(ByteOrder.LITTLE_ENDIAN);
	FileChannel fischan = fis.getChannel();
	fischan.read(data);
	data.position(0);
	fis.close();
		
	return data;
    }
	
    private ByteBuffer convert(String data)
    {
	ByteBuffer result = ByteBuffer.allocate(data.length()+1);
	result.position(0);
		
	for (int i=0; i<data.length(); i++)
	    {
		result.put((byte)data.charAt(i));
	    }
	result.put((byte)0);
		
	result.position(0);
	return result;
    }
	
    static public File concFile(File root, File name)
    {
	if (name.isAbsolute())
	    return name;
		
	return new File(root, name.toString());
    }
	
    public void fireStepChange(int percentComplete, String state)
    {
	for (Iterator i=m_listeners.iterator(); i.hasNext(); )
	    {
		ExeCompiler.StepListener l = (ExeCompiler.StepListener)i.next();
		l.setNewState(percentComplete, state);
	    }
    }
	
    public void fireFailedChange()
    {
	for (Iterator i=m_listeners.iterator(); i.hasNext(); )
	    {
		ExeCompiler.StepListener l = (ExeCompiler.StepListener)i.next();
		l.failed();
	    }
    }
    public void fireCompleteChange()
    {
	for (Iterator i=m_listeners.iterator(); i.hasNext(); )
	    {
		ExeCompiler.StepListener l = (ExeCompiler.StepListener)i.next();
		l.complete();
	    }
    }
	
}
