# frozen_string_literal: true

require_relative "rkremap/version"
require_relative 'rkremap/keycode'
require_relative 'rkremap/winattr'
require_relative 'rkremap/evdev'

class Rkremap
  include KeyCode

  EVENT_TYPE_VALUE = {
    release: 0,
    press:   1,
    repeat:  2,
  }

  attr_accessor :grab
  attr_accessor :modifiers
  attr_accessor :x11
  attr_accessor :auto_detect

  # @param devices [Array<String>, String]
  def initialize(devices=nil)
    if devices
      devices = Array(devices)
      @inputs = devices.map{|d| Evdev.new(d)}
    else
      @auto_detect = true
      @inputs = Evdev.detect.select(&:keyboard?)
    end
    raise 'Unable to detect keyboard device' if @inputs.empty?
    @uinput = Uinput.new
    @grab = false
    @x11 = false
    @modifiers = [
      KEY_LEFTCTRL,  KEY_RIGHTCTRL,
      KEY_LEFTSHIFT, KEY_RIGHTSHIFT,
      KEY_LEFTALT,   KEY_RIGHTALT,
      KEY_LEFTMETA,  KEY_RIGHTMETA,
    ]
    @events = []
    @mutex = Mutex.new
  end

  # @param code [Integer]
  # @param type [Symbol] :press / :release / :repeat
  def match(code: nil, type: nil, &block)
    @events.push [{code: code, type: type}, block]
  end

  def start(&block)
    detect_device_loop if @auto_detect
    @mod_state = @modifiers.map.to_h{|m| [m, false]}
    @winattr = WinAttr.new if @x11
    @inputs.each(&:grab) if @grab
    while true
      @keys = []
      time, type, code, value = read_event
      next if type != EV_KEY
      event = Event.new(time, code, value, @winattr)
      @events.each do |cond, b|
        synchronize{ b.call event } if event.match?(**cond)
        break if event.skipped
      end
      next if event.skipped
      synchronize{ proc_event(code, value, event) }
      @keys.each do |c, mod, app|
        synchronize{ block.call(c, mod, app) } if block
      end
    end
  end

  def detect_device_loop
    Thread.new do
      while true
        sleep 3
        new_devs = Evdev.detect.select(&:keyboard?)
        unless new_devs.empty?
          new_devs.each(&:grab) if @grab
          @inputs += new_devs
        end
      end
    end
  end

  def read_event
    while true
      begin
        input = Evdev.select(@inputs, 3)
        next unless input
        return input.read_event
      rescue Errno::ENODEV
        input.close rescue nil
        @inputs.delete input
      end
    end
  end

  def synchronize(&block)
    @mutex.synchronize(&block)
  end

  # @param code [Integer]
  # @param type [Symbol] :press / :release / :repeat
  # @param ev [Rkmap::Event]
  def event(code: nil, type: nil, event: nil)
    value = EVENT_TYPE_VALUE[type] or raise "invalid type: #{type.inspect}"
    proc_event(code, value, event)
  end

  # @param code [Integer]
  # @param mod [Hash] {MOD_KEY => true, ...}
  def key(code, mod={})
    mod_diff(mod).each do |mcode, state|
      write_event(mcode, state ? 1 : 0)
    end
    write_event(code, 1)
    write_event(code, 0)
    mod_diff(mod).each do |mcode, _|
      write_event(mcode, @mod_state[mcode] ? 1 : 0)
    end
  end

  private

  def proc_event(code, value, event)
    if @mod_state.include?(code)
      @mod_state[code] = value != 0
      write_event(code, value)
    elsif value != 0
      @keys.push [code, @mod_state.dup, App.new(event&.win, @winattr)]
    end
  end

  def mod_diff(mod)
    mod.select{|k, v| @mod_state[k] != v}
  end

  # @param code [Integer]
  # @param value [Integer] 0:release / 1:press / 2:repeat
  def write_event(code, value)
    @uinput.write_event(EV_KEY, code, value)
    @uinput.write_event(EV_SYN, SYN_REPORT, 0)
  end

  class Event
    attr_reader :time
    attr_reader :code
    attr_reader :type
    attr_reader :win
    attr_reader :skipped

    def initialize(time, code, value, winattr)
      @time = time
      @code = code
      @value = value
      @type = value == 1 ? :press : value == 0 ? :release : :repeat
      @win = winattr&.focus_win
      @skipped = false
    end

    def match?(code: nil, type: nil)
      (code.nil? || Array(code).include?(@code)) && (type.nil? || Array(type).include?(@type))
    end

    def skip
      @skipped = true
    end
  end

  class App
    def initialize(win, winattr)
      @win = win
      @winattr = winattr
    end

    def class_name
      @winattr.app_win(@win)[2] if @winattr && @win
    end

    def title
      @winattr.app_title(@win) if @winattr && @win
    end
  end
end
