import { symbol, LinkedList, ListNode, dict, isDict, debugToString, EMPTY_ARRAY, isObject } from '@glimmer/util';
import { valueForTag, validateTag, CONSTANT_TAG, combine, createUpdatableTag, dirtyTag, updateTag, track, isConst, isConstTag } from '@glimmer/validator';
import { DEBUG } from '@glimmer/env';

class CachedReference {
  constructor() {
    this.lastRevision = null;
    this.lastValue = null;
  }

  value() {
    let {
      tag,
      lastRevision,
      lastValue
    } = this;

    if (lastRevision === null || !validateTag(tag, lastRevision)) {
      lastValue = this.lastValue = this.compute();
      this.lastRevision = valueForTag(tag);
    }

    return lastValue;
  }

  invalidate() {
    this.lastRevision = null;
  }

} //////////


class ReferenceCache {
  constructor(reference) {
    this.lastValue = null;
    this.lastRevision = null;
    this.initialized = false;
    this.tag = reference.tag;
    this.reference = reference;
  }

  peek() {
    if (!this.initialized) {
      return this.initialize();
    }

    return this.lastValue;
  }

  revalidate() {
    if (!this.initialized) {
      return this.initialize();
    }

    let {
      reference,
      lastRevision
    } = this;
    let tag = reference.tag;
    if (validateTag(tag, lastRevision)) return NOT_MODIFIED;
    let {
      lastValue
    } = this;
    let currentValue = reference.value();
    this.lastRevision = valueForTag(tag);
    if (currentValue === lastValue) return NOT_MODIFIED;
    this.lastValue = currentValue;
    return currentValue;
  }

  initialize() {
    let {
      reference
    } = this;
    let currentValue = this.lastValue = reference.value();
    this.lastRevision = valueForTag(reference.tag);
    this.initialized = true;
    return currentValue;
  }

}

const NOT_MODIFIED = symbol('NOT_MODIFIED');

function isModified(value) {
  return value !== NOT_MODIFIED;
}

class PrimitiveReference {
  constructor(inner) {
    this.inner = inner;
    this.tag = CONSTANT_TAG;
  }

  value() {
    return this.inner;
  }

  get(_key) {
    return UNDEFINED_REFERENCE;
  }

}

const UNDEFINED_REFERENCE = new PrimitiveReference(undefined);

class ConstReference {
  constructor(inner) {
    this.inner = inner;
    this.tag = CONSTANT_TAG;
  }

  value() {
    return this.inner;
  }

  get(_key) {
    return UNDEFINED_REFERENCE;
  }

}

class ListItem extends ListNode {
  constructor(iterable, result) {
    super(iterable.valueReferenceFor(result));
    this.retained = false;
    this.seen = false;
    this.key = result.key;
    this.iterable = iterable;
    this.memo = iterable.memoReferenceFor(result);
  }

  update(item) {
    this.retained = true;
    this.iterable.updateValueReference(this.value, item);
    this.iterable.updateMemoReference(this.memo, item);
  }

  shouldRemove() {
    return !this.retained;
  }

  reset() {
    this.retained = false;
    this.seen = false;
  }

}

class IterationArtifacts {
  constructor(iterable) {
    this.iterator = null;
    this.map = new Map();
    this.list = new LinkedList();
    this.tag = iterable.tag;
    this.iterable = iterable;
  }

  isEmpty() {
    let iterator = this.iterator = this.iterable.iterate();
    return iterator.isEmpty();
  }

  iterate() {
    let iterator;

    if (this.iterator === null) {
      iterator = this.iterable.iterate();
    } else {
      iterator = this.iterator;
    }

    this.iterator = null;
    return iterator;
  }

  advanceToKey(key, current) {
    let seek = current;

    while (seek !== null && seek.key !== key) {
      seek = this.advanceNode(seek);
    }

    return seek;
  }

  has(key) {
    return this.map.has(key);
  }

  get(key) {
    return this.map.get(key);
  }

  wasSeen(key) {
    let node = this.map.get(key);
    return node !== undefined && node.seen;
  }

  update(item) {
    let found = this.get(item.key);
    found.update(item);
    return found;
  }

  append(item) {
    let {
      map,
      list,
      iterable
    } = this;
    let node = new ListItem(iterable, item);
    map.set(item.key, node);
    list.append(node);
    return node;
  }

