package railo.runtime.tag;


import java.awt.Dimension;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;

import railo.commons.io.IOUtil;
import railo.commons.io.res.Resource;
import railo.commons.io.res.util.ResourceUtil;
import railo.commons.lang.StringUtil;
import railo.commons.pdf.PDFDocument;
import railo.commons.pdf.PDFException;
import railo.commons.pdf.PDFPageMark;
import railo.runtime.Info;
import railo.runtime.PageContextImpl;
import railo.runtime.exp.ApplicationException;
import railo.runtime.exp.PageException;
import railo.runtime.exp.TemplateException;
import railo.runtime.ext.tag.BodyTagImpl;
import railo.runtime.op.Caster;
import railo.runtime.text.pdf.PDFUtil;
import railo.runtime.type.ReadOnlyStruct;

import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.PdfCopy;
import com.lowagie.text.pdf.PdfImportedPage;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.SimpleBookmark;

public final class Document extends BodyTagImpl {

	
	//private static final String STYLE_BG_INVISIBLE = "background-color: transparent; background-image: none;";
	

	
	private Resource filename=null;
	private boolean overwrite=false;
	private String name=null;
	private Dimension pagetype=PDFDocument.PAGETYPE_LETTER;
	private double pageheight=0;
	private double pagewidth=0;
	private boolean isLandscape=false;

	
	private double unitFactor=PDFDocument.UNIT_FACTOR_IN;
	private int encryption=PDFDocument.ENC_NONE;

	private String ownerpassword=null;
	private String userpassword="empty";
	private int scale=-1;

	// TODO impl. tag Document backgroundvisible,fontembed,scale
	private boolean backgroundvisible;
	private int fontembed=PDFDocument.FONT_EMBED_YES;
	
	private int permissions=0;
	private PDFDocument _document;



	private ArrayList<PDFDocument> documents=new ArrayList<PDFDocument>(); 
	
	public Document() {
		this._document=null;
	}
	
	@Override
	public void release() {
		super.release();
		filename=null;
		overwrite=false;
		name=null;
		pagetype=PDFDocument.PAGETYPE_LETTER;
		pageheight=0;
		pagewidth=0;
		isLandscape=false;
		unitFactor=PDFDocument.UNIT_FACTOR_IN;
		encryption=PDFDocument.ENC_NONE;
		ownerpassword=null;
		userpassword="empty";
		permissions=0;
		scale=-1;
		documents.clear();
		_document=null;
		backgroundvisible=false;
		fontembed=PDFDocument.FONT_EMBED_YES;

		
		
	}
	
	private PDFDocument getDocument() {
		//SerialNumber sn = pageContext.getConfig().getSerialNumber();
		if(_document==null){
			_document=new PDFDocument();
		}
		return _document;
	}
	
	

	/** set the value proxyserver
	*  Host name or IP address of a proxy server.
	* @param proxyserver value to set
	**/
	public void setProxyserver(String proxyserver)	{
		getDocument().setProxyserver(proxyserver);
	}
	public void setProxyhost(String proxyserver)	{
		getDocument().setProxyserver(proxyserver);
	}
	
	/** set the value proxyport
	*  The port number on the proxy server from which the object is requested. Default is 80. When 
	* 	used with resolveURL, the URLs of retrieved documents that specify a port number are automatically 
	* 	resolved to preserve links in the retrieved document.
	* @param proxyport value to set
	**/
	public void setProxyport(double proxyport)	{
		getDocument().setProxyport((int)proxyport);
	}

	/** set the value username
	*  When required by a proxy server, a valid username.
	* @param proxyuser value to set
	**/
	public void setProxyuser(String proxyuser)	{
		getDocument().setProxyuser(proxyuser);
	}

	/** set the value password
	*  When required by a proxy server, a valid password.
	* @param proxypassword value to set
	**/
	public void setProxypassword(String proxypassword)	{
		getDocument().setProxypassword(proxypassword);
	}

	public void setSaveasname(String saveAsName) {
		// TODO impl
	}
	
	/**
	 * @param authUser the authUser to set
	 */
	public void setAuthuser(String authUser) {
		getDocument().setAuthUser(authUser);
	}

	/**
	 * @param authPassword the authPassword to set
	 */
	public void setAuthpassword(String authPassword) {
		getDocument().setAuthPassword(authPassword);
	}

	/**
	 * @param userAgent the userAgent to set
	 */
	public void setUseragent(String userAgent) {
		getDocument().setUserAgent(userAgent);
	}
	
