/**
 * @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 {ConstantPool} from '../../constant_pool';
import {InitialStylingFlags} from '../../core';
import {AST, BindingType, ParseSpan} from '../../expression_parser/ast';
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers';

import {parse as parseStyle} from './style_parser';
import {ValueConverter} from './template';


/**
 * A styling expression summary that is to be processed by the compiler
 */
export interface StylingInstruction {
  sourceSpan: ParseSourceSpan|null;
  reference: o.ExternalReference;
  buildParams(convertFn: (value: any) => o.Expression): o.Expression[];
}

/**
 * An internal record of the input data for a styling binding
 */
interface BoundStylingEntry {
  name: string;
  unit: string|null;
  sourceSpan: ParseSourceSpan;
  value: AST;
}


/**
 * Produces creation/update instructions for all styling bindings (class and style)
 *
 * The builder class below handles producing instructions for the following cases:
 *
 * - Static style/class attributes (style="..." and class="...")
 * - Dynamic style/class map bindings ([style]="map" and [class]="map|string")
 * - Dynamic style/class property bindings ([style.prop]="exp" and [class.name]="exp")
 *
 * Due to the complex relationship of all of these cases, the instructions generated
 * for these attributes/properties/bindings must be done so in the correct order. The
 * order which these must be generated is as follows:
 *
 * if (createMode) {
 *   elementStyling(...)
 * }
 * if (updateMode) {
 *   elementStylingMap(...)
 *   elementStyleProp(...)
 *   elementClassProp(...)
 *   elementStylingApp(...)
 * }
 *
 * The creation/update methods within the builder class produce these instructions.
 */
export class StylingBuilder {
  public readonly hasBindingsOrInitialValues = false;

  private _classMapInput: BoundStylingEntry|null = null;
  private _styleMapInput: BoundStylingEntry|null = null;
  private _singleStyleInputs: BoundStylingEntry[]|null = null;
  private _singleClassInputs: BoundStylingEntry[]|null = null;
  private _lastStylingInput: BoundStylingEntry|null = null;

  // maps are used instead of hash maps because a Map will
  // retain the ordering of the keys
  private _stylesIndex = new Map<string, number>();
  private _classesIndex = new Map<string, number>();
  private _initialStyleValues: {[propName: string]: string} = {};
  private _initialClassValues: {[className: string]: boolean} = {};
  private _useDefaultSanitizer = false;
  private _applyFnRequired = false;

  constructor(private _elementIndexExpr: o.Expression, private _directiveExpr: o.Expression|null) {}

  registerBoundInput(input: t.BoundAttribute): boolean {
    // [attr.style] or [attr.class] are skipped in the code below,
    // they should not be treated as styling-based bindings since
    // they are intended to be written directly to the attr and
    // will therefore skip all style/class resolution that is present
    // with style="", [style]="" and [style.prop]="", class="",
    // [class.prop]="". [class]="" assignments
    const name = input.name;
    let binding: BoundStylingEntry|null = null;
    switch (input.type) {
      case BindingType.Property:
        if (name == 'style') {
          binding = this.registerStyleInput(null, input.value, '', input.sourceSpan);
        } else if (isClassBinding(input.name)) {
          binding = this.registerClassInput(null, input.value, input.sourceSpan);
        }
        break;
      case BindingType.Style:
        binding = this.registerStyleInput(input.name, input.value, input.unit, input.sourceSpan);
        break;
      case BindingType.Class:
        binding = this.registerClassInput(input.name, input.value, input.sourceSpan);
        break;
    }
    return binding ? true : false;
  }

