import * as WireFormat from '@glimmer/wire-format';
import OpcodeBuilder from '../compiled/opcodes/builder';
import { CompiledExpression } from '../compiled/expressions';
import CompiledValue from '../compiled/expressions/value';
import CompiledHasBlock, { CompiledHasBlockParams } from '../compiled/expressions/has-block';
import { BaselineSyntax } from '../scanner';
import { LOGGER, Opaque, Option, dict, assert, unwrap, unreachable } from '@glimmer/util';
import CompiledLookup, {
  CompiledSelf,
  CompiledSymbol,
  CompiledInPartialName
} from '../compiled/expressions/lookups';
import CompiledHelper from '../compiled/expressions/helper';
import CompiledConcat from '../compiled/expressions/concat';
import {
  COMPILED_EMPTY_POSITIONAL_ARGS,
  COMPILED_EMPTY_NAMED_ARGS,
  EMPTY_BLOCKS,
  CompiledArgs,
  CompiledPositionalArgs,
  CompiledNamedArgs,
  Blocks as BlocksSyntax
} from '../compiled/expressions/args';
import {
  CompiledGetBlockBySymbol,
  CompiledInPartialGetBlock
} from '../compiled/expressions/has-block';
import { PublicVM as VM } from '../vm';
import AppendVM from '../vm/append';

import { CompiledFunctionExpression } from '../compiled/expressions/function';
const { defaultBlock, params, hash } = BaselineSyntax.NestedBlock;

export type SexpExpression = BaselineSyntax.AnyExpression & { 0: number };
export type Syntax = SexpExpression | BaselineSyntax.AnyStatement;
export type CompilerFunction<T extends Syntax, U> = ((sexp: T, builder: OpcodeBuilder) => U);
export type Name = BaselineSyntax.AnyStatement[0];
export type debugGet = ((path: string) => any);

export interface DebugContext {
  context: Opaque;
  get: debugGet;
}

export type debugCallback = ((context: Opaque, get: debugGet) => DebugContext);

function debugCallback(context: Opaque, get: debugGet) {
  console.info('Use `context`, and `get(<path>)` to debug this template.');
  /* tslint:disable */
  debugger;
  /* tslint:enable */
  return { context, get };
}

function getter(vm: VM, builder: OpcodeBuilder) {
  return (path: string) => {
    let parts = path.split('.') as any;

    if (parts[0] === 'this') {
      parts[0] = null;
    }

    return compileRef(parts, builder).evaluate(vm as AppendVM);
  };
}

let callback = debugCallback;

// For testing purposes
export function setDebuggerCallback(cb: debugCallback) {
  callback = cb;
}

export function resetDebuggerCallback() {
  callback = debugCallback;
}

export class Compilers<T extends Syntax, CompileTo> {
  private names = dict<number>();
  private funcs: CompilerFunction<T, CompileTo>[] = [];

  add(name: number, func: CompilerFunction<T, CompileTo>): void {
    this.funcs.push(func);
    this.names[name] = this.funcs.length - 1;
  }

  compile(sexp: T, builder: OpcodeBuilder): CompileTo {
    let name: number = sexp[0];
    let index = this.names[name];
    let func = this.funcs[index];
    assert(!!func, `expected an implementation for ${sexp[0]}`);
    return func(sexp, builder);
  }
}

import S = WireFormat.Statements;

let { Ops } = WireFormat;

export const STATEMENTS = new Compilers<BaselineSyntax.AnyStatement, void>();

STATEMENTS.add(Ops.Text, (sexp: S.Text, builder: OpcodeBuilder) => {
  builder.text(sexp[1]);
});

STATEMENTS.add(Ops.Comment, (sexp: S.Comment, builder: OpcodeBuilder) => {
  builder.comment(sexp[1]);
});

STATEMENTS.add(Ops.CloseElement, (_sexp, builder: OpcodeBuilder) => {
  LOGGER.trace('close-element statement');
  builder.closeElement();
});

STATEMENTS.add(Ops.FlushElement, (_sexp, builder: OpcodeBuilder) => {
  builder.flushElement();
});

