"use strict";

let expressions = [
	function foo({id, other_prop}) {
		return (other_prop < 34 && other_prop >= 2 && id !== "foo");
	},
	(data) => {
		let id = data.id;
		let someProp = data.prop;

		let otherProp = someProp;

		if (id !== "foo") {
			if (someProp < 34) {
				return (otherProp >= 2);
			} else {
				return false;
			}
		}
	},
	({foo: bar}) => bar > 4,
	function (data) {
		let {foo} = data;

		return (foo < 4);
	},
	42,
	"bar"
];

const util = require("util");
const acorn = require("acorn");
const estreeAssignParent = require("estree-assign-parent");
const scopeAnalyzer = require("scope-analyzer");
const astw = require("astw");

function logAst(ast, depth = 7) {
	return util.inspect(ast, {colors: true, depth: depth});
}

function analyzeFunction(func) {
	let source = func.toString();
	console.log(source);
	console.log("");
	
	
	/* The below is a hack to make anonymous functions parse correctly. */
	let ast = acorn.parse(`[${source}]`);
	let functionAst = ast.body[0].expression.elements[0];

	estreeAssignParent(functionAst);
	scopeAnalyzer.crawl(functionAst);

	let functionArg = functionAst.params[0];

	if (functionArg.type === "ObjectPattern") {
		for (let property of functionArg.properties) {
			// key = from prop, value = to var
			let sourcePropertyName = property.key.name;

			property.value._sourceProp = sourcePropertyName;
		}
	} else if (functionArg.type === "Identifier") {
		functionArg._dataSource = true;
	} else {
		throw new Error(`Encountered unrecognized function argument type: ${functionArg.type}`);
	}

	let walk = astw(functionAst);

	walk((node) => {
		if (node.type === "VariableDeclarator") {
			if (node.id.type === "ObjectPattern") {
				if (node.init.type === "Identifier" && scopeAnalyzer.getBinding(node.init).definition._dataSource === true) {
					for (let property of node.id.properties) {
						property.value._sourceProp = property.key.name;
					}
				}
			} else if (node.id.type === "Identifier") {
				if (node.init.type === "MemberExpression" && scopeAnalyzer.getBinding(node.init.object).definition._dataSource === true) {
					node.id._sourceProp = node.init.property.name;
				} else if (node.init.type === "Identifier") {
					let definition = scopeAnalyzer.getBinding(node.init).definition;

					if (definition._sourceProp != null) {
						node.id._sourceProp = definition._sourceProp;
					}
				}
			} else {
				throw new Error(`Encountered unrecognized assignment id type: ${node.id.type}`);
			}
		}
	});

	let walkBody = astw(functionAst.body);

	function getDataProperty(node) {
		if (node.type === "Identifier" && node.parent.type !== "MemberExpression") {
			let binding = scopeAnalyzer.getBinding(node);

			if (binding != null) {
				let definition = binding.definition;
				
				if (definition != null) {
					if (definition._dataSource !== true) {
						if (definition._sourceProp != null) {
							return definition._sourceProp;
						} else {
							throw new Error(`Could not connect variable '${node.name}' to a data property`);
						}
					}
				} else {
					throw new Error(`Encountered undefined variable: ${node.name}`);
				}
			} else {
				throw new Error(`Encountered undefined variable: ${node.name}`);
			}
		} else {
			throw new Error("Node is not a free-standing (variable) identifier");
		}
	}

	walkBody((node) => {
		if (node.type === "Identifier" && node.parent.type !== "MemberExpression") {
			let sourceProperty = getDataProperty(node);

			if (sourceProperty != null) {
				console.log(`Variable '${node.name}' is derived from data property '${sourceProperty}'`);
			}
		}
	});

	// console.log(util.inspect(functionAst, {depth: 7, colors: true}));
}

for (let expression of expressions) {
	if (typeof expression === "function") {
		let result = analyzeFunction(expression);
		// console.log(expression.toString());
	} else {
		console.log(`!! ${expression} is not a function`);
	}
	console.log("\n=\n");
}