/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

const SVG_PREFIX = ':svg:';

// Element | Node interfaces
// see https://developer.mozilla.org/en-US/docs/Web/API/Element
// see https://developer.mozilla.org/en-US/docs/Web/API/Node
const ELEMENT_IF = '[Element]';
// HTMLElement interface
// see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
const HTMLELEMENT_IF = '[HTMLElement]';

const HTMLELEMENT_TAGS =
    'abbr,address,article,aside,b,bdi,bdo,cite,code,dd,dfn,dt,em,figcaption,figure,footer,header,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr';

const ALL_HTML_TAGS =
    // https://www.w3.org/TR/html5/index.html
    'a,abbr,address,area,article,aside,audio,b,base,bdi,bdo,blockquote,body,br,button,canvas,caption,cite,code,col,colgroup,data,datalist,dd,del,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hr,html,i,iframe,img,input,ins,kbd,keygen,label,legend,li,link,main,map,mark,meta,meter,nav,noscript,object,ol,optgroup,option,output,p,param,pre,progress,q,rb,rp,rt,rtc,ruby,s,samp,script,section,select,small,source,span,strong,style,sub,sup,table,tbody,td,template,textarea,tfoot,th,thead,time,title,tr,track,u,ul,var,video,wbr,' +
    // https://html.spec.whatwg.org/
    'details,summary,menu,menuitem';

// Elements missing from Chrome (HtmlUnknownElement), to be manually added
const MISSING_FROM_CHROME: {[el: string]: string[]} = {
  'data^[HTMLElement]': ['value'],
  'keygen^[HTMLElement]': ['!autofocus', 'challenge', '!disabled', 'form', 'keytype', 'name'],
  // TODO(vicb): Figure out why Chrome and WhatWG do not agree on the props
  // 'menu^[HTMLElement]': ['type', 'label'],
  'menuitem^[HTMLElement]':
      ['type', 'label', 'icon', '!disabled', '!checked', 'radiogroup', '!default'],
  'summary^[HTMLElement]': [],
  'time^[HTMLElement]': ['dateTime'],
  ':svg:cursor^:svg:': [],
};

const _G: any = typeof window != 'undefined' && window || typeof global != 'undefined' && global ||
    typeof self != 'undefined' && self;

const document: any = typeof _G['document'] == 'object' ? _G['document'] : null;

export function extractSchema(): Map<string, string[]>|null {
  if (!document) return null;
  const SVGGraphicsElement = _G['SVGGraphicsElement'];
  if (!SVGGraphicsElement) return null;

  const element = document.createElement('video');
  const descMap: Map<string, string[]> = new Map();
  const visited: {[name: string]: boolean} = {};

  // HTML top level
  extractProperties(Node, element, visited, descMap, ELEMENT_IF, '');
  extractProperties(Element, element, visited, descMap, ELEMENT_IF, '');
  extractProperties(HTMLElement, element, visited, descMap, HTMLELEMENT_IF, ELEMENT_IF);
  extractProperties(HTMLElement, element, visited, descMap, HTMLELEMENT_TAGS, HTMLELEMENT_IF);
  extractProperties(HTMLMediaElement, element, visited, descMap, 'media', HTMLELEMENT_IF);

  // SVG top level
  const svgAnimation = document.createElementNS('http://www.w3.org/2000/svg', 'set');
  const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  const svgFeFuncA = document.createElementNS('http://www.w3.org/2000/svg', 'feFuncA');
  const svgGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
  const svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text');

  const SVGAnimationElement = _G['SVGAnimationElement'];
  const SVGGeometryElement = _G['SVGGeometryElement'];
  const SVGComponentTransferFunctionElement = _G['SVGComponentTransferFunctionElement'];
  const SVGGradientElement = _G['SVGGradientElement'];
  const SVGTextContentElement = _G['SVGTextContentElement'];
  const SVGTextPositioningElement = _G['SVGTextPositioningElement'];

  extractProperties(SVGElement, svgText, visited, descMap, SVG_PREFIX, HTMLELEMENT_IF);
  extractProperties(
      SVGGraphicsElement, svgText, visited, descMap, SVG_PREFIX + 'graphics', SVG_PREFIX);
  extractProperties(
      SVGAnimationElement, svgAnimation, visited, descMap, SVG_PREFIX + 'animation', SVG_PREFIX);
  extractProperties(
      SVGGeometryElement, svgPath, visited, descMap, SVG_PREFIX + 'geometry', SVG_PREFIX);
  extractProperties(
      SVGComponentTransferFunctionElement, svgFeFuncA, visited, descMap,
      SVG_PREFIX + 'componentTransferFunction', SVG_PREFIX);
  extractProperties(
      SVGGradientElement, svgGradient, visited, descMap, SVG_PREFIX + 'gradient', SVG_PREFIX);
  extractProperties(
      SVGTextContentElement, svgText, visited, descMap, SVG_PREFIX + 'textContent',
      SVG_PREFIX + 'graphics');
  extractProperties(
      SVGTextPositioningElement, svgText, visited, descMap, SVG_PREFIX + 'textPositioning',
      SVG_PREFIX + 'textContent');

  // Get all element types
  const types = Object.getOwnPropertyNames(window).filter(k => /^(HTML|SVG).*?Element$/.test(k));

  types.sort();

  types.forEach(type => { extractRecursiveProperties(visited, descMap, (window as any)[type]); });

  // Add elements missed by Chrome auto-detection
  Object.keys(MISSING_FROM_CHROME).forEach(elHierarchy => {
    descMap.set(elHierarchy, MISSING_FROM_CHROME[elHierarchy]);
  });

  assertNoMissingTags(descMap);

  return descMap;
}

