package railo.runtime.tag;

import java.awt.Color;
import java.io.IOException;

import javax.servlet.jsp.JspException;

import railo.commons.color.ColorCaster;
import railo.commons.io.res.Resource;
import railo.commons.io.res.util.ResourceUtil;
import railo.commons.lang.StringUtil;
import railo.runtime.exp.ApplicationException;
import railo.runtime.exp.ExpressionException;
import railo.runtime.exp.PageException;
import railo.runtime.ext.tag.TagImpl;
import railo.runtime.functions.other.CreateUUID;
import railo.runtime.functions.system.ContractPath;
import railo.runtime.img.ImageUtil;
import railo.runtime.img.MarpleCaptcha;
import railo.runtime.op.Caster;
import railo.runtime.type.Struct;
import railo.runtime.type.util.ArrayUtil;
import railo.runtime.type.util.ListUtil;

// GetWriteableImageFormats
// GetReadableImageFormats


/**
* Lets you resize and add labels to GIF and JPEG format images.
*
*
*
**/
public final class Image extends TagImpl {
	
	private static int ACTION_BORDER=0;
	private static int ACTION_CAPTCHA=1;
	private static int ACTION_CONVERT=2;
	private static int ACTION_INFO=3;
	private static int ACTION_READ=4;
	private static int ACTION_RESIZE=5;
	private static int ACTION_ROTATE=6;
	private static int ACTION_WRITE=7;
	private static int ACTION_WRITE_TO_BROWSER=8;


	private int action=ACTION_READ;
	private String strAction="read";
	private int angle=-1;
	private Color color=Color.BLACK;
	private Resource destination;
	private int difficulty=MarpleCaptcha.DIFFICULTY_LOW;
	private String[] fonts=new String[]{"arial"};
	private int fontsize=24;
	private String format="png";
	private String height;
	private String width;
	private boolean isbase64;
	private String name;
	private boolean overwrite;
	private float quality=.75F;
	private Object oSource;
	private railo.runtime.img.Image source;
	private String structName;
	private String text;
	private int thickness=1;
	private String passthrough;
	private boolean base64;
	



	@Override
	public void release() {
		super.release();
		action=ACTION_READ;
		strAction="read";
		angle=-1;
		color=Color.BLACK;
		destination=null;
		difficulty=MarpleCaptcha.DIFFICULTY_LOW;
		fonts=new String[]{"arial"};
		fontsize=24;
		format="png";
		height=null;
		width=null;
		isbase64=false;
		name=null;
		overwrite=false;
		quality=0.75F;
		source=null;
		oSource=null;
		structName=null;
		text=null;
		thickness=1;
		passthrough=null;
		base64=false;
	}


	/**
	 * @param action the action to set
	 * @throws ApplicationException 
	 */
	public void setAction(String strAction) throws ApplicationException {
		this.strAction = strAction;
		strAction=strAction.trim().toLowerCase();
		if(StringUtil.isEmpty(strAction))action=ACTION_READ;
		else if("border".equals(strAction))action=ACTION_BORDER;
		else if("captcha".equals(strAction))action=ACTION_CAPTCHA;
		else if("convert".equals(strAction))action=ACTION_CONVERT;
		else if("info".equals(strAction))action=ACTION_INFO;
		else if("read".equals(strAction))action=ACTION_READ;
		else if("resize".equals(strAction))action=ACTION_RESIZE;
		else if("rotate".equals(strAction))action=ACTION_ROTATE;
		else if("write".equals(strAction))action=ACTION_WRITE;
		else if("writetobrowser".equals(strAction))action=ACTION_WRITE_TO_BROWSER;
		else if("write-to-browser".equals(strAction))action=ACTION_WRITE_TO_BROWSER;
		else if("write_to_browser".equals(strAction))action=ACTION_WRITE_TO_BROWSER;
		else throw new ApplicationException("invalid action ["+this.strAction+"], " +
		"valid actions are [border,captcha,convert,info,read,resize,rotate,write,writeToBrowser]");
	}


	/**
	 * @param base64 the base64 to set
	 */
	public void setBase64(boolean base64) {
		this.base64 = base64;
	}


