/**
 * @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 {ParseLocation, ParseSourceSpan} from '../parse_util';

import {CssToken, CssTokenType} from './css_lexer';

export enum BlockType {
  Import,
  Charset,
  Namespace,
  Supports,
  Keyframes,
  MediaQuery,
  Selector,
  FontFace,
  Page,
  Document,
  Viewport,
  Unsupported
}

export interface CssAstVisitor {
  visitCssValue(ast: CssStyleValueAst, context?: any): any;
  visitCssInlineRule(ast: CssInlineRuleAst, context?: any): any;
  visitCssAtRulePredicate(ast: CssAtRulePredicateAst, context?: any): any;
  visitCssKeyframeRule(ast: CssKeyframeRuleAst, context?: any): any;
  visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAst, context?: any): any;
  visitCssMediaQueryRule(ast: CssMediaQueryRuleAst, context?: any): any;
  visitCssSelectorRule(ast: CssSelectorRuleAst, context?: any): any;
  visitCssSelector(ast: CssSelectorAst, context?: any): any;
  visitCssSimpleSelector(ast: CssSimpleSelectorAst, context?: any): any;
  visitCssPseudoSelector(ast: CssPseudoSelectorAst, context?: any): any;
  visitCssDefinition(ast: CssDefinitionAst, context?: any): any;
  visitCssBlock(ast: CssBlockAst, context?: any): any;
  visitCssStylesBlock(ast: CssStylesBlockAst, context?: any): any;
  visitCssStyleSheet(ast: CssStyleSheetAst, context?: any): any;
  visitCssUnknownRule(ast: CssUnknownRuleAst, context?: any): any;
  visitCssUnknownTokenList(ast: CssUnknownTokenListAst, context?: any): any;
}

export abstract class CssAst {
  constructor(public location: ParseSourceSpan) {}
  get start(): ParseLocation {
    return this.location.start;
  }
  get end(): ParseLocation {
    return this.location.end;
  }
  abstract visit(visitor: CssAstVisitor, context?: any): any;
}

export class CssStyleValueAst extends CssAst {
  constructor(location: ParseSourceSpan, public tokens: CssToken[], public strValue: string) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssValue(this);
  }
}

export abstract class CssRuleAst extends CssAst {
  constructor(location: ParseSourceSpan) {
    super(location);
  }
}

export class CssBlockRuleAst extends CssRuleAst {
  constructor(
      public location: ParseSourceSpan, public type: BlockType, public block: CssBlockAst,
      public name: CssToken|null = null) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssBlock(this.block, context);
  }
}

export class CssKeyframeRuleAst extends CssBlockRuleAst {
  constructor(location: ParseSourceSpan, name: CssToken, block: CssBlockAst) {
    super(location, BlockType.Keyframes, block, name);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssKeyframeRule(this, context);
  }
}

export class CssKeyframeDefinitionAst extends CssBlockRuleAst {
  constructor(location: ParseSourceSpan, public steps: CssToken[], block: CssBlockAst) {
    super(location, BlockType.Keyframes, block, mergeTokens(steps, ','));
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssKeyframeDefinition(this, context);
  }
}

export class CssBlockDefinitionRuleAst extends CssBlockRuleAst {
  constructor(
      location: ParseSourceSpan, public strValue: string, type: BlockType,
      public query: CssAtRulePredicateAst, block: CssBlockAst) {
    super(location, type, block);
    const firstCssToken: CssToken = query.tokens[0];
    this.name = new CssToken(
        firstCssToken.index, firstCssToken.column, firstCssToken.line, CssTokenType.Identifier,
        this.strValue);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssBlock(this.block, context);
  }
}

export class CssMediaQueryRuleAst extends CssBlockDefinitionRuleAst {
  constructor(
      location: ParseSourceSpan, strValue: string, query: CssAtRulePredicateAst,
      block: CssBlockAst) {
    super(location, strValue, BlockType.MediaQuery, query, block);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssMediaQueryRule(this, context);
  }
}

export class CssAtRulePredicateAst extends CssAst {
  constructor(location: ParseSourceSpan, public strValue: string, public tokens: CssToken[]) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssAtRulePredicate(this, context);
  }
}

export class CssInlineRuleAst extends CssRuleAst {
  constructor(location: ParseSourceSpan, public type: BlockType, public value: CssStyleValueAst) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssInlineRule(this, context);
  }
}

export class CssSelectorRuleAst extends CssBlockRuleAst {
  public strValue: string;

  constructor(location: ParseSourceSpan, public selectors: CssSelectorAst[], block: CssBlockAst) {
    super(location, BlockType.Selector, block);
    this.strValue = selectors.map(selector => selector.strValue).join(',');
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssSelectorRule(this, context);
  }
}

export class CssDefinitionAst extends CssAst {
  constructor(
      location: ParseSourceSpan, public property: CssToken, public value: CssStyleValueAst) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssDefinition(this, context);
  }
}

export abstract class CssSelectorPartAst extends CssAst {
  constructor(location: ParseSourceSpan) {
    super(location);
  }
}

export class CssSelectorAst extends CssSelectorPartAst {
  public strValue: string;
  constructor(location: ParseSourceSpan, public selectorParts: CssSimpleSelectorAst[]) {
    super(location);
    this.strValue = selectorParts.map(part => part.strValue).join('');
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssSelector(this, context);
  }
}

export class CssSimpleSelectorAst extends CssSelectorPartAst {
  constructor(
      location: ParseSourceSpan, public tokens: CssToken[], public strValue: string,
      public pseudoSelectors: CssPseudoSelectorAst[], public operator: CssToken) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssSimpleSelector(this, context);
  }
}

export class CssPseudoSelectorAst extends CssSelectorPartAst {
  constructor(
      location: ParseSourceSpan, public strValue: string, public name: string,
      public tokens: CssToken[], public innerSelectors: CssSelectorAst[]) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssPseudoSelector(this, context);
  }
}

export class CssBlockAst extends CssAst {
  constructor(location: ParseSourceSpan, public entries: CssAst[]) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssBlock(this, context);
  }
}

/*
 a style block is different from a standard block because it contains
 css prop:value definitions. A regular block can contain a list of Ast entries.
 */
export class CssStylesBlockAst extends CssBlockAst {
  constructor(location: ParseSourceSpan, public definitions: CssDefinitionAst[]) {
    super(location, definitions);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssStylesBlock(this, context);
  }
}

export class CssStyleSheetAst extends CssAst {
  constructor(location: ParseSourceSpan, public rules: CssAst[]) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssStyleSheet(this, context);
  }
}

export class CssUnknownRuleAst extends CssRuleAst {
  constructor(location: ParseSourceSpan, public ruleName: string, public tokens: CssToken[]) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssUnknownRule(this, context);
  }
}

export class CssUnknownTokenListAst extends CssRuleAst {
  constructor(location: ParseSourceSpan, public name: string, public tokens: CssToken[]) {
    super(location);
  }
  visit(visitor: CssAstVisitor, context?: any): any {
    return visitor.visitCssUnknownTokenList(this, context);
  }
}

export function mergeTokens(tokens: CssToken[], separator: string = ''): CssToken {
  const mainToken = tokens[0];
  let str = mainToken.strValue;
  for (let i = 1; i < tokens.length; i++) {
    str += separator + tokens[i].strValue;
  }

  return new CssToken(mainToken.index, mainToken.column, mainToken.line, mainToken.type, str);
}
