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

// We are temporarily importing the existing viewEngine_from core so we can be sure we are
// correctly implementing its interfaces for backwards compatibility.

import {Type} from '../interface/type';
import {ElementRef as ViewEngine_ElementRef} from '../linker/element_ref';
import {QueryList} from '../linker/query_list';
import {TemplateRef as ViewEngine_TemplateRef} from '../linker/template_ref';
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';

import {assertPreviousIsParent} from './assert';
import {getNodeInjectable, locateDirectiveOrProvider} from './di';
import {NG_ELEMENT_ID} from './fields';
import {load, store, storeCleanupWithContext} from './instructions';
import {unusedValueExportToPlacateAjd as unused1} from './interfaces/definition';
import {unusedValueExportToPlacateAjd as unused2} from './interfaces/injector';
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, unusedValueExportToPlacateAjd as unused3} from './interfaces/node';
import {LQueries, unusedValueExportToPlacateAjd as unused4} from './interfaces/query';
import {CONTENT_QUERIES, HEADER_OFFSET, LView, QUERIES, TVIEW} from './interfaces/view';
import {getCurrentQueryIndex, getIsParent, getLView, setCurrentQueryIndex} from './state';
import {createElementRef, createTemplateRef} from './view_engine_compatibility';

const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4;

/**
 * A predicate which determines if a given element/directive should be included in the query
 * results.
 */
export interface QueryPredicate<T> {
  /**
   * If looking for directives then it contains the directive type.
   */
  type: Type<T>|null;

  /**
   * If selector then contains local names to query for.
   */
  selector: string[]|null;

  /**
   * Indicates which token should be read from DI for this query.
   */
  read: Type<T>|null;
}

/**
 * An object representing a query, which is a combination of:
 * - query predicate to determines if a given element/directive should be included in the query
 * - values collected based on a predicate
 * - `QueryList` to which collected values should be reported
 */
export interface LQuery<T> {
  /**
   * Next query. Used when queries are stored as a linked list in `LQueries`.
   */
  next: LQuery<any>|null;

  /**
   * Destination to which the value should be added.
   */
  list: QueryList<T>;

  /**
   * A predicate which determines if a given element/directive should be included in the query
   * results.
   */
  predicate: QueryPredicate<T>;

  /**
   * Values which have been located.
   *
   * This is what builds up the `QueryList._valuesTree`.
   */
  values: any[];

  /**
   * A pointer to an array that stores collected values from views. This is necessary so we know a
   * container into which to insert nodes collected from views.
   */
  containerValues: any[]|null;
}

export class LQueries_ implements LQueries {
  constructor(
      public parent: LQueries_|null, private shallow: LQuery<any>|null,
      private deep: LQuery<any>|null) {}

  track<T>(queryList: QueryList<T>, predicate: Type<T>|string[], descend?: boolean, read?: Type<T>):
      void {
    if (descend) {
      this.deep = createQuery(this.deep, queryList, predicate, read != null ? read : null);
    } else {
      this.shallow = createQuery(this.shallow, queryList, predicate, read != null ? read : null);
    }
  }

  clone(): LQueries { return new LQueries_(this, null, this.deep); }

  container(): LQueries|null {
    const shallowResults = copyQueriesToContainer(this.shallow);
    const deepResults = copyQueriesToContainer(this.deep);
    return shallowResults || deepResults ? new LQueries_(this, shallowResults, deepResults) : null;
  }

  createView(): LQueries|null {
    const shallowResults = copyQueriesToView(this.shallow);
    const deepResults = copyQueriesToView(this.deep);

    return shallowResults || deepResults ? new LQueries_(this, shallowResults, deepResults) : null;
  }

  insertView(index: number): void {
    insertView(index, this.shallow);
    insertView(index, this.deep);
  }

  addNode(tNode: TElementNode|TContainerNode|TElementContainerNode): void {
    add(this.deep, tNode);
    add(this.shallow, tNode);
  }

  removeView(): void {
    removeView(this.shallow);
    removeView(this.deep);
  }
}

function copyQueriesToContainer(query: LQuery<any>| null): LQuery<any>|null {
  let result: LQuery<any>|null = null;

  while (query) {
    const containerValues: any[] = [];  // prepare room for views
    query.values.push(containerValues);
    const clonedQuery: LQuery<any> = {
      next: result,
      list: query.list,
      predicate: query.predicate,
      values: containerValues,
      containerValues: null
    };
    result = clonedQuery;
    query = query.next;
  }

  return result;
}

function copyQueriesToView(query: LQuery<any>| null): LQuery<any>|null {
  let result: LQuery<any>|null = null;

  while (query) {
    const clonedQuery: LQuery<any> = {
      next: result,
      list: query.list,
      predicate: query.predicate,
      values: [],
      containerValues: query.values
    };
    result = clonedQuery;
    query = query.next;
  }

  return result;
}