	/**
	 * @param angle the angle to set
	 */
	public void setAngle(double angle) {
		this.angle = (int) angle;
	}


	/**
	 * @param color the color to set
	 * @throws ExpressionException 
	 */
	public void setColor(String strColor) throws ExpressionException {
		this.color = ColorCaster.toColor(strColor);
	}


	/**
	 * @param destination the destination to set
	 */
	public void setDestination(String destination) {
		this.destination = ResourceUtil.toResourceNotExisting(pageContext, destination);
	}


	/**
	 * @param difficulty the difficulty to set
	 * @throws ApplicationException 
	 */
	public void setDifficulty(String strDifficulty) throws ApplicationException {
		strDifficulty=strDifficulty.trim().toLowerCase();
		if(StringUtil.isEmpty(strDifficulty))	difficulty=MarpleCaptcha.DIFFICULTY_LOW;
		else if("low".equals(strDifficulty))	difficulty=MarpleCaptcha.DIFFICULTY_LOW;
		else if("medium".equals(strDifficulty))	difficulty=MarpleCaptcha.DIFFICULTY_MEDIUM;
		else if("high".equals(strDifficulty))	difficulty=MarpleCaptcha.DIFFICULTY_HIGH;
		else throw new ApplicationException("invalid difficulty level ["+strDifficulty+"], " +
		"valid difficulty level are [low,medium,high]");
		
	}


	/**
	 * @param fonts the fonts to set
	 * @throws PageException 
	 */
	public void setFonts(String fontList) throws PageException {
		fonts=ArrayUtil.trim(ListUtil.toStringArray(ListUtil.listToArray(fontList, ',')));
	}

	

	/**
	 * @param passthrough the passthrough to set
	 */
	public void setPassthrough(String passthrough) {
		this.passthrough = passthrough;
	}

	/**
	 * @param fontsize the fontsize to set
	 */
	public void setFontsize(double fontsize) {
		this.fontsize = (int) fontsize;
	}


	/**
	 * @param format the format to set
	 * @throws ApplicationException 
	 */
	public void setFormat(String format) throws ApplicationException {
		format=format.trim().toLowerCase();
		if("gif".equalsIgnoreCase(format))		this.format =  "gif";
		else if("jpg".equalsIgnoreCase(format))	this.format =  "jpg";
		else if("jpe".equalsIgnoreCase(format))	this.format =  "jpg";
		else if("jpeg".equalsIgnoreCase(format))this.format =  "jpg";
		else if("png".equalsIgnoreCase(format))	this.format =  "png";
		else if("tiff".equalsIgnoreCase(format))this.format =  "tiff";
		else if("bmp".equalsIgnoreCase(format))	this.format =  "bmp";
		else throw new ApplicationException("invalid format ["+format+"], " +
		"valid formats are [gif,jpg,png,tiff,bmp]");
	}


	/**
	 * @param height the height to set
	 */
	public void setHeight(String height) {
		this.height = height;
	}


	/**
	 * @param width the width to set
	 */
	public void setWidth(String width) {
		this.width = width;
	}


	/**
	 * @param isbase64 the isbase64 to set
	 */
	public void setIsbase64(boolean isbase64) {
		this.isbase64 = isbase64;
	}


	/**
	 * @param name the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}


	/**
	 * @param overwrite the overwrite to set
	 */
	public void setOverwrite(boolean overwrite) {
		this.overwrite = overwrite;
	}


	/**
	 * @param quality the quality to set
	 * @throws ApplicationException 
	 */
	public void setQuality(double quality) throws ApplicationException {
		this.quality = (float) quality;
		if(quality<0 || quality>1)
			throw new ApplicationException("quality ("+Caster.toString(quality)+") has to be a value between 0 and 1");
	}


	/**
	 * @param source the source to set
	 * @throws PageException 
	 */
	public void setSource(Object source) {
		this.oSource=source;
		// this.source=railo.runtime.img.Image.createImage(pageContext, source, false, false);
	}


	/**
	 * @param structName the structName to set
	 */
	public void setStructname(String structName) {
		this.structName = structName;
	}


	/**
	 * @param structName the structName to set
	 */
	public void setResult(String structName) {
		this.structName = structName;
	}


