import { Callable, Callable1, Control, Expression, GArray, GDictionary, GError, InputEvent, RichTextLabel, Variant } from 'godot'
import { export_, onready } from 'godot.annotations'
import AutocompleteLine from './autocomplete_line'
import { GArrayEnumerator } from '../../src/collection/enumerable'
import { Enumerable } from '../enumerable-ts/src/index'

export const Action = {
  Toggle: '_dev_console_toggle',
  Cancel: 'ui_cancel',
  Up: 'ui_up',
  Down: 'ui_down',
  TextComplete: 'ui_text_completion_replace',
  FocusNext: 'ui_focus_next',
  FocusPrevious: 'ui_focus_prev'
} as const

export default class DevConsole extends Control {
  @export_(Variant.Type.TYPE_BOOL)
  private pause_when_open: boolean = true

  @onready("VBoxContainer/Output")
  private output!: RichTextLabel

  @onready("VBoxContainer/Input")
  private input!: AutocompleteLine

  private history: string[] = []
  private expression = new Expression()
  private parse_callable!: Callable1<string>
  private history_index: number = -1

  _ready(): void {
    this._toggle()
    const method_names = Enumerable.from(new GArrayEnumerator(this.get_method_list()))
      .map((x: GDictionary) => x.get('name'))
      .filter((x: string) => !x.startsWith('_'))

    this.input.autocomplete_list = method_names.toArray()
    this.parse_callable = Callable.create(
      this,
      this._submit_command
    )
  }

  _input(event: InputEvent): void {
    if (event.is_action_pressed(Action.Toggle)) {
      this._toggle()
      return
    }

    if (!this.visible) {
      return
    }

    if (event.is_action_pressed(Action.Cancel)) {
      this._cancel()
    } else if (event.is_action_pressed(Action.Up)) {
      this._move_history(-1)
    } else if (event.is_action_pressed(Action.Down)) {
      this._move_history(1)
    }
  }

  _reset() {
    this.history_index = -1
  }

  _cancel() {
    this._reset()

    if (this.input.has_focus()) {
      this.input.release_focus()
    } else {
      this._hide()
    }
  }

  async _move_history(direction: (-1 | 1)) {
    this.history_index = (this.history_index + direction) % this.history.length

    if (this.history_index < 0) {
      this.history_index = this.history.length - 1
    }

    this.input.text = this.history[this.history_index]
    await this.get_tree().process_frame.as_promise()
    this.input.set_caret_to_end()
  }

  async _show() {
    await this.get_tree().process_frame.as_promise()
    if (this.pause_when_open) {
      this.get_tree().paused = true
    }

    this.input.text_submitted.connect(this.parse_callable)
    this.input.grab_focus()
  }

  _hide() {
    if (this.pause_when_open) {
      this.get_tree().paused = false
    }

    this.input.text_submitted.disconnect(this.parse_callable)
    this.input.release_focus()
  }

  async _toggle() {
    this._reset()

    this.visible = !this.visible

    if (this.visible) {
      this._show()
    } else {
      this._hide()
    }
  }

  _print(value: string) {
    this.output.append_text(value)
    this.output.append_text('\n')
  }

  _wrap_in_tag(text: string, tag: [string, string]): string {
    return `[${tag[0]}=${tag[1]}]${text}[/${tag[0]}]`
  }

  _format(text: string, tags: Record<string, string>): string {
    return Object.entries(tags).reduce(this._wrap_in_tag, text)
  }

  _print_error(error: string) {
    this._print(this._format(error, { color: 'red' }))
  }

  _echo(command: string) {
    this._print(this._format(`> ${command}`, { color: 'white' }))
  }

  _submit_command(command: string) {
    this.input.clear()
    this.history.push(command)
    this._echo(command)
    const error = this.expression.parse(command)

    if (error !== GError.OK) {
      this._print_error(this.expression.get_error_text())
      return
    }

    const result = this.expression.execute(new GArray(), this)

    if (this.expression.has_execute_failed()) {
      this._print_error(this.expression.get_error_text())
    } else {
      this._print(result.toString())
    }
  }

  clear() {
    this.output.clear()
  }

  echo(...args: any) {
    this._print(args.map((arg: any) => arg.toString()).join(' '))
  }
}