
import reduceUsing, { CloneReducer } from 'shift-reducer';
import analyzeScope, { ScopeLookup } from 'shift-scope';

import { check as isReservedWord } from 'reserved-words';

class RegexReducer extends CloneReducer {
	reduceLiteralRegExpExpression(node) {
		return {
			...node,
			unicode: false,
		};
	}
}

class NoReservedKeywordsReducer extends CloneReducer {
	isSafeName(name) {
		return !isReservedWord(name, 3)
			&& !isReservedWord(name, 6, true)
			&& name !== 'arguments'
			&& name !== 'eval';
	}

	reduceLabeledStatement(node, rest) {
		if (this.isSafeName(node.label)) {
			return super.reduceLabeledStatement(node, rest);
		}

		return {
			...node,
			...rest,
			label: node.label + '_avoid_reserved_word',
		};
	}

	reduceBindingIdentifier(node) {
		if (this.isSafeName(node.name)) {
			return node;
		}

		return {
			...node,
			name: node.name + '_avoid_reserved_word',
		};
	}

	reduceAssignmentTargetIdentifier(...args) {
		return this.reduceBindingIdentifier(...args);
	}

	reduceIdentifierExpression(...args) {
		return this.reduceBindingIdentifier(...args);
	}
}

class NoIdentifierDuplicatingAnImportReducer extends CloneReducer {
	reduceImportNamespace(node, { namespaceBinding, ...rest }) {
		return {
			...node,
			...rest,
			namespaceBinding: namespaceBinding && {
				...namespaceBinding,
				name: namespaceBinding.name + '_avoid_capture',
			},
		};
	}
}

class ScopeLookupCloneReducer extends CloneReducer {
	constructor(scopeLookup) {
		super();
		this.scopeLookup = scopeLookup;
	}
}

class NoUndeclaredExportReducer extends ScopeLookupCloneReducer {
	reduceExportLocals(node, { namedExports, ...rest }) {
		return {
			...node,
			...rest,
			namedExports: namedExports.filter(export_ => !export_._isUndeclaredVariable),
		};
	}

	reduceExportLocalSpecifier(node, { name: { _isUndeclaredVariable, ...name }, ...rest }) {
		return {
			...node,
			...rest,
			name,
			_isUndeclaredVariable,
		};
	}

	reduceIdentifierExpression(node) {
		const [ variable ] = this.scopeLookup.lookup(node);
		return {
			...node,
			_isUndeclaredVariable: variable && variable.declarations.length === 0,
		};
	}
}

const removingHOR = Reducer => class extends Reducer {
	constructor(...args) {
		super(...args);
		this.nodesToRemove = new Set();
		this.shouldKeep = statement => !this.nodesToRemove.has(statement);
	}

	markForRemoval(node) {
		this.nodesToRemove.add(node);
	}

	propagateRemoval(from, to) {
		if (this.nodesToRemove.has(from)) {
			this.nodesToRemove.add(to);
		}
	}

	reduceSwitchCase(node, { consequent, ...rest }) {
		return {
			...node,
			...rest,
			consequent: consequent.filter(this.shouldKeep),
		};
	}

	reduceSwitchDefault(node, { consequent, ...rest }) {
		return {
			...node,
			...rest,
			consequent: consequent.filter(this.shouldKeep),
		};
	}

	reduceScript(node, { statements, ...rest }) {
		return {
			...node,
			...rest,
			statements: statements.filter(this.shouldKeep),
		};
	}

	reduceFunctionBody(node, { statements, ...rest }) {
		return {
			...node,
			...rest,
			statements: statements.filter(this.shouldKeep),
		};
	}

	reduceBlock(node, { statements, ...rest }) {
		return {
			...node,
			...rest,
			statements: statements.filter(this.shouldKeep),
		};
	}

	reduceVariableDeclaration(node, { declarators, ...rest }) {
		declarators = declarators.filter(this.shouldKeep);

		node = {
			...node,
			...rest,
			declarators,
		};

		if (declarators.length === 0) {
			this.markForRemoval(node);
		}

		return node;
	}

	reduceVariableDeclarationStatement(node, { declaration }) {
		this.propagateRemoval(declaration, node);
		return node;
	}

	reduceForStatement(node, { body }) {
		this.propagateRemoval(body, node);
		return node;
	}

	reduceWhileStatement(node, { body }) {
		this.propagateRemoval(body, node);
		return node;
	}

	reduceLabeledStatement(node, { body }) {
		this.propagateRemoval(body, node);
		return node;
	}

	reduceForInStatement(node, { body }) {
		this.propagateRemoval(body, node);
		return node;
	}

	reduceIfStatement(node, { consequent, alternate }) {
		this.propagateRemoval(consequent, node);
		this.propagateRemoval(alternate, node);
		return node;
	}

	reduceDoWhileStatement(node, { body }) {
		this.propagateRemoval(body, node);
		return node;
	}
};

class NoNonStrictFeaturesReducer extends removingHOR(CloneReducer) {
	reduceWithStatement(node) {
		this.markForRemoval(node);
		return node;
	}

	reduceUnaryExpression(node) {
		if (node.operator === 'delete' && node.operand.type === 'IdentifierExpression') {
			return node.operand;
		}

		return node;
	}
}

class NoLabeledFunctionDeclarationReducer extends CloneReducer {
	reduceLabeledStatement(node, { body }) {
		if (body.type === 'FunctionDeclaration') {
			return body;
		}

		return node;
	}
}

class NoLabeledBreakContinueReducer extends CloneReducer {
	reduceBreakStatement(node) {
		return {
			...node,
			label: null,
		};
	}
}

class UniqueBindingIdentifiersReducer extends CloneReducer {
	constructor() {
		super();
		this.boundNames = new Set();
	}

	reduceBindingIdentifier(node) {
		if (!this.boundNames.has(node.name)) {
			this.boundNames.add(node.name);
			return node;
		}

		return {
			...node,
			name: node.name + Math.random().toString(16).slice(2),
		};
	}
}

export default shiftAST => {
	[
		NoNonStrictFeaturesReducer,
		RegexReducer,
		NoReservedKeywordsReducer,
		NoIdentifierDuplicatingAnImportReducer,
		NoUndeclaredExportReducer,
		NoLabeledFunctionDeclarationReducer,
		UniqueBindingIdentifiersReducer,
		NoLabeledBreakContinueReducer,
	].forEach(Reducer => {
		const scope = analyzeScope(shiftAST);
		const scopeLookup = new ScopeLookup(scope);
		shiftAST = reduceUsing(new Reducer(scopeLookup), shiftAST);
	});

	return shiftAST;
};
