# frozen_string_literal: true
class Object
  undef :type if respond_to?(:type)
end

module Tk
  @register = Hash.new(0)
  @widgets = {}
  @callbacks = {}
  @mutex = Mutex.new

  class << self
    attr_reader :callbacks, :widgets
  end

  module_function

  def init
    @interp = if RUN_EVENTLOOP_ON_MAIN_THREAD
                FFI::Tcl.setup_eventloop_on_main_thread
              else
                FFI::Tcl.setup_eventloop_on_new_thread
              end

    FFI::Tcl.init(@interp)
    FFI::Tcl::EvalResult.reset_types(@interp)
    FFI::Tk.init(@interp)

    @root = Root.new

    @interp.eval('namespace eval RubyFFI {}')

    FFI::Tcl.create_obj_command(@interp, 'RubyFFI::callback', TCL_CALLBACK, 0, TCL_DELETE)
    FFI::Tcl.create_obj_command(@interp, 'RubyFFI::event',    TCL_EVENT,    0, TCL_DELETE)

    module_eval('class << Tk; attr_reader :interp, :root; end')

    @interp
  end

  # A little something so people don't have to call Tk.init
  def interp
    Tk.init
    @interp
  end

  # A little something so people don't have to call Tk.init
  def root
    Tk.init
    @root
  end

  def mainloop
    @running = true

    interp.do_one_event(0) while @running && interp.wait_for_event(0.05)
  end

  def stop
    @running = false
  end

  def eval(string)
    interp.eval(string)
  end

  def execute_only(*args)
    interp.eval(convert_arguments(*args))
  end

  def execute(*args)
    interp.eval(convert_arguments(*args))
    result.tap{|r| puts "eval= %p" % [r] if $DEBUG }
  end

  def result
    interp.guess_result
  end

  def exit
    execute('exit')
  end

  def update(idletasks = None)
    execute('update', idletasks)
  end

  def callback_break
    throw :callback_break
  end

  def callback_continue
    throw :callback_continue
  end

  # without our callbacks, nothing goes anymore, abort mission
  def tcl_delete(_client_data)
    raise 'tcl function is going to be removed'
  end
  TCL_DELETE = method(:tcl_delete)

  # TODO: support for break and continue return status (by catch/throw)
  # 1 means true, 0 means false.
  def tcl_callback(_client_data, interp, objc, objv)
    cmd, id, *args = tcl_cmd_args(interp, objc, objv)
    id = id.first if id.is_a?(Array)

    catch :callback_break do
      catch :callback_continue do
        result = handle_callback(id, *args)
        FFI::Tcl::Interp.new(interp).obj_result = result

        return OK
      end

      return CONTINUE
    end

    return BREAK
  rescue => ex
    FFI::Tcl::Interp.new(interp).obj_result = ex
    return ERROR
  end
  TCL_CALLBACK = method(:tcl_callback)

  # TODO: support for break and continue return status (by catch/throw)
  def tcl_event(_client_data, interp, objc, objv)
    cmd, id, sequence, *args = tcl_cmd_args(interp, objc, objv)

    catch :callback_break do
      catch :callback_continue do
        Event::Data.new(id.to_i, sequence.to_s, *args).call

        return OK
      end

      return CONTINUE
    end

    return BREAK
  rescue => ex
    FFI::Tcl::Interp.new(interp).obj_result = ex
    return ERROR
  end
  TCL_EVENT = method(:tcl_event)

  def tcl_cmd_args(interp, objc, objv)
    length = FFI::MemoryPointer.new(0)
    array = objv.read_array_of_pointer(objc)
    array.map do |e|
      obj = FFI::Tcl::EvalResult.guess(interp, e)
      case obj
      when Integer, Float
        obj
      when nil
        nil
      else
        obj.respond_to?(:dup) ? obj.dup : obj
      end
    end
  end

  def handle_callback(id, *args)
    callback = @callbacks.fetch(id.to_i)
    callback.call(*args)
  end

  def register_object(parent, object)
    parent_name = parent.respond_to?(:tk_pathname) ? parent.tk_pathname : parent
    cmd = object.class.tk_command

    id = "#{cmd}#{uuid(cmd)}".gsub('::', '__') # :: is namespace convention.
    pathname = [parent_name, id].join('.').squeeze('.')
    @widgets[pathname] = object

    pathname
  end

  def unregister_object(object)
    @widgets.delete_if { |_path, obj| obj == object }
  end

  def unregister_objects(*objects)
    @widgets.delete_if { |_path, obj| objects.include?(obj) }
  end

  def register_proc(proc, argument_string = '')
    id = uuid(:proc) { |uuid| @callbacks[uuid] = proc }
    [id, %(RubyFFI::callback #{id} #{argument_string})]
  end

  def unregister_proc(id)
    @callbacks.delete(id)
  end

  def uuid(name)
    @mutex.synchronize do
      id = @register[name]
      @register[name] += 1
      yield id if block_given?
      id
    end
  end

  def boolean(obj)
    boolean_pointer = FFI::MemoryPointer.new(:int)
    FFI::Tcl.get_boolean(interp, obj.to_s, boolean_pointer)
    boolean_pointer.get_int(0) == 1
  end

  def convert_arguments(*args)
    args.map(&:to_tcl).compact.join(' ')
  end

  def pathname_to_widget(pathname)
    @widgets[pathname]
  end
end