  insertBefore(item, reference) {
    let {
      map,
      list,
      iterable
    } = this;
    let node = new ListItem(iterable, item);
    map.set(item.key, node);
    node.retained = true;
    list.insertBefore(node, reference);
    return node;
  }

  move(item, reference) {
    let {
      list
    } = this;
    item.retained = true;
    list.remove(item);
    list.insertBefore(item, reference);
  }

  remove(item) {
    let {
      list
    } = this;
    list.remove(item);
    this.map.delete(item.key);
  }

  nextNode(item) {
    return this.list.nextNode(item);
  }

  advanceNode(item) {
    item.seen = true;
    return this.list.nextNode(item);
  }

  head() {
    return this.list.head();
  }

}

class ReferenceIterator {
  // if anyone needs to construct this object with something other than
  // an iterable, let @wycats know.
  constructor(iterable) {
    this.iterator = null;
    let artifacts = new IterationArtifacts(iterable);
    this.artifacts = artifacts;
  }

  next() {
    let {
      artifacts
    } = this;
    let iterator = this.iterator = this.iterator || artifacts.iterate();
    let item = iterator.next();
    if (item === null) return null;
    return artifacts.append(item);
  }

}

var Phase;

(function (Phase) {
  Phase[Phase["Append"] = 0] = "Append";
  Phase[Phase["Prune"] = 1] = "Prune";
  Phase[Phase["Done"] = 2] = "Done";
})(Phase || (Phase = {}));

const END = symbol('END');

class IteratorSynchronizer {
  constructor({
    target,
    artifacts,
    env
  }) {
    this.target = target;
    this.artifacts = artifacts;
    this.iterator = artifacts.iterate();
    this.current = artifacts.head();
    this.env = env;
  }

  sync() {
    let phase = Phase.Append;

    while (true) {
      switch (phase) {
        case Phase.Append:
          phase = this.nextAppend();
          break;

        case Phase.Prune:
          phase = this.nextPrune();
          break;

        case Phase.Done:
          this.nextDone();
          return;
      }
    }
  }

  advanceToKey(key) {
    let {
      current,
      artifacts
    } = this;
    if (current === null) return;
    let next = artifacts.advanceNode(current);

    if (next.key === key) {
      this.current = artifacts.advanceNode(next);
      return;
    }

    let seek = artifacts.advanceToKey(key, current);

    if (seek) {
      this.move(seek, current);
      this.current = artifacts.nextNode(current);
    }
  }

  move(item, reference) {
    if (item.next !== reference) {
      this.artifacts.move(item, reference);
      this.target.move(this.env, item.key, item.value, item.memo, reference ? reference.key : END);
    }
  }

  nextAppend() {
    let {
      iterator,
      current,
      artifacts
    } = this;
    let item = iterator.next();

    if (item === null) {
      return this.startPrune();
    }

    let {
      key
    } = item;

    if (current !== null && current.key === key) {
      this.nextRetain(item, current);
    } else if (artifacts.has(key)) {
      this.nextMove(item);
    } else {
      this.nextInsert(item);
    }

    return Phase.Append;
  }

  nextRetain(item, current) {
    let {
      artifacts
    } = this; // current = expect(current, 'BUG: current is empty');

    current.update(item);
    this.current = artifacts.nextNode(current);
    this.target.retain(this.env, item.key, current.value, current.memo);
  }

  nextMove(item) {
    let {
      current,
      artifacts
    } = this;
    let {
      key
    } = item;
    let found = artifacts.update(item);

    if (artifacts.wasSeen(key)) {
      this.move(found, current);
    } else {
      this.advanceToKey(key);
    }
  }

  nextInsert(item) {
    let {
      artifacts,
      target,
      current
    } = this;
    let node = artifacts.insertBefore(item, current);
    target.insert(this.env, node.key, node.value, node.memo, current ? current.key : null);
  }

  startPrune() {
    this.current = this.artifacts.head();
    return Phase.Prune;
  }

  nextPrune() {
    let {
      artifacts,
      target,
      current
    } = this;

    if (current === null) {
      return Phase.Done;
    }

    let node = current;
    this.current = artifacts.nextNode(node);

    if (node.shouldRemove()) {
      artifacts.remove(node);
      target.delete(this.env, node.key);
    } else {
      node.reset();
    }

    return Phase.Prune;
  }

