import {
  type DataTable,
  type DocString,
  type GherkinDocument,
  type IdGenerator,
  type Pickle,
  type PickleDocString,
  type PickleStep,
  type PickleStepArgument,
  PickleStepType,
  type PickleTable,
  type PickleTag,
  type Rule,
  type Scenario,
  type Step,
  StepKeywordType,
  type TableCell,
  type TableRow,
  type Tag,
} from '@cucumber/messages'

const pickleStepTypeFromKeyword: { [key in StepKeywordType]: PickleStepType } = {
  [StepKeywordType.UNKNOWN]: PickleStepType.UNKNOWN,
  [StepKeywordType.CONTEXT]: PickleStepType.CONTEXT,
  [StepKeywordType.ACTION]: PickleStepType.ACTION,
  [StepKeywordType.OUTCOME]: PickleStepType.OUTCOME,
  [StepKeywordType.CONJUNCTION]: null,
}

export default function compile(
  gherkinDocument: GherkinDocument,
  uri: string,
  newId: IdGenerator.NewId
): readonly Pickle[] {
  const pickles: Pickle[] = []

  if (gherkinDocument.feature == null) {
    return pickles
  }

  const feature = gherkinDocument.feature
  const language = feature.language
  const featureTags = feature.tags
  let featureBackgroundSteps: Step[] = []

  feature.children.forEach((stepsContainer) => {
    if (stepsContainer.background) {
      featureBackgroundSteps = [].concat(stepsContainer.background.steps)
    } else if (stepsContainer.rule) {
      compileRule(
        featureTags,
        featureBackgroundSteps,
        stepsContainer.rule,
        language,
        pickles,
        uri,
        newId
      )
    } else if (stepsContainer.scenario.examples.length === 0) {
      compileScenario(
        featureTags,
        featureBackgroundSteps,
        stepsContainer.scenario,
        language,
        pickles,
        uri,
        newId
      )
    } else {
      compileScenarioOutline(
        featureTags,
        featureBackgroundSteps,
        stepsContainer.scenario,
        language,
        pickles,
        uri,
        newId
      )
    }
  })
  return pickles
}

function compileRule(
  featureTags: readonly Tag[],
  featureBackgroundSteps: readonly Step[],
  rule: Rule,
  language: string,
  pickles: Pickle[],
  uri: string,
  newId: IdGenerator.NewId
) {
  let ruleBackgroundSteps = [].concat(featureBackgroundSteps)

  const tags = [].concat(featureTags).concat(rule.tags)

  rule.children.forEach((stepsContainer) => {
    if (stepsContainer.background) {
      ruleBackgroundSteps = ruleBackgroundSteps.concat(stepsContainer.background.steps)
    } else if (stepsContainer.scenario.examples.length === 0) {
      compileScenario(
        tags,
        ruleBackgroundSteps,
        stepsContainer.scenario,
        language,
        pickles,
        uri,
        newId
      )
    } else {
      compileScenarioOutline(
        tags,
        ruleBackgroundSteps,
        stepsContainer.scenario,
        language,
        pickles,
        uri,
        newId
      )
    }
  })
}

function compileScenario(
  inheritedTags: readonly Tag[],
  backgroundSteps: readonly Step[],
  scenario: Scenario,
  language: string,
  pickles: Pickle[],
  uri: string,
  newId: IdGenerator.NewId
) {
  let lastKeywordType = StepKeywordType.UNKNOWN
  const steps = [] as PickleStep[]

  if (scenario.steps.length !== 0) {
    backgroundSteps.forEach((step) => {
      lastKeywordType =
        step.keywordType === StepKeywordType.CONJUNCTION ? lastKeywordType : step.keywordType
      steps.push(pickleStep(step, [], null, newId, lastKeywordType))
    })
  }

  const tags = [].concat(inheritedTags).concat(scenario.tags)

  scenario.steps.forEach((step) => {
    lastKeywordType =
      step.keywordType === StepKeywordType.CONJUNCTION ? lastKeywordType : step.keywordType
    steps.push(pickleStep(step, [], null, newId, lastKeywordType))
  })

  const pickle: Pickle = {
    id: newId(),
    uri,
    location: scenario.location,
    astNodeIds: [scenario.id],
    tags: pickleTags(tags),
    name: scenario.name,
    language,
    steps,
  }
  pickles.push(pickle)
}

