/**
 * @license
 * Copyright Google LLC 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 {SecurityContext} from '../core';
import {
  AbsoluteSourceSpan,
  AST,
  ASTWithSource,
  Binary,
  BindingPipe,
  BindingType,
  BoundElementProperty,
  Conditional,
  EmptyExpr,
  KeyedRead,
  NonNullAssert,
  ParsedEvent,
  ParsedEventType,
  ParsedProperty,
  ParsedPropertyType,
  ParsedVariable,
  ParserError,
  PrefixNot,
  PropertyRead,
  RecursiveAstVisitor,
  TemplateBinding,
  VariableBinding,
} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
import {InterpolationConfig} from '../ml_parser/defaults';
import {mergeNsAndName} from '../ml_parser/tags';
import {InterpolatedAttributeToken, InterpolatedTextToken} from '../ml_parser/tokens';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util';

const PROPERTY_PARTS_SEPARATOR = '.';
const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';
const TEMPLATE_ATTR_PREFIX = '*';
const ANIMATE_PROP_PREFIX = 'animate-';

export interface HostProperties {
  [key: string]: string;
}

export interface HostListeners {
  [key: string]: string;
}

/**
 * Parses bindings in templates and in the directive host area.
 */
export class BindingParser {
  constructor(
    private _exprParser: Parser,
    private _interpolationConfig: InterpolationConfig,
    private _schemaRegistry: ElementSchemaRegistry,
    public errors: ParseError[],
    private _allowInvalidAssignmentEvents = false,
  ) {}

  get interpolationConfig(): InterpolationConfig {
    return this._interpolationConfig;
  }

  createBoundHostProperties(
    properties: HostProperties,
    sourceSpan: ParseSourceSpan,
  ): ParsedProperty[] | null {
    const boundProps: ParsedProperty[] = [];
    for (const propName of Object.keys(properties)) {
      const expression = properties[propName];
      if (typeof expression === 'string') {
        this.parsePropertyBinding(
          propName,
          expression,
          true,
          false,
          sourceSpan,
          sourceSpan.start.offset,
          undefined,
          [],
          // Use the `sourceSpan` for  `keySpan`. This isn't really accurate, but neither is the
          // sourceSpan, as it represents the sourceSpan of the host itself rather than the
          // source of the host binding (which doesn't exist in the template). Regardless,
          // neither of these values are used in Ivy but are only here to satisfy the function
          // signature. This should likely be refactored in the future so that `sourceSpan`
          // isn't being used inaccurately.
          boundProps,
          sourceSpan,
        );
      } else {
        this._reportError(
          `Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
          sourceSpan,
        );
      }
    }
    return boundProps;
  }

  createDirectiveHostEventAsts(
    hostListeners: HostListeners,
    sourceSpan: ParseSourceSpan,
  ): ParsedEvent[] | null {
    const targetEvents: ParsedEvent[] = [];
    for (const propName of Object.keys(hostListeners)) {
      const expression = hostListeners[propName];
      if (typeof expression === 'string') {
        // Use the `sourceSpan` for  `keySpan` and `handlerSpan`. This isn't really accurate, but
        // neither is the `sourceSpan`, as it represents the `sourceSpan` of the host itself
        // rather than the source of the host binding (which doesn't exist in the template).
        // Regardless, neither of these values are used in Ivy but are only here to satisfy the
        // function signature. This should likely be refactored in the future so that `sourceSpan`
        // isn't being used inaccurately.
        this.parseEvent(
          propName,
          expression,
          /* isAssignmentEvent */ false,
          sourceSpan,
          sourceSpan,
          [],
          targetEvents,
          sourceSpan,
        );
      } else {
        this._reportError(
          `Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
          sourceSpan,
        );
      }
    }
    return targetEvents;
  }

  parseInterpolation(
    value: string,
    sourceSpan: ParseSourceSpan,
    interpolatedTokens: InterpolatedAttributeToken[] | InterpolatedTextToken[] | null,
  ): ASTWithSource {
    const sourceInfo = sourceSpan.start.toString();
    const absoluteOffset = sourceSpan.fullStart.offset;

    try {
      const ast = this._exprParser.parseInterpolation(
        value,
        sourceInfo,
        absoluteOffset,
        interpolatedTokens,
        this._interpolationConfig,
      )!;
      if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
      return ast;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
    }
  }

