package writers;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ProgramElementDoc;
import com.sun.javadoc.SeeTag;
import com.sun.javadoc.Tag;

public class BaseWriter {
	// Some utilities

	public final static String MODE_JAVASCRIPT = "js";

	public BaseWriter()
	{

	}

	protected static boolean needsWriting(ProgramElementDoc doc){
		if( (doc != null) && Shared.i().isWebref(doc) )
		{
			return hasXMLDocument( doc );
		}
		return false;
	}

	protected static BufferedWriter makeWriter(String anchor) throws IOException
	{
		return makeWriter(anchor, false);
	}

	protected static String getWriterPath( String anchor, Boolean isLocal )
	{
		if (!isLocal) {
			return Shared.i().getOutputDirectory() + "/" + anchor;
		} else
		{
			return Shared.i().getLocalOutputDirectory() + anchor;
		}
	}

	protected static BufferedWriter makeWriter(String anchor, Boolean isLocal) throws IOException {
		FileWriter fw = new FileWriter( getWriterPath( anchor, isLocal ) );

		return new BufferedWriter(fw);
	}

	protected static String getAnchor(ProgramElementDoc doc)
	{
		String ret = getAnchorFromName(getName(doc));

		if(doc.containingClass() != null && !Shared.i().isRootLevel(doc.containingClass()))
		{
			ret = doc.containingClass().name() + "_" + ret;
		}

		if(!Shared.i().isCore(doc)){
			//add package name to anchor
			String[] parts = doc.containingPackage().name().split("\\.");
			String pkg = parts[parts.length-1] + "/";
			ret = "libraries/" + pkg + ret;
		}

		return ret;
	}

	protected static String getLocalAnchor(ProgramElementDoc doc)
	{
		String ret = getAnchorFromName(getName(doc));
		if(doc.containingClass() != null){
			ret = doc.containingClass().name() + "_" + ret;
		}

		return ret;
	}

	protected static String getReturnTypes(MethodDoc doc)
	{
		String ret = nameInPDE(doc.returnType().toString());
		if(doc.containingClass() != null)
		{
			for(MethodDoc m : doc.containingClass().methods())
			{
				if( m.name().equals(doc.name()) && m.returnType() != doc.returnType() )
				{
					String name = getSimplifiedType( nameInPDE(m.returnType().toString()) );
					if( ! ret.contains( name ) )
					{ // add return type name if it's not already included
						ret += ", " + name;
					}
				}
			}
		}

		// add "or" (split first to make sure we don't mess up the written description)
		ret = ret.replaceFirst( ",([^,]+)$", ", or$1" );
		if( ! ret.matches(".+,.+,.+") )
		{
			ret = ret.replace( ",", "" );
		}

		return ret;
	}

	protected static String getSimplifiedType( String str )
	{
		if( str.equals("long") ){ return "int"; }
		if( str.equals("double") ){ return "float"; }

		return str;
	}

	protected static String getName(Doc doc) { // handle
		String ret = doc.name();
		if(doc instanceof MethodDoc)
		{
			ret = ret.concat("()");
		} else if (doc.isField()){
			// add [] if needed
			FieldDoc d = (FieldDoc) doc;
			ret = ret.concat(d.type().dimension());
		}
		return ret;
	}

	protected static String getAnchorFromName(String name){
		// change functionName() to functionName_
		if( name.endsWith("()") ){
			name = name.replace("()", "_");
		}
		// change "(some thing)" to something
		if( name.contains("(") && name.contains(")") ){
			int start = name.lastIndexOf("(") + 1;
			int end = name.lastIndexOf(")");
			name = name.substring(start, end);
			name = name.replace(" ", "");
		}
		// change thing[] to thing
		if( name.contains("[]")){
			name = name.replaceAll("\\[\\]", "");
		}
		// change "some thing" to "some_thing.html"
		name = name.replace(" ", "_").concat(".html");
		return name;
	}