function compileScenarioOutline(
  inheritedTags: readonly Tag[],
  backgroundSteps: readonly Step[],
  scenario: Scenario,
  language: string,
  pickles: Pickle[],
  uri: string,
  newId: IdGenerator.NewId
) {
  scenario.examples
    .filter((e) => e.tableHeader)
    .forEach((examples) => {
      const variableCells = examples.tableHeader.cells
      examples.tableBody.forEach((valuesRow) => {
        let lastKeywordType = StepKeywordType.UNKNOWN
        const steps = [] as PickleStep[]
        if (scenario.steps.length !== 0) {
          backgroundSteps.forEach((step) => {
            lastKeywordType =
              step.keywordType === StepKeywordType.CONJUNCTION ? lastKeywordType : step.keywordType
            steps.push(pickleStep(step, [], null, newId, lastKeywordType))
          })
        }

        scenario.steps.forEach((scenarioOutlineStep) => {
          lastKeywordType =
            scenarioOutlineStep.keywordType === StepKeywordType.CONJUNCTION
              ? lastKeywordType
              : scenarioOutlineStep.keywordType
          const step = pickleStep(
            scenarioOutlineStep,
            variableCells,
            valuesRow,
            newId,
            lastKeywordType
          )
          steps.push(step)
        })

        const id = newId()
        const tags = pickleTags(
          [].concat(inheritedTags).concat(scenario.tags).concat(examples.tags)
        )

        pickles.push({
          id,
          uri,
          location: valuesRow.location,
          astNodeIds: [scenario.id, valuesRow.id],
          name: interpolate(scenario.name, variableCells, valuesRow.cells),
          language,
          steps,
          tags,
        })
      })
    })
}

function pickleDocString(
  argumentIndex: number,
  argument: DocString,
  variableCells: readonly TableCell[],
  valueCells: readonly TableCell[]
): PickleDocString {
  return {
    argumentIndex,
    content: interpolate(argument.content, variableCells, valueCells),
    mediaType: argument.mediaType
      ? interpolate(argument.mediaType, variableCells, valueCells)
      : undefined,
  }
}

function pickleTable(
  argumentIndex: number,
  argument: DataTable,
  variableCells: readonly TableCell[],
  valueCells: readonly TableCell[]
): PickleTable {
  return {
    argumentIndex,
    rows: argument.rows.map((row) => {
      return {
        cells: row.cells.map((cell) => {
          return {
            value: interpolate(cell.value, variableCells, valueCells),
          }
        }),
      }
    }),
  }
}

function createPickleArguments(
  step: Step,
  variableCells: readonly TableCell[],
  valueCells: readonly TableCell[]
): PickleStepArgument | undefined {
  if (step.dataTable && step.docString) {
    const tableFirst = step.docString.location.line > step.dataTable.location.line
    return {
      docString: pickleDocString(tableFirst ? 2 : 1, step.docString, variableCells, valueCells),
      dataTable: pickleTable(tableFirst ? 1 : 2, step.dataTable, variableCells, valueCells),
    }
  } else if (step.dataTable) {
    return {
      dataTable: pickleTable(undefined, step.dataTable, variableCells, valueCells),
    }
  } else if (step.docString) {
    return {
      docString: pickleDocString(undefined, step.docString, variableCells, valueCells),
    }
  }

  return undefined
}

function interpolate(
  name: string,
  variableCells: readonly TableCell[],
  valueCells: readonly TableCell[]
) {
  variableCells.forEach((variableCell, n) => {
    const valueCell = valueCells[n]
    const valuePattern = `<${variableCell.value}>`
    const escapedPattern = valuePattern.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
    const regexp = new RegExp(escapedPattern, 'g')
    // JS Specific - dollar sign needs to be escaped with another dollar sign
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
    const replacement = valueCell.value.replace(/\$/g, '$$$$')
    name = name.replace(regexp, replacement)
  })
  return name
}

function pickleStep(
  step: Step,
  variableCells: readonly TableCell[],
  valuesRow: TableRow | null,
  newId: IdGenerator.NewId,
  keywordType: StepKeywordType
): PickleStep {
  const astNodeIds = [step.id]
  if (valuesRow) {
    astNodeIds.push(valuesRow.id)
  }
  const valueCells = valuesRow ? valuesRow.cells : []

  return {
    id: newId(),
    text: interpolate(step.text, variableCells, valueCells),
    type: pickleStepTypeFromKeyword[keywordType],
    argument: createPickleArguments(step, variableCells, valueCells),
    astNodeIds: astNodeIds,
  }
}

function pickleTags(tags: Tag[]): readonly PickleTag[] {
  return tags.map(pickleTag)
}

function pickleTag(tag: Tag): PickleTag {
  return {
    name: tag.name,
    astNodeId: tag.id,
  }
}
