import DIALECTS from './gherkin-languages.json'
import Dialect from './Dialect'
import { NoSuchLanguageException } from './Errors'
import { messages } from 'cucumber-messages'
import Token from './Token'
import { TokenType } from './Parser'

const DIALECT_DICT: { [key: string]: Dialect } = DIALECTS
const LANGUAGE_PATTERN = /^\s*#\s*language\s*:\s*([a-zA-Z\-_]+)\s*$/

export default class TokenMatcher {
  private dialect: Dialect
  private dialectName: string
  private activeDocStringSeparator: string
  private indentToRemove: number

  constructor(private readonly defaultDialectName: string = 'en') {
    this.reset()
  }

  public changeDialect(newDialectName: string, location?: messages.ILocation) {
    const newDialect = DIALECT_DICT[newDialectName]
    if (!newDialect) {
      throw NoSuchLanguageException.create(newDialectName, location)
    }

    this.dialectName = newDialectName
    this.dialect = newDialect
  }

  public reset() {
    if (this.dialectName !== this.defaultDialectName) {
      this.changeDialect(this.defaultDialectName)
    }
    this.activeDocStringSeparator = null
    this.indentToRemove = 0
  }

  public match_TagLine(token: Token) {
    if (token.line.startsWith('@')) {
      this.setTokenMatched(
        token,
        TokenType.TagLine,
        null,
        null,
        null,
        token.line.getTags()
      )
      return true
    }
    return false
  }

  public match_FeatureLine(token: Token) {
    return this.matchTitleLine(
      token,
      TokenType.FeatureLine,
      this.dialect.feature
    )
  }

  public match_ScenarioLine(token: Token) {
    return (
      this.matchTitleLine(
        token,
        TokenType.ScenarioLine,
        this.dialect.scenario
      ) ||
      this.matchTitleLine(
        token,
        TokenType.ScenarioLine,
        this.dialect.scenarioOutline
      )
    )
  }

  public match_BackgroundLine(token: Token) {
    return this.matchTitleLine(
      token,
      TokenType.BackgroundLine,
      this.dialect.background
    )
  }

  public match_ExamplesLine(token: Token) {
    return this.matchTitleLine(
      token,
      TokenType.ExamplesLine,
      this.dialect.examples
    )
  }

  public match_RuleLine(token: Token) {
    return this.matchTitleLine(token, TokenType.RuleLine, this.dialect.rule)
  }

  public match_TableRow(token: Token) {
    if (token.line.startsWith('|')) {
      // TODO: indent
      this.setTokenMatched(
        token,
        TokenType.TableRow,
        null,
        null,
        null,
        token.line.getTableCells()
      )
      return true
    }
    return false
  }

  public match_Empty(token: Token) {
    if (token.line.isEmpty) {
      this.setTokenMatched(token, TokenType.Empty, null, null, 0)
      return true
    }
    return false
  }

  public match_Comment(token: Token) {
    if (token.line.startsWith('#')) {
      const text = token.line.getLineText(0) // take the entire line, including leading space
      this.setTokenMatched(token, TokenType.Comment, text, null, 0)
      return true
    }
    return false
  }

  public match_Language(token: Token) {
    const match = token.line.trimmedLineText.match(LANGUAGE_PATTERN)
    if (match) {
      const newDialectName = match[1]
      this.setTokenMatched(token, TokenType.Language, newDialectName)

      this.changeDialect(newDialectName, token.location)
      return true
    }
    return false
  }

  public match_DocStringSeparator(token: Token) {
    return this.activeDocStringSeparator == null
      ? // open
        this._match_DocStringSeparator(token, '"""', true) ||
          this._match_DocStringSeparator(token, '```', true)
      : // close
        this._match_DocStringSeparator(
          token,
          this.activeDocStringSeparator,
          false
        )
  }

  public _match_DocStringSeparator(
    token: Token,
    separator: string,
    isOpen: boolean
  ) {
    if (token.line.startsWith(separator)) {
      let contentType = null
      if (isOpen) {
        contentType = token.line.getRestTrimmed(separator.length)
        this.activeDocStringSeparator = separator
        this.indentToRemove = token.line.indent
      } else {
        this.activeDocStringSeparator = null
        this.indentToRemove = 0
      }

      // TODO: Use the separator as keyword. That's needed for pretty printing.
      this.setTokenMatched(token, TokenType.DocStringSeparator, contentType)
      return true
    }
    return false
  }

  public match_EOF(token: Token) {
    if (token.isEof) {
      this.setTokenMatched(token, TokenType.EOF)
      return true
    }
    return false
  }

  public match_StepLine(token: Token) {
    const keywords = []
      .concat(this.dialect.given)
      .concat(this.dialect.when)
      .concat(this.dialect.then)
      .concat(this.dialect.and)
      .concat(this.dialect.but)
    for (const keyword of keywords) {
      if (token.line.startsWith(keyword)) {
        const title = token.line.getRestTrimmed(keyword.length)
        this.setTokenMatched(token, TokenType.StepLine, title, keyword)
        return true
      }
    }
    return false
  }

  public match_Other(token: Token) {
    const text = token.line.getLineText(this.indentToRemove) // take the entire line, except removing DocString indents
    this.setTokenMatched(
      token,
      TokenType.Other,
      this.unescapeDocString(text),
      null,
      0
    )
    return true
  }

  private matchTitleLine(
    token: Token,
    tokenType: TokenType,
    keywords: readonly string[]
  ) {
    for (const keyword of keywords) {
      if (token.line.startsWithTitleKeyword(keyword)) {
        const title = token.line.getRestTrimmed(keyword.length + ':'.length)
        this.setTokenMatched(token, tokenType, title, keyword)
        return true
      }
    }
    return false
  }

  private setTokenMatched(
    token: Token,
    matchedType: TokenType,
    text?: string,
    keyword?: string,
    indent?: number,
    items?: any[]
  ) {
    token.matchedType = matchedType
    token.matchedText = text
    token.matchedKeyword = keyword
    token.matchedIndent =
      typeof indent === 'number'
        ? indent
        : token.line == null
        ? 0
        : token.line.indent
    token.matchedItems = items || []

    token.location.column = token.matchedIndent + 1
    token.matchedGherkinDialect = this.dialectName
  }

  private unescapeDocString(text: string) {
    return this.activeDocStringSeparator != null
      ? text.replace('\\"\\"\\"', '"""')
      : text
  }
}
