'use strict';

const React = require("react");
const createReactClass = require("create-react-class");

/* FIXME: Replace all inline simpleness determinations with calls to .isSimple */

function stringifyNode(node) {
	if (node.type === "Identifier") {
		return node.name;
	} else if (node.type === "AssignmentPattern") {
		return `${stringifyNode(node.left)} = ${stringifyNode(node.right)}`;
	} else if (node.type === "Literal") {
		return node.raw;
	} else if (node.type === "MemberExpression") {
		/* FIXME: Handle dynamic properties. */
		if (node.computed === true) {
			return `${stringifyNode(node.object)}[${stringifyNode(node.property)}]`;
		} else {
			return `${stringifyNode(node.object)}.${stringifyNode(node.property)}`;
		}
	} else if (node.type === "ThisExpression") {
		return "this";
	} else if (node.type === "ContinueStatement") {
		return "continue";
	} else if (node.type === "LogicalExpression" || node.type === "BinaryExpression") {
		return `${stringifyNode(node.left)} ${node.operator} ${stringifyNode(node.right)}`;
	} else if (node.type === "UnaryExpression" || node.type === "UpdateExpression") {
		/* FIXME: Add spacing for typeof etc. but not for symbol operators like ++? */
		if (node.prefix === true) {
			return `${node.operator} ${stringifyNode(node.argument)}`;
		} else {
			return `${stringifyNode(node.argument)} ${node.operator}`;
		}
	} else if (node.type === "CallExpression") {
		return `${stringifyNode(node.callee)}(${node.arguments.map((arg) => stringifyNode(arg)).join(", ")})`;
	} else if (node.type === "ObjectExpression") {
		if (node.properties.length > 0) {
			throw new Error("Can only stringify zero-property object literals");
		} else {
			return "{}";
		}
	} else if (node.type === "ArrayExpression") {
		if (node.elements.length > 0) {
			throw new Error("Can only stringify zero-element array literals");
		} else {
			return "[]";
		}
	} else if (node.type === "ConditionalExpression") {
		return `(${stringifyNode(node.test)} ? ${stringifyNode(node.consequent)} : ${stringifyNode(node.alternate)})`;
	} else if (node.type === "AssignmentExpression") {
		/* FIXME: Other assignment operators - does that include things like += ? */
		return `${stringifyNode(node.left)} = ${stringifyNode(node.right)}`;
	} else {
		throw new Error(`Unrecognized node type: ${node.type}`);
	}
}

function collectNestedExpressions(root, type, attribute) {
	let collected = [ root ];
	let last = root;

	while (last[attribute] != null && last[attribute].type === type) {
		collected.push(last[attribute]);
		last = last[attribute];
	}

	return [collected, last];
}