  /**
   * Similar to `parseInterpolation`, but treats the provided string as a single expression
   * element that would normally appear within the interpolation prefix and suffix (`{{` and `}}`).
   * This is used for parsing the switch expression in ICUs.
   */
  parseInterpolationExpression(expression: string, sourceSpan: ParseSourceSpan): ASTWithSource {
    const sourceInfo = sourceSpan.start.toString();
    const absoluteOffset = sourceSpan.start.offset;

    try {
      const ast = this._exprParser.parseInterpolationExpression(
        expression,
        sourceInfo,
        absoluteOffset,
      );
      if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
      return ast;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
    }
  }

  /**
   * Parses the bindings in a microsyntax expression, and converts them to
   * `ParsedProperty` or `ParsedVariable`.
   *
   * @param tplKey template binding name
   * @param tplValue template binding value
   * @param sourceSpan span of template binding relative to entire the template
   * @param absoluteValueOffset start of the tplValue relative to the entire template
   * @param targetMatchableAttrs potential attributes to match in the template
   * @param targetProps target property bindings in the template
   * @param targetVars target variables in the template
   */
  parseInlineTemplateBinding(
    tplKey: string,
    tplValue: string,
    sourceSpan: ParseSourceSpan,
    absoluteValueOffset: number,
    targetMatchableAttrs: string[][],
    targetProps: ParsedProperty[],
    targetVars: ParsedVariable[],
    isIvyAst: boolean,
  ) {
    const absoluteKeyOffset = sourceSpan.start.offset + TEMPLATE_ATTR_PREFIX.length;
    const bindings = this._parseTemplateBindings(
      tplKey,
      tplValue,
      sourceSpan,
      absoluteKeyOffset,
      absoluteValueOffset,
    );

    for (const binding of bindings) {
      // sourceSpan is for the entire HTML attribute. bindingSpan is for a particular
      // binding within the microsyntax expression so it's more narrow than sourceSpan.
      const bindingSpan = moveParseSourceSpan(sourceSpan, binding.sourceSpan);
      const key = binding.key.source;
      const keySpan = moveParseSourceSpan(sourceSpan, binding.key.span);
      if (binding instanceof VariableBinding) {
        const value = binding.value ? binding.value.source : '$implicit';
        const valueSpan = binding.value
          ? moveParseSourceSpan(sourceSpan, binding.value.span)
          : undefined;
        targetVars.push(new ParsedVariable(key, value, bindingSpan, keySpan, valueSpan));
      } else if (binding.value) {
        const srcSpan = isIvyAst ? bindingSpan : sourceSpan;
        const valueSpan = moveParseSourceSpan(sourceSpan, binding.value.ast.sourceSpan);
        this._parsePropertyAst(
          key,
          binding.value,
          false,
          srcSpan,
          keySpan,
          valueSpan,
          targetMatchableAttrs,
          targetProps,
        );
      } else {
        targetMatchableAttrs.push([key, '' /* value */]);
        // Since this is a literal attribute with no RHS, source span should be
        // just the key span.
        this.parseLiteralAttr(
          key,
          null /* value */,
          keySpan,
          absoluteValueOffset,
          undefined /* valueSpan */,
          targetMatchableAttrs,
          targetProps,
          keySpan,
        );
      }
    }
  }