  nextDone() {
    this.target.done(this.env);
  }

}

const UPDATE_REFERENCED_VALUE = symbol('UPDATE_REFERENCED_VALUE');
/**
 * RootReferences refer to a constant root value within a template. For
 * instance, the `this` in `{{this.some.prop}}`. This is typically a:
 *
 * - Component
 * - Controller
 * - Helper
 *
 * Or another "top level" template construct, if you will. PropertyReferences
 * chain off a root reference in the template, and can then be passed around and
 * used at will.
 */

class RootReference {
  constructor(env) {
    this.env = env;
    this.children = dict();
    this.tag = CONSTANT_TAG;
  }

  get(key) {
    // References should in general be identical to one another, so we can usually
    // deduplicate them in production. However, in DEBUG we need unique references
    // so we can properly key off them for the logging context.
    if (DEBUG) {
      // We register the template debug context now since the reference is
      // created before the component itself. It shouldn't be possible to cause
      // errors when accessing the root, only subproperties of the root, so this
      // should be fine for the time being. The exception is helpers, but they
      // set their context earlier.
      //
      // TODO: This points to a need for more first class support for arguments in
      // the debugRenderTree. The fact that we can't accurately relate an argument
      // reference to its component is problematic for debug tooling.
      if (!this.didSetupDebugContext) {
        this.didSetupDebugContext = true;
        this.env.setTemplatePathDebugContext(this, this.debugLogName || 'this', null);
      }

      return new PropertyReference(this, key, this.env);
    } else {
      let ref = this.children[key];

      if (ref === undefined) {
        ref = this.children[key] = new PropertyReference(this, key, this.env);
      }

      return ref;
    }
  }

}

class ComponentRootReference extends RootReference {
  constructor(inner, env) {
    super(env);
    this.inner = inner;
  }

  value() {
    return this.inner;
  }

}

class HelperRootReference extends RootReference {
  constructor(fn, args, env, debugName) {
    super(env);
    this.fn = fn;
    this.args = args;
    this.computeRevision = null;
    this.computeTag = null;

    if (DEBUG) {
      let name = debugName || fn.name;
      env.setTemplatePathDebugContext(this, `(result of a \`${name}\` helper)`, null);
      this.didSetupDebugContext = true;
    }

    if (isConst(args)) {
      this.compute();
    }

    let {
      tag,
      computeTag
    } = this;

    if (computeTag !== null && isConstTag(computeTag)) {
      // If the args are constant, and the first computation is constant, then
      // the helper itself is constant and will never update.
      tag = this.tag = CONSTANT_TAG;
      this.computeRevision = valueForTag(tag);
    } else {
      let valueTag = this.valueTag = createUpdatableTag();
      tag = this.tag = combine([args.tag, valueTag]);

      if (computeTag !== null) {
        // We computed once, so setup the cache state correctly
        updateTag(valueTag, computeTag);
        this.computeRevision = valueForTag(tag);
      }
    }
  }

  compute() {
    this.computeTag = track(() => {
      this.computeValue = this.fn(this.args);
    }, DEBUG && this.env.getTemplatePathDebugContext(this));
  }

  value() {
    let {
      tag,
      computeRevision
    } = this;

    if (computeRevision === null || !validateTag(tag, computeRevision)) {
      this.compute();
      updateTag(this.valueTag, this.computeTag);
      this.computeRevision = valueForTag(tag);
    }

    return this.computeValue;
  }

}
/**
 * PropertyReferences represent a property that has been accessed on a root, or
 * another property (or iterable, see below). `some` and `prop` in
 * `{{this.some.prop}}` are each property references, `some` being a property of
 * `this`, and `prop` being a property of `some`. They are constructed by
 * recursively calling `get` on the previous reference as a template chain is
 * followed.
 */


class PropertyReference {
  constructor(parentReference, propertyKey, env) {
    this.parentReference = parentReference;
    this.propertyKey = propertyKey;
    this.env = env;
    this.children = dict();
    this.lastRevision = null;

    if (DEBUG) {
      env.setTemplatePathDebugContext(this, propertyKey, parentReference);
    }

    let valueTag = this.valueTag = createUpdatableTag();
    let parentReferenceTag = parentReference.tag;
    this.tag = combine([parentReferenceTag, valueTag]);
  }

