/*
 * Copyright (c) 2012 DeNA Co., Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

import "./classdef.jsx";
import "./type.jsx";
import "./parser.jsx";
import "./compiler.jsx";
import "./util.jsx";
import "./meta.jsx";

class DocCommentNode {

	var _description : string;

	function constructor () {
		this._description = "";
	}

	function getDescription () : string {
		return this._description;
	}

	function appendDescription (s : string) : void {
		s = s.trim();
		// append
		if (s != "") {
			if (this._description != "") {
				this._description += " ";
			}
			this._description += s;
		}
	}

}

class DocCommentParameter extends DocCommentNode {

	var _token : Token;

	function constructor (token : Token) {
		super();
		this._token = token;
	}

	function getToken () : Token {
		return this._token;
	}

	function getParamName () : string {
		return this._token.getValue();
	}

}

class DocCommentTag extends DocCommentNode {

	var _tagName : string;

	function constructor (tagName : string) {
		super();
		this._tagName = tagName;
	}

	function getTagName () : string {
		return this._tagName;
	}

}

class DocComment extends DocCommentNode {

	var _params : DocCommentParameter[];
	var _tags : DocCommentTag[];

	function constructor () {
		super();
		this._params = new DocCommentParameter[];
		this._tags = new DocCommentTag[];
	}

	function getParams () : DocCommentParameter[] {
		return this._params;
	}

	function getTags () : DocCommentTag[] {
		return this._tags;
	}

	function getTagByName (tagName : string) : DocCommentTag {
		for (var i = 0; i < this._tags.length; ++i) {
			if (this._tags[i].getTagName() == tagName) {
				return this._tags[i];
			}
		}
		return null;
	}

	function getTagsByName (tagName : string) : DocCommentTag[] {
		var tags = new DocCommentTag[];
		for (var i = 0; i < this._tags.length; ++i) {
			if (this._tags[i].getTagName() == tagName) {
				tags.push(this._tags[i]);
			}
		}
		return tags;
	}
}

class DocumentGenerator {

	var _compiler : Compiler;
	var _templatePath : string; // directory
	var _outputPath : string; // directory
	var _pathFilter : function(:string):boolean;
	var _resourceFiles : string[];
	var _classDefToHTMLCache = new TypedMap.<ClassDefinition,string>;

	/**
	 * @param compiler Compiler instance
	 * @param templatePath a directory which has document resources (template.html and style.css)
	 * @param outputPath   the output directory
	 */
	function constructor (compiler : Compiler, templatePath : string, outputPath : string) {
		this._compiler = compiler;
		this._templatePath = templatePath;
		this._outputPath = outputPath;
		this._resourceFiles = new string[];
		this._pathFilter = null;
	}

	function setResourceFiles (files : string[]) : DocumentGenerator {
		this._resourceFiles = files;
		return this;
	}

	function setPathFilter (pathFilter : function(:string):boolean) : DocumentGenerator {
		this._pathFilter = pathFilter;
		return this;
	}

	function buildDoc () : void {
		var platform = this._compiler.getPlatform();
		// resource files are copied regardless of the template
		this._resourceFiles.forEach((file) -> {
			platform.save(
				this._outputPath + "/" + file,
				platform.load(this._templatePath + "/" + file));
		});
		// output each file
		this._compiler.getParsers().forEach(function (parser) {
			var encodedFilename = platform.encodeFilename(parser.getPath());
			if (this._pathFilter(encodedFilename)) {
				var outputFile = this._outputPath + "/" + parser.getPath() + ".html";
				var html = this._buildDocOfFile(parser);
				platform.save(outputFile, html);
			}
		});
	}

	function _buildDocOfFile (parser : Parser) : string {
		var htmlFile = this._templatePath + "/template.html";
		return this._compiler.getPlatform().load(htmlFile).replace(
			/<%JSX:(.*?)%>/g,
			function (matched) {
				var key = matched.substring(6, matched.length-2);
				switch (key) {
				case "BASE_HREF":
					// convert each component of dirname to ..
					return parser.getPath().replace(/\/[^\/]+$/, "").replace(/[^\/]+/g, "..");
				case "TITLE":
					return this._escape(parser.getPath());
				case "BODY":
					return this._buildBodyOfFile(parser);
				case "FOOTER":
					return this._buildFooterOfFile(parser);
				default:
					throw new Error("unknown template key:" + key + " in file: " + htmlFile);
				}
			});
	}

	function _buildBodyOfFile (parser : Parser) : string {
		var _ = "";

?<div class="jsxdoc">
?<div class="file">
?<h1><?= this._escape(parser.getPath()) ?></h1>
?<?= this._descriptionToHTML(parser.getDocComment()) ?>
?</div><!--/file-->
?<?= this._buildListOfClasses(parser) ?>
?</div><!--/jsxdoc-->

		return _;
	}

	function _buildFooterOfFile (parser : Parser) : string {
		var _ = "";
		var docComment = parser.getDocComment();
		if (docComment) {
			var version = docComment.getTagByName("version");
			if (version) {
?<p>This is <strong><?= this._escape(parser.getPath()) ?> version <?= this._escape(version.getDescription()) ?></strong>.</p>
			}
			var author = docComment.getTagByName("author");
			if (author) {
				var d = author.getDescription();
				var endWithDot = (d.charAt(d.length - 1) == ".");
?<p>Copyright &copy; <?= this._escape(d) + (endWithDot ? "" : ".") ?></p>
			}
		}
?<p class="jsxdoc-notice">This document was automatically generated by <a href="http://jsx.github.io/">JSX</a> <?= Meta.VERSION_STRING ?><br />
?at <?= this._escape( (new Date).toISOString() ) ?>.</p>
		return _;
	}

	function _buildListOfClasses (parser : Parser) : string {
		var _ = "";

?<div class="classes">

		parser.getTemplateClassDefs().forEach(function (classDef) {
			if (! this._isPrivate(classDef)) {
?<?= this._buildDocOfClass(parser, classDef) ?>
			}
		});

		parser.getClassDefs().forEach(function (classDef) {
			if (! (classDef instanceof InstantiatedClassDefinition) && ! this._isPrivate(classDef)) {
?<?= this._buildDocOfClass(parser, classDef) ?>
			}
		});

?</div>

		return _;
	}

	function _buildDocOfClass (parser : Parser, classDef : ClassDefinition) : string {
		var typeName = "class";
		if ((classDef.flags() & ClassDefinition.IS_INTERFACE) != 0) {
			typeName = "interface";
		} else if ((classDef.flags() & ClassDefinition.IS_MIXIN) != 0) {
			typeName = "mixin";
		}
		var typeArgs = classDef instanceof TemplateClassDefinition ? (classDef as TemplateClassDefinition).getTypeArguments() : new Token[];

		var _ = "";

?<div class="class" id="class-<?= this._escape(classDef.className()) ?>">
?<h2><?= this._flagsToHTML(classDef.flags()) + " " + this._escape(typeName + " " + classDef.className()) + this._formalTypeArgsToHTML(typeArgs) ?></h2>
?<?= this._descriptionToHTML(classDef.getDocComment()) ?>

		if (this._hasPublicProperties(classDef)) {
			classDef.forEachMemberVariable(function (varDef) {
				if (! this._isPrivate(varDef)) {
?<div class="member property">
?<h3>
?<?= this._flagsToHTML(varDef.flags()) ?> var <?= varDef.name() ?> : <?= this._typeToHTML(parser, varDef.getType()) ?>
?</h3>
?<?= this._descriptionToHTML(varDef.getDocComment()) ?>
?</div>
				}
				return true;
			});
		}

		classDef.forEachMemberFunction(function (funcDef) {
			if (! (funcDef instanceof InstantiatedMemberFunctionDefinition) && this._isConstructor(funcDef) && (funcDef.flags() & ClassDefinition.IS_DELETE) == 0) {
?<?= this._buildDocOfFunction(parser, funcDef) ?>
			}
			return true;
		});

		if (this._hasPublicFunctions(classDef)) {
			classDef.forEachMemberFunction(function (funcDef) {
				if (! (funcDef instanceof InstantiatedMemberFunctionDefinition || this._isConstructor(funcDef) || this._isPrivate(funcDef))) {
?<?= this._buildDocOfFunction(parser, funcDef) ?>
				}
				return true;
			});
		}

?</div>

		return _;
	}

	function _buildDocOfFunction (parser : Parser, funcDef : MemberFunctionDefinition) : string {
		var _ = "";
		var funcName = this._isConstructor(funcDef) ? "new " + funcDef.getClassDef().className() : this._flagsToHTML(funcDef.flags()) + " function " + funcDef.name();
		var args = funcDef.getArguments();
		var argsHTML = args.map.<string>(function (arg) {
			return this._escape(arg.getName().getValue()) + " : " + this._typeToHTML(parser, arg.getType());
		}).join(", ");

?<div class="member function">
?<h3>
?<?= this._escape(funcName) + this._formalTypeArgsToHTML(funcDef instanceof TemplateFunctionDefinition ? (funcDef as TemplateFunctionDefinition).getTypeArguments() : new Token[]) ?>(<?= argsHTML ?>)
		if (! this._isConstructor(funcDef)) {
? : <?= this._typeToHTML(parser, funcDef.getReturnType()) ?>
		}
?</h3>
?<?= this._descriptionToHTML(funcDef.getDocComment()) ?>
		if (this._argsHasDocComment(funcDef)) {
?<table class="arguments">
			args.forEach(function (arg) {
				var argName = arg.getName().getValue();
?<tr>
?<td class="param-name"><?= this._escape(argName) ?></td>
?<td class="param-desc"><?= this._argumentDescriptionToHTML(argName, funcDef.getDocComment()) ?></td>
?</tr>
			});
?</table>

		}

?</div>

		return _;
	}

	function _descriptionToHTML (docComment : DocComment) : string {
		var _ = "";
		if (docComment != null) {
			if (docComment.getDescription() != "") {
?<div class="description">
?<?= docComment.getDescription() ?>
?</div>
			}
			var seeTags = docComment.getTagsByName("see");
			if (seeTags.length > 0) {
?<ul class="see">
				seeTags.forEach((tag) -> {
?<li><?= this._autoLink(tag.getDescription()) ?></li>
				});
?</ul>
			}
		}
		return _;
	}

	function _autoLink (str : string) : string {
		var uri = /^https?:\/\/[A-Za-z0-9\-\._~:\/?#\[\]@!$&'()*+,;=]+/;
		return str.replace(uri, (matched) -> {
			return Util.format('<a href="%1">%1</a>', [matched]);
		});
	}

	function _argumentDescriptionToHTML (name : string, docComment : DocComment) : string {
		return docComment != null ? this._getDescriptionOfNamedArgument(docComment, name): "";
	}

	function _formalTypeArgsToHTML (typeArgs : Token[]) : string {
		if (typeArgs.length == 0) {
			return "";
		}
		return ".&lt;"
			+ typeArgs.map.<string>(function (typeArg) { return this._escape(typeArg.getValue()); }).join(", ")
			+ "&gt;";
	}

	function _typeToHTML (parser : Parser, type : Type) : string {
		// TODO create links for object types
		if (type instanceof ObjectType) {
			var classDef = type.getClassDef();
			if (classDef != null) {
				return this._classDefToHTML(parser, classDef);
			} else if (type instanceof ParsedObjectType && (type as ParsedObjectType).getTypeArguments().length != 0) {
				classDef = (type as ParsedObjectType).getQualifiedName().getTemplateClass(parser);
				if (classDef != null) {
					return this._classDefToHTML(parser, classDef)
						+ ".&lt;"
						+ (type as ParsedObjectType).getTypeArguments().map.<string>(function (type) { return this._typeToHTML(parser, type); }).join(", ")
						+ "&gt;";
				}
			}
		} else if (type instanceof FunctionType) {
			return "function "
				+ "("
				+ (type as ResolvedFunctionType).getArgumentTypes().map.<string>(function (type) {
					return ":" + this._typeToHTML(parser, type);
				}).join(", ")
				+ ") : " + this._typeToHTML(parser, (type as ResolvedFunctionType).getReturnType());
		} else if (type instanceof VariableLengthArgumentType) {
			return "..." + this._typeToHTML(parser, (type as VariableLengthArgumentType).getBaseType());
		}
		return this._escape(type.toString());
	}

	function _classDefToHTML (parser : Parser, classDef : ClassDefinition) : string {
		// instantiated classes should be handled separately
		if (classDef instanceof InstantiatedClassDefinition) {
			return this._classDefToHTML(parser, (classDef as InstantiatedClassDefinition).getTemplateClass())
				+ ".&lt;"
				+ (classDef as InstantiatedClassDefinition).getTypeArguments().map.<string>(function (type) { return this._typeToHTML(parser, type); }).join(", ")
				+ "&gt;";
		}

		// lokup the cache
		var result = this._classDefToHTMLCache.get(classDef);
		if (result != null) {
			return result;
		}

		// determine the parser to which the classDef belongs
		function determineParserOfClassDef () : Parser {
			var parsers = this._compiler.getParsers();
			for (var i = 0; i < parsers.length; ++i) {
				if (classDef instanceof TemplateClassDefinition) {
					var templateClassDefs = parsers[i].getTemplateClassDefs();
					for (var j = 0; j < templateClassDefs.length; ++j) {
						templateClassDefs = templateClassDefs.concat(templateClassDefs[j].getTemplateInnerClasses());
					}
					if (templateClassDefs.indexOf(classDef as TemplateClassDefinition) != -1) {
						return parsers[i];
					}
				} else {
					var classDefs = parsers[i].getClassDefs();
					for (var j = 0; j < classDefs.length; ++j) {
						classDefs = classDefs.concat(classDefs[j].getInnerClasses());
					}
					if (classDefs.indexOf(classDef) != -1) {
						return parsers[i];
					}
				}
			}
			throw new Error("could not determine the parser to which the class belongs:" + classDef.className());
		};
		var parserOfClassDef = determineParserOfClassDef();
		// return text if we cannot linkify the class name
		if (! this._pathFilter(parserOfClassDef.getPath())) {
			return this._escape(classDef.className());
		}
		// linkify and return
		var _ = "";
?<a href="<?= this._escape(parserOfClassDef.getPath()) ?>.html#class-<?= this._escape(classDef.className()) ?>"><?= this._escape(classDef.className()) ?></a>
		_ = _.trim();
		this._classDefToHTMLCache.set(classDef, _);
		return _;
	}

	function _flagsToHTML (flags : number) : string {
		var strs = new string[];
		// does not expose internal properties
		if ((flags & ClassDefinition.IS_STATIC) != 0)
			strs.push("static");
		if ((flags & ClassDefinition.IS_CONST) != 0)
			strs.push("const");
		if ((flags & ClassDefinition.IS_ABSTRACT) != 0)
			strs.push("abstract");
		if ((flags & ClassDefinition.IS_FINAL) != 0)
			strs.push("final");
		if ((flags & ClassDefinition.IS_OVERRIDE) != 0)
			strs.push("override");
		if ((flags & ClassDefinition.IS_INLINE) != 0)
			strs.push("inline");
		return strs.join(" ");
	}

	function _escape (str : string) : string {
		return str.replace(/[<>&'"]/g, function (ch) {
			return {
				"<": "&lt;",
				">": "&gt;",
				"&": "&amp;",
				"'": "&#39;",
				"\"": "&quot;"
			}[ch];
		});
	}

	function _hasPublicProperties (classDef : ClassDefinition) : boolean {
		return ! classDef.forEachMemberVariable(function (varDef) {
			if (! this._isPrivate(varDef)) {
				return false;
			}
			return true;
		});
	}

	function _hasPublicFunctions (classDef : ClassDefinition) : boolean {
		return ! classDef.forEachMemberFunction(function (funcDef) {
			if (funcDef instanceof InstantiatedMemberFunctionDefinition
				|| this._isConstructor(funcDef)
				|| this._isPrivate(funcDef)) {
				return true;
			}
			return false;
		});
	}

	function _argsHasDocComment (funcDef : MemberFunctionDefinition) : boolean {
		var docComment = funcDef.getDocComment();
		if (docComment == null) {
			return false;
		}
		var args = funcDef.getArguments();
		for (var argIndex = 0; argIndex < args.length; ++argIndex) {
			if (this._getDescriptionOfNamedArgument(docComment, args[argIndex].getName().getValue()) != "") {
				return true;
			}
		}
		return false;
	}

	function _getDescriptionOfNamedArgument (docComment : DocComment, argName : string) : string {
		var params = docComment.getParams();
		for (var paramIndex = 0; paramIndex < params.length; ++paramIndex) {
			if (params[paramIndex].getParamName() == argName) {
				return params[paramIndex].getDescription();
			}
		}
		return "";
	}

	function _isConstructor (funcDef : MemberFunctionDefinition) : boolean {
		return funcDef.name() == "constructor"
			&& (funcDef.flags() & ClassDefinition.IS_STATIC) == 0;
	}

	function _isPrivate (classDef : ClassDefinition) : boolean {
		return classDef.className().charAt(0) == "_";
	}

	function _isPrivate (memberDef : MemberDefinition) : boolean {
		return memberDef.name().charAt(0) == "_";
	}

}