function insertView(index: number, query: LQuery<any>| null) {
  while (query) {
    ngDevMode && assertViewQueryhasPointerToDeclarationContainer(query);
    query.containerValues !.splice(index, 0, query.values);

    // mark a query as dirty only when inserted view had matching modes
    if (query.values.length) {
      query.list.setDirty();
    }

    query = query.next;
  }
}

function removeView(query: LQuery<any>| null) {
  while (query) {
    ngDevMode && assertViewQueryhasPointerToDeclarationContainer(query);

    const containerValues = query.containerValues !;
    const viewValuesIdx = containerValues.indexOf(query.values);
    const removed = containerValues.splice(viewValuesIdx, 1);

    // mark a query as dirty only when removed view had matching modes
    ngDevMode && assertEqual(removed.length, 1, 'removed.length');
    if (removed[0].length) {
      query.list.setDirty();
    }

    query = query.next;
  }
}

function assertViewQueryhasPointerToDeclarationContainer(query: LQuery<any>) {
  assertDefined(query.containerValues, 'View queries need to have a pointer to container values.');
}

/**
 * Iterates over local names for a given node and returns directive index
 * (or -1 if a local name points to an element).
 *
 * @param tNode static data of a node to check
 * @param selector selector to match
 * @returns directive index, -1 or null if a selector didn't match any of the local names
 */
function getIdxOfMatchingSelector(tNode: TNode, selector: string): number|null {
  const localNames = tNode.localNames;
  if (localNames) {
    for (let i = 0; i < localNames.length; i += 2) {
      if (localNames[i] === selector) {
        return localNames[i + 1] as number;
      }
    }
  }
  return null;
}


// TODO: "read" should be an AbstractType (FW-486)
function queryByReadToken(read: any, tNode: TNode, currentView: LView): any {
  const factoryFn = (read as any)[NG_ELEMENT_ID];
  if (typeof factoryFn === 'function') {
    return factoryFn();
  } else {
    const matchingIdx =
        locateDirectiveOrProvider(tNode, currentView, read as Type<any>, false, false);
    if (matchingIdx !== null) {
      return getNodeInjectable(
          currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode);
    }
  }
  return null;
}

function queryByTNodeType(tNode: TNode, currentView: LView): any {
  if (tNode.type === TNodeType.Element || tNode.type === TNodeType.ElementContainer) {
    return createElementRef(ViewEngine_ElementRef, tNode, currentView);
  }
  if (tNode.type === TNodeType.Container) {
    return createTemplateRef(ViewEngine_TemplateRef, ViewEngine_ElementRef, tNode, currentView);
  }
  return null;
}

function queryByTemplateRef(
    templateRefToken: ViewEngine_TemplateRef<any>, tNode: TNode, currentView: LView,
    read: any): any {
  const templateRefResult = (templateRefToken as any)[NG_ELEMENT_ID]();
  if (read) {
    return templateRefResult ? queryByReadToken(read, tNode, currentView) : null;
  }
  return templateRefResult;
}

function queryRead(tNode: TNode, currentView: LView, read: any, matchingIdx: number): any {
  if (read) {
    return queryByReadToken(read, tNode, currentView);
  }
  if (matchingIdx > -1) {
    return getNodeInjectable(
        currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode);
  }
  // if read token and / or strategy is not specified,
  // detect it using appropriate tNode type
  return queryByTNodeType(tNode, currentView);
}

function add(
    query: LQuery<any>| null, tNode: TElementNode | TContainerNode | TElementContainerNode) {
  const currentView = getLView();

  while (query) {
    const predicate = query.predicate;
    const type = predicate.type as any;
    if (type) {
      let result = null;
      if (type === ViewEngine_TemplateRef) {
        result = queryByTemplateRef(type, tNode, currentView, predicate.read);
      } else {
        const matchingIdx = locateDirectiveOrProvider(tNode, currentView, type, false, false);
        if (matchingIdx !== null) {
          result = queryRead(tNode, currentView, predicate.read, matchingIdx);
        }
      }
      if (result !== null) {
        addMatch(query, result);
      }
    } else {
      const selector = predicate.selector !;
      for (let i = 0; i < selector.length; i++) {
        const matchingIdx = getIdxOfMatchingSelector(tNode, selector[i]);
        if (matchingIdx !== null) {
          const result = queryRead(tNode, currentView, predicate.read, matchingIdx);
          if (result !== null) {
            addMatch(query, result);
          }
        }
      }
    }
    query = query.next;
  }
}

function addMatch(query: LQuery<any>, matchingValue: any): void {
  query.values.push(matchingValue);
  query.list.setDirty();
}

