require 'thread'
require 'strscan'
require 'pty'

class VER::Buffer::Terminal
  def initialize(parent)
    @parent = parent
    setup_widgets
    pty
  end

  def bench(name, &block)
    require 'benchmark'
    p name => Benchmark.realtime(&block)
  end

  def setup_widgets
    Tk.set_palette('black') # maybe use themes instead?

    @option_cache ||= {}
    @font ||= VER.options[:font]
    @font_actual ||= @font.actual_hash

    @text = Tk::Text.new(font: @font, insertofftime: 1)
    @text.pack expand: true, fill: :both
    @text.focus

    @text.bind('<Key>') do |event|
      @pty_queue << event.unicode
      Tk.callback_break
    end

    @text.bind('<Return>') do |event|
      @pty_queue << event.unicode
      # Tk.callback_break
    end
  end

  def destroy
    @text.destroy
  rescue Errno::EIO, PTY::ChildExited
  ensure
    @parent.focus
  end

  def pty
    @pty_queue = queue = Queue.new

    Thread.new do
      shell = ENV['SHELL'] || 'bash'

      PTY.spawn(shell) do |r_pty, w_pty, _pid|
        Thread.new do
          while chunk = queue.shift
            w_pty.print chunk
            w_pty.flush
          end
        end

        begin
          loop do
            c = r_pty.sysread(1 << 15)
            on_chunk(c) if c
          end
        rescue Errno::EIO, PTY::ChildExited
          destroy
        end
      end
    end
  rescue Errno::EIO, PTY::ChildExited
    destroy
  end

  ANSI_CODES = []
  ANSI_CODES[0]  = :reset
  ANSI_CODES[1]  = :bold
  ANSI_CODES[2]  = :dark
  ANSI_CODES[3]  = :italic
  ANSI_CODES[4]  = :underline
  ANSI_CODES[5]  = :blink
  ANSI_CODES[6]  = :rapid_blink
  ANSI_CODES[7]  = :reverse
  ANSI_CODES[8]  = :concealed
  ANSI_CODES[9]  = :strikethrough
  ANSI_CODES[30] = :black
  ANSI_CODES[31] = :red
  ANSI_CODES[32] = :green
  ANSI_CODES[33] = :yellow
  ANSI_CODES[34] = :blue
  ANSI_CODES[35] = :magenta
  ANSI_CODES[36] = :cyan
  ANSI_CODES[37] = :white
  ANSI_CODES[40] = :on_black
  ANSI_CODES[41] = :on_red
  ANSI_CODES[42] = :on_green
  ANSI_CODES[43] = :on_yellow
  ANSI_CODES[44] = :on_blue
  ANSI_CODES[45] = :on_magenta
  ANSI_CODES[46] = :on_cyan
  ANSI_CODES[47] = :on_white

  # Trying to build a little dictionary of escape patterns:
  #
  # | Sequence       | Meaning         |
  # | \e]0;          | reset fg/bg     |
  # | \e[?1034h      | ???             |
  # | \e[01;34mbin\e | fg: 1, bg: 34   |

  def on_chunk(chunk)
    @buffer ||= ''
    @buffer << chunk
    s = StringScanner.new(@buffer)

    until s.eos?
      pos = s.pos

      case s.peek(1)
      when "\e"
        if    s.scan(/\e\](\d+);/)
          color s[1]
        elsif s.scan(/\e\[(\d+)([A-Z])/)
          color s[1]
        elsif s.scan(/\e\[\?(\d+h)/)
          color s[1]
        elsif s.scan(/\e\[(\d+);(\d+)m/) # \e[01;34m
          color s[1], s[2]
        elsif s.scan(/\e\[(\d+)[m;]/)
          color s[1]
        elsif s.scan(/\e\[([A-Z])/)
          color s[1]
        elsif s.scan(/\e\[m/)
          # nothing?
        elsif s.scan(/\e=\r/)
          delete 'insert linestart', 'insert lineend'
        else
          p s.rest
        end
      when "\r"
        if s.scan(/\r\n/)
          insert :end, "\n"
        elsif s.scan(/\r\r/)
          delete 'insert linestart', 'insert lineend'
        elsif s.scan(/\r[^\n\r]/)
          p no_rn: s.matched
        else
          p r_fail: s.rest
        end
      when "\x07"
        s.pos += 1
        delete 'insert linestart', 'insert lineend'
      when "\x08"
        s.pos += 1
        delete 'insert - 1 chars'
      else
        insert :end, s.matched if s.scan(/\A[^\e\r\x08\x07]+/)
      end
      #  p s.matched

      next unless s.pos == pos
      warn(format('Scanner stopped at: %p', s))
      @buffer = s.rest
      return
    end

    @buffer = ''
    @text.see :end
  rescue StringScanner::Error => ex
    VER.error(ex)
  rescue StandardError => ex
    puts "#{ex.class}: #{ex}", *ex.backtrace
    @buffer = ''
    destroy
  end

  def color(fg, bg = nil)
    if bg
      fg = ANSI_CODES[fg.to_i]
      bg = ANSI_CODES[bg.to_i]

      @tag = [:ansi, fg, bg].join('_')
      @text.tag_configure @tag, options_for(@tag, fg, bg)
    else
      fg = ANSI_CODES[fg.to_i]

      @tag = [:ansi, fg].join('_')
      @text.tag_configure @tag, options_for(@tag, fg)
    end
  end

  FG_COLORS = %i[black red green yellow blue magenta cyan white]
  BG_COLORS = %i[on_black on_red on_green on_yellow on_blue on_magenta on_cyan on_white]

  def options_for(tag, fg, bg = nil)
    if found = @option_cache[tag]
      return found
    end

    options = {}

    case fg
    when nil
    when :reset
      options[:font] = @font
      options[:background] = nil
      options[:foreground] = nil
    when :bold
      options[:font] = font_for(@font_actual.merge(weight: :bold))
    when *FG_COLORS
      options[:foreground] = fg
    when *BG_COLORS
      options[:background] = bg.to_s.sub(/on_/, '')
    else
      p fg: fg
    end

    case bg
    when nil
    when :reset
      options[:font] = @font
      options[:background] = nil
      options[:foreground] = nil
    when *FG_COLORS
      options[:foreground] = bg
    when *BG_COLORS
      options[:background] = bg.to_s.sub(/on_/, '')
    else
      p bg: bg
    end

    @option_cache[options] = options
    options
  end

  def font_for(options)
    @fonts ||= {}
    @fonts[options] ||= Tk::Font.new(options)
  end

  def ansi2sym(ansi)
    COLORS[ansi.to_i]
  end

  def insert(index, chars)
    # p insert: [index, chars]
    @text.insert(index, chars, @tag)
  end

  def delete(*indices)
    # p delete: indices
    @text.delete(*indices)
  end
end