STATEMENTS.add(Ops.Modifier, (sexp: S.Modifier, builder: OpcodeBuilder) => {
  let [, path, params, hash] = sexp;

  let args = compileArgs(params, hash, builder);

  if (builder.env.hasModifier(path[0], builder.symbolTable)) {
    builder.modifier(path[0], args);
  } else {
    throw new Error(`Compile Error ${path.join('.')} is not a modifier: Helpers may not be used in the element form.`);
  }
});

STATEMENTS.add(Ops.StaticAttr, (sexp: S.StaticAttr, builder: OpcodeBuilder) => {
  let [, name, value, namespace] = sexp;
  builder.staticAttr(name, namespace, value as string);
});

STATEMENTS.add(Ops.AnyDynamicAttr, (sexp: BaselineSyntax.AnyDynamicAttr, builder: OpcodeBuilder) => {
  let [, name, value, namespace, trusting] = sexp;

  builder.putValue(value);

  if (namespace) {
    builder.dynamicAttrNS(name, namespace, trusting);
  } else {
    builder.dynamicAttr(name, trusting);
  }
});

STATEMENTS.add(Ops.OpenElement, (sexp: BaselineSyntax.OpenPrimitiveElement, builder: OpcodeBuilder) => {
  LOGGER.trace('open-element statement');
  builder.openPrimitiveElement(sexp[1]);
});

STATEMENTS.add(Ops.OptimizedAppend, (sexp: BaselineSyntax.OptimizedAppend, builder: OpcodeBuilder) => {
  let [, value, trustingMorph] = sexp;

  let { inlines } = builder.env.macros();
  let returned = inlines.compile(sexp, builder) || value;

  if (returned === true) return;

  builder.putValue(returned[1]);

  if (trustingMorph) {
    builder.trustingAppend();
  } else {
    builder.cautiousAppend();
  }
});

STATEMENTS.add(Ops.UnoptimizedAppend, (sexp: BaselineSyntax.UnoptimizedAppend, builder) => {
  let [, value, trustingMorph] = sexp;
  let { inlines } = builder.env.macros();
  let returned = inlines.compile(sexp, builder) || value;

  if (returned === true) return;

  if (trustingMorph) {
    builder.guardedTrustingAppend(returned[1]);
  } else {
    builder.guardedCautiousAppend(returned[1]);
  }
});

STATEMENTS.add(Ops.NestedBlock, (sexp: BaselineSyntax.NestedBlock, builder: OpcodeBuilder) => {
  let { blocks } = builder.env.macros();
  blocks.compile(sexp, builder);
});

STATEMENTS.add(Ops.ScannedBlock, (sexp: BaselineSyntax.ScannedBlock, builder) => {
  let [, path, params, hash, template, inverse] = sexp;

  let templateBlock = template && template.scan();
  let inverseBlock = inverse && inverse.scan();

  let { blocks } = builder.env.macros();
  blocks.compile([Ops.NestedBlock, path, params, hash, templateBlock, inverseBlock], builder);
});

// this fixes an issue with Ember versions using glimmer-vm@0.22 when attempting
// to use nested web components.  This is obviously not correct for angle bracket components
// but since no consumers are currently using them with glimmer@0.22.x we are hard coding
// support to just use the fallback case.
STATEMENTS.add(Ops.Component, (sexp: WireFormat.Statements.Component, builder) => {
  let [, tag, component ] = sexp;
  let { attrs, statements } = component;

  builder.openPrimitiveElement(tag);

  for (let i = 0; i < attrs.length; i++) {
    STATEMENTS.compile(attrs[i], builder);
  }

  builder.flushElement();

  for (let i = 0; i < statements.length; i++) {
    STATEMENTS.compile(statements[i], builder);
  }

  builder.closeElement();
});

STATEMENTS.add(Ops.ScannedComponent, (sexp: BaselineSyntax.ScannedComponent, builder) => {
  let [, tag, attrs, rawArgs, rawBlock] = sexp;
  let block = rawBlock && rawBlock.scan();

  let args = compileBlockArgs(null, rawArgs, { default: block, inverse: null }, builder);

  let definition = builder.env.getComponentDefinition(tag, builder.symbolTable);

  builder.putComponentDefinition(definition);
  builder.openComponent(args, attrs.scan());
  builder.closeComponent();
});