function assertNoMissingTags(descMap: Map<string, string[]>): void {
  const extractedTags: string[] = [];

  Array.from(descMap.keys()).forEach((key: string) => {
    extractedTags.push(...key.split('|')[0].split('^')[0].split(','));
  });

  const missingTags = ALL_HTML_TAGS.split(',').filter(tag => extractedTags.indexOf(tag) == -1);

  if (missingTags.length) {
    throw new Error(`DOM schema misses tags: ${missingTags.join(',')}`);
  }
}

function extractRecursiveProperties(
    visited: {[name: string]: boolean}, descMap: Map<string, string[]>, type: Function): string {
  const name = extractName(type) !;

  if (visited[name]) {
    return name;
  }

  let superName: string;
  switch (name) {
    case ELEMENT_IF:
      // ELEMENT_IF is the top most interface (Element | Node)
      superName = '';
      break;
    case HTMLELEMENT_IF:
      superName = ELEMENT_IF;
      break;
    default:
      superName =
          extractRecursiveProperties(visited, descMap, type.prototype.__proto__.constructor);
  }

  let instance: HTMLElement|null = null;
  name.split(',').forEach(tagName => {
    instance = type['name'].startsWith('SVG') ?
        document.createElementNS('http://www.w3.org/2000/svg', tagName.replace(SVG_PREFIX, '')) :
        document.createElement(tagName);

    let htmlType: Function;

    switch (tagName) {
      case 'cite':
        // <cite> interface is `HTMLQuoteElement`
        htmlType = HTMLElement;
        break;
      default:
        htmlType = type;
    }

    if (!(instance instanceof htmlType)) {
      throw new Error(`Tag <${tagName}> is not an instance of ${htmlType['name']}`);
    }
  });

  extractProperties(type, instance, visited, descMap, name, superName);

  return name;
}

function extractProperties(
    type: Function, instance: any, visited: {[name: string]: boolean},
    descMap: Map<string, string[]>, name: string, superName: string) {
  if (!type) return;

  visited[name] = true;

  const fullName = name + (superName ? '^' + superName : '');

  const props: string[] = descMap.has(fullName) ? descMap.get(fullName) ! : [];

  const prototype = type.prototype;
  const keys = Object.getOwnPropertyNames(prototype);

  keys.sort();
  keys.forEach((name) => {
    if (name.startsWith('on')) {
      props.push('*' + name.substr(2));
    } else {
      const typeCh = _TYPE_MNEMONICS[typeof instance[name]];
      const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
      const isSetter = descriptor && descriptor.set;
      if (typeCh !== void 0 && !name.startsWith('webkit') && isSetter) {
        props.push(typeCh + name);
      }
    }
  });

  // There is no point in using `Node.nodeValue`, filter it out
  descMap.set(fullName, type === Node ? props.filter(p => p != '%nodeValue') : props);
}

function extractName(type: Function): string|null {
  let name = type['name'];

  // The polyfill @webcomponents/custom-element/src/native-shim.js overrides the
  // window.HTMLElement and does not have the name property. Check if this is the
  // case and if so, set the name manually.
  if (name === '' && type === HTMLElement) {
    name = 'HTMLElement';
  }

  switch (name) {
    // see https://www.w3.org/TR/html5/index.html
    // TODO(vicb): generate this map from all the element types
    case 'Element':
      return ELEMENT_IF;
    case 'HTMLElement':
      return HTMLELEMENT_IF;
    case 'HTMLImageElement':
      return 'img';
    case 'HTMLAnchorElement':
      return 'a';
    case 'HTMLDListElement':
      return 'dl';
    case 'HTMLDirectoryElement':
      return 'dir';
    case 'HTMLHeadingElement':
      return 'h1,h2,h3,h4,h5,h6';
    case 'HTMLModElement':
      return 'ins,del';
    case 'HTMLOListElement':
      return 'ol';
    case 'HTMLParagraphElement':
      return 'p';
    case 'HTMLQuoteElement':
      return 'q,blockquote,cite';
    case 'HTMLTableCaptionElement':
      return 'caption';
    case 'HTMLTableCellElement':
      return 'th,td';
    case 'HTMLTableColElement':
      return 'col,colgroup';
    case 'HTMLTableRowElement':
      return 'tr';
    case 'HTMLTableSectionElement':
      return 'tfoot,thead,tbody';
    case 'HTMLUListElement':
      return 'ul';
    case 'SVGGraphicsElement':
      return SVG_PREFIX + 'graphics';
    case 'SVGMPathElement':
      return SVG_PREFIX + 'mpath';
    case 'SVGSVGElement':
      return SVG_PREFIX + 'svg';
    case 'SVGTSpanElement':
      return SVG_PREFIX + 'tspan';
    default:
      const isSVG = name.startsWith('SVG');
      if (name.startsWith('HTML') || isSVG) {
        name = name.replace('HTML', '').replace('SVG', '').replace('Element', '');
        if (isSVG && name.startsWith('FE')) {
          name = 'fe' + name.substring(2);
        } else if (name) {
          name = name.charAt(0).toLowerCase() + name.substring(1);
        }
        return isSVG ? SVG_PREFIX + name : name.toLowerCase();
      }
  }

  return null;
}

const _TYPE_MNEMONICS: {[type: string]: string} = {
  'string': '',
  'number': '#',
  'boolean': '!',
  'object': '%',
};