  /**
   * Parses the bindings in a microsyntax expression, e.g.
   * ```
   *    <tag *tplKey="let value1 = prop; let value2 = localVar">
   * ```
   *
   * @param tplKey template binding name
   * @param tplValue template binding value
   * @param sourceSpan span of template binding relative to entire the template
   * @param absoluteKeyOffset start of the `tplKey`
   * @param absoluteValueOffset start of the `tplValue`
   */
  private _parseTemplateBindings(
    tplKey: string,
    tplValue: string,
    sourceSpan: ParseSourceSpan,
    absoluteKeyOffset: number,
    absoluteValueOffset: number,
  ): TemplateBinding[] {
    const sourceInfo = sourceSpan.start.toString();

    try {
      const bindingsResult = this._exprParser.parseTemplateBindings(
        tplKey,
        tplValue,
        sourceInfo,
        absoluteKeyOffset,
        absoluteValueOffset,
      );
      this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan);
      bindingsResult.warnings.forEach((warning) => {
        this._reportError(warning, sourceSpan, ParseErrorLevel.WARNING);
      });
      return bindingsResult.templateBindings;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return [];
    }
  }

  parseLiteralAttr(
    name: string,
    value: string | null,
    sourceSpan: ParseSourceSpan,
    absoluteOffset: number,
    valueSpan: ParseSourceSpan | undefined,
    targetMatchableAttrs: string[][],
    targetProps: ParsedProperty[],
    keySpan: ParseSourceSpan,
  ) {
    if (isAnimationLabel(name)) {
      name = name.substring(1);
      if (keySpan !== undefined) {
        keySpan = moveParseSourceSpan(
          keySpan,
          new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset),
        );
      }
      if (value) {
        this._reportError(
          `Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` +
            ` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`,
          sourceSpan,
          ParseErrorLevel.ERROR,
        );
      }
      this._parseAnimation(
        name,
        value,
        sourceSpan,
        absoluteOffset,
        keySpan,
        valueSpan,
        targetMatchableAttrs,
        targetProps,
      );
    } else {
      targetProps.push(
        new ParsedProperty(
          name,
          this._exprParser.wrapLiteralPrimitive(value, '', absoluteOffset),
          ParsedPropertyType.LITERAL_ATTR,
          sourceSpan,
          keySpan,
          valueSpan,
        ),
      );
    }
  }

  parsePropertyBinding(
    name: string,
    expression: string,
    isHost: boolean,
    isPartOfAssignmentBinding: boolean,
    sourceSpan: ParseSourceSpan,
    absoluteOffset: number,
    valueSpan: ParseSourceSpan | undefined,
    targetMatchableAttrs: string[][],
    targetProps: ParsedProperty[],
    keySpan: ParseSourceSpan,
  ) {
    if (name.length === 0) {
      this._reportError(`Property name is missing in binding`, sourceSpan);
    }

    let isAnimationProp = false;
    if (name.startsWith(ANIMATE_PROP_PREFIX)) {
      isAnimationProp = true;
      name = name.substring(ANIMATE_PROP_PREFIX.length);
      if (keySpan !== undefined) {
        keySpan = moveParseSourceSpan(
          keySpan,
          new AbsoluteSourceSpan(
            keySpan.start.offset + ANIMATE_PROP_PREFIX.length,
            keySpan.end.offset,
          ),
        );
      }
    } else if (isAnimationLabel(name)) {
      isAnimationProp = true;
      name = name.substring(1);
      if (keySpan !== undefined) {
        keySpan = moveParseSourceSpan(
          keySpan,
          new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset),
        );
      }
    }

    if (isAnimationProp) {
      this._parseAnimation(
        name,
        expression,
        sourceSpan,
        absoluteOffset,
        keySpan,
        valueSpan,
        targetMatchableAttrs,
        targetProps,
      );
    } else {
      this._parsePropertyAst(
        name,
        this.parseBinding(expression, isHost, valueSpan || sourceSpan, absoluteOffset),
        isPartOfAssignmentBinding,
        sourceSpan,
        keySpan,
        valueSpan,
        targetMatchableAttrs,
        targetProps,
      );
    }
  }

  parsePropertyInterpolation(
    name: string,
    value: string,
    sourceSpan: ParseSourceSpan,
    valueSpan: ParseSourceSpan | undefined,
    targetMatchableAttrs: string[][],
    targetProps: ParsedProperty[],
    keySpan: ParseSourceSpan,
    interpolatedTokens: InterpolatedAttributeToken[] | InterpolatedTextToken[] | null,
  ): boolean {
    const expr = this.parseInterpolation(value, valueSpan || sourceSpan, interpolatedTokens);
    if (expr) {
      this._parsePropertyAst(
        name,
        expr,
        false,
        sourceSpan,
        keySpan,
        valueSpan,
        targetMatchableAttrs,
        targetProps,
      );
      return true;
    }
    return false;
  }

  private _parsePropertyAst(
    name: string,
    ast: ASTWithSource,
    isPartOfAssignmentBinding: boolean,
    sourceSpan: ParseSourceSpan,
    keySpan: ParseSourceSpan,
    valueSpan: ParseSourceSpan | undefined,
    targetMatchableAttrs: string[][],
    targetProps: ParsedProperty[],
  ) {
    targetMatchableAttrs.push([name, ast.source!]);
    targetProps.push(
      new ParsedProperty(
        name,
        ast,
        isPartOfAssignmentBinding ? ParsedPropertyType.TWO_WAY : ParsedPropertyType.DEFAULT,
        sourceSpan,
        keySpan,
        valueSpan,
      ),
    );
  }

  private _parseAnimation(
    name: string,
    expression: string | null,
    sourceSpan: ParseSourceSpan,
    absoluteOffset: number,
    keySpan: ParseSourceSpan,
    valueSpan: ParseSourceSpan | undefined,
    targetMatchableAttrs: string[][],
    targetProps: ParsedProperty[],
  ) {
    if (name.length === 0) {
      this._reportError('Animation trigger is missing', sourceSpan);
    }

    // This will occur when a @trigger is not paired with an expression.
    // For animations it is valid to not have an expression since */void
    // states will be applied by angular when the element is attached/detached
    const ast = this.parseBinding(
      expression || 'undefined',
      false,
      valueSpan || sourceSpan,
      absoluteOffset,
    );
    targetMatchableAttrs.push([name, ast.source!]);
    targetProps.push(
      new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan, keySpan, valueSpan),
    );
  }

  parseBinding(
    value: string,
    isHostBinding: boolean,
    sourceSpan: ParseSourceSpan,
    absoluteOffset: number,
  ): ASTWithSource {
    const sourceInfo = ((sourceSpan && sourceSpan.start) || '(unknown)').toString();

    try {
      const ast = isHostBinding
        ? this._exprParser.parseSimpleBinding(
            value,
            sourceInfo,
            absoluteOffset,
            this._interpolationConfig,
          )
        : this._exprParser.parseBinding(
            value,
            sourceInfo,
            absoluteOffset,
            this._interpolationConfig,
          );
      if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
      return ast;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
    }
  }

  createBoundElementProperty(
    elementSelector: string,
    boundProp: ParsedProperty,
    skipValidation: boolean = false,
    mapPropertyName: boolean = true,
  ): BoundElementProperty {
    if (boundProp.isAnimation) {
      return new BoundElementProperty(
        boundProp.name,
        BindingType.Animation,
        SecurityContext.NONE,
        boundProp.expression,
        null,
        boundProp.sourceSpan,
        boundProp.keySpan,
        boundProp.valueSpan,
      );
    }

    let unit: string | null = null;
    let bindingType: BindingType = undefined!;
    let boundPropertyName: string | null = null;
    const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
    let securityContexts: SecurityContext[] = undefined!;

    // Check for special cases (prefix style, attr, class)
    if (parts.length > 1) {
      if (parts[0] == ATTRIBUTE_PREFIX) {
        boundPropertyName = parts.slice(1).join(PROPERTY_PARTS_SEPARATOR);
        if (!skipValidation) {
          this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
        }
        securityContexts = calcPossibleSecurityContexts(
          this._schemaRegistry,
          elementSelector,
          boundPropertyName,
          true,
        );

        const nsSeparatorIdx = boundPropertyName.indexOf(':');
        if (nsSeparatorIdx > -1) {
          const ns = boundPropertyName.substring(0, nsSeparatorIdx);
          const name = boundPropertyName.substring(nsSeparatorIdx + 1);
          boundPropertyName = mergeNsAndName(ns, name);
        }

        bindingType = BindingType.Attribute;
      } else if (parts[0] == CLASS_PREFIX) {
        boundPropertyName = parts[1];
        bindingType = BindingType.Class;
        securityContexts = [SecurityContext.NONE];
      } else if (parts[0] == STYLE_PREFIX) {
        unit = parts.length > 2 ? parts[2] : null;
        boundPropertyName = parts[1];
        bindingType = BindingType.Style;
        securityContexts = [SecurityContext.STYLE];
      }
    }

    // If not a special case, use the full property name
    if (boundPropertyName === null) {
      const mappedPropName = this._schemaRegistry.getMappedPropName(boundProp.name);
      boundPropertyName = mapPropertyName ? mappedPropName : boundProp.name;
      securityContexts = calcPossibleSecurityContexts(
        this._schemaRegistry,
        elementSelector,
        mappedPropName,
        false,
      );
      bindingType =
        boundProp.type === ParsedPropertyType.TWO_WAY ? BindingType.TwoWay : BindingType.Property;
      if (!skipValidation) {
        this._validatePropertyOrAttributeName(mappedPropName, boundProp.sourceSpan, false);
      }
    }

    return new BoundElementProperty(
      boundPropertyName,
      bindingType,
      securityContexts[0],
      boundProp.expression,
      unit,
      boundProp.sourceSpan,
      boundProp.keySpan,
      boundProp.valueSpan,
    );
  }

  // TODO: keySpan should be required but was made optional to avoid changing VE parser.
  parseEvent(
    name: string,
    expression: string,
    isAssignmentEvent: boolean,
    sourceSpan: ParseSourceSpan,
    handlerSpan: ParseSourceSpan,
    targetMatchableAttrs: string[][],
    targetEvents: ParsedEvent[],
    keySpan: ParseSourceSpan,
  ) {
    if (name.length === 0) {
      this._reportError(`Event name is missing in binding`, sourceSpan);
    }

    if (isAnimationLabel(name)) {
      name = name.slice(1);
      if (keySpan !== undefined) {
        keySpan = moveParseSourceSpan(
          keySpan,
          new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset),
        );
      }
      this._parseAnimationEvent(name, expression, sourceSpan, handlerSpan, targetEvents, keySpan);
    } else {
      this._parseRegularEvent(
        name,
        expression,
        isAssignmentEvent,
        sourceSpan,
        handlerSpan,
        targetMatchableAttrs,
        targetEvents,
        keySpan,
      );
    }
  }

  calcPossibleSecurityContexts(
    selector: string,
    propName: string,
    isAttribute: boolean,
  ): SecurityContext[] {
    const prop = this._schemaRegistry.getMappedPropName(propName);
    return calcPossibleSecurityContexts(this._schemaRegistry, selector, prop, isAttribute);
  }

  private _parseAnimationEvent(
    name: string,
    expression: string,
    sourceSpan: ParseSourceSpan,
    handlerSpan: ParseSourceSpan,
    targetEvents: ParsedEvent[],
    keySpan: ParseSourceSpan,
  ) {
    const matches = splitAtPeriod(name, [name, '']);
    const eventName = matches[0];
    const phase = matches[1].toLowerCase();
    const ast = this._parseAction(expression, handlerSpan);
    targetEvents.push(
      new ParsedEvent(
        eventName,
        phase,
        ParsedEventType.Animation,
        ast,
        sourceSpan,
        handlerSpan,
        keySpan,
      ),
    );

    if (eventName.length === 0) {
      this._reportError(`Animation event name is missing in binding`, sourceSpan);
    }
    if (phase) {
      if (phase !== 'start' && phase !== 'done') {
        this._reportError(
          `The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`,
          sourceSpan,
        );
      }
    } else {
      this._reportError(
        `The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`,
        sourceSpan,
      );
    }
  }

  private _parseRegularEvent(
    name: string,
    expression: string,
    isAssignmentEvent: boolean,
    sourceSpan: ParseSourceSpan,
    handlerSpan: ParseSourceSpan,
    targetMatchableAttrs: string[][],
    targetEvents: ParsedEvent[],
    keySpan: ParseSourceSpan,
  ): void {
    // long format: 'target: eventName'
    const [target, eventName] = splitAtColon(name, [null!, name]);
    const prevErrorCount = this.errors.length;
    const ast = this._parseAction(expression, handlerSpan);
    const isValid = this.errors.length === prevErrorCount;
    targetMatchableAttrs.push([name!, ast.source!]);

    // Don't try to validate assignment events if there were other
    // parsing errors to avoid adding more noise to the error logs.
    if (isAssignmentEvent && isValid && !this._isAllowedAssignmentEvent(ast)) {
      this._reportError('Unsupported expression in a two-way binding', sourceSpan);
    }

    targetEvents.push(
      new ParsedEvent(
        eventName,
        target,
        isAssignmentEvent ? ParsedEventType.TwoWay : ParsedEventType.Regular,
        ast,
        sourceSpan,
        handlerSpan,
        keySpan,
      ),
    );
    // Don't detect directives for event names for now,
    // so don't add the event name to the matchableAttrs
  }

  private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
    const sourceInfo = ((sourceSpan && sourceSpan.start) || '(unknown').toString();
    const absoluteOffset = sourceSpan && sourceSpan.start ? sourceSpan.start.offset : 0;

    try {
      const ast = this._exprParser.parseAction(
        value,
        sourceInfo,
        absoluteOffset,
        this._interpolationConfig,
      );
      if (ast) {
        this._reportExpressionParserErrors(ast.errors, sourceSpan);
      }
      if (!ast || ast.ast instanceof EmptyExpr) {
        this._reportError(`Empty expressions are not allowed`, sourceSpan);
        return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
      }
      return ast;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
    }
  }

  private _reportError(
    message: string,
    sourceSpan: ParseSourceSpan,
    level: ParseErrorLevel = ParseErrorLevel.ERROR,
  ) {
    this.errors.push(new ParseError(sourceSpan, message, level));
  }

  private _reportExpressionParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
    for (const error of errors) {
      this._reportError(error.message, sourceSpan);
    }
  }

  /**
   * @param propName the name of the property / attribute
   * @param sourceSpan
   * @param isAttr true when binding to an attribute
   */
  private _validatePropertyOrAttributeName(
    propName: string,
    sourceSpan: ParseSourceSpan,
    isAttr: boolean,
  ): void {
    const report = isAttr
      ? this._schemaRegistry.validateAttribute(propName)
      : this._schemaRegistry.validateProperty(propName);
    if (report.error) {
      this._reportError(report.msg!, sourceSpan, ParseErrorLevel.ERROR);
    }
  }

  /**
   * Returns whether a parsed AST is allowed to be used within the event side of a two-way binding.
   * @param ast Parsed AST to be checked.
   */
  private _isAllowedAssignmentEvent(ast: AST): boolean {
    if (ast instanceof ASTWithSource) {
      return this._isAllowedAssignmentEvent(ast.ast);
    }

    if (ast instanceof NonNullAssert) {
      return this._isAllowedAssignmentEvent(ast.expression);
    }

    if (ast instanceof PropertyRead || ast instanceof KeyedRead) {
      return true;
    }

    // TODO(crisbeto): this logic is only here to support the automated migration away
    // from invalid bindings. It should be removed once the migration is deleted.
    if (!this._allowInvalidAssignmentEvents) {
      return false;
    }

    if (ast instanceof Binary) {
      return (
        (ast.operation === '&&' || ast.operation === '||' || ast.operation === '??') &&
        (ast.right instanceof PropertyRead || ast.right instanceof KeyedRead)
      );
    }

    return ast instanceof Conditional || ast instanceof PrefixNot;
  }
}

