/**
 * @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
 */

import {WrappedValue, devModeEqual} from '../change_detection/change_detection';
import {ViewEncapsulation} from '../metadata/view';
import {RendererType2} from '../render/api';
import {looseIdentical, stringify} from '../util';

import {expressionChangedAfterItHasBeenCheckedError} from './errors';
import {BindingDef, BindingFlags, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types';

export const NOOP: any = () => {};

const _tokenKeyCache = new Map<any, string>();

export function tokenKey(token: any): string {
  let key = _tokenKeyCache.get(token);
  if (!key) {
    key = stringify(token) + '_' + _tokenKeyCache.size;
    _tokenKeyCache.set(token, key);
  }
  return key;
}

export function unwrapValue(view: ViewData, nodeIdx: number, bindingIdx: number, value: any): any {
  if (value instanceof WrappedValue) {
    value = value.wrapped;
    let globalBindingIdx = view.def.nodes[nodeIdx].bindingIndex + bindingIdx;
    let oldValue = view.oldValues[globalBindingIdx];
    if (oldValue instanceof WrappedValue) {
      oldValue = oldValue.wrapped;
    }
    view.oldValues[globalBindingIdx] = new WrappedValue(oldValue);
  }
  return value;
}

const UNDEFINED_RENDERER_TYPE_ID = '$$undefined';
const EMPTY_RENDERER_TYPE_ID = '$$empty';

// Attention: this function is called as top level function.
// Putting any logic in here will destroy closure tree shaking!
export function createRendererType2(values: {
  styles: (string | any[])[],
  encapsulation: ViewEncapsulation,
  data: {[kind: string]: any[]}
}): RendererType2 {
  return {
    id: UNDEFINED_RENDERER_TYPE_ID,
    styles: values.styles,
    encapsulation: values.encapsulation,
    data: values.data
  };
}

let _renderCompCount = 0;

export function resolveRendererType2(type?: RendererType2 | null): RendererType2|null {
  if (type && type.id === UNDEFINED_RENDERER_TYPE_ID) {
    // first time we see this RendererType2. Initialize it...
    const isFilled =
        ((type.encapsulation != null && type.encapsulation !== ViewEncapsulation.None) ||
         type.styles.length || Object.keys(type.data).length);
    if (isFilled) {
      type.id = `c${_renderCompCount++}`;
    } else {
      type.id = EMPTY_RENDERER_TYPE_ID;
    }
  }
  if (type && type.id === EMPTY_RENDERER_TYPE_ID) {
    type = null;
  }
  return type || null;
}

export function checkBinding(
    view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean {
  const oldValues = view.oldValues;
  if ((view.state & ViewState.FirstCheck) ||
      !looseIdentical(oldValues[def.bindingIndex + bindingIdx], value)) {
    return true;
  }
  return false;
}

export function checkAndUpdateBinding(
    view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean {
  if (checkBinding(view, def, bindingIdx, value)) {
    view.oldValues[def.bindingIndex + bindingIdx] = value;
    return true;
  }
  return false;
}

export function checkBindingNoChanges(
    view: ViewData, def: NodeDef, bindingIdx: number, value: any) {
  const oldValue = view.oldValues[def.bindingIndex + bindingIdx];
  if ((view.state & ViewState.BeforeFirstCheck) || !devModeEqual(oldValue, value)) {
    throw expressionChangedAfterItHasBeenCheckedError(
        Services.createDebugContext(view, def.index), oldValue, value,
        (view.state & ViewState.BeforeFirstCheck) !== 0);
  }
}

export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}

export function markParentViewsForCheckProjectedViews(view: ViewData, endView: ViewData) {
  let currView: ViewData|null = view;
  while (currView && currView !== endView) {
    currView.state |= ViewState.CheckProjectedViews;
    currView = currView.viewContainerParent || currView.parent;
  }
}

export function dispatchEvent(
    view: ViewData, nodeIndex: number, eventName: string, event: any): boolean {
  const nodeDef = view.def.nodes[nodeIndex];
  const startView =
      nodeDef.flags & NodeFlags.ComponentView ? asElementData(view, nodeIndex).componentView : view;
  markParentViewsForCheck(startView);
  return Services.handleEvent(view, nodeIndex, eventName, event);
}

export function declaredViewContainer(view: ViewData): ElementData|null {
  if (view.parent) {
    const parentView = view.parent;
    return asElementData(parentView, view.parentNodeDef !.index);
  }
  return null;
}

/**
 * for component views, this is the host element.
 * for embedded views, this is the index of the parent node
 * that contains the view container.
 */
export function viewParentEl(view: ViewData): NodeDef|null {
  const parentView = view.parent;
  if (parentView) {
    return view.parentNodeDef !.parent;
  } else {
    return null;
  }
}

export function renderNode(view: ViewData, def: NodeDef): any {
  switch (def.flags & NodeFlags.Types) {
    case NodeFlags.TypeElement:
      return asElementData(view, def.index).renderElement;
    case NodeFlags.TypeText:
      return asTextData(view, def.index).renderText;
  }
}

export function elementEventFullName(target: string | null, name: string): string {
  return target ? `${target}:${name}` : name;
}

export function isComponentView(view: ViewData): boolean {
  return !!view.parent && !!(view.parentNodeDef !.flags & NodeFlags.Component);
}

export function isEmbeddedView(view: ViewData): boolean {
  return !!view.parent && !(view.parentNodeDef !.flags & NodeFlags.Component);
}

export function filterQueryId(queryId: number): number {
  return 1 << (queryId % 32);
}

export function splitMatchedQueriesDsl(
    matchedQueriesDsl: [string | number, QueryValueType][] | null): {
  matchedQueries: {[queryId: string]: QueryValueType},
  references: {[refId: string]: QueryValueType},
  matchedQueryIds: number
} {
  const matchedQueries: {[queryId: string]: QueryValueType} = {};
  let matchedQueryIds = 0;
  const references: {[refId: string]: QueryValueType} = {};
  if (matchedQueriesDsl) {
    matchedQueriesDsl.forEach(([queryId, valueType]) => {
      if (typeof queryId === 'number') {
        matchedQueries[queryId] = valueType;
        matchedQueryIds |= filterQueryId(queryId);
      } else {
        references[queryId] = valueType;
      }
    });
  }
  return {matchedQueries, references, matchedQueryIds};
}

export function getParentRenderElement(view: ViewData, renderHost: any, def: NodeDef): any {
  let renderParent = def.renderParent;
  if (renderParent) {
    if ((renderParent.flags & NodeFlags.TypeElement) === 0 ||
        (renderParent.flags & NodeFlags.ComponentView) === 0 ||
        (renderParent.element !.componentRendererType &&
         renderParent.element !.componentRendererType !.encapsulation ===
             ViewEncapsulation.Native)) {
      // only children of non components, or children of components with native encapsulation should
      // be attached.
      return asElementData(view, def.renderParent !.index).renderElement;
    }
  } else {
    return renderHost;
  }
}

const VIEW_DEFINITION_CACHE = new WeakMap<any, ViewDefinition>();

export function resolveViewDefinition(factory: ViewDefinitionFactory): ViewDefinition {
  let value: ViewDefinition = VIEW_DEFINITION_CACHE.get(factory) !;
  if (!value) {
    value = factory(() => NOOP);
    value.factory = factory;
    VIEW_DEFINITION_CACHE.set(factory, value);
  }
  return value;
}

export function rootRenderNodes(view: ViewData): any[] {
  const renderNodes: any[] = [];
  visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes);
  return renderNodes;
}

export const enum RenderNodeAction {Collect, AppendChild, InsertBefore, RemoveChild}

export function visitRootRenderNodes(
    view: ViewData, action: RenderNodeAction, parentNode: any, nextSibling: any, target?: any[]) {
  // We need to re-compute the parent node in case the nodes have been moved around manually
  if (action === RenderNodeAction.RemoveChild) {
    parentNode = view.renderer.parentNode(renderNode(view, view.def.lastRenderRootNode !));
  }
  visitSiblingRenderNodes(
      view, action, 0, view.def.nodes.length - 1, parentNode, nextSibling, target);
}

export function visitSiblingRenderNodes(
    view: ViewData, action: RenderNodeAction, startIndex: number, endIndex: number, parentNode: any,
    nextSibling: any, target?: any[]) {
  for (let i = startIndex; i <= endIndex; i++) {
    const nodeDef = view.def.nodes[i];
    if (nodeDef.flags & (NodeFlags.TypeElement | NodeFlags.TypeText | NodeFlags.TypeNgContent)) {
      visitRenderNode(view, nodeDef, action, parentNode, nextSibling, target);
    }
    // jump to next sibling
    i += nodeDef.childCount;
  }
}

export function visitProjectedRenderNodes(
    view: ViewData, ngContentIndex: number, action: RenderNodeAction, parentNode: any,
    nextSibling: any, target?: any[]) {
  let compView: ViewData|null = view;
  while (compView && !isComponentView(compView)) {
    compView = compView.parent;
  }
  const hostView = compView !.parent;
  const hostElDef = viewParentEl(compView !);
  const startIndex = hostElDef !.index + 1;
  const endIndex = hostElDef !.index + hostElDef !.childCount;
  for (let i = startIndex; i <= endIndex; i++) {
    const nodeDef = hostView !.def.nodes[i];
    if (nodeDef.ngContentIndex === ngContentIndex) {
      visitRenderNode(hostView !, nodeDef, action, parentNode, nextSibling, target);
    }
    // jump to next sibling
    i += nodeDef.childCount;
  }
  if (!hostView !.parent) {
    // a root view
    const projectedNodes = view.root.projectableNodes[ngContentIndex];
    if (projectedNodes) {
      for (let i = 0; i < projectedNodes.length; i++) {
        execRenderNodeAction(view, projectedNodes[i], action, parentNode, nextSibling, target);
      }
    }
  }
}

function visitRenderNode(
    view: ViewData, nodeDef: NodeDef, action: RenderNodeAction, parentNode: any, nextSibling: any,
    target?: any[]) {
  if (nodeDef.flags & NodeFlags.TypeNgContent) {
    visitProjectedRenderNodes(
        view, nodeDef.ngContent !.index, action, parentNode, nextSibling, target);
  } else {
    const rn = renderNode(view, nodeDef);
    if (action === RenderNodeAction.RemoveChild && (nodeDef.flags & NodeFlags.ComponentView) &&
        (nodeDef.bindingFlags & BindingFlags.CatSyntheticProperty)) {
      // Note: we might need to do both actions.
      if (nodeDef.bindingFlags & (BindingFlags.SyntheticProperty)) {
        execRenderNodeAction(view, rn, action, parentNode, nextSibling, target);
      }
      if (nodeDef.bindingFlags & (BindingFlags.SyntheticHostProperty)) {
        const compView = asElementData(view, nodeDef.index).componentView;
        execRenderNodeAction(compView, rn, action, parentNode, nextSibling, target);
      }
    } else {
      execRenderNodeAction(view, rn, action, parentNode, nextSibling, target);
    }
    if (nodeDef.flags & NodeFlags.EmbeddedViews) {
      const embeddedViews = asElementData(view, nodeDef.index).viewContainer !._embeddedViews;
      for (let k = 0; k < embeddedViews.length; k++) {
        visitRootRenderNodes(embeddedViews[k], action, parentNode, nextSibling, target);
      }
    }
    if (nodeDef.flags & NodeFlags.TypeElement && !nodeDef.element !.name) {
      visitSiblingRenderNodes(
          view, action, nodeDef.index + 1, nodeDef.index + nodeDef.childCount, parentNode,
          nextSibling, target);
    }
  }
}

function execRenderNodeAction(
    view: ViewData, renderNode: any, action: RenderNodeAction, parentNode: any, nextSibling: any,
    target?: any[]) {
  const renderer = view.renderer;
  switch (action) {
    case RenderNodeAction.AppendChild:
      renderer.appendChild(parentNode, renderNode);
      break;
    case RenderNodeAction.InsertBefore:
      renderer.insertBefore(parentNode, renderNode, nextSibling);
      break;
    case RenderNodeAction.RemoveChild:
      renderer.removeChild(parentNode, renderNode);
      break;
    case RenderNodeAction.Collect:
      target !.push(renderNode);
      break;
  }
}

const NS_PREFIX_RE = /^:([^:]+):(.+)$/;

export function splitNamespace(name: string): string[] {
  if (name[0] === ':') {
    const match = name.match(NS_PREFIX_RE) !;
    return [match[1], match[2]];
  }
  return ['', name];
}

export function calcBindingFlags(bindings: BindingDef[]): BindingFlags {
  let flags = 0;
  for (let i = 0; i < bindings.length; i++) {
    flags |= bindings[i].flags;
  }
  return flags;
}

export function interpolate(valueCount: number, constAndInterp: string[]): string {
  let result = '';
  for (let i = 0; i < valueCount * 2; i = i + 2) {
    result = result + constAndInterp[i] + _toStringWithNull(constAndInterp[i + 1]);
  }
  return result + constAndInterp[valueCount * 2];
}

export function inlineInterpolate(
    valueCount: number, c0: string, a1: any, c1: string, a2?: any, c2?: string, a3?: any,
    c3?: string, a4?: any, c4?: string, a5?: any, c5?: string, a6?: any, c6?: string, a7?: any,
    c7?: string, a8?: any, c8?: string, a9?: any, c9?: string): string {
  switch (valueCount) {
    case 1:
      return c0 + _toStringWithNull(a1) + c1;
    case 2:
      return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2;
    case 3:
      return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) +
          c3;
    case 4:
      return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) +
          c3 + _toStringWithNull(a4) + c4;
    case 5:
      return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) +
          c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5;
    case 6:
      return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) +
          c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5 + _toStringWithNull(a6) + c6;
    case 7:
      return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) +
          c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5 + _toStringWithNull(a6) +
          c6 + _toStringWithNull(a7) + c7;
    case 8:
      return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) +
          c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5 + _toStringWithNull(a6) +
          c6 + _toStringWithNull(a7) + c7 + _toStringWithNull(a8) + c8;
    case 9:
      return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) +
          c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5 + _toStringWithNull(a6) +
          c6 + _toStringWithNull(a7) + c7 + _toStringWithNull(a8) + c8 + _toStringWithNull(a9) + c9;
    default:
      throw new Error(`Does not support more than 9 expressions`);
  }
}

function _toStringWithNull(v: any): string {
  return v != null ? v.toString() : '';
}

export const EMPTY_ARRAY: any[] = [];
export const EMPTY_MAP: {[key: string]: any} = {};