  value() {
    let {
      tag,
      lastRevision,
      lastValue,
      parentReference,
      valueTag,
      propertyKey
    } = this;

    if (lastRevision === null || !validateTag(tag, lastRevision)) {
      let parentValue = parentReference.value();

      if (isDict(parentValue)) {
        let combined = track(() => {
          lastValue = this.env.getPath(parentValue, propertyKey);
        }, DEBUG && this.env.getTemplatePathDebugContext(this));
        updateTag(valueTag, combined);
      } else {
        lastValue = undefined;
      }

      this.lastValue = lastValue;
      this.lastRevision = valueForTag(tag);
    }

    return lastValue;
  }

  get(key) {
    // References should in general be identical to one another, so we can usually
    // deduplicate them in production. However, in DEBUG we need unique references
    // so we can properly key off them for the logging context.
    if (DEBUG) {
      return new PropertyReference(this, key, this.env);
    } else {
      let ref = this.children[key];

      if (ref === undefined) {
        ref = this.children[key] = new PropertyReference(this, key, this.env);
      }

      return ref;
    }
  }

  [UPDATE_REFERENCED_VALUE](value) {
    let {
      parentReference,
      propertyKey
    } = this;
    let parentValue = parentReference.value();
    this.env.setPath(parentValue, propertyKey, value);
  }

} //////////

/**
 * IterationItemReferences represent an individual item in an iterable `each`.
 * They are similar to PropertyReferences, but since iteration items need to be
 * updated they have slightly different behavior. Concretely, they are the
 * `item` in:
 *
 * ```hbs
 * {{#each this.items as |item|}}
 *   {{item.foo}}
 * {{/each}}
 * ```
 *
 * Properties can chain off an iteration item, just like with the other template
 * reference types.
 */


class IterationItemReference {
  constructor(parentReference, itemValue, itemKey, env) {
    this.parentReference = parentReference;
    this.itemValue = itemValue;
    this.env = env;
    this.tag = createUpdatableTag();
    this.children = dict();

    if (DEBUG) {
      env.setTemplatePathDebugContext(this, debugToString(itemKey), parentReference);
    }
  }

  value() {
    return this.itemValue;
  }

  update(value) {
    dirtyTag(this.tag);
    this.itemValue = value;
  }

  get(key) {
    // References should in general be identical to one another, so we can usually
    // deduplicate them in production. However, in DEBUG we need unique references
    // so we can properly key off them for the logging context.
    if (DEBUG) {
      return new PropertyReference(this, key, this.env);
    } else {
      let ref = this.children[key];

      if (ref === undefined) {
        ref = this.children[key] = new PropertyReference(this, key, this.env);
      }

      return ref;
    }
  }

}

const NULL_IDENTITY = {};

const KEY = (_, index) => index;

const INDEX = (_, index) => String(index);

const IDENTITY = item => {
  if (item === null) {
    // Returning null as an identity will cause failures since the iterator
    // can't tell that it's actually supposed to be null
    return NULL_IDENTITY;
  }

  return item;
};

function keyForPath(path, getPath) {
  if (DEBUG && path[0] === '@') {
    throw new Error(`invalid keypath: '${path}', valid keys: @index, @identity, or a path`);
  }

  return uniqueKeyFor(item => getPath(item, path));
}

function makeKeyFor(key, getPath) {
  switch (key) {
    case '@key':
      return uniqueKeyFor(KEY);

    case '@index':
      return uniqueKeyFor(INDEX);

    case '@identity':
      return uniqueKeyFor(IDENTITY);

    default:
      return keyForPath(key, getPath);
  }
}

class WeakMapWithPrimitives {
  get weakMap() {
    if (this._weakMap === undefined) {
      this._weakMap = new WeakMap();
    }

    return this._weakMap;
  }

  get primitiveMap() {
    if (this._primitiveMap === undefined) {
      this._primitiveMap = new Map();
    }

    return this._primitiveMap;
  }

  set(key, value) {
    if (isObject(key) || typeof key === 'function') {
      this.weakMap.set(key, value);
    } else {
      this.primitiveMap.set(key, value);
    }
  }

