import { walk } from 'estree-walker';
import Scope from './ast/Scope.js';
import attachScopes from './ast/attachScopes.js';
import getLocation from './utils/getLocation.js';

const modifierNodes = {
	AssignmentExpression: 'left',
	UpdateExpression: 'argument'
};

function isIife ( node, parent ) {
	return parent && parent.type === 'CallExpression' && node === parent.callee;
}

function isReference ( node, parent ) {
	if ( node.type === 'MemberExpression' ) {
		return !node.computed && isReference( node.object, node );
	}

	if ( node.type === 'Identifier' ) {
		// TODO is this right?
		if ( parent.type === 'MemberExpression' ) return parent.computed || node === parent.object;

		// disregard the `bar` in { bar: foo }
		if ( parent.type === 'Property' && node !== parent.value ) return false;

		// disregard the `bar` in `class Foo { bar () {...} }`
		if ( parent.type === 'MethodDefinition' ) return false;

		// disregard the `bar` in `export { foo as bar }`
		if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return;

		return true;
	}
}

class Reference {
	constructor ( node, scope ) {
		this.node = node;
		this.scope = scope;

		this.declaration = null; // bound later

		this.parts = [];

		let root = node;
		while ( root.type === 'MemberExpression' ) {
			this.parts.unshift( root.property.name );
			root = root.object;
		}

		this.name = root.name;

		this.start = node.start;
		this.end = node.start + this.name.length; // can be overridden in the case of namespace members
		this.rewritten = false;
	}
}

export default class Statement {
	constructor ( node, module, start, end ) {
		this.node = node;
		this.module = module;
		this.start = start;
		this.end = end;
		this.next = null; // filled in later

		this.scope = new Scope();

		this.references = [];
		this.stringLiteralRanges = [];

		this.isIncluded = false;

		this.isImportDeclaration = node.type === 'ImportDeclaration';
		this.isExportDeclaration = /^Export/.test( node.type );
		this.isReexportDeclaration = this.isExportDeclaration && !!node.source;
	}

	analyse () {
		if ( this.isImportDeclaration ) return; // nothing to analyse

		// attach scopes
		attachScopes( this );

		// attach statement to each top-level declaration,
		// so we can mark statements easily
		this.scope.eachDeclaration( ( name, declaration ) => {
			declaration.statement = this;
		});

		// find references
		let { module, references, scope, stringLiteralRanges } = this;
		let readDepth = 0;

		walk( this.node, {
			enter ( node, parent ) {
				// warn about eval
				if ( node.type === 'CallExpression' && node.callee.name === 'eval' && !scope.contains( 'eval' ) ) {
					module.bundle.onwarn( `Use of \`eval\` (in ${module.id}) is discouraged, as it may cause issues with minification. See https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval for more details` );
				}

				if ( node.type === 'TemplateElement' ) stringLiteralRanges.push([ node.start, node.end ]);
				if ( node.type === 'Literal' && typeof node.value === 'string' && /\n/.test( node.raw ) ) {
					stringLiteralRanges.push([ node.start + 1, node.end - 1 ]);
				}

				if ( node._scope ) scope = node._scope;
				if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth += 1;

				// special case – shorthand properties. because node.key === node.value,
				// we can't differentiate once we've descended into the node
				if ( node.type === 'Property' && node.shorthand ) {
					const reference = new Reference( node.key, scope );
					reference.isShorthandProperty = true; // TODO feels a bit kludgy
					references.push( reference );
					return this.skip();
				}

				let isReassignment;

				if ( parent && parent.type in modifierNodes ) {
					let subject = parent[ modifierNodes[ parent.type ] ];
					let depth = 0;

					while ( subject.type === 'MemberExpression' ) {
						subject = subject.object;
						depth += 1;
					}

					const importDeclaration = module.imports[ subject.name ];

					if ( !scope.contains( subject.name ) && importDeclaration ) {
						const minDepth = importDeclaration.name === '*' ?
							2 : // cannot do e.g. `namespace.foo = bar`
							1;  // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine

						if ( depth < minDepth ) {
							const err = new Error( `Illegal reassignment to import '${subject.name}'` );
							err.file = module.id;
							err.loc = getLocation( module.magicString.toString(), subject.start );
							throw err;
						}
					}

					isReassignment = !depth;
				}

				if ( isReference( node, parent ) ) {
					// function declaration IDs are a special case – they're associated
					// with the parent scope
					const referenceScope = parent.type === 'FunctionDeclaration' && node === parent.id ?
						scope.parent :
						scope;

					const reference = new Reference( node, referenceScope );
					references.push( reference );

					reference.isImmediatelyUsed = !readDepth;
					reference.isReassignment = isReassignment;

					this.skip(); // don't descend from `foo.bar.baz` into `foo.bar`
				}
			},
			leave ( node, parent ) {
				if ( node._scope ) scope = scope.parent;
				if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1;
			}
		});
	}

	mark () {
		if ( this.isIncluded ) return; // prevent infinite loops
		this.isIncluded = true;

		this.references.forEach( reference => {
			if ( reference.declaration ) reference.declaration.use();
		});
	}

	markSideEffect () {
		if ( this.isIncluded ) return;

		const statement = this;
		let hasSideEffect = false;

		walk( this.node, {
			enter ( node ) {
				// Don't descend into (quasi) function declarations – we'll worry about those
				// if they get called
				if ( node.type === 'FunctionDeclaration' || node.type === 'VariableDeclarator' && node.init && /FunctionExpression/.test( node.init.type ) ) {
					return this.skip();
				}

				// If this is a top-level call expression, or an assignment to a global,
				// this statement will need to be marked
				if ( node.type === 'NewExpression' ) {
					hasSideEffect = true;
				}

				else if ( node.type === 'CallExpression' ) {
					if ( node.callee.type === 'Identifier' ) {
						const declaration = statement.module.trace( node.callee.name );

						if ( !declaration || !declaration.isFunctionDeclaration ) {
							hasSideEffect = true;
						}

						else {
							const mutatedByFunction = declaration.mutates();
							let i = mutatedByFunction.length;
							while ( i-- ) {
								const mutatedDeclaration = statement.module.trace( mutatedByFunction[i] );

								if ( !mutatedDeclaration || mutatedDeclaration.isUsed ) {
									hasSideEffect = true;
									break;
								}
							}

							// if ( declaration.hasSideEffect() ) {
							// 	// if calling this function creates side-effects...
							// 	hasSideEffect = true;
							// }
							//
							// else {
							// 	// ...or mutates inputs that are included...
							// 	hasSideEffect = true;
							// }

							// TODO does function mutate inputs that are needed?
						}
					}

					else if ( node.callee.type === 'MemberExpression' ) {
						// if we're calling e.g. Object.keys(thing), there are no side-effects
						// TODO

						hasSideEffect = true;
					}
				}

				else if ( node.type in modifierNodes ) {
					let subject = node[ modifierNodes[ node.type ] ];
					while ( subject.type === 'MemberExpression' ) subject = subject.object;

					const declaration = statement.module.trace( subject.name );

					if ( !declaration || declaration.isExternal || declaration.statement.isIncluded ) {
						hasSideEffect = true;
					}
				}

				if ( hasSideEffect ) this.skip();
			}
		});

		if ( hasSideEffect ) statement.mark();
		return hasSideEffect;
	}

	source () {
		return this.module.source.slice( this.start, this.end );
	}

	toString () {
		return this.module.magicString.slice( this.start, this.end );
	}
}