  registerStyleInput(
      propertyName: string|null, value: AST, unit: string|null,
      sourceSpan: ParseSourceSpan): BoundStylingEntry {
    const entry = { name: propertyName, unit, value, sourceSpan } as BoundStylingEntry;
    if (propertyName) {
      (this._singleStyleInputs = this._singleStyleInputs || []).push(entry);
      this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName);
      registerIntoMap(this._stylesIndex, propertyName);
      (this as any).hasBindingsOrInitialValues = true;
    } else {
      this._useDefaultSanitizer = true;
      this._styleMapInput = entry;
    }
    this._lastStylingInput = entry;
    (this as any).hasBindingsOrInitialValues = true;
    this._applyFnRequired = true;
    return entry;
  }

  registerClassInput(className: string|null, value: AST, sourceSpan: ParseSourceSpan):
      BoundStylingEntry {
    const entry = { name: className, value, sourceSpan } as BoundStylingEntry;
    if (className) {
      (this._singleClassInputs = this._singleClassInputs || []).push(entry);
      (this as any).hasBindingsOrInitialValues = true;
      registerIntoMap(this._classesIndex, className);
    } else {
      this._classMapInput = entry;
    }
    this._lastStylingInput = entry;
    (this as any).hasBindingsOrInitialValues = true;
    this._applyFnRequired = true;
    return entry;
  }

  registerStyleAttr(value: string) {
    this._initialStyleValues = parseStyle(value);
    Object.keys(this._initialStyleValues).forEach(prop => {
      registerIntoMap(this._stylesIndex, prop);
      (this as any).hasBindingsOrInitialValues = true;
    });
  }

  registerClassAttr(value: string) {
    this._initialClassValues = {};
    value.split(/\s+/g).forEach(className => {
      this._initialClassValues[className] = true;
      registerIntoMap(this._classesIndex, className);
      (this as any).hasBindingsOrInitialValues = true;
    });
  }

  private _buildInitExpr(registry: Map<string, number>, initialValues: {[key: string]: any}):
      o.Expression|null {
    const exprs: o.Expression[] = [];
    const nameAndValueExprs: o.Expression[] = [];

    // _c0 = [prop, prop2, prop3, ...]
    registry.forEach((value, key) => {
      const keyLiteral = o.literal(key);
      exprs.push(keyLiteral);
      const initialValue = initialValues[key];
      if (initialValue) {
        nameAndValueExprs.push(keyLiteral, o.literal(initialValue));
      }
    });

    if (nameAndValueExprs.length) {
      // _c0 = [... MARKER ...]
      exprs.push(o.literal(InitialStylingFlags.VALUES_MODE));
      // _c0 = [prop, VALUE, prop2, VALUE2, ...]
      exprs.push(...nameAndValueExprs);
    }

    return exprs.length ? o.literalArr(exprs) : null;
  }

  buildCreateLevelInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
      StylingInstruction|null {
    if (this.hasBindingsOrInitialValues) {
      const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues);
      const initialStyles = this._buildInitExpr(this._stylesIndex, this._initialStyleValues);

      // in the event that a [style] binding is used then sanitization will
      // always be imported because it is not possible to know ahead of time
      // whether style bindings will use or not use any sanitizable properties
      // that isStyleSanitizable() will detect
      const useSanitizer = this._useDefaultSanitizer;
      const params: (o.Expression)[] = [];

      if (initialClasses) {
        // the template compiler handles initial class styling (e.g. class="foo") values
        // in a special command called `elementClass` so that the initial class
        // can be processed during runtime. These initial class values are bound to
        // a constant because the inital class values do not change (since they're static).
        params.push(constantPool.getConstLiteral(initialClasses, true));
      } else if (initialStyles || useSanitizer || this._directiveExpr) {
        // no point in having an extra `null` value unless there are follow-up params
        params.push(o.NULL_EXPR);
      }

      if (initialStyles) {
        // the template compiler handles initial style (e.g. style="foo") values
        // in a special command called `elementStyle` so that the initial styles
        // can be processed during runtime. These initial styles values are bound to
        // a constant because the inital style values do not change (since they're static).
        params.push(constantPool.getConstLiteral(initialStyles, true));
      } else if (useSanitizer || this._directiveExpr) {
        // no point in having an extra `null` value unless there are follow-up params
        params.push(o.NULL_EXPR);
      }

      if (useSanitizer || this._directiveExpr) {
        params.push(useSanitizer ? o.importExpr(R3.defaultStyleSanitizer) : o.NULL_EXPR);
        if (this._directiveExpr) {
          params.push(this._directiveExpr);
        }
      }

      return {sourceSpan, reference: R3.elementStyling, buildParams: () => params};
    }
    return null;
  }

  private _buildStylingMap(valueConverter: ValueConverter): StylingInstruction|null {
    if (this._classMapInput || this._styleMapInput) {
      const stylingInput = this._classMapInput ! || this._styleMapInput !;

      // these values must be outside of the update block so that they can
      // be evaluted (the AST visit call) during creation time so that any
      // pipes can be picked up in time before the template is built
      const mapBasedClassValue =
          this._classMapInput ? this._classMapInput.value.visit(valueConverter) : null;
      const mapBasedStyleValue =
          this._styleMapInput ? this._styleMapInput.value.visit(valueConverter) : null;

      return {
        sourceSpan: stylingInput.sourceSpan,
        reference: R3.elementStylingMap,
        buildParams: (convertFn: (value: any) => o.Expression) => {
          const params: o.Expression[] = [this._elementIndexExpr];

          if (mapBasedClassValue) {
            params.push(convertFn(mapBasedClassValue));
          } else if (this._styleMapInput) {
            params.push(o.NULL_EXPR);
          }

          if (mapBasedStyleValue) {
            params.push(convertFn(mapBasedStyleValue));
          } else if (this._directiveExpr) {
            params.push(o.NULL_EXPR);
          }

          if (this._directiveExpr) {
            params.push(this._directiveExpr);
          }

          return params;
        }
      };
    }
    return null;
  }

  private _buildSingleInputs(
      reference: o.ExternalReference, inputs: BoundStylingEntry[], mapIndex: Map<string, number>,
      allowUnits: boolean, valueConverter: ValueConverter): StylingInstruction[] {
    return inputs.map(input => {
      const bindingIndex: number = mapIndex.get(input.name) !;
      const value = input.value.visit(valueConverter);
      return {
        sourceSpan: input.sourceSpan,
        reference,
        buildParams: (convertFn: (value: any) => o.Expression) => {
          const params = [this._elementIndexExpr, o.literal(bindingIndex), convertFn(value)];
          if (allowUnits) {
            if (input.unit) {
              params.push(o.literal(input.unit));
            } else if (this._directiveExpr) {
              params.push(o.NULL_EXPR);
            }
          }

          if (this._directiveExpr) {
            params.push(this._directiveExpr);
          }
          return params;
        }
      };
    });
  }

  private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] {
    if (this._singleClassInputs) {
      return this._buildSingleInputs(
          R3.elementClassProp, this._singleClassInputs, this._classesIndex, false, valueConverter);
    }
    return [];
  }

  private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] {
    if (this._singleStyleInputs) {
      return this._buildSingleInputs(
          R3.elementStyleProp, this._singleStyleInputs, this._stylesIndex, true, valueConverter);
    }
    return [];
  }

  private _buildApplyFn(): StylingInstruction {
    return {
      sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null,
      reference: R3.elementStylingApply,
      buildParams: () => {
        const params: o.Expression[] = [this._elementIndexExpr];
        if (this._directiveExpr) {
          params.push(this._directiveExpr);
        }
        return params;
      }
    };
  }

  buildUpdateLevelInstructions(valueConverter: ValueConverter) {
    const instructions: StylingInstruction[] = [];
    if (this.hasBindingsOrInitialValues) {
      const mapInstruction = this._buildStylingMap(valueConverter);
      if (mapInstruction) {
        instructions.push(mapInstruction);
      }
      instructions.push(...this._buildStyleInputs(valueConverter));
      instructions.push(...this._buildClassInputs(valueConverter));
      if (this._applyFnRequired) {
        instructions.push(this._buildApplyFn());
      }
    }
    return instructions;
  }
}

function isClassBinding(name: string): boolean {
  return name == 'className' || name == 'class';
}

function registerIntoMap(map: Map<string, number>, key: string) {
  if (!map.has(key)) {
    map.set(key, map.size);
  }
}

function isStyleSanitizable(prop: string): boolean {
  return prop === 'background-image' || prop === 'background' || prop === 'border-image' ||
      prop === 'filter' || prop === 'list-style' || prop === 'list-style-image';
}
