module VER
  class Text < Tk::Text
    autoload :Index, 'ver/text/index'
    include Methods

    MODE_CURSOR = {
      :insert       => {insertbackground: 'red',    blockcursor: false},
      :control      => {insertbackground: 'green',  blockcursor: true},
      :select_char  => {insertbackground: 'yellow', blockcursor: true},
      :select_line  => {insertbackground: 'yellow', blockcursor: true},
      :select_block => {insertbackground: 'yellow', blockcursor: true},
    }

    GUESS_ENCODING_ORDER = [
      Encoding::US_ASCII,
      Encoding::UTF_8,
      Encoding::Shift_JIS,
      Encoding::EUC_JP,
      Encoding::EucJP_ms,
      Encoding::Big5,
      Encoding::UTF_16BE,
      Encoding::UTF_16LE,
      Encoding::UTF_32BE,
      Encoding::UTF_32LE,
      Encoding::CP949,
      Encoding::Emacs_Mule,
      Encoding::EUC_KR,
      Encoding::EUC_TW,
      Encoding::GB18030,
      Encoding::GBK,
      Encoding::Stateless_ISO_2022_JP,
      Encoding::CP51932,
      Encoding::EUC_CN,
      Encoding::GB12345,
      Encoding::Windows_31J,
      Encoding::MacJapanese,
      Encoding::UTF8_MAC,
      Encoding::BINARY,
    ]

    MATCH_WORD_RIGHT =  /[^a-zA-Z0-9]+[a-zA-Z0-9'"{}\[\]\n-]/
    MATCH_WORD_LEFT =  /(^|\b)\S+(\b|$)/

    attr_accessor :keymap, :view, :status
    attr_reader :filename, :encoding, :pristine, :syntax, :undoer

    # attributes for diverse functionality
    attr_accessor :selection_mode, :selection_start

    def initialize(view, options = {})
      super
      self.view = view
      @options = Options.new(:text, VER.options)

      keymap_name = @options.keymap
      self.keymap = Keymap.get(name: keymap_name, receiver: self)

      apply_mode_style(keymap.mode) # for startup
      setup_tags

      @undoer = VER::Undo::Tree.new(self)

      self.selection_start = nil
      @pristine = true
      @syntax = nil
      @encoding = Encoding.default_internal
      @dirty_indices = []

      self.mode = keymap.mode
    end

    def index(idx)
      Index.new(self, execute('index', idx).to_s)
    end

    def message(*args)
      status.message(*args)
    end

    def noop(*args)
      message "Noop %p in mode %p" % [args, keymap.mode]
    end

    def short_filename
      filename.sub(Dir.pwd + '/', '') if filename
    end

    def filename=(path)
      @filename = Pathname(path.to_s).expand_path
    end

    def layout
      view.layout
    end

    def status_projection(into)
      format = options.statusline.dup

      format.gsub!(/%([[:alpha:]]+)/, '#{\1()}')
      format.gsub!(/%_([[:alpha:]]+)/, '#{(_ = \1()) ? " #{_}" : ""}')
      format.gsub!(/%([+-]?\d+)([[:alpha:]]+)/, '#{\2(\1)}')
      format = "%{#{format}}"

      context = Status::Context.new(self)
      line = context.instance_eval(format)

      into.value = line
    rescue => ex
      puts ex, ex.backtrace
    end

    TAG_ALL_MATCHING_OPTIONS = { from: '1.0', to: 'end - 1 chars' }

    def tag_all_matching(name, regexp, options = {})
      name = name.to_s
      options = TAG_ALL_MATCHING_OPTIONS.merge(options)
      from, to = options.values_at(:from, :to)

      if tag_exists?(name)
        tag_remove(name, from, to)
      else
        fg, bg = options.values_at(:foreground, :background)
        tag_configure(name, foreground: fg, background: bg)
      end

      search_all(regexp, from, to) do |match, match_from, match_to|
        name = yield(match, match_from, match_to) if block_given?
        fast_tag_add name, match_from, match_to
      end
    end

    def search_all(regexp, from = '1.0', to = 'end - 1 chars')
      return Enumerator.new(self, :search_all, regexp, from) unless block_given?
      from, to = from.to_s, to.to_s

      while result = search(regexp, from, to, :count)
        pos, len = result
        break if !pos || len == 0

        match = get(pos, "#{pos} + #{len} chars")
        from = "#{pos} + #{len} chars"

        yield(match, pos, from)
      end
    end

    def rsearch_all(regexp, from = 'end', to = '1.0')
      return Enumerator.new(self, :rsearch_all, regexp, from) unless block_given?

      while result = rsearch(regexp, from, to, :count)
        pos, len = result
        break if !pos || len == 0

        match = get(pos, "#{pos} + #{len} chars")
        from = index("#{pos} - #{len} chars")

        yield(match, pos, from)
      end
    end

    def tag_exists?(given_path)
      tag_names.include?(given_path)
    rescue RuntimeError => ex
      false
    end

    # Wrap Tk methods to behave as we want and to generate events
    def mark_set(mark_name, index)
      super
      return unless mark_name == :insert
      Tk::Event.generate(self, '<<Movement>>')
    end

    def insert(index, string)
      index = index(index) unless index.respond_to?(:to_index)

      undo_record do |record|
        record.insert(index, string)
      end
    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 the
    # tag_list arguments, and the section on [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.
    #
    # replace index1 index2 chars ?tagList chars tagList ...?
    def replace(index1, index2, string)
      index1 = index(index1) unless index1.respond_to?(:to_index)
      index2 = index(index2) unless index2.respond_to?(:to_index)
      return if index1 == index2

      undo_record do |record|
        record.replace(index1, index2, string)
      end
    end

    def focus
      super
      Tk::Event.generate(self, '<<Focus>>')
    end

    def fast_tag_add(tag, *indices)
      execute('tag', 'add', tag, *indices)
      self
    rescue RuntimeError => ex
      VER.error(ex)
    end

    def set_window_title
      if filename
        home = Pathname(ENV['HOME'])
        dir, file = filename.split
        dir_relative_to_home = dir.relative_path_from(home)

        if dir_relative_to_home.to_s.start_with?('../')
          title = "#{file} (#{dir}) - VER"
        else
          title = "#{file} (#{dir_relative_to_home}) - VER"
        end
      else
        title = "[No Name] - VER"
      end

      VER.root.wm_title = title
    end

    def setup_highlight
      return unless filename
      return if @encoding == Encoding::BINARY

      if @syntax = Syntax.from_filename(filename)
        defer{ syntax.highlight(self, value) }
        status_projection(status) if status
      end
    end

    def schedule_line_highlight(raw_index)
      return unless @syntax
      index = index(raw_index)
      schedule_line_highlight!(index.y - 1, index.linestart, index.lineend)
    end

    def schedule_highlight(options = {})
      return unless @syntax
      schedule_highlight!
    end

    # TODO: maybe we can make this one faster when many lines are going to be
    #       highlighted at once by bundling them.
    def touch!(*args)
      args.each{|arg| schedule_line_highlight(arg) } if @syntax
      Tk::Event.generate(self, '<<Modified>>')
    end

    private

    def schedule_highlight!(*args)
      defer do
        syntax.highlight(self, value)
        tag_all_trailing_whitespace
        tag_all_uris
      end
    end

    # TODO: only tag the current line.
    def schedule_line_highlight!(line, from, to)
      defer do
        syntax.highlight(self, get(from, to), line, from, to)
        tag_all_trailing_whitespace(from: from, to: to)
        tag_all_uris(from: from, to: to)
      end
    end

    def mode=(name)
      keymap.mode = mode = name.to_sym
      undo_separator
      apply_mode_style(mode)
      status_projection(status) if status
    end

    def apply_mode_style(mode)
      cursor = MODE_CURSOR[mode]
      return unless cursor
      configure cursor

      return unless status && color = cursor[:insertbackground]
      style = status.style
      Tk::Tile::Style.configure style, fieldbackground: color
    end

    def load_theme(name)
      return unless syntax
      return unless found = Theme.find(name)

      syntax.theme = Theme.load(found)
      schedule_highlight

      message "Theme #{found} loaded"
    end

    def load_syntax(name)
      return false unless syntax

      theme = syntax.theme

      if name.is_a?(Syntax)
        @syntax = Syntax.new(name.name, theme)
      elsif found = Syntax.find(name)
        @syntax = Syntax.new(name, theme)
      else
        return false
      end

      schedule_highlight

      message "Syntax #{@syntax.name} loaded"
    end

    def load_preferences
      return unless @syntax

      name = @syntax.name
      file = VER.find_in_loadpath("preferences/#{name}.json")
      @preferences = JSON.load(File.read(file))
    rescue Errno::ENOENT, TypeError => ex
      VER.error(ex)
    end

    def setup_tags
      setup_highlight_trailing_whitespace
      setup_highlight_links
    end

    def setup_highlight_trailing_whitespace
      tag_configure 'invalid.trailing-whitespace', background: '#f00'
      tag_all_trailing_whitespace
    end

    def setup_highlight_links
      tag_configure 'markup.underline.link' , underline: true, foreground: '#00f'

      tag_bind('markup.underline.link', '<1>') do |event|
        pos = index("@#{event.x},#{event.y}")

        uri = tag_ranges('markup.underline.link').find{|from, to|
          if index(from) <= pos && index(to) >= pos
            break get(from, to)
          end
        }

        if uri
          browser = ENV['BROWSER'] || ['links', '-g']
          system(*browser, uri)
          message "%p opens the uri: %s" % [browser, uri]
        end
      end

      tag_all_uris
    end

    def tag_all_uris(given_options = {})
      tag_all_matching('markup.underline.link', /https?:\/\/[^)\]}\s'"]+/, given_options)
    end

    def tag_all_trailing_whitespace(given_options = {})
      tag_all_matching('invalid.trailing-whitespace', /[ \t]+$/, given_options)
    end

    def defer
      Tk::After.idle do
        begin
          yield
        rescue Exception => ex
          VER.error(ex)
        end
      end
    end

    def font(given_options = nil)
      if given_options
        options.font.configure(given_options)
      else
        options.font
      end
    end
  end
end