/* eslint-disable @typescript-eslint/no-this-alias */
import { drop, findLast, pull } from 'lodash'
import { FormObjectVariable, FormRepeatableVariable } from '../form/FormVariable'
import { JinjaContextValue } from './JinjaContextValue'
import { JinjaObjectVariable, JinjaVariable, VariableChangeListener } from './JinjaVariable'

function getVariableInPath(variable: JinjaVariable, path: string[]): JinjaVariable {
  const [head, ...tail] = path
  if (!head) return tail.length ? getVariableInPath(variable, tail) : variable
  if (variable instanceof FormRepeatableVariable) {
    return getVariableInPath(variable.condensed, path)
  }
  if (variable instanceof FormObjectVariable || variable instanceof JinjaObjectVariable) {
    const child = variable.children[head]
    if (!child || (Array.isArray(child) && !child.length)) return variable
    return getVariableInPath(Array.isArray(child) ? findLast(child, v => v.getState().visible)! : child, tail)
  }
  return variable
}

function contextHasKey(context: ContextValue, key: string) {
  return (context instanceof JinjaContextValue && context.prefix === key) ? true : context.has(key)
}

export abstract class ContextValue {
  readonly name?: string
  readonly abstract parent?: ContextValue
  private changeVariableSetupListeners: (() => void)[] = []

  constructor(name?: string) {
    this.name = name
  }

  protected abstract findVariableByKey(name: string): JinjaVariable | undefined

  protected createKey(key: string): string {
    return String(key)
  }

  has(key: string): boolean {
    return !!this.findVariableByKey(key)
  }

  getVariable(path: string, ignore?: string[]): JinjaVariable | undefined {
    let current: ContextValue | undefined = this
    path = path.trim()
    
    let [head, ...tail] = path.split(/[.[\]]/)
    while (current && (ignore?.includes(current.name ?? '') || !contextHasKey(current, head))) current = current.parent
    if (current instanceof JinjaContextValue && current.prefix) {
      if (current.prefix !== head) return
      head = tail[0]
      tail = drop(tail)
    }
    const variable = current?.findVariableByKey(head)
    return variable ? getVariableInPath(variable, tail) : undefined
  }

  get(key: string, ignore?: string[]): any {
    return this.getVariable(key, ignore)?.get()
  }

  abstract getVariableInContext(key: string): JinjaVariable | undefined

  abstract getAllInContext(): any

  getInContext(key: string): any {
    return this.getVariableInContext(key)?.get()
  }

  getAll(ignore?: string[]): Record<string, any> {
    let current: ContextValue | undefined = this
    const branch: ContextValue[] = []
    while (current) {
      if (!ignore?.includes(current.name ?? '')) branch.push(current)
      current = current.parent
    }
    let variables: Record<string, any> = {}
    for (let i = branch.length - 1; i >= 0; i--) {
      variables = { ...variables, ...branch[i].getAll() }
    }
    return variables
  }

  set(key: string, value: any): void {
    this.findVariableByKey(this.createKey(key))?.set(value)
  }

  onChange(key: string, listener: VariableChangeListener): () => void {
    return this.findVariableByKey(this.createKey(key))?.onChange(listener) ?? (() => {})
  }

  closest(name: string): ContextValue | undefined {
    let current: ContextValue | undefined = this
    while (current && current.name !== name) current = current.parent
    return current
  }

  abstract forEachVariable(iteratee: (v: JinjaVariable) => void): void

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  protected addVariablesToContext(_variables: JinjaVariable[]): void {
    // this looks like a bug in Typescript. Declaring protected abstract methods don't work.    
  }

  onChangeVariableSetup(listener: () => void) {
    this.changeVariableSetupListeners.push(listener)
    return () => {
      pull(this.changeVariableSetupListeners, listener)
    }
  }

  addVariables(variables: JinjaVariable[]) {
    this.addVariablesToContext(variables)
    this.changeVariableSetupListeners.forEach(l => l())
  }
}