	/**
	 * @param text the text to set
	 */
	public void setText(String text) {
		this.text = text;
	}


	/**
	 * @param thickness the thickness to set
	 */
	public void setThickness(double thickness) {
		this.thickness = (int) thickness;
	}


	@Override
	public int doStartTag() throws JspException {
		try {
			if(this.oSource!=null){
				if(isbase64) this.source=new railo.runtime.img.Image(Caster.toString(oSource));
				else this.source=railo.runtime.img.Image.createImage(pageContext, oSource, false, false,true,null);
			}
			
			if(action==ACTION_BORDER)		doActionBorder();
			else if(action==ACTION_CAPTCHA) doActionCaptcha();
			else if(action==ACTION_CONVERT) doActionConvert();
			else if(action==ACTION_INFO) 	doActionInfo();
			else if(action==ACTION_READ) 	doActionRead();
			else if(action==ACTION_RESIZE) 	doActionResize();
			else if(action==ACTION_ROTATE) 	doActionRotate();
			else if(action==ACTION_WRITE) 	doActionWrite();
			else if(action==ACTION_WRITE_TO_BROWSER) 
											doActionWriteToBrowser();
		}
		catch(Throwable t) {
			throw Caster.toPageException(t);
		}
		return SKIP_BODY;
	}

	// Add a border to an image 
	private void doActionBorder() throws PageException, IOException {
		required("source", source);
		
		source.addBorder(thickness,color,railo.runtime.img.Image.BORDER_TYPE_CONSTANT);
		write();
		    
	}


	// Create a CAPTCHA image
	private void doActionCaptcha() throws PageException, IOException {
		required("height", height);
		required("width", width);
		required("text", text);

        boolean doRenderHtmlTag = ( destination == null );

		String path=null;
		
		// create destination
		if(StringUtil.isEmpty(name))path=touchDestination();
		
		MarpleCaptcha c=new MarpleCaptcha();
		source=new railo.runtime.img.Image(c.generate(text, Caster.toIntValue(width),Caster.toIntValue(height), fonts, true, 
				Color.BLACK,fontsize,
				difficulty));
		
		// link destination
		if ( doRenderHtmlTag )
			writeLink( path );
		
		// write out
		write();
	}
	
	private void writeLink(String path) throws IOException, PageException {
		String add="";
		if(passthrough!=null) {
            add=" "+passthrough;
        }
		
		if(base64) {
			String b64 = source.getBase64String(format);
			pageContext.write("<img src=\"data:image/"+ImageUtil.getMimeTypeFromFormat(format)+";base64,"+b64+"\" width=\""+source.getWidth()+"\" height=\""+source.getHeight()+"\""+add+" />");
			return;
		}
		pageContext.write("<img src=\""+path+"\" width=\""+source.getWidth()+"\" height=\""+source.getHeight()+"\""+add+" />");
	
	
	}


	private String touchDestination() throws IOException {
		if(destination==null) {
			String name=CreateUUID.call(pageContext)+"."+format;
			Resource folder = pageContext.getConfig().getTempDirectory().getRealResource("graph");
			if(!folder.exists())folder.createDirectory(true);
			destination = folder.getRealResource(name);
			cleanOld(folder);
			
			// create path
			String cp = pageContext.getHttpServletRequest().getContextPath();
			if(StringUtil.isEmpty(cp)) cp="";
			return cp+"/railo-context/graph.cfm?img="+name+"&type="+(ListUtil.last(ImageUtil.getMimeTypeFromFormat(format),'/').trim());
		}
		return ContractPath.call(pageContext, destination.getAbsolutePath());
	}


	private static void cleanOld(Resource folder) throws IOException {
		if(!folder.exists())folder.createDirectory(true);
		else if(folder.isDirectory() && ResourceUtil.getRealSize(folder)>(1024*1024)) {
			
			Resource[] children = folder.listResources();
			long maxAge=System.currentTimeMillis()-(1000*60);
			for(int i=0;i<children.length;i++) {
				if(children[i].lastModified()<maxAge)
					children[i].delete();
			}
		}
	}