	static protected String getBasicDescriptionFromSource(ProgramElementDoc doc) {
		return getBasicDescriptionFromSource(longestText(doc));
	}

	static protected String getBriefDescriptionFromSource(ProgramElementDoc doc) {
		Tag[] sta = doc.tags("brief");
		if(sta.length > 0){
			return sta[0].text();
		}
		return getBasicDescriptionFromSource(doc);
	}

	static protected String longestText(ProgramElementDoc doc){
		if(Shared.i().isWebref(doc)){
			//override longest rule if the element has an @webref tag
			return doc.commentText();
		}

		String s = doc.commentText();
		if( doc.isMethod() ){
			for(ProgramElementDoc d : doc.containingClass().methods()){
				if(d.name().equals(doc.name() ) ){
					if(d.commentText().length() > s.length()){
						s = d.commentText();
					}
				}
			}
		} else if(doc.isField()){
			for(ProgramElementDoc d : doc.containingClass().fields()){
				if(d.name().equals(doc.name() ) ){
					if(d.commentText().length() > s.length()){
						s = d.commentText();
					}
				}
			}
		}
		return s;
	}

	static protected String getBasicDescriptionFromSource(String s){
		String[] sa = s.split("(?i)(<h\\d>Advanced:?</h\\d>)|(=advanced)");
		if (sa.length != 0)
			s = sa[0];
		return s;
	}

	static protected String getAdvancedDescriptionFromSource(ProgramElementDoc doc) {
		return getAdvancedDescriptionFromString(longestText(doc));
	}
	static private String getAdvancedDescriptionFromString(String s) {
		String[] sa = s.split("(?i)(<h\\d>Advanced:?</h\\d>)|(=advanced)");
		if (sa.length > 1)
			s = sa[1];
		return s;
	}



	//

	protected static String getXMLPath(ProgramElementDoc doc) {
		String path = Shared.i().getXMLDirectory();
		String name = doc.name();
		String suffix = ".xml";
		if(doc.containingClass() != null){
			if(Shared.i().isRootLevel(doc.containingClass())){
				//inside PApplet or other root-level class
				if(doc instanceof FieldDoc){
					//if there is a method of the same name, append _var
					for( Doc d : doc.containingClass().methods()){
						if(d.name().equals(doc.name())){
							suffix = "_var" + suffix;
							break;	//don't append multiple times
						}
					}
				}
			} else {
				name = doc.containingClass().name() + "_" + name;
			}
		}

		if( !Shared.i().isCore(doc)){
			//if documentation is for a library element
			String[] pkg = doc.containingPackage().name().split("\\.");
			path = path + "LIB_" + pkg[pkg.length-1] + "/";
		}


		return path + name + suffix;
	}

	static protected String getExamples(ProgramElementDoc doc) throws IOException{
		return getExamples(getXMLPath(doc));
	}

	static private Document getXMLDocument(String path) throws IOException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setNamespaceAware(true);
		DocumentBuilder builder = null;
		Document doc = null;
		try {
			builder = factory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			System.out.println( "Failed to load XML: " + e.getMessage());
		}

		if( builder != null )
		{
			try {
				doc = builder.parse(path);
			} catch (SAXException e) {
				System.out.println( "Failed to parse XML: " + e.getMessage() );
			} catch (IOException e) {
				System.out.println( "Failed to parse XML: " + e.getMessage() );
			}
		}


