# frozen_string_literal: false
#
#  tk/event.rb - module for event
#

module TkEvent
end

########################

require 'tkutil'
require 'tk' unless Object.const_defined? :TkComm

########################

module TkEvent
  class Event < TkUtil::CallbackSubst
    module Grp
      KEY         =           0x1
      BUTTON      =           0x2
      MOTION      =           0x4
      CROSSING    =           0x8
      FOCUS       =           0x10
      EXPOSE      =           0x20
      VISIBILITY  =           0x40
      CREATE      =           0x80
      DESTROY     =           0x100
      UNMAP       =           0x200
      MAP         =           0x400
      REPARENT    =           0x800
      CONFIG      =           0x1000
      GRAVITY     =           0x2000
      CIRC        =           0x4000
      PROP        =           0x8000
      COLORMAP    =           0x10000
      VIRTUAL     =           0x20000
      ACTIVATE    =           0x40000
      MAPREQ      =           0x80000
      CONFIGREQ   =           0x100000
      RESIZEREQ   =           0x200000
      CIRCREQ     =           0x400000

      MWHEEL      =           KEY

      STRING_DATA =           0x80000000  # special flag for 'data' field

      ALL         =           0xFFFFFFFF

      KEY_BUTTON_MOTION_VIRTUAL  = (KEY|MWHEEL|BUTTON|MOTION|VIRTUAL)
      KEY_BUTTON_MOTION_CROSSING = (KEY|MWHEEL|BUTTON|MOTION|CROSSING|VIRTUAL)
    end

    type_data = [
      #-----+-------------------+------------------+-----------------------#
      #  ID |  const            |  group_flag      |  context_name         #
      #-----+-------------------+------------------+-----------------------#
      [  2,  :KeyPress,          Grp::KEY,         'KeyPress',    'Key'    ],
      [  3,  :KeyRelease,        Grp::KEY,         'KeyRelease'            ],
      [  4,  :ButtonPress,       Grp::BUTTON,      'ButtonPress', 'Button' ],
      [  5,  :ButtonRelease,     Grp::BUTTON,      'ButtonRelease'         ],
      [  6,  :MotionNotify,      Grp::MOTION,      'Motion'                ],
      [  7,  :EnterNotify,       Grp::CROSSING,    'Enter'                 ],
      [  8,  :LeaveNotify,       Grp::CROSSING,    'Leave'                 ],
      [  9,  :FocusIn,           Grp::FOCUS,       'FocusIn'               ],
      [ 10,  :FocusOut,          Grp::FOCUS,       'FocusOut'              ],
      [ 11,  :KeymapNotify,      0,                                        ],
      [ 12,  :Expose,            Grp::EXPOSE,      'Expose'                ],
      [ 13,  :GraphicsExpose,    Grp::EXPOSE,                              ],
      [ 14,  :NoExpose,          0,                                        ],
      [ 15,  :VisibilityNotify,  Grp::VISIBILITY,  'Visibility'            ],
      [ 16,  :CreateNotify,      Grp::CREATE,      'Create'                ],
      [ 17,  :DestroyNotify,     Grp::DESTROY,     'Destroy'               ],
      [ 18,  :UnmapNotify,       Grp::UNMAP,       'Unmap'                 ],
      [ 19,  :MapNotify,         Grp::MAP,         'Map'                   ],
      [ 20,  :MapRequest,        Grp::MAPREQ,      'MapRequest'            ],
      [ 21,  :ReparentNotify,    Grp::REPARENT,    'Reparent'              ],
      [ 22,  :ConfigureNotify,   Grp::CONFIG,      'Configure'             ],
      [ 23,  :ConfigureRequest,  Grp::CONFIGREQ,   'ConfigureRequest'      ],
      [ 24,  :GravityNotify,     Grp::GRAVITY,     'Gravity'               ],
      [ 25,  :ResizeRequest,     Grp::RESIZEREQ,   'ResizeRequest'         ],
      [ 26,  :CirculateNotify,   Grp::CIRC,        'Circulate'             ],
      [ 27,  :CirculateRequest,  0,                'CirculateRequest'      ],
      [ 28,  :PropertyNotify,    Grp::PROP,        'Property'              ],
      [ 29,  :SelectionClear,    0,                                        ],
      [ 30,  :SelectionRequest,  0,                                        ],
      [ 31,  :SelectionNotify,   0,                                        ],
      [ 32,  :ColormapNotify,    Grp::COLORMAP,    'Colormap'              ],
      [ 33,  :ClientMessage,     0,                                        ],
      [ 34,  :MappingNotify,     0,                                        ],
      [ 35,  :VirtualEvent,      Grp::VIRTUAL,                             ],
      [ 36,  :ActivateNotify,    Grp::ACTIVATE,    'Activate'              ],
      [ 37,  :DeactivateNotify,  Grp::ACTIVATE,    'Deactivate'            ],
      [ 38,  :MouseWheelEvent,   Grp::MWHEEL,      'MouseWheel'            ],
      [ 39,  :TK_LASTEVENT,      0,                                        ]
    ]

    module TypeNum
    end

    TYPE_NAME_TBL  = Hash.new
    TYPE_ID_TBL    = Hash.new
    TYPE_GROUP_TBL = Hash.new

    type_data.each{|id, c_name, g_flag, *t_names|
      TypeNum.const_set(c_name, id)
      t_names.each{|t_name| t_name.freeze; TYPE_NAME_TBL[t_name] = id }
      TYPE_ID_TBL[id]    = t_names
      TYPE_GROUP_TBL[id] = g_flag
    }

    TYPE_NAME_TBL.freeze
    TYPE_ID_TBL.freeze

    def self.type_id(name)
      TYPE_NAME_TBL[name.to_s]
    end

    def self.type_name(id)
      TYPE_ID_TBL[id] && TYPE_ID_TBL[id][0]
    end

    def self.group_flag(id)
      TYPE_GROUP_TBL[id] || 0
    end

    #############################################

    module StateMask
      ShiftMask      =        (1<<0)
      LockMask       =        (1<<1)
      ControlMask    =        (1<<2)
      Mod1Mask       =        (1<<3)
      Mod2Mask       =        (1<<4)
      Mod3Mask       =        (1<<5)
      Mod4Mask       =        (1<<6)
      Mod5Mask       =        (1<<7)
      Button1Mask    =        (1<<8)
      Button2Mask    =        (1<<9)
      Button3Mask    =        (1<<10)
      Button4Mask    =        (1<<11)
      Button5Mask    =        (1<<12)

      AnyModifier    =        (1<<15)

      META_MASK      =  (AnyModifier<<1)
      ALT_MASK       =  (AnyModifier<<2)
      EXTENDED_MASK  =  (AnyModifier<<3)

      CommandMask    =  Mod1Mask
      OptionMask     =  Mod2Mask
    end

    #############################################

    FIELD_FLAG = {
      # key  =>  flag
      'above'       => Grp::CONFIG,
      'borderwidth' => (Grp::CREATE|Grp::CONFIG),
      'button'      => Grp::BUTTON,
      'count'       => Grp::EXPOSE,
      'data'        => (Grp::VIRTUAL|Grp::STRING_DATA),
      'delta'       => Grp::MWHEEL,
      'detail'      => (Grp::FOCUS|Grp::CROSSING),
      'focus'       => Grp::CROSSING,
      'height'      => (Grp::EXPOSE|Grp::CONFIG),
      'keycode'     => Grp::KEY,
      'keysym'      => Grp::KEY,
      'mode'        => (Grp::CROSSING|Grp::FOCUS),
      'override'    => (Grp::CREATE|Grp::MAP|Grp::REPARENT|Grp::CONFIG),
      'place'       => Grp::CIRC,
      'root'        => (Grp::KEY_BUTTON_MOTION_VIRTUAL|Grp::CROSSING),
      'rootx'       => (Grp::KEY_BUTTON_MOTION_VIRTUAL|Grp::CROSSING),
      'rooty'       => (Grp::KEY_BUTTON_MOTION_VIRTUAL|Grp::CROSSING),
      'sendevent'   => Grp::ALL,
      'serial'      => Grp::ALL,
      'state'       => (Grp::KEY_BUTTON_MOTION_VIRTUAL|
                        Grp::CROSSING|Grp::VISIBILITY),
      'subwindow'   => (Grp::KEY_BUTTON_MOTION_VIRTUAL|Grp::CROSSING),
      'time'        => (Grp::KEY_BUTTON_MOTION_VIRTUAL|Grp::CROSSING|
                        Grp::PROP),
      'warp'        => Grp::KEY_BUTTON_MOTION_VIRTUAL,
      'width'       => (Grp::EXPOSE|Grp::CREATE|Grp::CONFIG),
      'window'      => (Grp::CREATE|Grp::UNMAP|Grp::MAP|Grp::REPARENT|
                        Grp::CONFIG|Grp::GRAVITY|Grp::CIRC),
      'when'        => Grp::ALL,
      'x'           => (Grp::KEY_BUTTON_MOTION_VIRTUAL|Grp::CROSSING|
                        Grp::EXPOSE|Grp::CREATE|Grp::CONFIG|Grp::GRAVITY|
                        Grp::REPARENT),
      'y'           => (Grp::KEY_BUTTON_MOTION_VIRTUAL|Grp::CROSSING|
                        Grp::EXPOSE|Grp::CREATE|Grp::CONFIG|Grp::GRAVITY|
                        Grp::REPARENT),
    }

    FIELD_OPERATION = {
      'root' => proc{|val|
        begin
          Tk.tk_call_without_enc('winfo', 'pathname', val)
          val
        rescue
          nil
        end
      },

      'subwindow' => proc{|val|
        begin
          Tk.tk_call_without_enc('winfo', 'pathname', val)
          val
        rescue
          nil
        end
      },

      'window' => proc{|val| nil}
    }

    #-------------------------------------------

    def valid_fields(group_flag=nil)
      group_flag = self.class.group_flag(self.type) unless group_flag

      fields = {}
      FIELD_FLAG.each{|key, flag|
        next if (flag & group_flag) == 0
        begin
          val = self.__send__(key)
        rescue
          next
        end
        # next if !val || val == '??'
        next if !val || (val == '??' && (flag & Grp::STRING_DATA))
        fields[key] = val
      }

      fields
    end

    def valid_for_generate(group_flag=nil)
      fields = valid_fields(group_flag)

      FIELD_OPERATION.each{|key, cmd|
        next unless fields.has_key?(key)
        val = FIELD_OPERATION[key].call(fields[key])
        if val
          fields[key] = val
        else
          fields.delete(key)
        end
      }

      fields
    end

    def generate(win, modkeys={})
      klass = self.class

      if modkeys.has_key?(:type) || modkeys.has_key?('type')
        modkeys = TkComm._symbolkey2str(modkeys)
        type_id = modkeys.delete('type')
      else
        type_id = self.type
      end

      type_name  = klass.type_name(type_id)
      unless type_name
        fail RuntimeError, "type_id #{type_id} is invalid"
      end

      group_flag = klass.group_flag(type_id)

      opts = valid_for_generate(group_flag)

      modkeys.each{|key, val|
        if val
          opts[key.to_s] = val
        else
          opts.delete(key.to_s)
        end
      }

      if group_flag != Grp::KEY
        Tk.event_generate(win, type_name, opts)
      else
        # If type is KEY event, focus should be set to target widget.
        # If not set, original widget will get the same event.
        # That will make infinite loop.
        w = Tk.tk_call_without_enc('focus')
        begin
          Tk.tk_call_without_enc('focus', win)
          Tk.event_generate(win, type_name, opts)
        ensure
          Tk.tk_call_without_enc('focus', w)
        end
      end
    end

    #############################################

    # [ <'%' subst-key char>, <proc type char>, <instance var (accessor) name>]
    KEY_TBL = [
      [ ?#, ?n, :serial ],
      [ ?a, ?s, :above ],
      [ ?b, ?n, :num ],
      [ ?c, ?n, :count ],
      [ ?d, ?s, :detail ],
      # ?e
      [ ?f, ?b, :focus ],
      # ?g
      [ ?h, ?n, :height ],
      [ ?i, ?s, :win_hex ],
      # ?j
      [ ?k, ?n, :keycode ],
      # ?l
      [ ?m, ?s, :mode ],
      # ?n
      [ ?o, ?b, :override ],
      [ ?p, ?s, :place ],
      # ?q
      # ?r
      [ ?s, ?x, :state ],
      [ ?t, ?n, :time ],
      # ?u
      [ ?v, ?n, :value_mask ],
      [ ?w, ?n, :width ],
      [ ?x, ?n, :x ],
      [ ?y, ?n, :y ],
      # ?z
      [ ?A, ?s, :char ],
      [ ?B, ?n, :borderwidth ],
      # ?C
      [ ?D, ?n, :wheel_delta ],
      [ ?E, ?b, :send_event ],
      # ?F
      # ?G
      # ?H
      # ?I
      # ?J
      [ ?K, ?s, :keysym ],
      # ?L
      # ?M
      [ ?N, ?n, :keysym_num ],
      # ?O
      [ ?P, ?s, :property ],
      # ?Q
      [ ?R, ?s, :rootwin_id ],
      [ ?S, ?s, :subwindow ],
      [ ?T, ?n, :type ],
      # ?U
      # ?V
      [ ?W, ?w, :widget ],
      [ ?X, ?n, :x_root ],
      [ ?Y, ?n, :y_root ],
      # ?Z
      nil
    ]

    # [ <'%' subst-key str>, <proc type char>, <instance var (accessor) name>]
    #   the subst-key string will be converted to a bytecode (128+idx).
    LONGKEY_TBL = [
      # for example, for %CTT and %CST subst-key on tkdnd-2.0
      # ['CTT', ?l, :drop_target_type],
      # ['CST', ?l, :drop_source_type],
    ]

    # [ <proc type char>, <proc/method to convert tcl-str to ruby-obj>]
    PROC_TBL = [
      [ ?n, TkComm.method(:num_or_str) ],
      [ ?s, TkComm.method(:string) ],
      [ ?b, TkComm.method(:bool) ],
      [ ?w, TkComm.method(:window) ],

      [ ?x, proc{|val|
          begin
            TkComm::number(val)
          rescue ArgumentError
            val
          end
        }
      ],

      nil
    ]

=begin
    # for Ruby m17n :: ?x --> String --> char-code ( getbyte(0) )
    KEY_TBL.map!{|inf|
      if inf.kind_of?(Array)
        inf[0] = inf[0].getbyte(0) if inf[0].kind_of?(String)
        inf[1] = inf[1].getbyte(0) if inf[1].kind_of?(String)
      end
      inf
    }

    PROC_TBL.map!{|inf|
      if inf.kind_of?(Array)
        inf[0] = inf[0].getbyte(0) if inf[0].kind_of?(String)
      end
      inf
    }
=end

    # setup tables to be used by scan_args, _get_subst_key, _get_all_subst_keys
    #
    #     _get_subst_key() and _get_all_subst_keys() generates key-string
    #     which describe how to convert callback arguments to ruby objects.
    #     When binding parameters are given, use _get_subst_key().
    #     But when no parameters are given, use _get_all_subst_keys() to
    #     create a Event class object as a callback parameter.
    #
    #     scan_args() is used when doing callback. It convert arguments
    #     ( which are Tcl strings ) to ruby objects based on the key string
    #     that is generated by _get_subst_key() or _get_all_subst_keys().
    #
    _setup_subst_table(KEY_TBL, PROC_TBL)
    # _setup_subst_table(KEY_TBL, LONGKEY_TBL, PROC_TBL) # if use longname-keys

    #
    # NOTE: The order of parameters which passed to callback procedure is
    #        <extra_arg>, <extra_arg>, ... , <subst_arg>, <subst_arg>, ...
    #

    # If you need support extra arguments given by Tcl/Tk,
    # please override _get_extra_args_tbl
    #
    #def self._get_extra_args_tbl
    #  # return an array of convert procs
    #  []
    #end

=begin
    alias button num
    alias delta  wheel_delta
    alias root   rootwin_id
    alias rootx  x_root
    alias root_x x_root
    alias rooty  y_root
    alias root_y y_root
    alias sendevent send_event
=end
    ALIAS_TBL = {
      :button    => :num,
      :data      => :detail,
      :delta     => :wheel_delta,
      :root      => :rootwin_id,
      :rootx     => :x_root,
      :root_x    => :x_root,
      :rooty     => :y_root,
      :root_y    => :y_root,
      :sendevent => :send_event,
      :window    => :widget
    }

    _define_attribute_aliases(ALIAS_TBL)

  end

  ###############################################

  def install_bind_for_event_class(klass, cmd, *args)
    extra_args_tbl = klass._get_extra_args_tbl

    if args.compact.size > 0
      args.map!{|arg| klass._sym2subst(arg)}
      args = args.join(' ')
      keys = klass._get_subst_key(args)

      if cmd.kind_of?(String)
        id = cmd
      elsif cmd.kind_of?(TkCallbackEntry)
        id = install_cmd(cmd)
      else
        id = install_cmd(proc{|*arg|
          ex_args = []
          extra_args_tbl.reverse_each{|conv| ex_args << conv.call(arg.pop)}
          begin
            TkUtil.eval_cmd(cmd, *(ex_args.concat(klass.scan_args(keys, arg))))
          rescue Exception=>e
            if TkCore::INTERP.kind_of?(TclTkIp)
              fail e
            else
              # MultiTkIp
              fail Exception, "#{e.class}: #{e.message.dup}"
            end
          end
        })
      end
    elsif cmd.respond_to?(:arity) && cmd.arity == 0  # args.size == 0
      args = ''
      if cmd.kind_of?(String)
        id = cmd
      elsif cmd.kind_of?(TkCallbackEntry)
        id = install_cmd(cmd)
      else
        id = install_cmd(proc{
                           begin
                             TkUtil.eval_cmd(cmd)
                           rescue Exception=>e
                             if TkCore::INTERP.kind_of?(TclTkIp)
                               fail e
                             else
                               # MultiTkIp
                               fail Exception, "#{e.class}: #{e.message.dup}"
                             end
                           end
                         })
      end
    else
      keys, args = klass._get_all_subst_keys

      if cmd.kind_of?(String)
        id = cmd
      elsif cmd.kind_of?(TkCallbackEntry)
        id = install_cmd(cmd)
      else
        id = install_cmd(proc{|*arg|
          ex_args = []
          extra_args_tbl.reverse_each{|conv| ex_args << conv.call(arg.pop)}
          begin
            TkUtil.eval_cmd(cmd, *(ex_args << klass.new(*klass.scan_args(keys, arg))))
          rescue Exception=>e
            if TkCore::INTERP.kind_of?(TclTkIp)
              fail e
            else
              # MultiTkIp
              fail Exception, "#{e.class}: #{e.message.dup}"
            end
          end
        })
      end
    end

    if TkCore::INTERP.kind_of?(TclTkIp)
      id + ' ' + args
    else
      # MultiTkIp
      "if {[set st [catch {#{id} #{args}} ret]] != 0} {
         if {$st == 4} {
           return -code continue $ret
         } elseif {$st == 3} {
           return -code break $ret
         } elseif {$st == 2} {
           return -code return $ret
         } elseif {[regexp {^Exception: (TkCallbackContinue: .*)$} \
                                                               $ret m msg]} {
           return -code continue $msg
         } elseif {[regexp {^Exception: (TkCallbackBreak: .*)$} $ret m msg]} {
           return -code break $msg
         } elseif {[regexp {^Exception: (TkCallbackReturn: .*)$} $ret m msg]} {
           return -code return $msg
         } elseif {[regexp {^Exception: (\\S+: .*)$} $ret m msg]} {
           return -code return $msg
         } else {
           return -code error $ret
         }
       } else {
          set ret
       }"
    end
  end

  def install_bind(cmd, *args)
    install_bind_for_event_class(TkEvent::Event, cmd, *args)
  end
end
