/**
 * @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 {ElementRef} from '../linker/element_ref';
import {QueryList} from '../linker/query_list';
import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref';

import {createTemplateRef, createViewContainerRef} from './refs';
import {NodeDef, NodeFlags, NodeType, QueryBindingDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, asElementData, asProviderData, asQueryList} from './types';
import {declaredViewContainer, filterQueryId, isEmbeddedView, viewParentEl} from './util';

export function queryDef(
    flags: NodeFlags, id: number, bindings: {[propName: string]: QueryBindingType}): NodeDef {
  let bindingDefs: QueryBindingDef[] = [];
  for (let propName in bindings) {
    const bindingType = bindings[propName];
    bindingDefs.push({propName, bindingType});
  }

  return {
    type: NodeType.Query,
    // will bet set by the view definition
    index: undefined,
    reverseChildIndex: undefined,
    parent: undefined,
    renderParent: undefined,
    bindingIndex: undefined,
    outputIndex: undefined,
    // regular values
    flags,
    childFlags: 0,
    childMatchedQueries: 0,
    ngContentIndex: undefined,
    matchedQueries: {},
    matchedQueryIds: 0,
    references: {},
    childCount: 0,
    bindings: [],
    outputs: [],
    element: undefined,
    provider: undefined,
    text: undefined,
    pureExpression: undefined,
    query: {id, filterId: filterQueryId(id), bindings: bindingDefs},
    ngContent: undefined
  };
}

export function createQuery(): QueryList<any> {
  return new QueryList();
}

export function dirtyParentQueries(view: ViewData) {
  const queryIds = view.def.nodeMatchedQueries;
  while (view.parent && isEmbeddedView(view)) {
    let tplDef = view.parentNodeDef;
    view = view.parent;
    // content queries
    const end = tplDef.index + tplDef.childCount;
    for (let i = 0; i <= end; i++) {
      const nodeDef = view.def.nodes[i];
      if ((nodeDef.flags & NodeFlags.HasContentQuery) &&
          (nodeDef.flags & NodeFlags.HasDynamicQuery) &&
          (nodeDef.query.filterId & queryIds) === nodeDef.query.filterId) {
        asQueryList(view, i).setDirty();
      }
      if ((nodeDef.type === NodeType.Element && i + nodeDef.childCount < tplDef.index) ||
          !(nodeDef.childFlags & NodeFlags.HasContentQuery) ||
          !(nodeDef.childFlags & NodeFlags.HasDynamicQuery)) {
        // skip elements that don't contain the template element or no query.
        i += nodeDef.childCount;
      }
    }
  }

  // view queries
  if (view.def.nodeFlags & NodeFlags.HasViewQuery) {
    for (let i = 0; i < view.def.nodes.length; i++) {
      const nodeDef = view.def.nodes[i];
      if ((nodeDef.flags & NodeFlags.HasViewQuery) && (nodeDef.flags & NodeFlags.HasDynamicQuery)) {
        asQueryList(view, i).setDirty();
      }
      // only visit the root nodes
      i += nodeDef.childCount;
    }
  }
}

export function checkAndUpdateQuery(view: ViewData, nodeDef: NodeDef) {
  const queryList = asQueryList(view, nodeDef.index);
  if (!queryList.dirty) {
    return;
  }
  let directiveInstance: any;
  let newValues: any[];
  if (nodeDef.flags & NodeFlags.HasContentQuery) {
    const elementDef = nodeDef.parent.parent;
    newValues = calcQueryValues(
        view, elementDef.index, elementDef.index + elementDef.childCount, nodeDef.query, []);
    directiveInstance = asProviderData(view, nodeDef.parent.index).instance;
  } else if (nodeDef.flags & NodeFlags.HasViewQuery) {
    newValues = calcQueryValues(view, 0, view.def.nodes.length - 1, nodeDef.query, []);
    directiveInstance = view.component;
  }
  queryList.reset(newValues);
  const bindings = nodeDef.query.bindings;
  let notify = false;
  for (let i = 0; i < bindings.length; i++) {
    const binding = bindings[i];
    let boundValue: any;
    switch (binding.bindingType) {
      case QueryBindingType.First:
        boundValue = queryList.first;
        break;
      case QueryBindingType.All:
        boundValue = queryList;
        notify = true;
        break;
    }
    directiveInstance[binding.propName] = boundValue;
  }
  if (notify) {
    queryList.notifyOnChanges();
  }
}

function calcQueryValues(
    view: ViewData, startIndex: number, endIndex: number, queryDef: QueryDef,
    values: any[]): any[] {
  for (let i = startIndex; i <= endIndex; i++) {
    const nodeDef = view.def.nodes[i];
    const valueType = nodeDef.matchedQueries[queryDef.id];
    if (valueType != null) {
      values.push(getQueryValue(view, nodeDef, valueType));
    }
    if (nodeDef.type === NodeType.Element && nodeDef.element.template &&
        (nodeDef.element.template.nodeMatchedQueries & queryDef.filterId) === queryDef.filterId) {
      // check embedded views that were attached at the place of their template.
      const elementData = asElementData(view, i);
      const embeddedViews = elementData.embeddedViews;
      if (embeddedViews) {
        for (let k = 0; k < embeddedViews.length; k++) {
          const embeddedView = embeddedViews[k];
          const dvc = declaredViewContainer(embeddedView);
          if (dvc && dvc === elementData) {
            calcQueryValues(embeddedView, 0, embeddedView.def.nodes.length - 1, queryDef, values);
          }
        }
      }
      const projectedViews = elementData.projectedViews;
      if (projectedViews) {
        for (let k = 0; k < projectedViews.length; k++) {
          const projectedView = projectedViews[k];
          calcQueryValues(projectedView, 0, projectedView.def.nodes.length - 1, queryDef, values);
        }
      }
    }
    if ((nodeDef.childMatchedQueries & queryDef.filterId) !== queryDef.filterId) {
      // if no child matches the query, skip the children.
      i += nodeDef.childCount;
    }
  }
  return values;
}

export function getQueryValue(
    view: ViewData, nodeDef: NodeDef, queryValueType: QueryValueType): any {
  if (queryValueType != null) {
    // a match
    let value: any;
    switch (queryValueType) {
      case QueryValueType.RenderElement:
        value = asElementData(view, nodeDef.index).renderElement;
        break;
      case QueryValueType.ElementRef:
        value = new ElementRef(asElementData(view, nodeDef.index).renderElement);
        break;
      case QueryValueType.TemplateRef:
        value = createTemplateRef(view, nodeDef);
        break;
      case QueryValueType.ViewContainerRef:
        value = createViewContainerRef(view, nodeDef);
        break;
      case QueryValueType.Provider:
        value = asProviderData(view, nodeDef.index).instance;
        break;
    }
    return value;
  }
}