STATEMENTS.add(Ops.StaticPartial, (sexp: BaselineSyntax.StaticPartial, builder) => {
  let [, name] = sexp;

  if (!builder.env.hasPartial(name, builder.symbolTable)) {
    throw new Error(`Compile Error: Could not find a partial named "${name}"`);
  }

  let definition = builder.env.lookupPartial(name, builder.symbolTable);

  builder.putPartialDefinition(definition);
  builder.evaluatePartial();
});

STATEMENTS.add(Ops.DynamicPartial, (sexp: BaselineSyntax.DynamicPartial, builder) => {
  let [, name] = sexp;

    builder.startLabels();

    builder.putValue(name);
    builder.test('simple');
    builder.enter('BEGIN', 'END');
    builder.label('BEGIN');
    builder.jumpUnless('END');
    builder.putDynamicPartialDefinition();
    builder.evaluatePartial();
    builder.label('END');
    builder.exit();

    builder.stopLabels();
});

STATEMENTS.add(Ops.Yield, function(this: undefined, sexp: WireFormat.Statements.Yield, builder) {
  let [, to, params] = sexp;

  let args = compileArgs(params, null, builder);
  builder.yield(args, to);
});

STATEMENTS.add(Ops.Debugger, (sexp: BaselineSyntax.Debugger, builder: OpcodeBuilder) => {

  builder.putValue([Ops.Function, (vm: VM) => {
    let context = vm.getSelf().value();
    let get = (path: string) => {
      return getter(vm, builder)(path).value();
    };
    callback(context, get);
  }]);

  return sexp;
});

let EXPRESSIONS = new Compilers<SexpExpression, CompiledExpression<Opaque>>();

import E = WireFormat.Expressions;
import C = WireFormat.Core;

export function expr(expression: BaselineSyntax.AnyExpression, builder: OpcodeBuilder): CompiledExpression<Opaque> {
  if (Array.isArray(expression)) {
    return EXPRESSIONS.compile(expression, builder);
  } else {
    return new CompiledValue(expression);
  }
}

EXPRESSIONS.add(Ops.Unknown, (sexp: E.Unknown, builder: OpcodeBuilder) => {
  let path = sexp[1];
  let name = path[0];

  if (builder.env.hasHelper(name, builder.symbolTable)) {
    return new CompiledHelper(name, builder.env.lookupHelper(name, builder.symbolTable), CompiledArgs.empty(), builder.symbolTable);
  } else {
    return compileRef(path, builder);
  }
});

EXPRESSIONS.add(Ops.Concat, ((sexp: E.Concat, builder: OpcodeBuilder) => {
  let params = sexp[1].map(p => expr(p, builder));
  return new CompiledConcat(params);
}) as any);

EXPRESSIONS.add(Ops.Function, (sexp: BaselineSyntax.FunctionExpression, builder: OpcodeBuilder) => {
  return new CompiledFunctionExpression(sexp[1], builder.symbolTable);
});

EXPRESSIONS.add(Ops.Helper, (sexp: E.Helper, builder: OpcodeBuilder) => {
  let { env, symbolTable } = builder;
  let [, [name], params, hash] = sexp;

  if (env.hasHelper(name, symbolTable)) {
    let args = compileArgs(params, hash, builder);
    return new CompiledHelper(name, env.lookupHelper(name, symbolTable), args, symbolTable);
  } else {
    throw new Error(`Compile Error: ${name} is not a helper`);
  }
});

EXPRESSIONS.add(Ops.Get, (sexp: E.Get, builder: OpcodeBuilder) => {
  return compileRef(sexp[1], builder);
});

EXPRESSIONS.add(Ops.Undefined, (_sexp, _builder) => {
  return new CompiledValue(undefined);
});

EXPRESSIONS.add(Ops.Arg, (sexp: E.Arg, builder: OpcodeBuilder) => {
  let [, parts] = sexp;
  let head = parts[0];
  let named: Option<number>, partial: Option<number>;

  if (named = builder.symbolTable.getSymbol('named', head)) {
    let path = parts.slice(1);
    let inner = new CompiledSymbol(named, head);
    return CompiledLookup.create(inner, path);
  } else if (partial = builder.symbolTable.getPartialArgs()) {
    let path = parts.slice(1);
    let inner = new CompiledInPartialName(partial, head);
    return CompiledLookup.create(inner, path);
  } else {
    throw new Error(`[BUG] @${parts.join('.')} is not a valid lookup path.`);
  }
});