function createPredicate<T>(predicate: Type<T>| string[], read: Type<T>| null): QueryPredicate<T> {
  const isArray = Array.isArray(predicate);
  return {
    type: isArray ? null : predicate as Type<T>,
    selector: isArray ? predicate as string[] : null,
    read: read
  };
}

function createQuery<T>(
    previous: LQuery<any>| null, queryList: QueryList<T>, predicate: Type<T>| string[],
    read: Type<T>| null): LQuery<T> {
  return {
    next: previous,
    list: queryList,
    predicate: createPredicate(predicate, read),
    values: (queryList as any as QueryList_<T>)._valuesTree,
    containerValues: null
  };
}

type QueryList_<T> = QueryList<T>& {_valuesTree: any[]};

/**
 * Creates and returns a QueryList.
 *
 * @param predicate The type for which the query will search
 * @param descend Whether or not to descend into children
 * @param read What to save in the query
 * @returns QueryList<T>
 */
export function query<T>(
    // TODO: "read" should be an AbstractType (FW-486)
    predicate: Type<any>| string[], descend?: boolean, read?: any): QueryList<T> {
  ngDevMode && assertPreviousIsParent(getIsParent());
  const lView = getLView();
  const queryList = new QueryList<T>();
  const queries = lView[QUERIES] || (lView[QUERIES] = new LQueries_(null, null, null));
  (queryList as QueryList_<T>)._valuesTree = [];
  queries.track(queryList, predicate, descend, read);
  storeCleanupWithContext(lView, queryList, queryList.destroy);
  return queryList;
}

/**
 * Refreshes a query by combining matches from all active views and removing matches from deleted
 * views.
 * Returns true if a query got dirty during change detection, false otherwise.
 */
export function queryRefresh(queryList: QueryList<any>): boolean {
  const queryListImpl = (queryList as any as QueryList_<any>);
  if (queryList.dirty) {
    queryList.reset(queryListImpl._valuesTree || []);
    queryList.notifyOnChanges();
    return true;
  }
  return false;
}

/**
 * Creates new QueryList, stores the reference in LView and returns QueryList.
 *
 * @param predicate The type for which the query will search
 * @param descend Whether or not to descend into children
 * @param read What to save in the query
 * @returns QueryList<T>
 */
export function viewQuery<T>(
    // TODO: "read" should be an AbstractType (FW-486)
    predicate: Type<any>| string[], descend?: boolean, read?: any): QueryList<T> {
  const lView = getLView();
  const tView = lView[TVIEW];
  if (tView.firstTemplatePass) {
    tView.expandoStartIndex++;
  }
  const index = getCurrentQueryIndex();
  const viewQuery: QueryList<T> = query<T>(predicate, descend, read);
  store(index - HEADER_OFFSET, viewQuery);
  setCurrentQueryIndex(index + 1);
  return viewQuery;
}

/**
* Loads current View Query and moves the pointer/index to the next View Query in LView.
*/
export function loadViewQuery<T>(): T {
  const index = getCurrentQueryIndex();
  setCurrentQueryIndex(index + 1);
  return load<T>(index - HEADER_OFFSET);
}

/**
 * Registers a QueryList, associated with a content query, for later refresh (part of a view
 * refresh).
 *
 * @param directiveIndex Current directive index
 * @param predicate The type for which the query will search
 * @param descend Whether or not to descend into children
 * @param read What to save in the query
 * @returns QueryList<T>
 */
export function contentQuery<T>(
    directiveIndex: number, predicate: Type<any>| string[], descend?: boolean,
    // TODO: "read" should be an AbstractType (FW-486)
    read?: any): QueryList<T> {
  const lView = getLView();
  const tView = lView[TVIEW];
  const contentQuery: QueryList<T> = query<T>(predicate, descend, read);
  (lView[CONTENT_QUERIES] || (lView[CONTENT_QUERIES] = [])).push(contentQuery);
  if (tView.firstTemplatePass) {
    const tViewContentQueries = tView.contentQueries || (tView.contentQueries = []);
    const lastSavedDirectiveIndex =
        tView.contentQueries.length ? tView.contentQueries[tView.contentQueries.length - 1] : -1;
    if (directiveIndex !== lastSavedDirectiveIndex) {
      tViewContentQueries.push(directiveIndex);
    }
  }
  return contentQuery;
}

export function loadContentQuery<T>(): QueryList<T> {
  const lView = getLView();
  ngDevMode &&
      assertDefined(
          lView[CONTENT_QUERIES], 'Content QueryList array should be defined if reading a query.');

  const index = getCurrentQueryIndex();
  ngDevMode && assertDataInRange(lView[CONTENT_QUERIES] !, index);

  setCurrentQueryIndex(index + 1);
  return lView[CONTENT_QUERIES] ![index];
}