		return doc;
	}

	private static boolean hasXMLDocument( ProgramElementDoc doc )
	{
		String path = getXMLPath( doc );
		File f = new File( path );
		if( f.exists() )
		{
			return true;
		}
		return false;
	}

	static protected String getExamples(String path) throws IOException {
		Document doc = getXMLDocument(path);
		if( doc != null )
			return getExamples(doc);
		else
		{
			System.out.println("Unable to get examples from " + path + "; returning an empty string.");
			return "";
		}
	}

	protected static String getExamples(Document doc) throws IOException{
		//Parse the examples from an XML document
		TemplateWriter templateWriter = new TemplateWriter();
		ArrayList<HashMap<String, String>> exampleList = new ArrayList<HashMap<String, String>>();
		XPathFactory xpathFactory = XPathFactory.newInstance();
		XPath xpath = xpathFactory.newXPath();
		try {
			XPathExpression expr = xpath.compile("//example");
			Object result = expr.evaluate(doc, XPathConstants.NODESET);
			NodeList examples = (NodeList) result;

			for (int i = 0; i < examples.getLength(); i++) {
				HashMap<String, String> example = new HashMap<String, String>();

				expr = xpath.compile("image");
				String img = (String) expr.evaluate(examples.item(i),
						XPathConstants.STRING);
				expr = xpath.compile("code");
				String code = (String) expr.evaluate(examples.item(i),
						XPathConstants.STRING);

				example.put("image", Shared.i().getImageDirectory()
						+ img);
				if(img.equals(""))
				{	// if no image, replace with empty string
					example.put("image", "");
				}
				example.put("code", code);

				exampleList.add(example);
			}

		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}

		String exampleInner = templateWriter.writeLoop("/example.partial.html", exampleList);
		HashMap<String, String> map = new HashMap<String, String>();
		map.put("examples", exampleInner);
		return templateWriter.writePartial("examples.partial.html", map);
	}

	protected static String getXMLDescription(ProgramElementDoc doc) throws IOException {
		Document xmlDoc = getXMLDocument(getXMLPath(doc));
		if( xmlDoc != null )
			return getXMLDescription(xmlDoc);
		else {
			System.out.println("Unable to get description from " + getXMLPath(doc) + "; returning an empty string.");
			return "";
		}
	}

	/**
	 *	Based upon Shared.addDescriptionTag().
	 *
	 *	Hint: this loads and adds js_mode/description as well
	 */
	protected static String getXMLDescription(Document doc)
	{
		XPathFactory xpathFactory = XPathFactory.newInstance();
		XPath xpath = xpathFactory.newXPath();

		TemplateWriter templateWriter = new TemplateWriter();

		String desc = "";

		for( String component : Shared.i().getDescriptionSets() )
		{
			try
			{
				XPathExpression expr = xpath.compile(component);
				String result = (String) expr.evaluate(doc, XPathConstants.STRING);
				HashMap<String, String> vars = getDefaultDescriptionVars();
				if ( component.indexOf("js_mode") != -1 ) {
					vars.put( "description title", "JavaScript<br>\nNotes" );
				}
				if ( !result.equals("") )
				{
					vars.put( "description text", result );
					result = templateWriter.writePartial( "description.partial.html", vars );
				}
				desc += result;
			}
			catch ( XPathExpressionException e)
			{
				System.out.println("Error getting description from xml with expression: //" + component);
				e.printStackTrace();
			}
		}

		return desc;
	}

	protected static HashMap<String, String> getDefaultDescriptionVars ()
	{
		HashMap<String, String> vars = new HashMap<String, String>();
		vars.put("description title", "Description");
		vars.put("description text", "");
		return vars;
	}

	protected static String getTimestamp() {
		Calendar now = Calendar.getInstance();
		Locale us = new Locale("en");

		return now.getDisplayName(Calendar.MONTH, Calendar.LONG, us)
				+ " "
				+ now.get(Calendar.DAY_OF_MONTH)
				+ ", "
				+ now.get(Calendar.YEAR)
				+ " "
				+ FileUtils.nf(now.get(Calendar.HOUR), 2)
				+ ":"
				+ FileUtils.nf(now.get(Calendar.MINUTE), 2)
				+ ":"
				+ FileUtils.nf(now.get(Calendar.SECOND), 2)
				+ now.getDisplayName(Calendar.AM_PM, Calendar.SHORT, us)
						.toLowerCase()
				+ " "
				+ TimeZone.getDefault().getDisplayName(
						TimeZone.getDefault().inDaylightTime(now.getTime()),
						TimeZone.SHORT, us);
	}

	/*
	 * Get all the syntax possibilities for a method
	 */
	protected static ArrayList<HashMap<String, String>> getSyntax(MethodDoc doc, String instanceName) throws IOException
	{
		TemplateWriter templateWriter = new TemplateWriter();
		ArrayList<HashMap<String, String>> ret = new ArrayList<HashMap<String,String>>();

		for( MethodDoc methodDoc : doc.containingClass().methods() )
		{
			if(Shared.i().shouldOmit(methodDoc)){
				continue;
			}
			if( methodDoc.name().equals(doc.name() ))
			{
				HashMap<String, String> map = new HashMap<String, String>();
				map.put("name", methodDoc.name());
				map.put("object", instanceName);

				ArrayList<HashMap<String, String>> parameters = new ArrayList<HashMap<String,String>>();
				for( Parameter p : methodDoc.parameters() )
				{
					HashMap<String, String> paramMap = new HashMap<String, String>();
					paramMap.put("parameter", p.name());
					parameters.add(paramMap);
				}
				String params = templateWriter.writeLoop("method.parameter.partial.html", parameters, ", ");

				map.put("parameters", params);
				if( ! ret.contains(map) )
				{
					//don't put in duplicate function syntax
					ret.add(map);
				}
			}
		}
		return ret;
	}

	private static String removePackage(String name)
	{ // keep everything after the last dot
		if( name.contains(".") )
		{ return name.substring( name.lastIndexOf(".") + 1 ); }
		return name;
	}

	private static String nameInPDE(String fullName)
	{
		if( fullName.contains("<") && fullName.endsWith(">") )
		{	// if this type uses Java generics
			String parts[] = fullName.split("<");
			String generic = removePackage( parts[0] );
			String specialization = removePackage( parts[1] );
			specialization = specialization.substring( 0, specialization.length() - 1 );
			return generic + "&lt;" + specialization + "&gt;";
		}
		return removePackage( fullName );
	}

	protected static String getUsage(ProgramElementDoc doc){
		Tag[] tags = doc.tags("usage");
		if(tags.length != 0){
			return tags[0].text();
		}
		tags = doc.containingClass().tags("usage");
		if(tags.length != 0){
			return tags[0].text();
		}
		// return empty string if no usage is found
		return "";
	}

	protected static String getInstanceName(ProgramElementDoc doc){
		Tag[] tags = doc.containingClass().tags("instanceName");
		if(tags.length != 0){
			return tags[0].text().split("\\s")[0];
		}
		return "";
	}

	protected static String getInstanceDescription(ProgramElementDoc doc){
		Tag[] tags = doc.containingClass().tags("instanceName");
		if(tags.length != 0){
			String s = tags[0].text();
			return s.substring(s.indexOf(" "));
		}
		return "";
	}

	protected static String getParameters(MethodDoc doc) throws IOException{
		//get parent
		ClassDoc cd = doc.containingClass();
		ArrayList<HashMap<String, String>> ret = new ArrayList<HashMap<String,String>>();

		if(!Shared.i().isRootLevel(cd)){
			//add the parent parameter if this isn't a function of PApplet
			HashMap<String, String> parent = new HashMap<String, String>();
			parent.put("name", getInstanceName(doc));
			parent.put("description", cd.name() + ": " + getInstanceDescription(doc));
			ret.add(parent);
		}

		//get parameters from this and all other declarations of method
		for( MethodDoc m : cd.methods() ){
			if(Shared.i().shouldOmit(m)){
				continue;
			}
			if(m.name().equals(doc.name())){
				ret.addAll(parseParameters(m));
			}
		}

		removeDuplicateParameters(ret);


		TemplateWriter templateWriter = new TemplateWriter();
		return templateWriter.writeLoop("parameter.partial.html", ret);
	}

	protected static String getParameters(ClassDoc doc) throws IOException{
		ArrayList<HashMap<String, String>> ret = new ArrayList<HashMap<String,String>>();
		for( ConstructorDoc m : doc.constructors() ){
			if(Shared.i().shouldOmit(m)){
				continue;
			}
			ret.addAll(parseParameters(m));
		}
		removeDuplicateParameters(ret);

		TemplateWriter templateWriter = new TemplateWriter();
		return templateWriter.writeLoop("parameter.partial.html", ret);
	}

	protected static void removeDuplicateParameters(ArrayList<HashMap<String, String>> ret){
		// combine duplicate parameter names with differing types
		for(HashMap<String, String> parameter : ret)
		{
			String desc = parameter.get("description");
			if(!desc.endsWith(": "))
			{
				// if the chosen description has actual text
				// e.g. float: something about the float
				for(HashMap<String, String> parameter2 : ret)
				{
					String desc2 = parameter2.get("description");

					if( desc2.endsWith(": ") && parameter2.get("name").equals( parameter.get("name") ) )
					{
						// freshen up our variable with the latest description
						desc = parameter.get("description");

						if( ! desc.contains( desc2.substring( 0, desc2.indexOf(": ") ) ) )
						{
							// if the similar item doesn't have actual text
							// e.g. Boolean:
							String newDescription = desc2.replace(":", ",").concat( desc );
							parameter.put("description", newDescription);
						}
					}
				}
			}
		}
		//remove parameters without descriptions
		for( int i=ret.size()-1; i >= 0; i-- )
		{
			if(ret.get(i).get("description").endsWith(": "))
			{
				ret.remove(i);
			}
		}

		// add "or" (split first to make sure we don't mess up the written description)
		for( HashMap<String, String> param : ret )
		{
			String desc = param.get("description");
			String start = desc.substring( 0, desc.indexOf(":")+1 ).replaceFirst( ",([^,]+:)", ", or$1" );
			String end = desc.substring( desc.indexOf(":")+1, desc.length() );

			param.put( "description", start.concat( end ) );
		}
	}

	protected static ArrayList<HashMap<String, String>> parseParameters(ExecutableMemberDoc doc){
		ArrayList<HashMap<String, String>> ret = new ArrayList<HashMap<String,String>>();
		for( Parameter param : doc.parameters()){
			String type = getSimplifiedType( nameInPDE(param.type().toString()) ).concat(": ");
			String name = param.name();
			String desc = "";

			for( ParamTag tag : doc.paramTags() ){
				if(tag.parameterName().equals(name)){
					desc = desc.concat( tag.parameterComment() );
				}
			}

			HashMap<String, String> map = new HashMap<String, String>();
			map.put("name", name);
			map.put("description", type + desc);
			ret.add(map);
		}
		return ret;
	}

	/**
	 *	Modes should support all API, so if XML not explicitly states "not supported", then assume it does.
	 */
	protected static boolean isModeSupported ( ProgramElementDoc doc, String mode ) {

		Document xmlDoc = null;
		try {
			String xmlPath = getXMLPath( doc );
			xmlDoc = getXMLDocument( xmlPath );
		} catch ( IOException ioe ) {
			ioe.printStackTrace();
			return true;
		}

		XPathFactory xpathFactory = XPathFactory.newInstance();
		XPath xpath = xpathFactory.newXPath();

		try {

			String umraw = xpath.evaluate("//unsupported_modes", xmlDoc, XPathConstants.STRING).toString();
			String[] ums = umraw.split(",");
			for ( String s : ums ) {
				if ( s.trim().toLowerCase().equals(mode) )
					return false;
			}

		} catch ( XPathExpressionException e ) {

			e.printStackTrace();
		}

		return true;
	}

	protected static ArrayList<SeeTag> getAllSeeTags( ProgramElementDoc doc )
	{
		ArrayList<SeeTag> ret = new ArrayList<SeeTag>();
		ClassDoc cd = doc.containingClass();
		if( cd != null && doc.isMethod() )
		{	// if there is a containing class, get @see tags for all
			// methods with the same name as this one
			// Fixes gh issue 293
			for( MethodDoc m : cd.methods() )
			{
				if(m.name().equals(doc.name()))
				{
					for( SeeTag tag : m.seeTags() )
					{
						ret.add( tag );
					}
				}
			}
		}
		else
		{	// if no containing class (e.g. doc is a class)
			// just grab the see tags in the class doc comment
			for( SeeTag tag : doc.seeTags() )
			{
				ret.add( tag );
			}
		}
		return ret;
	}

	protected static String getRelated( ProgramElementDoc doc ) throws IOException
	{
		TemplateWriter templateWriter = new TemplateWriter();
		ArrayList<HashMap<String, String>> vars = new ArrayList<HashMap<String,String>>();

		// keep track of class members so that links to methods in e.g. PGraphics
		// that are copied into PApplet are correctly linked.
		HashMap<String, ProgramElementDoc> classMethods = new HashMap<String, ProgramElementDoc>();
		HashMap<String, ProgramElementDoc> classFields = new HashMap<String, ProgramElementDoc>();
		if( doc.isMethod() || doc.isField() )
		{	// fill our maps
			ClassDoc containingClass = doc.containingClass();
			for( MethodDoc m : containingClass.methods() ) {
				if( needsWriting( m ) ) {
					classMethods.put( m.name(), m );
				}
			}
			for( FieldDoc f : containingClass.fields() ) {
				if( needsWriting( f ) ) {
					classFields.put( f.name(), f );
				}
			}
		}

		// add link to each @see item
		for( SeeTag tag : getAllSeeTags( doc ) )
		{
			HashMap<String, String> map = new HashMap<String, String>();
			ProgramElementDoc ref = tag.referencedClass();
			if( tag.referencedMember() != null )
			{
				ref = tag.referencedMember();
				if( ref.isMethod() && classMethods.containsKey( ref.name() ) ) {
					// link to the member as it is in this class, instead of
					// as it is in another class
					ProgramElementDoc prior = classMethods.get( ref.name() );
					ref = prior;
				}
				else if ( ref.isField() && classFields.containsKey( ref.name() ) ) {
					ProgramElementDoc prior = classFields.get( ref.name() );
					ref = prior;
				}
			}
			if( needsWriting( ref ) )
			{ // add links to things that are actually written
				map.put("name", getName( ref ));
				map.put("anchor", getAnchor( ref ));
				vars.add(map);
			}
		}

		// add link to each @see_external item
		for( Tag tag : doc.tags( Shared.i().getSeeAlsoTagName() ) )
		{
			// get xml for method
			String filename = tag.text() + ".xml";
			String basePath = Shared.i().getXMLDirectory();
			File f = new File( basePath + filename );

			if( ! f.exists() )
			{
				basePath = Shared.i().getIncludeDirectory();
				f = new File( basePath + filename );
			}

			if( f.exists() )
			{
				Document xmlDoc = Shared.loadXmlDocument( f.getPath() );
				XPathFactory xpathFactory = XPathFactory.newInstance();
				XPath xpath = xpathFactory.newXPath();

				try
				{
					String name = (String) xpath.evaluate("//name", xmlDoc, XPathConstants.STRING);
					// get anchor from original filename
					String path = f.getAbsolutePath();
					String anchorBase = path.substring( path.lastIndexOf("/")+1, path.indexOf(".xml"));
					if( name.endsWith("()") )
					{
						if( !anchorBase.endsWith("_" ) )
						{
							anchorBase += "_";
						}
					}
					String anchor = anchorBase + ".html";

					// get method name from xml
					// get anchor from method name
					HashMap<String, String> map = new HashMap<String, String>();
					map.put( "name", name );
					map.put( "anchor", anchor );
					vars.add( map );
				} catch (XPathExpressionException e)
				{
					e.printStackTrace();
				}
			}
		}

		return templateWriter.writeLoop("related.partial.html", vars);
	}

	protected static String getEvents(ProgramElementDoc doc){
		return "";
	}

}