EXPRESSIONS.add(Ops.HasBlock, (sexp: E.HasBlock, builder) => {
  let blockName = sexp[1];

  let yields: Option<number>, partial: Option<number>;

  if (yields = builder.symbolTable.getSymbol('yields', blockName)) {
    let inner = new CompiledGetBlockBySymbol(yields, blockName);
    return new CompiledHasBlock(inner);
  } else if (partial = builder.symbolTable.getPartialArgs()) {
    let inner = new CompiledInPartialGetBlock(partial, blockName);
    return new CompiledHasBlock(inner);
  } else {
    throw new Error('[BUG] ${blockName} is not a valid block name.');
  }
});

EXPRESSIONS.add(Ops.HasBlockParams, (sexp: E.HasBlockParams, builder) => {
  let blockName = sexp[1];
  let yields: Option<number>, partial: Option<number>;

  if (yields = builder.symbolTable.getSymbol('yields', blockName)) {
    let inner = new CompiledGetBlockBySymbol(yields, blockName);
    return new CompiledHasBlockParams(inner);
  } else if (partial = builder.symbolTable.getPartialArgs()) {
    let inner = new CompiledInPartialGetBlock(partial, blockName);
    return new CompiledHasBlockParams(inner);
  } else {
    throw new Error('[BUG] ${blockName} is not a valid block name.');
  }

});

export function compileArgs(params: Option<WireFormat.Core.Params>, hash: Option<WireFormat.Core.Hash>, builder: OpcodeBuilder): CompiledArgs {
  let compiledParams = compileParams(params, builder);
  let compiledHash = compileHash(hash, builder);
  return CompiledArgs.create(compiledParams, compiledHash, EMPTY_BLOCKS);
}

export function compileBlockArgs(params: Option<WireFormat.Core.Params>, hash: Option<WireFormat.Core.Hash>, blocks: BlocksSyntax, builder: OpcodeBuilder): CompiledArgs {
  let compiledParams = compileParams(params, builder);
  let compiledHash = compileHash(hash, builder);
  return CompiledArgs.create(compiledParams, compiledHash, blocks);
}

export function compileBaselineArgs(args: BaselineSyntax.Args, builder: OpcodeBuilder): CompiledArgs {
  let [params, hash, _default, inverse] = args;
  return CompiledArgs.create(compileParams(params, builder), compileHash(hash, builder), { default: _default, inverse });
}

function compileParams(params: Option<WireFormat.Core.Params>, builder: OpcodeBuilder): CompiledPositionalArgs {
  if (!params || params.length === 0) return COMPILED_EMPTY_POSITIONAL_ARGS;
  let compiled = new Array(params.length);
  for (let i = 0; i < params.length; i++) {
    compiled[i] = expr(params[i], builder);
  }
  return CompiledPositionalArgs.create(compiled);
}

function compileHash(hash: Option<WireFormat.Core.Hash>, builder: OpcodeBuilder): CompiledNamedArgs {
  if (!hash) return COMPILED_EMPTY_NAMED_ARGS;
  let [keys, values] = hash;
  if (keys.length === 0) return COMPILED_EMPTY_NAMED_ARGS;
  let compiled = new Array(values.length);
  for (let i = 0; i < values.length; i++) {
    compiled[i] = expr(values[i], builder);
  }
  return new CompiledNamedArgs(keys, compiled);
}

function compileRef(parts: string[], builder: OpcodeBuilder) {
  let head = parts[0];
  let local: Option<number>;

  if (head === null) { // {{this.foo}}
    let inner = new CompiledSelf();
    let path = parts.slice(1) as string[];
    return CompiledLookup.create(inner, path);
  } else if (local = builder.symbolTable.getSymbol('local', head)) {
    let path = parts.slice(1) as string[];
    let inner = new CompiledSymbol(local, head);
    return CompiledLookup.create(inner, path);
  } else {
    let inner = new CompiledSelf();
    return CompiledLookup.create(inner, parts as string[]);
  }
}