	/**
	 * @param format the format to set
	 * @throws ApplicationException 
	 */
	public void setFormat(String format) throws ApplicationException {
		format = StringUtil.toLowerCase(format.trim());
		if(!"pdf".equals(format))
			throw new ApplicationException("invalid format ["+format+"], only the following format is supported [pdf]");
	}

	/**
	 * @param filename the filename to set
	 * @throws PageException 
	 */
	public void setFilename(String filename) throws PageException {
		this.filename = ResourceUtil.toResourceNotExisting(pageContext, filename);
		pageContext.getConfig().getSecurityManager().checkFileLocation(this.filename);
	}

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

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

	/**
	 * @param pagetype the pagetype to set
	 * @throws ApplicationException 
	 */
	public void setPagetype(String strPagetype) throws ApplicationException {
		strPagetype=StringUtil.toLowerCase(strPagetype.trim());
		if("legal".equals(strPagetype))			pagetype=PDFDocument.PAGETYPE_LEGAL;
		else if("letter".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_LETTER;
		else if("a4".equals(strPagetype))		pagetype=PDFDocument.PAGETYPE_A4;
		else if("a5".equals(strPagetype))		pagetype=PDFDocument.PAGETYPE_A5;
		else if("b4".equals(strPagetype))		pagetype=PDFDocument.PAGETYPE_B4;
		else if("b5".equals(strPagetype))		pagetype=PDFDocument.PAGETYPE_B5;
		else if("b4-jis".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_B4_JIS;
		else if("b4 jis".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_B4_JIS;
		else if("b4_jis".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_B4_JIS;
		else if("b4jis".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_B4_JIS;
		else if("b5-jis".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_B5_JIS;
		else if("b5 jis".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_B5_JIS;
		else if("b5_jis".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_B5_JIS;
		else if("b5jis".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_B5_JIS;
		else if("custom".equals(strPagetype))	pagetype=PDFDocument.PAGETYPE_CUSTOM;
		else throw new ApplicationException("invalid page type ["+strPagetype+"], valid page types are [legal,letter,a4,a5,b4,b5,b4-jis,b5-jis,custom]");
		
		
	}

	/**
	 * @param pageheight the pageheight to set
	 * @throws ApplicationException 
	 */
	public void setPageheight(double pageheight) throws ApplicationException {
		if(pageheight<1) throw new ApplicationException("pageheight must be a positive number");
		this.pageheight = pageheight;
	}

	/**
	 * @param pagewidth the pagewidth to set
	 * @throws ApplicationException 
	 */
	public void setPagewidth(double pagewidth) throws ApplicationException {
		if(pagewidth<1) throw new ApplicationException("pagewidth must be a positive number");
		this.pagewidth = pagewidth;
	}

	/**
	 * @param orientation the orientation to set
	 * @throws ApplicationException 
	 */
	public void setOrientation(String strOrientation) throws ApplicationException {
		strOrientation=StringUtil.toLowerCase(strOrientation.trim());
		if("portrait".equals(strOrientation))		isLandscape=false;
		else if("landscape".equals(strOrientation))	isLandscape=true;
		else throw new ApplicationException("invalid orientation ["+strOrientation+"], valid orientations are [portrait,landscape]");
		
	}

	/**
	 * @param marginbottom the marginbottom to set
	 */
	public void setMarginbottom(double marginbottom) {
		getDocument().setMarginbottom(marginbottom);
	}

	/**
	 * @param marginleft the marginleft to set
	 */
	public void setMarginleft(double marginleft) {
		getDocument().setMarginleft(marginleft);
	}

	/**
	 * @param marginright the marginright to set
	 */
	public void setMarginright(double marginright) {
		getDocument().setMarginright(marginright);
	}

	/**
	 * @param margintop the margintop to set
	 */
	public void setMargintop(double margintop) {
		getDocument().setMargintop(margintop);
	}

	/**
	 * @param bookmark the bookmark to set
	 */
	public void setBookmark(boolean bookmark) {
		getDocument().setBookmark(bookmark);
	}

	public void setHtmlbookmark(boolean bookmark) {
		getDocument().setHtmlBookmark(bookmark);
	}

	/**
	 * @param localUrl the localUrl to set
	 */
	public void setLocalurl(boolean localUrl) {
		getDocument().setLocalUrl(localUrl);
	}

	/**
	 * @param unitFactor the unit to set
	 * @throws ApplicationException 
	 */
	public void setUnit(String strUnit) throws ApplicationException {
		strUnit=StringUtil.toLowerCase(strUnit.trim());
		if("in".equals(strUnit))		unitFactor=PDFDocument.UNIT_FACTOR_IN;
		else if("cm".equals(strUnit))	unitFactor=PDFDocument.UNIT_FACTOR_CM;
		else if("point".equals(strUnit))	unitFactor=PDFDocument.UNIT_FACTOR_POINT;
		else throw new ApplicationException("invalid unit ["+strUnit+"], valid units are [cm,in,point]");
	}

	/**
	 * @param encryption the encryption to set
	 * @throws ApplicationException 
	 */
	public void setEncryption(String strEncryption) throws ApplicationException {
		strEncryption=StringUtil.toLowerCase(strEncryption.trim());
		if("none".equals(strEncryption))			encryption=PDFDocument.ENC_NONE;
		else if("40-bit".equals(strEncryption))		encryption=PDFDocument.ENC_40BIT;
		else if("40bit".equals(strEncryption))		encryption=PDFDocument.ENC_40BIT;
		else if("40 bit".equals(strEncryption))		encryption=PDFDocument.ENC_40BIT;
		else if("40_bit".equals(strEncryption))		encryption=PDFDocument.ENC_40BIT;
		else if("128-bit".equals(strEncryption))	encryption=PDFDocument.ENC_128BIT;
		else if("128bit".equals(strEncryption))		encryption=PDFDocument.ENC_128BIT;
		else if("128 bit".equals(strEncryption))	encryption=PDFDocument.ENC_128BIT;
		else if("128_bit".equals(strEncryption))	encryption=PDFDocument.ENC_128BIT;
		else throw new ApplicationException("invalid encryption ["+strEncryption+"], valid encryption values are [none, 40-bit, 128-bit]");
	}

	/**
	 * @param ownerpassword the ownerpassword to set
	 * @throws ApplicationException 
	 */
	public void setOwnerpassword(String ownerpassword) {
		this.ownerpassword = ownerpassword;
	}

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

	/**
	 * @param permissions the permissions to set
	 * @throws PageException 
	 */
	public void setPermissions(String strPermissions) throws PageException {
		permissions=PDFUtil.toPermissions(strPermissions);
	}

	/**
	 * @param scale the scale to set
	 * @throws ApplicationException 
	 */
	public void setScale(double scale) throws ApplicationException {
		if(scale<0) throw new ApplicationException("scale must be a positive number");
		if(scale>100) throw new ApplicationException("scale must be a number less or equal than 100");
		this.scale = (int) scale;
	}

	/**
	 * @param src the src to set
	 * @throws ApplicationException 
	 */
	public void setSrc(String src) throws ApplicationException {
		try {
			getDocument().setSrc(src);
		} catch (PDFException e) {
			throw new ApplicationException(e.getMessage());
		}
	}

	/**
	 * @param srcfile the srcfile to set
	 * @throws PageException 
	 * @throws  
	 */
	public void setSrcfile(String strSrcfile) throws PageException {
		Resource srcfile = ResourceUtil.toResourceExisting(pageContext, strSrcfile);
		pageContext.getConfig().getSecurityManager().checkFileLocation(srcfile);
		try {
			getDocument().setSrcfile(srcfile);
		} catch (PDFException e) {
			throw new ApplicationException(e.getMessage());
		}
	}

	/**
	 * @param mimetype the mimetype to set
	 */
	public void setMimetype(String strMimetype) {
		getDocument().setMimetype(strMimetype);
		strMimetype = strMimetype.toLowerCase().trim();
	}
	

	public void setHeader(PDFPageMark header) {
		getDocument().setHeader(header);
	}

	public void setFooter(PDFPageMark footer) {
		getDocument().setFooter(footer);
	}
	

	public void setBackgroundvisible(boolean backgroundvisible) {
		this.backgroundvisible=backgroundvisible;
	}

	public void setFontembed(String fontembed) throws PDFException {
		Boolean fe=Caster.toBoolean(fontembed,null);
		if(fe==null) {
			fontembed=StringUtil.toLowerCase(fontembed.trim());
			if("selective".equals(fontembed))
				this.fontembed=PDFDocument.FONT_EMBED_SELECCTIVE;
			else throw new PDFException("invalid value for fontembed ["+fontembed+"], valid values for fontembed are [yes,no,selective]");
			
		}
		else if(fe.booleanValue())this.fontembed=PDFDocument.FONT_EMBED_YES;
		else this.fontembed=PDFDocument.FONT_EMBED_NO;
		getDocument().setFontembed(this.fontembed);
	}

	public void addPDFDocument(PDFDocument document) {
		// set proxy settings
		if(_document!=null)	{
			if(_document.hasProxy()) {
				document.setProxyserver(_document.getProxyserver());
				document.setProxyport(_document.getProxyport());
				document.setProxyuser(_document.getProxyuser());
				document.setProxypassword(_document.getProxypassword());
			}
			document.setBookmark(_document.getBookmark());
			document.setLocalUrl(_document.getLocalUrl());
		}
		
		
		documents.add(document);
	}

    @Override
	public int doStartTag() throws PageException	{
		// SerialNumber sn = pageContext.getConfig().getSerialNumber();
	    //if(sn.getVersion()==SerialNumber.VERSION_COMMUNITY)
	    //    throw new SecurityException("no access to this functionality with the "+sn.getStringVersion()+" version of railo");

	    
	    ReadOnlyStruct cfdoc=new ReadOnlyStruct();
	    cfdoc.setEL("currentpagenumber", "{currentpagenumber}");
	    cfdoc.setEL("totalpagecount", "{totalpagecount}");
	    cfdoc.setEL("totalsectionpagecount", "{totalsectionpagecount}");
	    cfdoc.setEL("currentsectionpagenumber", "{currentsectionpagenumber}");
	    cfdoc.setReadOnly(true);
	    pageContext.variablesScope().setEL("cfdocument", cfdoc);

	    return EVAL_BODY_BUFFERED;
	}

	@Override
	public void doInitBody()	{
		
	}
	
	@Override
	public int doAfterBody()	{
		getDocument().setBody(bodyContent.getString());
		
		return SKIP_BODY;
	}
	
	@Override
	public int doEndTag() throws PageException {
		try {
			_doEndTag();
		}
		catch (Exception e) {
			throw Caster.toPageException(e);
		}	
		return EVAL_PAGE;
	}
	
	public void _doEndTag() throws JspException, IOException, DocumentException { 
		// set root header/footer to sections
		boolean doBookmarks=false;
		boolean doHtmlBookmarks=false;
		if(_document!=null){
			PDFPageMark header = _document.getHeader();
			PDFPageMark footer = _document.getFooter();
			boolean hasHeader=header!=null;
			boolean hasFooter=footer!=null;
			if(hasFooter || hasHeader) {
				Iterator<PDFDocument> it = documents.iterator();
				PDFDocument doc;
				while(it.hasNext()){
					doc=it.next();
					if(hasHeader && doc.getHeader()==null) doc.setHeader(header);
					if(hasFooter && doc.getFooter()==null) doc.setFooter(footer);
				}
			}
			doBookmarks=_document.getBookmark();
			doHtmlBookmarks=_document.getHtmlBookmark();
		}
		
		
		if(filename!=null)  {
			if(filename.exists() && !overwrite)
	    		throw new ApplicationException("file ["+filename+"] already exist","to allow overwrite the resource, set attribute [overwrite] to [true]");
	    	
	    	OutputStream os= null;
	    	try {
	    		os= filename.getOutputStream();
				render(os,doBookmarks,doHtmlBookmarks);
			} 
	    	finally {
				IOUtil.closeEL(os);
			}
	    	
	    }
		else if(!StringUtil.isEmpty(name)) {
			render(null,doBookmarks,doHtmlBookmarks);
		}
	    else  {
	    	HttpServletResponse rsp = pageContext. getHttpServletResponse();
		    if(rsp.isCommitted())
	            throw new ApplicationException("content is already flushed","you can't rewrite head of response after part of the page is flushed");
	        rsp.setContentType("application/pdf");
	    
	    	
	    	OutputStream os=getOutputStream();   
		    try {
		    	render(os,doBookmarks,doHtmlBookmarks);
		    } 
		    finally {
		    	IOUtil.flushEL(os);
		        IOUtil.closeEL(os);
		        ((PageContextImpl)pageContext).getRootOut().setClosed(true);
		    }
		    throw new railo.runtime.exp.Abort(railo.runtime.exp.Abort.SCOPE_REQUEST);
	    }
       
	}



	private void render(OutputStream os, boolean doBookmarks, boolean doHtmlBookmarks) throws IOException, PageException, DocumentException {
		byte[] pdf=null;
		// merge multiple docs to 1
		if(documents.size()>1) {
			PDFDocument[] pdfDocs=new PDFDocument[documents.size()];
			PdfReader[] pdfReaders = new PdfReader[pdfDocs.length];
			Iterator<PDFDocument> it = documents.iterator();
			int index=0;
			// generate pdf with pd4ml
			while(it.hasNext()) {
				pdfDocs[index]=it.next();
				pdfReaders[index]=
					new PdfReader(pdfDocs[index].render(getDimension(),unitFactor,pageContext,doHtmlBookmarks));
				index++;
			}
			
			// collect together
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			com.lowagie.text.Document document = 
				new com.lowagie.text.Document(pdfReaders[0].getPageSizeWithRotation(1));
			PdfCopy copy = new PdfCopy(document,baos);
			document.open();
			String name;
			ArrayList bookmarks=doBookmarks?new ArrayList():null;
			try {
				int size,totalPage=0;
				Map parent;
				for(int doc=0;doc<pdfReaders.length;doc++) {
					size=pdfReaders[doc].getNumberOfPages();
					
					PdfImportedPage ip;
					
					// bookmarks
					if(doBookmarks) {
						name=pdfDocs[doc].getName();
						if(!StringUtil.isEmpty(name)) {
							bookmarks.add(parent=PDFUtil.generateGoToBookMark(name, totalPage+1));
						}
						else parent=null;
						
						if(doHtmlBookmarks) {
							java.util.List pageBM = SimpleBookmark.getBookmark(pdfReaders[doc]);
							if(pageBM!=null) {
								if(totalPage>0)SimpleBookmark.shiftPageNumbers(pageBM, totalPage, null);
								if(parent!=null)PDFUtil.setChildBookmarks(parent,pageBM);
								else bookmarks.addAll(pageBM);
							}
						}
					}
					
					totalPage++;
					for(int page=1;page<=size;page++) {
						if(page>1)totalPage++;
						ip = copy.getImportedPage(pdfReaders[doc], page);
						
						//ip.getPdfDocument().setHeader(arg0);
						//ip.getPdfDocument().setFooter(arg0);
						copy.addPage(ip);
					}
				}
				if (doBookmarks && !bookmarks.isEmpty())copy.setOutlines(bookmarks);
			}
			finally {
				document.close();
			}
			pdf=baos.toByteArray();
		}
		else if(documents.size()==1){
			pdf=(documents.get(0)).render(getDimension(),unitFactor,pageContext,doHtmlBookmarks);
		}
		else {
			pdf=getDocument().render(getDimension(),unitFactor,pageContext,doHtmlBookmarks);
		}
		
		// permission/encryption
		if(PDFDocument.ENC_NONE!=encryption) {
			PdfReader reader = new PdfReader(pdf);
			com.lowagie.text.Document document = new com.lowagie.text.Document(reader.getPageSize(1));
			document.addCreator("Railo "+Info.getVersionAsString()+" "+Info.getStateAsString());
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			PdfCopy copy = new PdfCopy(document,baos);
			//PdfWriter writer = PdfWriter.getInstance(document, pdfOut);
			copy.setEncryption(PDFDocument.ENC_128BIT==encryption , userpassword , ownerpassword , permissions);
			document.open();
			int size=reader.getNumberOfPages();
			for(int page=1;page<=size;page++) {
				copy.addPage(copy.getImportedPage(reader, page));
			}
			document.close();
			pdf=baos.toByteArray();
		}
		
		// write out
		if(os!=null)IOUtil.copy(new ByteArrayInputStream(pdf), os,true,false);
		if(!StringUtil.isEmpty(name)) {
			pageContext.setVariable(name,pdf);
		}
	}

	private OutputStream getOutputStream() throws PageException, IOException {
        try {
        	return ((PageContextImpl)pageContext).getResponseStream();
        } 
        catch(IllegalStateException ise) {
            throw new TemplateException("content is already send to user, flush");
        }
    }
	

	private Dimension getDimension() throws ApplicationException {
		// page size custom
		if(isCustom(pagetype)) {
			if(pageheight==0 || pagewidth==0)
				throw new ApplicationException("when attribute pagetype has value [custom], the attributes [pageheight, pagewidth] must have a positive numeric value");
			pagetype=new Dimension(PDFDocument.toPoint(pagewidth,unitFactor),PDFDocument.toPoint(pageheight,unitFactor));
		}
		// page orientation
		if(isLandscape)pagetype=new Dimension(pagetype.height, pagetype.width);
		return pagetype;
	}
	

	private boolean isCustom(Dimension d) throws ApplicationException {
		if(d.height<=0 || d.width<=0)
			throw new ApplicationException("if you define pagetype as custom, you have to define attribute pageheight and pagewith with a positive numeric value");
		
		
		return (d.width+d.height)==2;
	}
	
	/**
	 * sets if has body or not
	 * @param hasBody
	 */
	public void hasBody(boolean hasBody) {
	    
	}



}
	