export class PipeCollector extends RecursiveAstVisitor {
  pipes = new Map<string, BindingPipe>();
  override visitPipe(ast: BindingPipe, context: any): any {
    this.pipes.set(ast.name, ast);
    ast.exp.visit(this);
    this.visitAll(ast.args, context);
    return null;
  }
}

function isAnimationLabel(name: string): boolean {
  return name[0] == '@';
}

export function calcPossibleSecurityContexts(
  registry: ElementSchemaRegistry,
  selector: string,
  propName: string,
  isAttribute: boolean,
): SecurityContext[] {
  const ctxs: SecurityContext[] = [];
  CssSelector.parse(selector).forEach((selector) => {
    const elementNames = selector.element ? [selector.element] : registry.allKnownElementNames();
    const notElementNames = new Set(
      selector.notSelectors
        .filter((selector) => selector.isElementSelector())
        .map((selector) => selector.element),
    );
    const possibleElementNames = elementNames.filter(
      (elementName) => !notElementNames.has(elementName),
    );

    ctxs.push(
      ...possibleElementNames.map((elementName) =>
        registry.securityContext(elementName, propName, isAttribute),
      ),
    );
  });
  return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort();
}

/**
 * Compute a new ParseSourceSpan based off an original `sourceSpan` by using
 * absolute offsets from the specified `absoluteSpan`.
 *
 * @param sourceSpan original source span
 * @param absoluteSpan absolute source span to move to
 */
function moveParseSourceSpan(
  sourceSpan: ParseSourceSpan,
  absoluteSpan: AbsoluteSourceSpan,
): ParseSourceSpan {
  // The difference of two absolute offsets provide the relative offset
  const startDiff = absoluteSpan.start - sourceSpan.start.offset;
  const endDiff = absoluteSpan.end - sourceSpan.end.offset;
  return new ParseSourceSpan(
    sourceSpan.start.moveBy(startDiff),
    sourceSpan.end.moveBy(endDiff),
    sourceSpan.fullStart.moveBy(startDiff),
    sourceSpan.details,
  );
}
