import wrap from './wrap.js';
import Node from './Node.js';
import Scope from './Scope.js';
import CompileError from '../utils/CompileError.js';
import destructure from '../utils/destructure.js';

function isUseStrict ( node ) {
	if ( !node ) return false;
	if ( node.type !== 'ExpressionStatement' ) return false;
	if ( node.expression.type !== 'Literal' ) return false;
	return node.expression.value === 'use strict';
}

export default class BlockStatement extends Node {
	createScope () {
		this.parentIsFunction = /Function/.test( this.parent.type );
		this.isFunctionBlock = this.parentIsFunction || this.parent.type === 'Root';
		this.scope = new Scope({
			block: !this.isFunctionBlock,
			parent: this.parent.findScope( false )
		});

		if ( this.parentIsFunction ) {
			this.parent.params.forEach( node => {
				this.scope.addDeclaration( node, 'param' );
			});
		}
	}

	initialise ( transforms ) {
		this.thisAlias = null;
		this.argumentsAlias = null;
		this.defaultParameters = [];

		// normally the scope gets created here, during initialisation,
		// but in some cases (e.g. `for` statements), we need to create
		// the scope early, as it pertains to both the init block and
		// the body of the statement
		if ( !this.scope ) this.createScope();

		this.body.forEach( node => node.initialise( transforms ) );

		this.scope.consolidate();
	}

	findLexicalBoundary () {
		if ( this.type === 'Program' ) return this;
		if ( /^Function/.test( this.parent.type ) ) return this;

		return this.parent.findLexicalBoundary();
	}

	findScope ( functionScope ) {
		if ( functionScope && !this.isFunctionBlock ) return this.parent.findScope( functionScope );
		return this.scope;
	}

	getArgumentsAlias () {
		if ( !this.argumentsAlias ) {
			this.argumentsAlias = this.scope.createIdentifier( 'arguments' );
		}

		return this.argumentsAlias;
	}

	getArgumentsArrayAlias () {
		if ( !this.argumentsArrayAlias ) {
			this.argumentsArrayAlias = this.scope.createIdentifier( 'argsArray' );
		}

		return this.argumentsArrayAlias;
	}

	getThisAlias () {
		if ( !this.thisAlias ) {
			this.thisAlias = this.scope.createIdentifier( 'this' );
		}

		return this.thisAlias;
	}

	getIndentation () {
		if ( this.indentation === undefined ) {
			const source = this.program.magicString.original;

			const useOuter = this.synthetic || !this.body.length;
			let c = useOuter ? this.start : this.body[0].start;

			while ( c && source[c] !== '\n' ) c -= 1;

			this.indentation = '';

			while ( true ) {
				c += 1;
				const char = source[c];

				if ( char !== ' ' && char !== '\t' ) break;

				this.indentation += char;
			}

			const indentString = this.program.magicString.getIndentString();

			// account for dedented class constructors
			let parent = this.parent;
			while ( parent ) {
				if ( parent.kind === 'constructor' && !parent.parent.parent.superClass ) {
					this.indentation = this.indentation.replace( indentString, '' );
				}

				parent = parent.parent;
			}

			if ( useOuter ) this.indentation += indentString;
		}

		return this.indentation;
	}

	transpile ( code, transforms ) {
		const indentation = this.getIndentation();

		let introStatementGenerators = [];

		if ( this.argumentsAlias ) {
			introStatementGenerators.push( ( start, prefix, suffix ) => {
				const assignment = `${prefix}var ${this.argumentsAlias} = arguments;${suffix}`;
				code.insertLeft( start, assignment );
			});
		}

		if ( this.thisAlias ) {
			introStatementGenerators.push( ( start, prefix, suffix ) => {
				const assignment = `${prefix}var ${this.thisAlias} = this;${suffix}`;
				code.insertLeft( start, assignment );
			});
		}

		if ( this.argumentsArrayAlias ) {
			introStatementGenerators.push( ( start, prefix, suffix ) => {
				const i = this.scope.createIdentifier( 'i' );
				const assignment = `${prefix}var ${i} = arguments.length, ${this.argumentsArrayAlias} = Array(${i});\n${indentation}while ( ${i}-- ) ${this.argumentsArrayAlias}[${i}] = arguments[${i}];${suffix}`;
				code.insertLeft( start, assignment );
			});
		}

		if ( /Function/.test( this.parent.type ) ) {
			this.transpileParameters( code, transforms, indentation, introStatementGenerators );
		}

		if ( transforms.letConst && this.isFunctionBlock ) {
			this.transpileBlockScopedIdentifiers( code );
		}

		super.transpile( code, transforms );

		if ( this.synthetic ) {
			if ( this.parent.type === 'ArrowFunctionExpression' ) {
				const expr = this.body[0];

				if ( introStatementGenerators.length ) {
					code.insertLeft( this.start, `{` ).insertRight( this.end, `${this.parent.getIndentation()}}` );

					code.insertRight( expr.start, `\n${indentation}return ` );
					code.insertLeft( expr.end, `;\n` );
				} else if ( transforms.arrow ) {
					code.insertRight( expr.start, `{ return ` );
					code.insertLeft( expr.end, `; }` );
				}
			}

			else if ( introStatementGenerators.length ) {
				code.insertLeft( this.start, `{` ).insertRight( this.end, `}` );
			}
		}

		let start;
		if ( isUseStrict( this.body[0] ) ) {
			start = this.body[0].end;
		} else if ( this.synthetic || this.parent.type === 'Root' ) {
			start = this.start;
		} else {
			start = this.start + 1;
		}

		let prefix = `\n${indentation}`;
		let suffix = '';
		introStatementGenerators.forEach( ( fn, i ) => {
			if ( i === introStatementGenerators.length - 1 ) suffix = `\n`;
			fn( start, prefix, suffix );
		});
	}

