/**
 * @license
 * Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at
 * http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at
 * http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at
 * http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at
 * http://polymer.github.io/PATENTS.txt
 */

import {TemplateInstance} from '../lib/template-instance.js';
import {Template} from '../lib/template.js';
import {directive, NodePart, Part, reparentNodes, TemplateResult} from '../lit-html.js';

type CachedTemplate = {
  instance: TemplateInstance,
  nodes: DocumentFragment
};
const templateCaches =
    new WeakMap<NodePart, WeakMap<Template, CachedTemplate>>();

/**
 * Enables fast switching between multiple templates by caching the DOM nodes
 * and TemplateInstances produced by the templates.
 *
 * Example:
 *
 * ```
 * let checked = false;
 *
 * html`
 *   ${cache(checked ? html`input is checked` : html`input is not checked`)}
 * `
 * ```
 */
export const cache = directive((value: unknown) => (part: Part) => {
  if (!(part instanceof NodePart)) {
    throw new Error('cache can only be used in text bindings');
  }

  let templateCache = templateCaches.get(part);

  if (templateCache === undefined) {
    templateCache = new WeakMap();
    templateCaches.set(part, templateCache);
  }

  const previousValue = part.value;

  // First, can we update the current TemplateInstance, or do we need to move
  // the current nodes into the cache?
  if (previousValue instanceof TemplateInstance) {
    if (value instanceof TemplateResult &&
        previousValue.template === part.options.templateFactory(value)) {
      // Same Template, just trigger an update of the TemplateInstance
      part.setValue(value);
      return;
    } else {
      // Not the same Template, move the nodes from the DOM into the cache.
      let cachedTemplate = templateCache.get(previousValue.template);
      if (cachedTemplate === undefined) {
        cachedTemplate = {
          instance: previousValue,
          nodes: document.createDocumentFragment(),
        };
        templateCache.set(previousValue.template, cachedTemplate);
      }
      reparentNodes(
          cachedTemplate.nodes, part.startNode.nextSibling, part.endNode);
    }
  }

  // Next, can we reuse nodes from the cache?
  if (value instanceof TemplateResult) {
    const template = part.options.templateFactory(value);
    const cachedTemplate = templateCache.get(template);
    if (cachedTemplate !== undefined) {
      // Move nodes out of cache
      part.setValue(cachedTemplate.nodes);
      part.commit();
      // Set the Part value to the TemplateInstance so it'll update it.
      part.value = cachedTemplate.instance;
    }
  }
  part.setValue(value);
});
