# frozen_string_literal: true
module Tk
  class Text < Widget
    include Cget, Configure, Scrollable

    def self.tk_command
      'text'
    end

    autoload :Peer, 'ffi-tk/widget/text/peer'

    SEARCH_MUTEX = Mutex.new

    def initialize(parent = Tk.root, options = None)
      @tag_commands = {}
      super
    end

    def value
      get '1.0', :end
    end

    def value=(string)
      clear
      insert :end, string
    end

    def clear
      delete '0.0', :end
    end

    # Returns a list of four elements describing the screen area of the
    # character given by index.
    # The first two elements of the list give the x and y coordinates of the
    # upper-left corner of the area occupied by the character, and the last two
    # elements give the width and height of the area.
    # If the character is only partially visible on the screen, then the return
    # value reflects just the visible part.
    # If the character is not visible on the screen then the return value is an
    # empty list.
    def bbox(index)
      execute('bbox', index)
    end

    # Compares the indices given by index1 and index2 according to the
    # relational operator given by op, and returns 1 if the relationship is
    # satisfied and 0 if it is not.
    # Op must be one of the operators <, <=, ==, >=, >, or !=.
    # If op is == then true is returned if the two indices refer to the same
    # character, if op is < then true is returned if index1 refers to an earlier
    # character in the text than index2, and so on.
    def compare(index1, op, index2)
      execute('compare', index1, op, index2).to_boolean
    end

    # Counts the number of relevant things between the two indices.
    # If index1 is after index2, the result will be a negative number (and this
    # holds for each of the possible options).
    # The actual items which are counted depend on the options given.
    # The result is a list of integers, one for the result of each counting option given.
    #
    # Valid counting options are :chars, :displaychars, :displayindices,
    # :displaylines, :indices, :lines, :xpixels and :ypixels.
    # The default value, if no option is specified, is :indices.
    # There is an additional possible option :update which is a modifier.
    # If given, then all subsequent options ensure that any possible out of date
    # information is recalculated.
    # This currently only has any effect for the :ypixels count (which, if
    # :update is not given, will use the text widget's current cached value for
    # each line).
    # The count options are interpreted as follows:
    #
    # :chars
    #   count all characters, whether elided or not. Do not count embedded
    #   windows or images.
    # :displaychars
    #   count all non-elided characters.
    # :displayindices
    #   count all non-elided characters, windows and images.
    # :displaylines
    #   count all display lines (i.e. counting one for each time a line wraps)
    #   from the line of the first index up to, but not including the display
    #   line of the second index. Therefore if they are both on the same display
    #   line, zero will be returned. By definition displaylines are visible and
    #   therefore this only counts portions of actual visible lines.
    # :indices
    #   count all characters and embedded windows or images (i.e. everything
    #   which counts in text-widget index space), whether they are elided or
    #   not.
    # :lines
    #   count all logical lines (irrespective of wrapping) from the line of the
    #   first index up to, but not including the line of the second index.
    #   Therefore if they are both on the same line, zero will be returned.
    #   Logical lines are counted whether they are currently visible
    #   (non-elided) or not.
    # :xpixels
    #   count the number of horizontal pixels from the first pixel of the first
    #   index to (but not including) the first pixel of the second index. To
    #   count the total desired width of the text widget (assuming wrapping is
    #   not enabled), first find the longest line and then use
    #   `text.count("#{line}.0", "#{line}.0", :xpixels)`.
    # :ypixels
    #   count the number of vertical pixels from the first pixel of the first
    #   index to (but not including) the first pixel of the second index. If
    #   both indices are on the same display line, zero will be returned. To
    #   count the total number of vertical pixels in the text widget, use
    #   `text.count('1.0', 'end', :ypixels)`, and to ensure this is up to date,
    #   use `text.count('1.0', 'end', :update, :ypixels)`.
    #
    # The command returns a positive or negative integer corresponding to the
    # number of items counted between the two indices. One such integer is
    # returned for each counting option given, so a list is returned if more
    # than one option was supplied. For example `text.count('1.3', '4.5',
    # :xpixels, :ypixels` is perfectly valid and will return a list of two
    # elements.
    def count(index1, index2, *options)
      args = options.map(&:to_tcl_option)
      execute('count', *args, index1, index2)
    end

    # If boolean is specified, then it must have one of the true or false values
    # accepted by Tcl_GetBoolean.
    # If the value is a true one then internal consistency checks will be turned
    # on in the B-tree code associated with text widgets.
    # If boolean has a false value then the debugging checks will be turned off.
    # In either case the command returns an empty string.
    # If boolean is not specified then the command returns on or off to indicate
    # whether or not debugging is turned on.
    # There is a single debugging switch shared by all text widgets: turning
    # debugging on or off in any widget turns it on or off for all widgets.
    # For widgets with large amounts of text, the consistency checks may cause a
    # noticeable slow-down.
    # When debugging is turned on, the drawing routines of the text widget set
    # the global variables tk_textRedraw and tk_textRelayout to the lists of
    # indices that are redrawn.
    # The values of these variables are tested by Tk's test suite.
    def debug(boolean = None)
      if boolean == None
        execute('debug') == 1
      else
        execute_only('debug', boolean ? true : false)
      end
    end

    def debug?
      execute('debug') == 1
    end

    # Delete a range of characters from the text.
    # If both index1 and index2 are specified, then delete all the characters
    # starting with the one given by index1 and stopping just before index2
    # (i.e. the character at index2 is not deleted).
    # If index2 does not specify a position later in the text than index1 then
    # no characters are deleted.
    # If index2 is not specified then the single character at index1 is deleted.
    # It is not allowable to delete characters in a way that would leave the
    # text without a newline as the last character.
    # The command returns an empty string.
    # If more indices are given, multiple ranges of text will be deleted.
    # All indices are first checked for validity before any deletions are made.
    # They are sorted and the text is removed from the last range to the first
    # range to deleted text does not cause an undesired index shifting
    # side-effects.
    # If multiple ranges with the same start index are given, then the longest
    # range is used.
    # If overlapping ranges are given, then they will be merged into spans that
    # do not cause deletion of text outside the given ranges due to text shifted
    # during deletion.
    def delete(index1, *rest)
      execute('delete', index1, *rest)
    end

    # Returns a list with five elements describing the area occupied by the
    # display line containing index.
    # The first two elements of the list give the x and y coordinates of the
    # upper-left corner of the area occupied by the line, the third and fourth
    # elements give the width and height of the area, and the fifth element
    # gives the position of the baseline for the line, measured down from the
    # top of the area.
    # All of this information is measured in pixels.
    # If the current wrap mode is none and the line extends beyond the
    # boundaries of the window, the area returned reflects the entire area of
    # the line, including the portions that are out of the window.
    # If the line is shorter than the full width of the window then the area
    # returned reflects just the portion of the line that is occupied by
    # characters and embedded windows.
    # If the display line containing index is not visible on the screen then the
    # return value is an empty list.
    def dlineinfo(index)
      info = execute('dlineinfo', index).to_a
      info.empty? ? nil : info.map(&:to_i)
    end

    # Return the contents of the text widget from index1 up to, but not
    # including index2, including the text and information about marks, tags,
    # and embedded windows. If index2 is not specified, then it defaults to one
    # character past index1. The information is returned in the following
    # format:
    #
    # pathName dump ?switches? index1 ?index2?
    def dump(*arguments, given_index)
      arguments = arguments.dup
      invocation = []
      indices = [given_index]

      loop do
        arg = arguments.shift
        break unless arg

        if arg.respond_to?(:to_tcl_option)
          case tcl_option = arg.to_tcl_option
          when '-command'
            command = arguments.shift
            invocation << ['-command', command]
          when /^-(?:all|image|mark|tag|text|window)$/
            invocation << tcl_option
          else
            indices.unshift(arg)
          end
        else
          indices.unshift(arg)
        end
      end

      execute('dump', *invocation, *indices).to_a.each_slice(3).to_a
    end

    # If boolean is not specified, returns the modified flag of the widget.
    # The insert, delete, edit undo and edit redo commands or the user can set
    # or clear the modified flag.
    # If boolean is specified, sets the modified flag of the widget to boolean.
    def edit_modified(boolean = None)
      if boolean == None
        execute('edit', 'modified')
      else
        execute_only('edit', 'modified', boolean ? true : false)
      end
    end

    def edit_modified?
      execute('edit', 'modified').to_boolean
    end

    # When the -undo option is true, reapplies the last undone edits provided no
    # other edits were done since then.
    # Generates an error when the redo stack is empty.
    # Does nothing when the -undo option is false.
    def edit_redo
      execute_only('edit', 'redo')
    end

    # Clears the undo and redo stacks.
    def edit_reset
      execute_only('edit', 'reset')
    end

    # Inserts a separator (boundary) on the undo stack. Does nothing when the
    # -undo option is false.
    def edit_separator
      execute_only('edit', 'separator')
    end

    # Undoes the last edit action when the -undo option is true.
    # An edit action is defined as all the insert and delete commands that are
    # recorded on the undo stack in between two separators.
    # Generates an error when the undo stack is empty.
    # Does nothing when the -undo option is false.
    def edit_undo
      execute_only('edit', 'undo')
    end

    # Return a range of characters from the text.
    #
    # The return value will be all the characters in the text starting with the
    # one whose index is index1 and ending just before the one whose index is
    # index2 (the character at index2 will not be returned).
    #
    # If index2 is omitted then the single character at index1 is returned.
    # If there are no characters in the specified range (e.g. index1 is past the
    # end of the file or index2 is less than or equal to index1) then an empty
    # string is returned.
    #
    # If the specified range contains embedded windows, no information about
    # them is included in the returned string.
    # If multiple index pairs are given, multiple ranges of text will be
    # returned in a list.
    # Invalid ranges will not be represented with empty strings in the list.
    # The ranges are returned in the order passed to [get].
    #
    # @see get_displaychars
    def get(index, *indices)
      execute('get', index, *indices).to_s
    end

    # Same as [get], but within each range, only those characters which are not
    # elided will be returned.
    # This may have the effect that some of the returned ranges are empty
    # strings.
    def get_displaychars(index, *indices)
      execute('get', '-displaychars', index, *indices).to_s
    end

    def image_cget(index, option)
      option = tcl_option(option)
      Cget.option_to_ruby(option, execute('image', 'cget', index))
    end

    # Query or modify the configuration options for an embedded image.
    # If no option is specified, returns a list describing all of the available
    # options for the embedded image at index (see Tk_ConfigureInfo for
    # information on the format of this list).
    # If option is specified with no value, then the command returns a list
    # describing the one named option (this list will be identical to the
    # corresponding sublist of the value returned if no option is specified).
    # If one or more option-value pairs are specified, then the command modifies
    # the given option(s) to have the given value(s); in this case the command
    # returns an empty string.
    # See EMBEDDED IMAGES for information on the options that are supported.
    def image_configure(index, options = None)
      common_configure([:image, :configure, index], options)
    end

    # This command creates a new image annotation, which will appear in the text
    # at the position given by index.
    # Any number of option-value pairs may be specified to configure the
    # annotation.
    # Returns a unique identifier that may be used as an index to refer to this
    # image.
    # See EMBEDDED IMAGES for information on the options that are supported, and
    # a description of the identifier returned.
    def image_create(index, options = {})
      execute('image', 'create', index, options.to_tcl_options)
    end

    def image_names
      execute('image', 'names')
    end

    # Returns the position corresponding to index in the form line.char where
    # line is the line number and char is the character number. Index may have
    # any of the forms described under INDICES above.
    def index(index)
      execute('index', index).to_s
    end

    # Inserts all of the chars arguments just before the character at index.
    #
    # If index refers to the end of the text (the character after the last
    # newline) then the new text is inserted just before the last newline
    # instead.
    # If there is a single chars argument and no tagList, then the new text will
    # receive any tags that are present on both the character before and the
    # character after the insertion point; if a tag is present on only one of
    # these characters then it will not be applied to the new text.
    # If tagList is specified then it consists of a list of tag names; the new
    # characters will receive all of the tags in this list and no others,
    # regardless of the tags present around the insertion point.
    # If multiple chars-tagList argument pairs are present, they produce the
    # same effect as if a separate pathName insert widget command had been
    # issued for each pair, in order.
    # The last tagList argument may be omitted.
    def insert(index, string, taglist = nil, *rest)
      execute_only('insert', index, *[string, taglist, *rest].compact)
    end

    # If direction is not specified, returns left or right to indicate which of
    # its adjacent characters markName is attached to.
    # If direction is specified, it must be left or right; the gravity of
    # markName is set to the given value.
    def mark_gravity(name, direction = None)
      if direction == None
        execute('mark', 'gravity', name)&.to_sym
      else
        execute_only('mark', 'gravity', name, direction)
      end
    end

    # Returns a list whose elements are the names of all the marks that are
    # currently set.
    def mark_names
      execute('mark', 'names').to_a.map(&:to_sym)
    end

    # Returns the name of the next mark at or after index.
    # If index is specified in numerical form, then the search for the next mark
    # begins at that index.
    # If index is the name of a mark, then the search for the next mark begins
    # immediately after that mark.
    # This can still return a mark at the same position if there are multiple
    # marks at the same index.
    # These semantics mean that the mark next operation can be used to step
    # through all the marks in a text widget in the same order as the mark
    # information returned by the pathName dump operation.
    # If a mark has been set to the special end index, then it appears to be
    # after end with respect to the pathName mark next operation.
    # nil is returned if there are no marks after index.
    def mark_next(index)
      execute('mark', 'next', index)&.to_sym
    end

    # Returns the name of the mark at or before index.
    # If index is specified in numerical form, then the search for the previous
    # mark begins with the character just before that index.
    # If index is the name of a mark, then the search for the next mark begins
    # immediately before that mark.
    # This can still return a mark at the same position if there are multiple
    # marks at the same index.
    # These semantics mean that the pathName mark previous operation can be used
    # to step through all the marks in a text widget in the reverse order as the
    # mark information returned by the pathName dump operation.
    # nil is returned if there are no marks before index.
    def mark_previous(index)
      execute('mark', 'previous', index)&.to_sym
    end

    # Sets the mark named markName to a position just before the character at
    # index.
    # If markName already exists, it is moved from its old position; if it does
    # not exist, a new mark is created.
    def mark_set(name, index)
      execute_only('mark', 'set', name, index)
    end

    # Remove the mark corresponding to each of the markName arguments.
    # The removed marks will not be usable in indices and will not be returned
    # by future calls to [mark_names].
    def mark_unset(*names)
      execute_only('mark', 'unset', *names)
    end

    # Creates a peer text widget, and any optional standard configuration
    # options (as for the text command).
    # By default the peer will have the same start and end line as the parent
    # widget, but these can be overridden with the standard configuration
    # options.
    def peer_create(options = {})
      Peer.new(self, options)
    end

    # Returns a list of peers of this widget (this does not include the widget
    # itself). The order within this list is undefined.
    def peer_names
      execute('peer', 'names').to_a.map(&:to_s)
    end

    # Replaces the range of characters between +index1+ and +index2+ with the
    # given characters and tags.
    # See the section on [insert] for an explanation of the handling of taglist
    # arguments, and the docs of [delete] for an explanation of the handling of
    # the indices.
    # If +index2+ corresponds to an index earlier in the text than +index1+, an
    # error will be generated.
    # The deletion and insertion are arranged so that no unnecessary scrolling
    # of the window or movement of insertion cursor occurs.
    # In addition the undo/redo stack are correctly modified, if undo operations
    # are active in the text widget.
    # The command returns nil.
    def replace(index1, index2, chars, *taglists_and_chars)
      execute_only(:replace, index1, index2, chars, *taglists_and_chars)
    end

    # Records +x+ and +y+ and the current view in the text window, for use in
    # conjunction with later pathName scan dragto commands.
    # Typically this command is associated with a mouse button press in the
    # widget.
    # It returns nil.
    def scan_mark(x, y)
      execute_only(:scan, :mark, x, y)
    end

    # This command computes the difference between its +x+ and +y+ arguments and
    # the +x+ and +y+ arguments to the last pathName scan mark command for the
    # widget. It then adjusts the view by 10 times the difference in
    # coordinates. This command is typically associated with mouse motion events
    # in the widget, to produce the effect of dragging the text at high speed
    # through the window.
    # The return value is nil.
    def scan_dargto(x, y)
      execute_only(:scan, :dragto, x, y)
    end

    # Searches the text starting at index for a range of characters that matches
    # pattern.
    # If a match is found, the index of the first character in the match is
    # returned as result; otherwise an empty string is returned.
    # One or more of the following switches may be specified to control the
    # search:
    #
    #   :forwards
    #     The search will proceed forward through the text, finding the first
    #     matching range starting at or after the position given by index.
    #     This is the default.
    #
    #   :backwards
    #     The search will proceed backward through the text, finding the
    #     matching range closest to index whose first character is before index
    #     (it is not allowed to be at index).
    #     Note that, for a variety of reasons, backwards searches can be
    #     substantially slower than forwards searches (particularly when using
    #     :regexp), so it is recommended that performance-critical code use
    #     forward searches.
    #
    #   :exact
    #     Use exact matching: the characters in the matching range must be
    #     identical to those in pattern.
    #     This is the default.
    #
    #   :regexp
    #     Treat pattern as a regular expression and match it against the text
    #     using the rules for regular expressions (see the regexp command for
    #     details). The default matching automatically passes both the
    #     :lineanchor and :linestop options to the regexp engine (unless
    #     :nolinestop is used), so that "^", "$" match beginning and end of
    #     line, and ".", "[^" sequences will never match the newline character
    #     "\n".
    #
    #   :nolinestop
    #     This allows "." and "[^" sequences to match the newline character "\n",
    #     which they will otherwise not do (see the regexp command for details).
    #     This option is only meaningful if :regexp is also given, and an error
    #     will be thrown otherwise.
    #     For example, to match the entire text, use `text.search(/.*/m)`.
    #
    #   :nocase
    #     Ignore case differences between the pattern and the text.
    #
    #   :count varName
    #     The argument following :count gives the name of a variable; if a match
    #     is found, the number of index positions between beginning and end of
    #     the matching range will be stored in the variable.
    #     If there are no embedded images or windows in the matching range (and
    #     there are no elided characters if :elide is not given), this is
    #     equivalent to the number of characters matched.
    #     In either case, the range matchIdx to matchIdx + $count chars will
    #     return the entire matched text.
    #
    #   :all
    #     Find all matches in the given range and return a list of the indices of
    #     the first character of each match.
    #
    #     If a :count switch is given, then the returned array has two elements
    #     in the form of `[index, count]` for each successful match.
    #     Note that, even for exact searches, the elements of this list may be
    #     different, if there are embedded images, windows or hidden text.
    #     Searches with :all behave very similarly to the Tcl command regexp
    #     :all, in that overlapping matches are not normally returned.
    #     For example, applying an :all search of the pattern `\w+` against
    #     "hello there" will just match twice, once for each word, and
    #     matching `Z[a-z]+Z` against "ZooZooZoo" will just match once.
    #
    #   :overlap
    #     When performing :all searches, the normal behaviour is that matches
    #     which overlap an already-found match will not be returned.
    #     This switch changes that behaviour so that all matches which are not
    #     totally enclosed within another match are returned.
    #     For example, applying an :overlap search of the pattern `\w+`
    #     against "hello there" will just match twice (i.e.
    #     no different to just :all), but matching `Z[a-z]+Z` against
    #     "ZooZooZoo" will now match twice.
    #     An error will be thrown if this switch is used without :all.
    #
    #   :strictlimits
    #     When performing any search, the normal behaviour is that the start and
    #     stop limits are checked with respect to the start of the matching
    #     text. With the :strictlimits flag, the entire matching range must lie
    #     inside the start and stop limits specified for the match to be valid.
    #
    #   :elide
    #     Find elided (hidden) text as well.
    #     By default only displayed text is searched.
    #
    #   --
    #     This switch has no effect except to terminate the list of switches:
    #     the next argument will be treated as pattern even if it starts with -.
    #
    # The matching range may be within a single line of text, or run across
    # multiple lines (if parts of the pattern can match a new-line).
    # For regular expression matching one can use the various newline-matching
    # features such as $ to match the end of a line, "^" to match the beginning
    # of a line, and to control whether "." is allowed to match a new-line.
    # If stop_index is specified, the search stops at that index: for forward
    # searches, no match at or after stop_index will be considered; for backward
    # searches, no match earlier in the text than stop_index will be considered.
    # If stop_index is omitted, the entire text will be searched: when the
    # beginning or end of the text is reached, the search continues at the other
    # end until the starting location is reached again; if stop_index is
    # specified, no wrap-around will occur.
    # This means that, for example, if the search is :forwards but stop_index is
    # earlier in the text than start_index, nothing will ever be found.
    # See KNOWN BUGS in [Text] for a number of minor limitations of [search] method.
    #
    # pathName search ?switches? pattern index ?stopIndex?
    #
    # FUNNY: stdlib tk simply gets the text into the ruby side and performs
    #        matches using the core regexp methods, but doesn't give any way to
    #        call the tcl/tk search function, this is new land!
    def search(pattern, from, to = None, *switches)
      switches << :regexp if pattern.class < CoreExtensions::Regexp
      to = :end if None == to

      switches.map! { |switch| switch.to_s.to_tcl_option }
      switches.uniq!

      if switches.include?('-all') && switches.delete('-count')
        count_all = 'RubyFFI::Text_search_count'
        switches << '-count' << count_all
      elsif switches.delete('-count')
        count = 'RubyFFI::Text_search_count'
        switches << '-count' << count
      end

      sep = TclString.new('--')

      if count
        SEARCH_MUTEX.synchronize do
          list = [*execute(:search, *switches, sep, pattern, from, to)]
          return list if list.empty?
          count_value = Tk.execute('set', count)
          [*list, count_value]
        end
      elsif count_all
        SEARCH_MUTEX.synchronize do
          list = [*execute(:search, *switches, sep, pattern, from, to)]
          return list if list.empty?
          count_list = Tk.execute('set', count_all)
          list.zip(count_list)
        end
      else
        [*execute(:search, *switches, sep, pattern, from, to)]
      end
    end

    def rsearch(pattern, from, to, *switches)
      search(pattern, from, to, *switches, :backwards)
    end

    # Adjusts the view in the window so that the character given by +index+ is
    # completely visible.
    # If +index+ is already visible then the command does nothing.
    # If +index+ is a short distance out of view, the command adjusts the view
    # just enough to make +index+ visible at the edge of the window.
    # If +index+ is far out of view, then the command centers +index+ in the
    # window.
    def see(index)
      execute_only(:see, index)
    end

    # Associate the tag tagName with all of the characters starting with index1
    # and ending just before index2 (the character at index2 is not tagged).
    # A single command may contain any number of index1-index2 pairs.
    # If the last index2 is omitted then the single character at index1 is
    # tagged. If there are no characters in the specified range (e.g.
    # index1 is past the end of the file or index2 is less than or equal to
    # index1) then the command has no effect.
    def tag_add(tag_name, index1, index2 = None, *indices)
      execute_only(:tag, :add, tag_name, index1, index2, *indices)
    end

    # This command associates script with the tag given by tagName.
    # Whenever the event sequence given by sequence occurs for a character that
    # has been tagged with tagName, the script will be invoked.
    # This widget command is similar to the bind command except that it
    # operates on characters in a text rather than entire widgets.
    # See the bind manual entry for complete details on the syntax of sequence
    # and the substitutions performed on script before invoking it.
    # If all arguments are specified then a new binding is created, replacing
    # any existing binding for the same sequence and tagName (if the first
    # character of script is "+" then script augments an existing binding
    # rather than replacing it).
    # In this case the return value is an empty string.
    # If script is omitted then the command returns the script associated
    # with tagName and sequence (an error occurs if there is no such binding).
    # If both script and sequence are omitted then the command returns a list
    # of all the sequences for which bindings have been defined for tagName.
    # The only events for which bindings may be specified are those related to
    # the mouse and keyboard (such as Enter, Leave, ButtonPress, Motion, and
    # KeyPress) or virtual events.
    # Event bindings for a text widget use the current mark described under
    # MARKS above.
    # An Enter event triggers for a tag when the tag first becomes present on
    # the current character, and a Leave event triggers for a tag when it
    # ceases to be present on the current character.
    # Enter and Leave events can happen either because the current mark moved
    # or because the character at that position changed.
    # Note that these events are different than Enter and Leave events for
    # windows. Mouse and keyboard events are directed to the current character.
    # If a virtual event is used in a binding, that binding can trigger only if
    # the virtual event is defined by an underlying mouse-related or
    # keyboard-related event.
    # It is possible for the current character to have multiple tags, and for
    # each of them to have a binding for a particular event sequence.
    # When this occurs, one binding is invoked for each tag, in order from
    # lowest-priority to highest priority.
    # If there are multiple matching bindings for a single tag, then the most
    # specific binding is chosen (see the manual entry for the bind command
    # for details).
    # continue and break commands within binding scripts are processed in the
    # same way as for bindings created with the bind command.
    # If bindings are created for the widget as a whole using the bind command,
    # then those bindings will supplement the tag bindings.
    # The tag bindings will be invoked first, followed by bindings for the
    # window as a whole.
    def tag_bind(tag_name, sequence = None, &script)
      if !script && None == sequence
        return Tk.execute(:tag, :bind, tag_name)
      elsif !script
        return Tk.execute(:tag, :bind, tag_name, sequence)
      end

      # create name for command
      name = "#{tk_pathname}_#{tag_name}".scan(/\w+/).join('_')
      @events ||= {}
      unregister_event(name)

      Event::Handler.register_custom(script) do |id|
        code = "%s tag bind %s %s { ::RubyFFI::event %d '' %s }"
        props = Event::Data::PROPERTIES.transpose[0].join(' ')
        tcl = code % [tk_pathname, tag_name, sequence, id, props]
        Tk.interp.eval(tcl)
        @events[name] = id
      end
    end

    def unregister_event(name, id = @events[name])
      return unless id
      @events.delete(id)
      Event::Handler.unregister(id)
    end

    # This command returns the current value of the option named option
    # associated with the tag given by tagName.
    # Option may have any of the values accepted by the pathName tag
    # configure widget command.
    def tag_cget(tag_name, option)
      value = execute(:tag, :cget, tag_name, option.to_tcl_option)
      Cget.option_to_ruby(option, value)
    end

    # This command is similar to the pathName configure widget command except
    # that it modifies options associated with the tag given by tagName instead
    # of modifying options for the overall text widget.
    # If no option is specified, the command returns a list describing all of
    # the available options for tagName (see Tk_ConfigureInfo for information
    # on the format of this list).
    # If option is specified with no value, then the command returns a list
    # describing the one named option (this list will be identical to the
    # corresponding sublist of the value returned if no option is specified).
    # If one or more option-value pairs are specified, then the command
    # modifies the given option(s) to have the given value(s) in tagName; in
    # this case the command returns an empty string.
    # See TAGS above for details on the options available for tags.
    def tag_configure(tag_name, options = None)
      common_configure([:tag, :configure, tag_name], options)
    end

    # Deletes all tag information for each of the tagName arguments.
    # The command removes the tags from all characters in the file and also
    # deletes any other information associated with the tags, such as
    # bindings and display information.
    # The command returns an empty string.
    def tag_delete(tag_name, *tag_names)
      execute_only(:tag, :delete, tag_name, *tag_names)
    end

    # Changes the priority of tag tagName so that it is just lower in priority
    # than the tag whose name is belowThis.
    # If belowThis is omitted, then tagName's priority is changed to make it
    # lowest priority of all tags.
    def tag_lower(tag_name, below = None)
      execute_only(:tag, :lower, tag_name, below)
    end

    # Returns a list whose elements are the names of all the tags that are
    # active at the character position given by index.
    # If index is omitted, then the return value will describe all of the tags
    # that exist for the text (this includes all tags that have been named in a
    # "pathName tag" widget command but have not been deleted by a "pathName
    # tag delete" widget command, even if no characters are currently marked
    # with the tag).
    # The list will be sorted in order from lowest priority to highest
    # priority.
    def tag_names(index = None)
      execute(:tag, :names, index).to_a
    end

    # This command searches the text for a range of characters tagged with
    # tagName where the first character of the range is no earlier than the
    # character at index1 and no later than the character just before index2 (a
    # range starting at index2 will not be considered).
    # If several matching ranges exist, the first one is chosen.
    # The command's return value is a list containing two elements, which are
    # the index of the first character of the range and the index of the
    # character just after the last one in the range.
    # If no matching range is found then the return value is an empty string.
    # If index2 is not given then it defaults to the end of the text.
    def tag_nextrange(tag_name, index1, index2 = None)
      execute(:tag, :nextrange, tag_name, index1, index2).to_a
    end

    # This command searches the text for a range of characters tagged with
    # tagName where the first character of the range is before the character at
    # index1 and no earlier than the character at index2 (a range starting at
    # index2 will be considered).
    # If several matching ranges exist, the one closest to index1 is chosen.
    # The command's return value is a list containing two elements, which are
    # the index of the first character of the range and the index of the
    # character just after the last one in the range.
    # If no matching range is found then the return value is an empty string.
    # If index2 is not given then it defaults to the beginning of the text.
    def tag_prevrange(tag_name, index1, index2 = None)
      execute(:tag, :prevrange, tag_name, index1, index2).to_a
    end

    # Changes the priority of tag tagName so that it is just higher in priority
    # than the tag whose name is aboveThis.
    # If aboveThis is omitted, then tagName's priority is changed to make it
    # highest priority of all tags.
    def tag_raise(tag_name, above = None)
      execute_only(:tag, :raise, tag_name, above)
    end

    # Returns a list describing all of the ranges of text that have been tagged
    # with tagName.
    # The first two elements of the list describe the first tagged range in the
    # text, the next two elements describe the second range, and so on.
    # The first element of each pair contains the index of the first character
    # of the range, and the second element of the pair contains the index of
    # the character just after the last one in the range.
    # If there are no characters tagged with tag then an empty string is
    # returned.
    def tag_ranges(tag_name)
      [*execute(:tag, :ranges, tag_name)].each_slice(2).to_a
    end

    # Remove the tag tagName from all of the characters starting at index1 and
    # ending just before index2 (the character at index2 is not affected).
    # A single command may contain any number of index1-index2 pairs.
    # If the last index2 is omitted then the tag is removed from the single
    # character at index1.
    # If there are no characters in the specified range (e.g.
    # index1 is past the end of the file or index2 is less than or equal to
    # index1) then the command has no effect.
    # This command returns an empty string.
    def tag_remove(tag_name, index1, index2 = None, *indices)
      execute_only(:tag, :remove, tag_name, index1, index2, *indices)
    end

    # Returns the value of a configuration option for an embedded window.
    # Index identifies the embedded window, and option specifies a particular
    # configuration option, which must be one of the ones listed in the section
    # EMBEDDED WINDOWS.
    def window_cget(index, option)
      execute(:window, :cget, index, option.to_tcl_option)
    end

    # Query or modify the configuration options for an embedded window.
    # If no option is specified, returns a list describing all of the available
    # options for the embedded window at index (see Tk_ConfigureInfo for
    # information on the format of this list).
    # If option is specified with no value, then the command returns a list
    # describing the one named option (this list will be identical to the
    # corresponding sublist of the value returned if no option is specified).
    # If one or more option-value pairs are specified, then the command
    # modifies the given option(s) to have the given value(s); in this case the
    # command returns an empty string.
    # See EMBEDDED WINDOWS for information on the options that are supported.
    def window_configure(index, options = None)
      common_configure([:window, :configure, index], options)
    end

    # This command creates a new window annotation, which will appear in the
    # text at the position given by index.
    # Any number of option-value pairs may be specified to configure the
    # annotation. See EMBEDDED WINDOWS for information on the options that are
    # supported. Returns an empty string.
    def window_create(index, options = None)
      if None == argument
        execute(:window, :create, index)
      else
        execute(:window, :create, index, options.to_tcl_options)
      end
    end

    # Returns a list whose elements are the names of all windows currently
    # embedded in window.
    def window_names
      execute(:window, :names).to_a
    end

    def copy
      Tk.execute(:tk_textCopy, self)
    end

    def cut
      Tk.execute(:tk_textCut, self)
    end

    def paste
      Tk.execute(:tk_textPaste, self)
    end

    def tk_prev_word_pos(start)
      Tk.execute('tk::TextPrevPos', self, start, 'tcl_startOfPreviousWord').to_s
    end

    def tk_next_word_pos(start)
      Tk.execute('tk::TextNextPos', self, start, 'tcl_startOfNextWord').to_s
    end

    def tk_next_word_pos_end(start)
      Tk.execute('tk::TextNextWord', self, start).to_s
    end

    def tk_prev_line_pos(count)
      Tk.execute('tk::TextUpDownLine', self, -count.abs).to_s
    end

    def tk_next_line_pos(count)
      Tk.execute('tk::TextUpDownLine', self, count).to_s
    end

    def tk_prev_page_pos(count)
      Tk.execute('tk::TextScrollPages', self, -count.abs).to_s
    end

    def tk_next_page_pos(count)
      Tk.execute('tk::TextScrollPages', self, count).to_s
    end
  end
end