  get(key) {
    if (isObject(key) || typeof key === 'function') {
      return this.weakMap.get(key);
    } else {
      return this.primitiveMap.get(key);
    }
  }

}

const IDENTITIES = new WeakMapWithPrimitives();

function identityForNthOccurence(value, count) {
  let identities = IDENTITIES.get(value);

  if (identities === undefined) {
    identities = [];
    IDENTITIES.set(value, identities);
  }

  let identity = identities[count];

  if (identity === undefined) {
    identity = {
      value,
      count
    };
    identities[count] = identity;
  }

  return identity;
}
/**
 * When iterating over a list, it's possible that an item with the same unique
 * key could be encountered twice:
 *
 * ```js
 * let arr = ['same', 'different', 'same', 'same'];
 * ```
 *
 * In general, we want to treat these items as _unique within the list_. To do
 * this, we track the occurences of every item as we iterate the list, and when
 * an item occurs more than once, we generate a new unique key just for that
 * item, and that occurence within the list. The next time we iterate the list,
 * and encounter an item for the nth time, we can get the _same_ key, and let
 * Glimmer know that it should reuse the DOM for the previous nth occurence.
 */


function uniqueKeyFor(keyFor) {
  let seen = new WeakMapWithPrimitives();
  return (value, memo) => {
    let key = keyFor(value, memo);
    let count = seen.get(value) || 0;
    seen.set(key, count + 1);

    if (count === 0) {
      return key;
    }

    return identityForNthOccurence(key, count);
  };
}

class IterableImpl {
  constructor(parentRef, key, env) {
    this.parentRef = parentRef;
    this.key = key;
    this.env = env;
    this.tag = parentRef.tag;
  }

  iterate() {
    let {
      parentRef,
      key,
      env
    } = this;
    let iterable = parentRef.value();
    let keyFor = makeKeyFor(key, env.getPath);

    if (Array.isArray(iterable)) {
      return new ArrayIterator(iterable, keyFor);
    }

    let maybeIterator = env.toIterator(iterable);

    if (maybeIterator === null) {
      return new ArrayIterator(EMPTY_ARRAY, () => null);
    }

    return new IteratorWrapper(maybeIterator, keyFor);
  }

  valueReferenceFor(item) {
    let {
      parentRef,
      env
    } = this;
    return new IterationItemReference(parentRef, item.value, item.memo, env);
  }

  updateValueReference(reference, item) {
    reference.update(item.value);
  }

  memoReferenceFor(item) {
    let {
      parentRef,
      env
    } = this;
    return new IterationItemReference(parentRef, item.memo, DEBUG ? `(key: ${debugToString(item.key)}` : '', env);
  }

  updateMemoReference(reference, item) {
    reference.update(item.memo);
  }

}

class IteratorWrapper {
  constructor(inner, keyFor) {
    this.inner = inner;
    this.keyFor = keyFor;
  }

  isEmpty() {
    return this.inner.isEmpty();
  }

  next() {
    let nextValue = this.inner.next();

    if (nextValue !== null) {
      nextValue.key = this.keyFor(nextValue.value, nextValue.memo);
    }

    return nextValue;
  }

}

class ArrayIterator {
  constructor(iterator, keyFor) {
    this.iterator = iterator;
    this.keyFor = keyFor;
    this.pos = 0;

    if (iterator.length === 0) {
      this.current = {
        kind: 'empty'
      };
    } else {
      this.current = {
        kind: 'first',
        value: iterator[this.pos]
      };
    }
  }

  isEmpty() {
    return this.current.kind === 'empty';
  }

  next() {
    let value;
    let current = this.current;

    if (current.kind === 'first') {
      this.current = {
        kind: 'progress'
      };
      value = current.value;
    } else if (this.pos >= this.iterator.length - 1) {
      return null;
    } else {
      value = this.iterator[++this.pos];
    }

    let {
      keyFor
    } = this;
    let key = keyFor(value, this.pos);
    let memo = this.pos;
    return {
      key,
      value,
      memo
    };
  }

}

export { CachedReference, ReferenceCache, isModified, ConstReference, ListItem, END, IterationArtifacts, ReferenceIterator, IteratorSynchronizer, UPDATE_REFERENCED_VALUE, RootReference, ComponentRootReference, HelperRootReference, PropertyReference, IterationItemReference, IterableImpl };