import { pull } from 'lodash'

export type JinjaExpression = string

export type VariableChangeListener = (value: any) => void

export class JinjaVariable<Metadata = any> {
  readonly name: string
  readonly path: string
  readonly metadata?: Metadata
  private value: any
  private listeners: VariableChangeListener[] = []
  
  constructor(name: string, value?: any, parent?: string, metadata?: Metadata) {
    this.name = name
    this.value = value
    this.path = parent ? `${parent}.${name}` : name
    this.metadata = metadata
  }

  get() {
    return this.value
  }

  set(value: any) {
    this.value = value
    this.listeners.forEach(l => l(value))
  }

  onChange(listener: VariableChangeListener): () => void {
    this.listeners.push(listener)
    return () => {
      pull(this.listeners, listener)
    }
  } 
}

export class JinjaObjectVariable<T> extends JinjaVariable {
  readonly children: Record<string, JinjaVariable>

  constructor(name: string, children: Record<string, JinjaVariable>, value?: T, parent?: string) {
    super(name, value, parent)
    this.children = children
    for (const key in children) {
      const child = children[key]
      // fixme: this can lead to a memory leak (crossed reference).
      // Ideally, this listener would be removed before tearing down this object.
      child.onChange(value => this.set({ ...this.get(), [child.name]: value }))
    }
  }
}

export class JinjaExpressionVariable<Metadata = any> extends JinjaVariable<Metadata> {
  /**
   * The value of the variable is actually a Jinja expression involving other variables. This may cause dependency cycles which
   * must be identified and throw before running.
   */
  readonly expression: JinjaExpression

  constructor(name: string, expression: string, parent?: string, metadata?: Metadata) {
    super(name, undefined, parent, metadata)
    this.expression = expression
  }
}