module.exports = function ({dependencies}) {
	let FunctionDeclarationItem = createReactClass({
		displayName: "FunctionDeclarationItem",
		render: function () {
			let GenericAstItem = dependencies.get("GenericAstItem");
			let AstItem = dependencies.get("AstItem");

			if (this.props.data.id == null || this.props.data.id.type !== "Identifier") {
				return (<GenericAstItem  {...this.props} />);
			} else {
				let functionName = this.props.data.id.name;

				let stringifiedParameters = this.props.data.params.map((param) => {
					return stringifyNode(param);
				});

				let itemProps = {
					abbreviated: true,
					hasFunctionScope: true, // TODO: Should this be hardcoded like this?
					hasBlockScope: false,
					type: this.props.data.type,
					preview: `function ${functionName} (${stringifiedParameters.join(", ")})`,
					scopeId: this.props.data._scopeId,
					scopeType: "function",
					children: {
						body: this.props.data.body
					}
				};

				return (
					<AstItem {...itemProps} data={this.props.data} />
				);
			}
		}
	});

	let CallExpressionItem = createReactClass({
		displayName: "CallExpressionItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");

			let showArguments = false;

			let stringifiedArguments = this.props.data.arguments.map((arg) => {
				try {
					return stringifyNode(arg);
				} catch (err) {
					/* FIXME: Either catch specific error, or use a .isSimpleNode method on AST nodes to determine whether we can stringify this call expression */
					showArguments = true;
					return `<${arg.type}>`;
				}
			});

			let itemProps = {
				abbreviated: true,
				type: this.props.data.type,
				preview: `${stringifyNode(this.props.data.callee)}(${stringifiedArguments.join(", ")})`,
				childSequences: {
					arguments: (showArguments === true ? this.props.data.arguments : null)
				}
			};

			return (
				<AstItem {...itemProps} data={this.props.data} />
			);
		}
	});

	let MemberExpressionItem = createReactClass({
		displayName: "MemberExpressionItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");
			let GenericAstItem = dependencies.get("GenericAstItem");

			/* The below is for handling recursive MemberExpressions, eg. for foo.bar.baz */
			/* FIXME: How to handle the 'computed' property? */
			// let [collected, last] = collectNestedExpressions(this.props.data, "MemberExpression", "object");

			let hasNonLiteralProperty = (this.props.data.property.type !== "Identifier");
			let hasNonLiteralObject = (this.props.data.object.type !== "Identifier");

			if (hasNonLiteralObject || hasNonLiteralProperty) {
				return (<GenericAstItem  {...this.props} />);
			} else {
				let itemProps = {
					abbreviated: true,
					type: this.props.data.type,
					preview: stringifyNode(this.props.data)
				};

				return (
					<AstItem {...itemProps} data={this.props.data} />
				);
			}
		}
	});

	let LiteralItem = createReactClass({
		displayName: "LiteralItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");

			let itemProps = {
				abbreviated: true,
				type: this.props.data.type,
				preview: stringifyNode(this.props.data)
			};

			return (
				<AstItem {...itemProps} data={this.props.data} />
			);
		}
	});

	let ObjectPropertyItem = createReactClass({
		displayName: "ObjectPropertyItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");
			let GenericAstItem = dependencies.get("GenericAstItem");

			if (this.props.data.computed === true || this.props.data.method === true || this.props.data.shorthand === true) {
				return (<GenericAstItem  {...this.props} />);
			} else {
				let preview, complexValue;

				if (this.props.data.value.type === "Literal") {
					preview = `${stringifyNode(this.props.data.key)}: ${this.props.data.value.raw}`;
					complexValue = null;
				} else {
					preview = `${stringifyNode(this.props.data.key)}: <${this.props.data.value.type}>`;
					complexValue = this.props.data.value;
				}

				let itemProps = {
					abbreviated: true,
					type: this.props.data.type,
					preview: preview,
					children: {
						value: complexValue
					}
				};

				return (
					<AstItem {...itemProps} data={this.props.data} />
				);
			}
		}
	});

	let VariableDeclaratorItem = createReactClass({
		displayName: "VariableDeclaratorItem",
		render: function () {
			let GenericAstItem = dependencies.get("GenericAstItem");
			let AstItem = dependencies.get("AstItem");

			if (this.props.data.id.type !== "Identifier") {
				return (<GenericAstItem  {...this.props} />);
			} else {
				let preview;
				let simpleValue = false;

				if (this.props.data.init != null) {
					try {
						let stringifiedValue = stringifyNode(this.props.data.init);
						simpleValue = true;

						preview = `${this.props.data._kind} ${this.props.data.id.name} = ${stringifiedValue}`;
					} catch (err) {
						/* FIXME: Error type */
						preview = `${this.props.data._kind} ${this.props.data.id.name} = <${this.props.data.init.type}>`;
					}
				} else {
					preview = `${this.props.data._kind} ${this.props.data.id.name}`;
				}

				let itemProps = {
					abbreviated: true,
					type: this.props.data.type,
					preview: preview,
					children: {
						init: (simpleValue === false) ? this.props.data.init : null
					}
				};

				return (
					<AstItem {...itemProps} data={this.props.data} />
				);
			}
		}
	});

	let VariableDeclarationItem = createReactClass({
		displayName: "VariableDeclarationItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");

			let itemProps = {
				abbreviated: true,
				type: this.props.data.type,
				// preview: `(${this.props.data.kind})`,
				childSequences: {
					declarations: this.props.data.declarations
				}
			};

			return (
				<AstItem {...itemProps} data={this.props.data} />
			);
		}
	});

	let AssignmentExpressionItem = createReactClass({
		displayName: "AssignmentExpressionItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");

			let preview, simpleValue, leftHand, rightHand, collectedAssignments;

			if (this.props.data.right.type === "AssignmentExpression") {
				/* We want to collapse multiple nested AssignmentExpressions into one (eg. `foo = bar = baz`) */
				let [collected, last] = collectNestedExpressions(this.props.data, "AssignmentExpression", "right");

				leftHand = collected.map((node) => {
					return stringifyNode(node.left);
				}).join(" = ");

				rightHand = last.right;
				collectedAssignments = collected;
			} else {
				leftHand = stringifyNode(this.props.data.left);
				rightHand = this.props.data.right;
			}

			if (rightHand.isSimple()) {
				preview = `${leftHand} = ${stringifyNode(rightHand)}`;
				simpleValue = true;
			} else {
				preview = `${leftHand} = <${rightHand.type}>`;
				simpleValue = false;
			}

			let itemProps = {
				abbreviated: true,
				type: "AssignmentExpression (nested)",
				preview: preview,
				children: {
					value: (simpleValue === false ? this.props.data.right : null)
				},
				childSequences: {
					assignments: (collectedAssignments != null ? collectedAssignments.map((node) => node.left) : null)
				}
			};

			return (
				<AstItem {...itemProps} data={this.props.data} />
			);
		}
	});

	let ObjectExpressionItem = createReactClass({
		displayName: "ObjectExpressionItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");

			let stringifiedProperties = this.props.data.properties.map((property) => {
				if (property.computed === true) {
					/* FIXME */
					return "[computed]";
				} else if (property.value.isSimple()) {
					return `${stringifyNode(property.key)}: ${property.value.raw}`;
				} else {
					return stringifyNode(property.key);
				}
			});

			let propertyPreviewLimit = 3;
			let shortenedStringifiedProperties;

			if (stringifiedProperties.length > propertyPreviewLimit) {
				shortenedStringifiedProperties = stringifiedProperties.slice(0, propertyPreviewLimit).concat([`(... ${stringifiedProperties.length - propertyPreviewLimit} more)`]);
			} else {
				shortenedStringifiedProperties = stringifiedProperties;
			}

			let itemProps = {
				abbreviated: true,
				type: this.props.data.type,
				preview: `{ ${shortenedStringifiedProperties.join(", ")} }`,
				childSequences: {
					properties: this.props.data.properties
				}
			};

			return (
				<AstItem {...itemProps} data={this.props.data} />
			);
		}
	});

	let FixedKeywordItem = createReactClass({
		displayName: "FixedKeywordItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");

			let itemProps = {
				abbreviated: true,
				type: this.props.data.type,
				preview: stringifyNode(this.props.data)
			};

			return (
				<AstItem {...itemProps} data={this.props.data} />
			);
		}
	});

	let OperatorExpressionItem = createReactClass({
		displayName: "OperatorExpressionItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");
			let GenericAstItem = dependencies.get("GenericAstItem");

			try {
				let itemProps = {
					abbreviated: true,
					type: this.props.data.type,
					preview: stringifyNode(this.props.data)
				};

				return (
					<AstItem {...itemProps} data={this.props.data} />
				);
			} catch (err) {
				/* FIXME: Error type */
				return (<GenericAstItem {...this.props} />);
			}
		}
	});

	let IfStatementItem = createReactClass({
		displayName: "IfStatementItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");
			let GenericAstItem = dependencies.get("GenericAstItem");

			if (this.props.data.test.isSimple()) {
				let stringifiedTest = stringifyNode(this.props.data.test);

				let itemProps = {
					abbreviated: true,
					type: this.props.data.type,
					preview: `if (${stringifiedTest}) ...`,
					children: {
						consequent: this.props.data.consequent,
						/* FIXME: else-if chains */
						alternate: this.props.data.alternate
					}
				};

				return (
					<AstItem {...itemProps} data={this.props.data} />
				);
			} else {
				return (<GenericAstItem {...this.props} />);
			}
		}
	});

	let ReturnStatementItem = createReactClass({
		displayName: "ReturnStatementItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");
			let GenericAstItem = dependencies.get("GenericAstItem");

			if (this.props.data.isSimple()) {
				let itemProps = {
					abbreviated: true,
					type: this.props.data.type,
					preview: `return ${stringifyNode(this.props.data.argument)}`
				};

				return (
					<AstItem {...itemProps} data={this.props.data} />
				);
			} else {
				return (<GenericAstItem {...this.props} />);
			}
		}
	});

	let ExpressionStatementItem = createReactClass({
		displayName: "ExpressionStatementItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");
			let GenericAstItem = dependencies.get("GenericAstItem");

			if (this.props.data.isSimple()) {
				let itemProps = {
					abbreviated: true,
					type: this.props.data.type,
					preview: stringifyNode(this.props.data.expression)
				};

				return (
					<AstItem {...itemProps} data={this.props.data} />
				);
			} else {
				return (<GenericAstItem {...this.props} />);
			}
		}
	});

	let IdentifierItem = createReactClass({
		displayName: "IdentifierItem",
		render: function () {
			let AstItem = dependencies.get("AstItem");

			let itemProps = {
				abbreviated: true,
				type: this.props.data.type,
				preview: this.props.data.name
			};

			return (
				<AstItem {...itemProps} data={this.props.data} />
			);
		}
	});

	let abbreviatedTypeMap = {
		FunctionDeclaration: FunctionDeclarationItem,
		FunctionExpression: FunctionDeclarationItem,
		MemberExpression: MemberExpressionItem,
		Identifier: IdentifierItem,
		Literal: LiteralItem,
		Property: ObjectPropertyItem,
		CallExpression: CallExpressionItem,
		VariableDeclarator: VariableDeclaratorItem,
		VariableDeclaration: VariableDeclarationItem,
		AssignmentExpression: AssignmentExpressionItem,
		ObjectExpression: ObjectExpressionItem,
		ThisExpression: FixedKeywordItem,
		ContinueStatement: FixedKeywordItem,
		IfStatement: IfStatementItem,
		ConditionalExpression: IfStatementItem,
		UpdateExpression: OperatorExpressionItem,
		UnaryExpression: OperatorExpressionItem,
		BinaryExpression: OperatorExpressionItem,
		LogicalExpression: OperatorExpressionItem,
		ReturnStatement: ReturnStatementItem,
		ExpressionStatement: ExpressionStatementItem
	};

	dependencies.set("AbbreviatedAstItem", createReactClass({
		displayName: "AbbreviatedAstItem",
		render: function () {
			let GenericAstItem = dependencies.get("GenericAstItem");

			if (abbreviatedTypeMap[this.props.data.type] != null) {
				let AbbreviatedItemType = abbreviatedTypeMap[this.props.data.type];

				return (<AbbreviatedItemType {...this.props} />);
			} else {
				return (<GenericAstItem {...this.props} />);
			}
		}
	}));
};