export type NestedBlockSyntax = BaselineSyntax.NestedBlock;
export type CompileBlockMacro = (sexp: NestedBlockSyntax, builder: OpcodeBuilder) => void;

export class Blocks {
  private names = dict<number>();
  private funcs: CompileBlockMacro[] = [];
  private missing: CompileBlockMacro;

  add(name: string, func: CompileBlockMacro) {
    this.funcs.push(func);
    this.names[name] = this.funcs.length - 1;
  }

  addMissing(func: CompileBlockMacro) {
    this.missing = func;
  }

  compile(sexp: BaselineSyntax.NestedBlock, builder: OpcodeBuilder): void {
    // assert(sexp[1].length === 1, 'paths in blocks are not supported');

    let name: string = sexp[1][0];
    let index = this.names[name];

    if (index === undefined) {
      assert(!!this.missing, `${name} not found, and no catch-all block handler was registered`);
      let func = this.missing;
      let handled = func(sexp, builder);
      assert(!!handled, `${name} not found, and the catch-all block handler didn't handle it`);
    } else {
      let func = this.funcs[index];
      func(sexp, builder);
    }
  }
}

export const BLOCKS = new Blocks();

export type AppendSyntax = BaselineSyntax.OptimizedAppend | BaselineSyntax.UnoptimizedAppend;
export type AppendMacro = (path: C.Path, params: Option<C.Params>, hash: Option<C.Hash>, builder: OpcodeBuilder) => ['expr', BaselineSyntax.AnyExpression] | true | false;

export class Inlines {
  private names = dict<number>();
  private funcs: AppendMacro[] = [];
  private missing: AppendMacro;

  add(name: string, func: AppendMacro) {
    this.funcs.push(func);
    this.names[name] = this.funcs.length - 1;
  }

  addMissing(func: AppendMacro) {
    this.missing = func;
  }

  compile(sexp: AppendSyntax, builder: OpcodeBuilder): ['expr', BaselineSyntax.AnyExpression] | true {
    let value = sexp[1];

    // TODO: Fix this so that expression macros can return
    // things like components, so that {{component foo}}
    // is the same as {{(component foo)}}

    if (!Array.isArray(value)) return ['expr', value];

    let path: C.Path;
    let params: Option<C.Params>;
    let hash: Option<C.Hash>;

    if (value[0] === Ops.Helper) {
      path = value[1];
      params = value[2];
      hash = value[3];
    } else if (value[0] === Ops.Unknown) {
      path = value[1];
      params = hash = null;
    } else {
      return ['expr', value];
    }

    if (path.length > 1 && !params && !hash) {
      return ['expr', value];
    }

    let name = path[0];
    let index = this.names[name];

    if (index === undefined && this.missing) {
      let func = this.missing;
      let returned = func(path, params, hash, builder);
      return returned === false ? ['expr', value] : returned;
    } else if (index !== undefined) {
      let func = this.funcs[index];
      let returned = func(path, params, hash, builder);
      return returned === false ? ['expr', value] : returned;
    } else {
      return ['expr', value];
    }
  }
}

export const INLINES = new Inlines();

populateBuiltins(BLOCKS, INLINES);

