/**
 * @license
 * Copyright (c) 2017 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 {removeNodes} from './dom.js';
import {insertNodeIntoTemplate, removeNodesFromTemplate} from './modify-template.js';
import {templateInstances} from './render.js';
import {templateCaches} from './template-factory.js';
import {TemplateInstance} from './template-instance.js';
import {TemplateResult} from './template-result.js';
import {Template} from './template.js';

export {html, svg, TemplateResult} from '../lit-html.js';

declare global {
  interface Window {
    ShadyCSS: any;
  }
  class ShadowRoot {}
}

// Get a key to lookup in `templateCaches`.
const getTemplateCacheKey = (type: string, scopeName: string) =>
    `${type}--${scopeName}`;

let compatibleShadyCSSVersion = true;

if (typeof window.ShadyCSS === 'undefined') {
  compatibleShadyCSSVersion = false;
} else if (typeof window.ShadyCSS.prepareTemplateDom === 'undefined') {
  console.warn(
    `Incompatible ShadyCSS version detected.` +
    `Please update to at least @webcomponents/webcomponentsjs@2.0.2 and` +
    `@webcomponents/shadycss@1.3.1.`);
  compatibleShadyCSSVersion = false;
}

/**
 * Template factory which scopes template DOM using ShadyCSS.
 * @param scopeName {string}
 */
const shadyTemplateFactory = (scopeName: string) =>
    (result: TemplateResult) => {
      const cacheKey = getTemplateCacheKey(result.type, scopeName);
      let templateCache = templateCaches.get(cacheKey);
      if (templateCache === undefined) {
        templateCache = new Map<TemplateStringsArray, Template>();
        templateCaches.set(cacheKey, templateCache);
      }
      let template = templateCache.get(result.strings);
      if (template === undefined) {
        const element = result.getTemplateElement();
        if (compatibleShadyCSSVersion) {
          window.ShadyCSS.prepareTemplateDom(element, scopeName);
        }
        template = new Template(result, element);
        templateCache.set(result.strings, template);
      }
      return template;
    };


const TEMPLATE_TYPES = ['html', 'svg'];

/**
 * Removes all style elements from Templates for the given scopeName.
 */
function removeStylesFromLitTemplates(scopeName: string) {
  TEMPLATE_TYPES.forEach((type) => {
    const templates = templateCaches.get(getTemplateCacheKey(type, scopeName));
    if (templates !== undefined) {
      templates.forEach((template) => {
        const {element: {content}} = template;
        // IE 11 doesn't support the iterable param Set constructor
        const styles = new Set<Element>();
        Array.from(content.querySelectorAll('style')).forEach((s: Element) => {
          styles.add(s);
        });
        removeNodesFromTemplate(template, styles);
      });
    }
  });
}

const shadyRenderSet = new Set<string>();

/**
 * For the given scope name, ensures that ShadyCSS style scoping is performed.
 * This is done just once per scope name so the fragment and template cannot
 * be modified.
 * (1) extracts styles from the rendered fragment and hands them to ShadyCSS
 * to be scoped and appended to the document
 * (2) removes style elements from all lit-html Templates for this scope name.
 *
 * Note, <style> elements can only be placed into templates for the
 * initial rendering of the scope. If <style> elements are included in templates
 * dynamically rendered to the scope (after the first scope render), they will
 * not be scoped and the <style> will be left in the template and rendered
 * output.
 */
const ensureStylesScoped =
    (fragment: DocumentFragment, template: Template, scopeName: string) => {
      // only scope element template once per scope name
      if (!shadyRenderSet.has(scopeName)) {
        shadyRenderSet.add(scopeName);
        const styleTemplate = document.createElement('template');
        Array.from(fragment.querySelectorAll('style')).forEach((s: Element) => {
          styleTemplate.content.appendChild(s);
        });
        window.ShadyCSS.prepareTemplateStyles(styleTemplate, scopeName);
        // Fix templates: note the expectation here is that the given `fragment`
        // has been generated from the given `template` which contains
        // the set of templates rendered into this scope.
        // It is only from this set of initial templates from which styles
        // will be scoped and removed.
        removeStylesFromLitTemplates(scopeName);
        // ApplyShim case
        if (window.ShadyCSS.nativeShadow) {
          const style = styleTemplate.content.querySelector('style');
          if (style !== null) {
            // Insert style into rendered fragment
            fragment.insertBefore(style, fragment.firstChild);
            // Insert into lit-template (for subsequent renders)
            insertNodeIntoTemplate(
                template,
                style.cloneNode(true),
                template.element.content.firstChild);
          }
        }
      }
    };

// NOTE: We're copying code from lit-html's `render` method here.
// We're doing this explicitly because the API for rendering templates is likely
// to change in the near term.
export function render(
    result: TemplateResult,
    container: Element|DocumentFragment,
    scopeName: string) {
  const templateFactory = shadyTemplateFactory(scopeName);
  const template = templateFactory(result);

  let instance = templateInstances.get(container);

  // Repeat render, just call update()
  if (instance !== undefined && instance.template === template &&
      instance.processor === result.processor) {
    instance.update(result.values);
    return;
  }

  // First render, create a new TemplateInstance and append it
  instance = new TemplateInstance(template, result.processor, templateFactory);
  templateInstances.set(container, instance);

  const fragment = instance._clone();
  instance.update(result.values);

  // If there's a shadow host, do ShadyCSS scoping...
  if (container instanceof ShadowRoot && compatibleShadyCSSVersion) {
    ensureStylesScoped(fragment, template, scopeName);
    window.ShadyCSS.styleElement(container.host);
  }

  removeNodes(container, container.firstChild);
  container.appendChild(fragment);
}