	transpileParameters ( code, transforms, indentation, introStatementGenerators ) {
		const params = this.parent.params;

		params.forEach( param => {
			if ( param.type === 'AssignmentPattern' && param.left.type === 'Identifier' ) {
				if ( transforms.defaultParameter ) {
					introStatementGenerators.push( ( start, prefix, suffix ) => {
						const lhs = `${prefix}if ( ${param.left.name} === void 0 ) ${param.left.name}`;

						code
							.insertRight( param.left.end, `${lhs}` )
							.move( param.left.end, param.right.end, start )
							.insertLeft( param.right.end, `;${suffix}` );
					});
				}
			}

			else if ( param.type === 'RestElement' ) {
				if ( transforms.spreadRest ) {
					introStatementGenerators.push( ( start, prefix, suffix ) => {
						const penultimateParam = params[ params.length - 2 ];

						if ( penultimateParam ) {
							code.remove( penultimateParam ? penultimateParam.end : param.start, param.end );
						} else {
							let start = param.start, end = param.end; // TODO https://gitlab.com/Rich-Harris/buble/issues/8

							while ( /\s/.test( code.original[ start - 1 ] ) ) start -= 1;
							while ( /\s/.test( code.original[ end ] ) ) end += 1;

							code.remove( start, end );
						}

						const name = param.argument.name;
						const len = this.scope.createIdentifier( 'len' );
						const count = params.length - 1;

						if ( count ) {
							code.insertLeft( start, `${prefix}var ${name} = [], ${len} = arguments.length - ${count};\n${indentation}while ( ${len}-- > 0 ) ${name}[ ${len} ] = arguments[ ${len} + ${count} ];${suffix}` );
						} else {
							code.insertLeft( start, `${prefix}var ${name} = [], ${len} = arguments.length;\n${indentation}while ( ${len}-- ) ${name}[ ${len} ] = arguments[ ${len} ];${suffix}` );
						}
					});
				}
			}

			else if ( param.type !== 'Identifier' ) {
				if ( transforms.parameterDestructuring ) {
					const ref = this.scope.createIdentifier( 'ref' );
					destructure( code, this.scope, param, ref, introStatementGenerators );
					code.insertLeft( param.start, ref );
				}
			}
		});
	}

	transpileBlockScopedIdentifiers ( code ) {
		Object.keys( this.scope.blockScopedDeclarations ).forEach( name => {
			const declarations = this.scope.blockScopedDeclarations[ name ];

			for ( let declaration of declarations ) {
				let cont = false; // TODO implement proper continue...

				if ( declaration.kind === 'for.let' ) {
					// special case
					const forStatement = declaration.node.findNearest( 'ForStatement' );

					if ( forStatement.shouldRewriteAsFunction ) {
						const outerAlias = this.scope.createIdentifier( name );
						const innerAlias = forStatement.reassigned[ name ] ?
							this.scope.createIdentifier( name ) :
							name;

						declaration.name = outerAlias;
						code.overwrite( declaration.node.start, declaration.node.end, outerAlias, true );

						forStatement.aliases[ name ] = {
							outer: outerAlias,
							inner: innerAlias
						};

						for ( const identifier of declaration.instances ) {
							const alias = forStatement.body.contains( identifier ) ?
								innerAlias :
								outerAlias;

							if ( name !== alias ) {
								code.overwrite( identifier.start, identifier.end, alias, true );
							}
						}

						cont = true;
					}
				}

				if ( !cont ) {
					const alias = this.scope.createIdentifier( name );

					if ( name !== alias ) {
						declaration.name = alias;
						code.overwrite( declaration.node.start, declaration.node.end, alias, true );

						for ( const identifier of declaration.instances ) {
							identifier.rewritten = true;
							code.overwrite( identifier.start, identifier.end, alias, true );
						}
					}
				}
			}
		});
	}
}