export function populateBuiltins(blocks: Blocks = new Blocks(), inlines: Inlines = new Inlines()): { blocks: Blocks, inlines: Inlines } {
  blocks.add('if', (sexp: BaselineSyntax.NestedBlock, builder: OpcodeBuilder) => {
    //        PutArgs
    //        Test(Environment)
    //        Enter(BEGIN, END)
    // BEGIN: Noop
    //        JumpUnless(ELSE)
    //        Evaluate(default)
    //        Jump(END)
    // ELSE:  Noop
    //        Evalulate(inverse)
    // END:   Noop
    //        Exit

    let [,, params, hash, _default, inverse] = sexp;
    let args = compileArgs(params, hash, builder);

    builder.putArgs(args);
    builder.test('environment');

    builder.labelled(null, b => {
      if (_default && inverse) {
        b.jumpUnless('ELSE');
        b.evaluate(_default);
        b.jump('END');
        b.label('ELSE');
        b.evaluate(inverse);
      } else if (_default) {
        b.jumpUnless('END');
        b.evaluate(_default);
      } else {
        throw unreachable();
      }
    });
  });

  blocks.add('-in-element', (sexp: BaselineSyntax.NestedBlock, builder: OpcodeBuilder) => {
    let block = defaultBlock(sexp);
    let args = compileArgs(params(sexp), null, builder);

    builder.putArgs(args);
    builder.test('simple');

    builder.labelled(null, b => {
      b.jumpUnless('END');
      b.pushRemoteElement();
      b.evaluate(unwrap(block));
      b.popRemoteElement();
    });
  });

  blocks.add('-with-dynamic-vars', (sexp, builder) => {
    let block = defaultBlock(sexp);
    let args = compileArgs(params(sexp), hash(sexp), builder);

    builder.unit(b => {
      b.putArgs(args);
      b.pushDynamicScope();
      b.bindDynamicScope(args.named.keys as string[]);
      b.evaluate(unwrap(block));
      b.popDynamicScope();
    });
  });

  blocks.add('unless', (sexp: BaselineSyntax.NestedBlock, builder: OpcodeBuilder) => {
    //        PutArgs
    //        Test(Environment)
    //        Enter(BEGIN, END)
    // BEGIN: Noop
    //        JumpUnless(ELSE)
    //        Evaluate(default)
    //        Jump(END)
    // ELSE:  Noop
    //        Evalulate(inverse)
    // END:   Noop
    //        Exit

    let [,, params, hash, _default, inverse] = sexp;
    let args = compileArgs(params, hash, builder);

    builder.putArgs(args);
    builder.test('environment');

    builder.labelled(null, b => {
      if (_default && inverse) {
        b.jumpIf('ELSE');
        b.evaluate(_default);
        b.jump('END');
        b.label('ELSE');
        b.evaluate( inverse);
      } else if (_default) {
        b.jumpIf('END');
        b.evaluate(_default);
      } else {
        throw unreachable();
      }
    });
  });

  blocks.add('with', (sexp: BaselineSyntax.NestedBlock, builder: OpcodeBuilder) => {
    //        PutArgs
    //        Test(Environment)
    //        Enter(BEGIN, END)
    // BEGIN: Noop
    //        JumpUnless(ELSE)
    //        Evaluate(default)
    //        Jump(END)
    // ELSE:  Noop
    //        Evalulate(inverse)
    // END:   Noop
    //        Exit

    let [,, params, hash, _default, inverse] = sexp;
    let args = compileArgs(params, hash, builder);

    builder.putArgs(args);
    builder.test('environment');

    builder.labelled(null, b => {
      if (_default && inverse) {
        b.jumpUnless('ELSE');
        b.evaluate(_default);
        b.jump('END');
        b.label('ELSE');
        b.evaluate(inverse);
      } else if (_default) {
        b.jumpUnless('END');
        b.evaluate(_default);
      } else {
        throw unreachable();
      }
    });
  });

  blocks.add('each', (sexp: BaselineSyntax.NestedBlock, builder: OpcodeBuilder) => {
    //         Enter(BEGIN, END)
    // BEGIN:  Noop
    //         PutArgs
    //         PutIterable
    //         JumpUnless(ELSE)
    //         EnterList(BEGIN2, END2)
    // ITER:   Noop
    //         NextIter(BREAK)
    //         EnterWithKey(BEGIN2, END2)
    // BEGIN2: Noop
    //         PushChildScope
    //         Evaluate(default)
    //         PopScope
    // END2:   Noop
    //         Exit
    //         Jump(ITER)
    // BREAK:  Noop
    //         ExitList
    //         Jump(END)
    // ELSE:   Noop
    //         Evalulate(inverse)
    // END:    Noop
    //         Exit

    let [,, params, hash, _default, inverse] = sexp;
    let args = compileArgs(params, hash, builder);

    builder.labelled(args, b => {
      b.putIterator();

      if (inverse) {
        b.jumpUnless('ELSE');
      } else {
        b.jumpUnless('END');
      }

      b.iter(b => {
        b.evaluate(unwrap(_default));
      });

      if (inverse) {
        b.jump('END');
        b.label('ELSE');
        b.evaluate(inverse);
      }
    });
  });

  return { blocks, inlines };
}