	// Convert an image file format
	private void doActionConvert() throws PageException, IOException {
		required("source", source);
		required("destination", destination);
		
		source.convert(ImageUtil.getFormat(destination));
		write();
	}

	// Retrieve information about an image
	private void doActionInfo() throws PageException {
		required("source",source);
		required("structname",structName);
		
		pageContext.setVariable(structName, source.info());
	}
	
	private Struct doActionInfo(railo.runtime.img.Image source) throws PageException {
		return source.info();
	}


	// Read an image into memory
	private void doActionRead() throws PageException {
		required("source",source);
		required("name",name);
		
		pageContext.setVariable(name, source);
	}


	// Resize an image
	private void doActionResize() throws PageException, IOException {
		required("source", source);
		
		//Info i = new Info(source);
		/*int w = toDimension("width",width,i);
		int h = toDimension("height",height,i);
		if(w==-1 && h==-1)
			throw new ApplicationException("Missing attribute [width or height]. The action ["+strAction+"] requires the attribute [width or height].");
		*/
		
		source.resize(width,height,"highestquality",1D);
		
		write();
	}
	
	// Rotate an image
	private void doActionRotate() throws PageException, IOException {
		required("source",source);
		required("angle",angle,-1);
		
		source.rotate(-1, -1, angle, railo.runtime.img.Image.INTERPOLATION_NONE);
		write();
		
	}

	// Write an image to a file
	private void doActionWrite() throws ApplicationException, IOException, ExpressionException {
		required("source", source);
		required("destination", destination);
		
		source.writeOut(destination,overwrite,quality);
	}

	// Write an image to the browser
	private void doActionWriteToBrowser() throws IOException, PageException {
		required("source", source);
		
		String path=null;
		
		// create destination
		if(!base64 || !StringUtil.isEmpty(name)) {
			path=touchDestination();
			write();
		}
		// link destination
		if(StringUtil.isEmpty(name))writeLink(path);
		
		
		
		
	}



	private void required(String label, Object value) throws ApplicationException {
		if(value==null)
			throw new ApplicationException("Missing attribute ["+label+"]. The action ["+strAction+"] requires the attribute [" + label + "].");
			//throw new ApplicationException("missing attribute ["+label+"], for the action ["+strAction+"] this attribute is required but was not passed in");
	}
	private void required(String label, int value,int nullValue) throws ApplicationException {
		if(value==nullValue) 
			throw new ApplicationException("Missing attribute ["+label+"]. The action ["+strAction+"] requires the attribute [" + label + "].");
			//throw new ApplicationException("missing attribute ["+label+"], for the action ["+strAction+"] this attribute is required but was not passed in");
	}


	private void write() throws IOException, PageException {
		if(destination!=null) {
			doActionWrite();
		}
		if(!StringUtil.isEmpty(name)) {
			required("source", source);
			pageContext.setVariable(name, source);// TODO ist das so gut
		}
		//if(writeToResponseWhenNoOtherTarget) doActionWriteToBrowser();
	}
	

	class Info {
		Struct struct;
		private railo.runtime.img.Image img;

		public Info(railo.runtime.img.Image img) {
			this.img=img;
		}

		/**
		 * @return the sct
		 * @throws PageException 
		 */
		public Struct getStruct() throws PageException {
			if(struct==null)
				struct=doActionInfo(img);
			return struct;
		}
	}


	private int toDimension(String label, String dimension, Info info) throws PageException {
		if(StringUtil.isEmpty(dimension)) return -1;
		dimension=dimension.trim();
		// int value
		int i=Caster.toIntValue(dimension,-1);
		if(i>-1) return i;
		
		// percent value
		if(StringUtil.endsWith(dimension, '%')) {
			float pro=Caster.toIntValue(
						dimension.substring(0,dimension.length()-1).trim(),
						-1);
			if(pro<0 || pro>100) 
				throw new ExpressionException("attribute ["+label+"] value has an invalid percent definition ["+dimension+"]"); 
			pro/=100F;
			return (int)(Caster.toFloatValue(info.getStruct().get(label))*pro);
		}
		throw new ExpressionException("attribute ["+label+"] value has an invalid definition ["+dimension+"]"); 
		
	}

}