#!/usr/bin/env ruby
# encoding: utf-8
#
#     ____      ____
#    / __/___  / __/
#   / /_/_  / / /_
#  / __/ / /_/ __/
# /_/   /___/_/    Fuzzy finder for your shell
#
# Version: 0.7.3 (February 20, 2014)
#
# Author:  Junegunn Choi
# URL:     https://github.com/junegunn/fzf
# License: MIT
#
# Copyright (c) 2014 Junegunn Choi
#
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require 'thread'
require 'curses'
require 'set'

unless String.method_defined? :force_encoding
  class String
    def force_encoding *arg
      self
    end
  end
end

class FZF
  C = Curses
  attr_reader :rxflag, :sort, :color, :mouse, :multi, :query, :filter, :extended

  class AtomicVar
    def initialize value
      @value = value
      @mutex = Mutex.new
    end

    def get
      @mutex.synchronize { @value }
    end

    def set value = nil
      @mutex.synchronize do
        @value = block_given? ? yield(@value) : value
      end
    end

    def method_missing sym, *args, &blk
      @mutex.synchronize { @value.send(sym, *args, &blk) }
    end
  end

  def initialize argv, source = $stdin
    @rxflag   = nil
    @sort     = ENV.fetch('FZF_DEFAULT_SORT', 1000).to_i
    @color    = true
    @multi    = false
    @extended = false
    @mouse    = true
    @filter   = nil

    argv =
      if opts = ENV['FZF_DEFAULT_OPTS']
        require 'shellwords'
        Shellwords.shellwords(opts) + argv
      else
        argv.dup
      end
    while o = argv.shift
      case o
      when '--version'           then version
      when '-h', '--help'        then usage 0
      when '-m', '--multi'       then @multi    = true
      when '+m', '--no-multi'    then @multi    = false
      when '-x', '--extended'    then @extended = true
      when '+x', '--no-extended' then @extended = false
      when '-i'                  then @rxflag   = Regexp::IGNORECASE
      when '+i'                  then @rxflag   = 0
      when '-c', '--color'       then @color    = true
      when '+c', '--no-color'    then @color    = false
      when       '--no-mouse'    then @mouse    = false
      when '+s', '--no-sort'     then @sort     = nil
      when '-q', '--query'
        usage 1, 'query string required' unless query = argv.shift
        @query = AtomicVar.new query.dup
      when /^-q(.*)$/, /^--query=(.*)$/
        @query = AtomicVar.new($1)
      when '-f', '--filter'
        usage 1, 'query string required' unless query = argv.shift
        @filter = query
      when /^-f(.*)$/, /^--filter=(.*)$/
        @filter = $1
      when '-s', '--sort'
        usage 1, 'sort size required' unless sort = argv.shift
        usage 1, 'invalid sort size'  unless sort =~ /^[0-9]+$/
        @sort = sort.to_i
      when /^-s([0-9]+)$/, /^--sort=([0-9]+)$/
        @sort = $1.to_i
      else
        usage 1, "illegal option: #{o}"
      end
    end

    @source   = source.clone
    @mtx      = Mutex.new
    @cv       = ConditionVariable.new
    @events   = {}
    @new      = []
    @queue    = Queue.new

    unless @filter
      @query  ||= AtomicVar.new('')
      @cursor_x = AtomicVar.new(@query.length)
      @matches  = AtomicVar.new([])
      @count    = AtomicVar.new(0)
      @vcursor  = AtomicVar.new(0)
      @vcursors = AtomicVar.new(Set.new)
      @spinner  = AtomicVar.new('-\|/-\|/'.split(//))
      @selects  = AtomicVar.new({}) # ordered >= 1.9
      @main     = Thread.current
      @plcount  = 0
    end
  end

  def start
    if @filter
      start_reader(false).join
      filter_list @new
    else
      @stdout = $stdout.clone
      $stdout.reopen($stderr)

      start_reader true
      init_screen
      start_renderer
      start_search
      start_loop
    end
  end

  def filter_list list
    matcher = (@extended ? ExtendedFuzzyMatcher : FuzzyMatcher).new @rxflag
    matches = matcher.match(list, @filter, '', '')
    if @sort && matches.length <= @sort
      matches = sort_by_rank(matches)
    end
    matches.each { |m| puts m.first }
  end

  def version
    File.open(__FILE__, 'r') do |f|
      f.each_line do |line|
        if line =~ /Version: (.*)/
          $stdout.puts "fzf " << $1
          exit
        end
      end
    end
  end

  def usage x, message = nil
    $stderr.puts message if message
    $stderr.puts %[usage: fzf [options]

  Options
    -m, --multi          Enable multi-select
    -x, --extended       Extended-search mode
    -q, --query=STR      Initial query
    -f, --filter=STR     Filter mode. Do not start interactive finder.
    -s, --sort=MAX       Maximum number of matched items to sort (default: 1000)
    +s, --no-sort        Do not sort the result. Keep the sequence unchanged.
    -i                   Case-insensitive match (default: smart-case match)
    +i                   Case-sensitive match
    +c, --no-color       Disable colors
        --no-mouse       Disable mouse

  Environment variables
    FZF_DEFAULT_COMMAND  Default command to use when input is tty
    FZF_DEFAULT_OPTS     Defaults options. (e.g. "-x -m --sort 10000")] + $/ + $/
    exit x
  end

  case RUBY_PLATFORM
  when /darwin/
    module UConv
      CHOSUNG   = 0x1100
      JUNGSUNG  = 0x1161
      JONGSUNG  = 0x11A7
      CHOSUNGS  = 19
      JUNGSUNGS = 21
      JONGSUNGS = 28
      JJCOUNT   = JUNGSUNGS * JONGSUNGS
      NFC_BEGIN = 0xAC00
      NFC_END   = NFC_BEGIN + CHOSUNGS * JUNGSUNGS * JONGSUNGS

      def self.nfd str
        str.split(//).map do |c|
          cp = c.ord
          if cp >= NFC_BEGIN && cp < NFC_END
            chr = ''
            idx  = cp - NFC_BEGIN
            cho  = CHOSUNG  + idx / JJCOUNT
            jung = JUNGSUNG + (idx % JJCOUNT) / JONGSUNGS
            jong = JONGSUNG + idx % JONGSUNGS
            chr << cho << jung
            chr << jong if jong != JONGSUNG
            chr
          else
            c
          end
        end
      end

      def self.to_nfc arr
        [NFC_BEGIN + arr[0] * JJCOUNT +
         (arr[1] || 0) * JONGSUNGS +
         (arr[2] || 0)].pack('U*')
      end

      if String.method_defined?(:each_char)
        def self.split str
          str.each_char.to_a
        end
      else
        def self.split str
          str.split('')
        end
      end

      def self.nfc str, offsets = []
        ret  = ''
        omap = []
        pend = []
        split(str).each_with_index do |c, idx|
          cp =
            begin
              c.ord
            rescue Exception
              next
            end
          omap << ret.length
          unless pend.empty?
            if cp >= JUNGSUNG && cp < JUNGSUNG + JUNGSUNGS
              pend << cp - JUNGSUNG
              next
            elsif cp >= JONGSUNG && cp < JONGSUNG + JONGSUNGS
              pend << cp - JONGSUNG
              next
            else
              omap[-1] = omap[-1] + 1
              ret << to_nfc(pend)
              pend.clear
            end
          end
          if cp >= CHOSUNG && cp < CHOSUNG + CHOSUNGS
            pend << cp - CHOSUNG
          else
            ret << c
          end
        end
        ret << to_nfc(pend) unless pend.empty?
        return [ret,
                offsets.map { |pair|
                  b, e = pair
                  [omap[b] || 0, omap[e] || ((omap.last || 0) + 1)] }]
      end
    end

    def convert_item item
      UConv.nfc(*item)
    end

    class Matcher
      def query_chars q
        UConv.nfd(q)
      end

      def sanitize q
        UConv.nfd(q).join
      end
    end
  else
    def convert_item item
      item
    end

    class Matcher
      def query_chars q
        q.split(//)
      end

      def sanitize q
        q
      end
    end
  end

  def emit event
    @mtx.synchronize do
      @events[event] = yield
      @cv.broadcast
    end
  end

  def max_items; C.lines - 2; end
  def cursor_y;  C.lines - 1; end
  def cprint str, col
    C.attron(col) do
      addstr_safe str
    end if str
  end
  def addstr_safe str
    C.addstr str.gsub("\0", '')
  end

  def print_input
    C.setpos cursor_y, 0
    C.clrtoeol
    cprint '> ', color(:prompt, true)
    C.attron(C::A_BOLD) do
      C.addstr @query.get
    end
  end

  def print_info msg = nil
    C.setpos cursor_y - 1, 0
    C.clrtoeol
    prefix =
      if spinner = @spinner.first
        cprint spinner, color(:spinner, true)
        ' '
      else
        '  '
      end
    C.attron color(:info, false) do
      C.addstr "#{prefix}#{@matches.length}/#{@count.get}"
      if (selected = @selects.length) > 0
        C.addstr " (#{selected})"
      end
      C.addstr msg if msg
    end
  end

  def refresh
    C.setpos cursor_y, 2 + width(@query[0, @cursor_x.get])
    C.refresh
  end

  def ctrl char
    char.to_s.ord - 'a'.ord + 1
  end

  def format line, limit, offsets
    offsets ||= []
    maxe = offsets.map { |e| e.last }.max || 0

    # Overflow
    if width(line) > limit
      ewidth = width(line[0...maxe])
      # Stri..
      if ewidth <= limit - 2
        line, _ = trim line, limit - 2, false
        line << '..'
      # ..ring
      else
        # ..ri..
        line = line[0...maxe] + '..' if ewidth < width(line) - 2
        line, diff = trim line, limit - 2, true
        offsets = offsets.map { |pair|
          b, e = pair
          b += 2 - diff
          e += 2 - diff
          b = [2, b].max
          [b, e]
        }
        line = '..' + line
      end
    end

    tokens = []
    index = 0
    offsets.select  { |pair| pair.first < pair.last }.
            sort_by { |pair| pair }.each do |pair|
      b, e = pair.map { |x| [index, x].max }
      tokens << [line[index...b], false]
      tokens << [line[b...e], true]
      index = e
    end
    tokens << [line[index..-1], false] if index < line.length
    tokens.reject { |pair| pair.first.empty? }
  end

  def print_item row, tokens, chosen, selected
    # Cursor
    C.setpos row, 0
    C.clrtoeol
    cprint chosen ? '>' : ' ', color(:cursor, true)
    cprint selected ? '>' : ' ',
      chosen ? color(:chosen) : (selected ? color(:selected, true) : 0)

    # Highlighted item
    C.attron color(:chosen, true) if chosen
    tokens.each do |pair|
      token, highlighted = pair

      if highlighted
        cprint   token, color(chosen ? :match! : :match, chosen)
        C.attron color(:chosen, true) if chosen
      else
        addstr_safe token
      end
    end
    C.attroff color(:chosen, true) if chosen
  end

  def sort_by_rank list
    list.sort_by { |tuple|
      line, offsets = tuple
      matchlen = 0
      pe = nil
      offsets.sort.each do |pair|
        b, e = pair
        b    = pe if pe && pe > b
        pe   = e
        matchlen += e - b
      end
      [matchlen, line.length, line]
    }
  end

  if RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join > '001009'
    @@wrx = Regexp.new '\p{Han}|\p{Katakana}|\p{Hiragana}|\p{Hangul}'
    def width str
      str.gsub(@@wrx, '  ').length rescue str.length
    end

    def trim str, len, left
      width = width str
      diff  = 0
      while width > len
        width -= (left ? str[0, 1] : str[-1, 1]) =~ @@wrx ? 2 : 1
        str    = left ? str[1..-1] : str[0...-1]
        diff  += 1
      end
      [str, diff]
    end
  else
    def width str
      str.length
    end

    def trim str, len, left
      diff = str.length - len
      if diff > 0
        [left ? str[diff..-1] : str[0...-diff], diff]
      else
        [str, 0]
      end
    end

    class ::String
      def ord
        self.unpack('c').first
      end
    end

    class ::Fixnum
      def ord
        self
      end
    end
  end

  def init_screen
    C.init_screen
    if @mouse
      C.mouseinterval 0
      C.mousemask C::ALL_MOUSE_EVENTS
    end
    C.stdscr.keypad(true)
    C.start_color
    dbg =
      if C.respond_to?(:use_default_colors)
        C.use_default_colors
        -1
      else
        C::COLOR_BLACK
      end
    C.raw
    C.nonl
    C.noecho

    if @color
      if ENV['TERM'].to_s =~ /256/
        C.init_pair 1, 110, dbg
        C.init_pair 2, 108, dbg
        C.init_pair 3, 254, 236
        C.init_pair 4, 151, 236
        C.init_pair 5, 148, dbg
        C.init_pair 6, 144, dbg
        C.init_pair 7, 161, 236
        C.init_pair 8, 168, 236
      else
        C.init_pair 1, C::COLOR_BLUE,    dbg
        C.init_pair 2, C::COLOR_GREEN,   dbg
        C.init_pair 3, C::COLOR_YELLOW,  C::COLOR_BLACK
        C.init_pair 4, C::COLOR_GREEN,   C::COLOR_BLACK
        C.init_pair 5, C::COLOR_GREEN,   dbg
        C.init_pair 6, C::COLOR_WHITE,   dbg
        C.init_pair 7, C::COLOR_RED,     C::COLOR_BLACK
        C.init_pair 8, C::COLOR_MAGENTA, C::COLOR_BLACK
      end

      def self.color sym, bold = false
        C.color_pair([:prompt, :match, :chosen, :match!,
                      :spinner, :info, :cursor, :selected].index(sym) + 1) |
          (bold ? C::A_BOLD : 0)
      end
    else
      def self.color sym, bold = false
        case sym
        when :chosen
          bold ? C::A_REVERSE : 0
        when :match
          C::A_UNDERLINE
        when :match!
          C::A_REVERSE | C::A_UNDERLINE
        else
          0
        end | (bold ? C::A_BOLD : 0)
      end
    end

    C.refresh
  end

  def start_reader curses
    stream =
      if @source.tty?
        if default_command = ENV['FZF_DEFAULT_COMMAND']
          IO.popen(default_command)
        elsif !`which find`.empty?
          IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null")
        else
          exit 1
        end
      else
        $stdin.reopen IO.open(IO.sysopen('/dev/tty'), 'r') if curses
        @source
      end

    Thread.new do
      while line = stream.gets
        emit(:new) { @new << line.chomp }
      end
      emit(:loaded) { true }
      @spinner.clear if curses
    end
  end

  def start_search
    matcher  = (@extended ? ExtendedFuzzyMatcher : FuzzyMatcher).new @rxflag
    searcher = Thread.new {
      lists   = []
      events  = {}
      fcache  = {}
      q       = ''
      delay   = -5

      begin
        while true
          @mtx.synchronize do
            while true
              events.merge! @events

              if @events.empty? # No new events
                @cv.wait @mtx
                next
              end
              @events.clear
              break
            end

            if events[:new]
              lists << @new
              @count.set { |c| c + @new.length }
              @spinner.set { |spinner|
                if e = spinner.shift
                  spinner.push e
                end; spinner
              }
              @new = []
              fcache.clear
            end
          end#mtx

          new_search = events[:key] || events.delete(:new)
          user_input = events[:key]
          progress   = 0
          started_at = Time.now

          if new_search && !lists.empty?
            q, cx = events.delete(:key) || [q, 0]
            empty = matcher.empty?(q)
            unless matches = fcache[q]
              found = []
              skip  = false
              cnt   = 0
              lists.each do |list|
                cnt += list.length
                skip = @mtx.synchronize { @events[:key] }
                break if skip

                if !empty && (progress = 100 * cnt / @count.get) < 100 && Time.now - started_at > 0.5
                  render { print_info " (#{progress}%)" }
                end

                found.concat(q.empty? ? list :
                             matcher.match(list, q, q[0, cx], q[cx..-1]))
              end
              next if skip
              matches = @sort ? found : found.reverse
              if !empty && @sort && matches.length <= @sort
                matches = sort_by_rank(matches)
              end
              fcache[q] = matches
            end

            # Atomic update
            @matches.set matches
          end#new_search

          # This small delay reduces the number of partial lists
          sleep((delay = [20, delay + 5].min) * 0.01) unless user_input

          update_list new_search
        end#while
      rescue Exception => e
        @main.raise e
      end
    }
  end

  def pick
    items = @matches[0, max_items]
    curr = [0, [@vcursor.get, items.length - 1].min].max
    [*items.fetch(curr, [])][0]
  end

  def update_list wipe
    render do
      items = @matches[0, max_items]

      # Wipe
      if items.length < @plcount
        @plcount.downto(items.length) do |idx|
          C.setpos cursor_y - idx - 2, 0
          C.clrtoeol
        end
      end
      @plcount = items.length

      maxc    = C.cols - 3
      vcursor = @vcursor.set { |v| [0, [v, items.length - 1].min].max }
      cleanse = Set[vcursor]
      @vcursors.set { |vs|
        cleanse.merge vs
        Set.new
      }
      items.each_with_index do |item, idx|
        next unless wipe || cleanse.include?(idx)
        row           = cursor_y - idx - 2
        chosen        = idx == vcursor
        selected      = @selects.include?([*item][0])
        line, offsets = convert_item item
        tokens        = format line, maxc, offsets
        print_item row, tokens, chosen, selected
      end
      print_info
      print_input
    end
  end

  def start_renderer
    Thread.new do
      begin
        while blk = @queue.shift
          blk.call
          refresh
        end
      rescue Exception => e
        @main.raise e
      end
    end
  end

  def render &blk
    @queue.push blk
    nil
  end

  def vselect &prc
    @vcursor.set { |v| @vcursors << v; prc.call v }
    update_list false
  end

  def num_unicode_bytes chr
    # http://en.wikipedia.org/wiki/UTF-8
    if chr & 0b10000000 > 0
      bytes = 0
      7.downto(2) do |shift|
        break if (chr >> shift) & 0x1 == 0
        bytes += 1
      end
      bytes
    else
      1
    end
  end

  def test_mouse st, *states
    states.any? { |s| s & st > 0 }
  end

  def start_loop
    got = nil
    begin
      input   = @query.get.dup
      cursor  = input.length
      backword = proc {
        cursor = (input[0, cursor].rindex(/\s\S/) || -1) + 1
      }
      actions = {
        :esc     => proc { exit 1 },
        ctrl(:d) => proc { exit 1 if input.empty? },
        ctrl(:m) => proc {
          got = pick
          exit 0
        },
        ctrl(:u) => proc { input = input[cursor..-1]; cursor = 0 },
        ctrl(:a) => proc { cursor = 0; nil },
        ctrl(:e) => proc { cursor = input.length; nil },
        ctrl(:j) => proc { vselect { |v| v - 1 } },
        ctrl(:k) => proc { vselect { |v| v + 1 } },
        ctrl(:w) => proc {
          pcursor = cursor
          backword.call
          input = input[0...cursor] + input[pcursor..-1]
        },
        ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 },
        ctrl(:i) => proc { |o|
          if @multi && sel = pick
            if @selects.has_key? sel
              @selects.delete sel
            else
              @selects[sel] = 1
            end
            vselect { |v|
              v + case o
                  when :select     then 0
                  when C::KEY_BTAB then 1
                  else -1
                  end
            }
          end
        },
        ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil },
        ctrl(:f) => proc { cursor = [input.length, cursor + 1].min; nil },
        ctrl(:l) => proc { render { C.clear; C.refresh }; update_list true },
        :alt_b => proc { backword.call; nil },
        :alt_f => proc {
          cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1
          nil
        },
      }
      actions[C::KEY_UP]        = actions[ctrl(:p)] = actions[ctrl(:k)]
      actions[C::KEY_DOWN]      = actions[ctrl(:n)] = actions[ctrl(:j)]
      actions[C::KEY_LEFT]      = actions[ctrl(:b)]
      actions[C::KEY_RIGHT]     = actions[ctrl(:f)]
      actions[C::KEY_BTAB]      = actions[:select]  = actions[ctrl(:i)]
      actions[C::KEY_BACKSPACE] = actions[127]      = actions[ctrl(:h)]
      actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc]

      emit(:key) { [@query.get, cursor] } unless @query.empty?
      pmv = nil
      while true
        @cursor_x.set cursor
        render { print_input }

        C.stdscr.timeout = -1
        ch = C.getch

        case ch
        when C::KEY_MOUSE
          if m = C.getmouse
            st = m.bstate
            if test_mouse(st, C::BUTTON1_PRESSED, C::BUTTON1_RELEASED)
              if m.y == cursor_y
                # TODO Wide-characters
                cursor = [0, [input.length, m.x - 2].min].max
              elsif m.x > 1 && m.y <= max_items
                vselect { |v|
                  tv = max_items - m.y - 1
                  if test_mouse(st, C::BUTTON1_RELEASED)
                    if test_mouse(st, C::BUTTON_SHIFT)
                      ch = :select
                    elsif pmv == tv
                      ch = ctrl(:m)
                    end
                    pmv = tv
                  end
                  tv
                }
              end
            elsif test_mouse(st, 0x8000000, C::BUTTON2_PRESSED)
              ch = C::KEY_DOWN
            elsif test_mouse(st, C::BUTTON4_PRESSED)
              ch = C::KEY_UP
            end
          end
        when 27
          C.stdscr.timeout = 0
          ch = # Typeahead arrow keys
            case ch2 = C.getch
            when '[', 91
              case ch3 = C.getch
              when 'D', 68 then ctrl(:b)
              when 'C', 67 then ctrl(:f)
              when 'B', 66 then ctrl(:j)
              when 'A', 65 then ctrl(:k)
              else ch3
              end
            when 'b',  98 then :alt_b
            when 'f', 102 then :alt_f
            when nil      then :esc
            else ch2
            end
        end

        upd = actions.fetch(ch, proc { |ch|
          if ch.is_a?(Fixnum)
            # Ruby 1.8
            if (ch.chr rescue '') =~ /[[:print:]]/
              ch = ch.chr
            elsif (nch = num_unicode_bytes(ch)) > 1
              chs = [ch]
              (nch - 1).times do |i|
                chs << C.getch
              end
              # UTF-8 TODO Ruby 1.8
              ch = chs.pack('C*').force_encoding('UTF-8')
            end
          end

          if ch.is_a?(String) && ch =~ /[[:print:]]/
            input.insert cursor, ch
            cursor += 1
          end
        }).call(ch)

        # Dispatch key event
        emit(:key) { [@query.set(input.dup), cursor] } if upd
      end
    ensure
      C.close_screen
      if got
        if @selects.empty?
          @stdout.puts got
        else
          @selects.each do |sel, _|
            @stdout.puts sel
          end
        end
      end
    end
  end

  class FuzzyMatcher < Matcher
    attr_reader :caches, :rxflag

    def initialize rxflag
      @caches = Hash.new { |h, k| h[k] = {} }
      @regexp = {}
      @rxflag = rxflag
    end

    def empty? q
      q.empty?
    end

    def rxflag_for q
      @rxflag || (q =~ /[A-Z]/ ? 0 : Regexp::IGNORECASE)
    end

    def fuzzy_regex q
      @regexp[q] ||= begin
        q = q.downcase if @rxflag == Regexp::IGNORECASE
        Regexp.new(query_chars(q).inject('') { |sum, e|
          e = Regexp.escape e
          sum << (e.length > 1 ? "(?:#{e}).*?" :  # FIXME: not equivalent
                                 "#{e}[^#{e}]*?")
        }, rxflag_for(q))
      end
    end

    def match list, q, prefix, suffix
      regexp = fuzzy_regex q

      cache = @caches[list.object_id]
      prefix_cache = nil
      (prefix.length - 1).downto(1) do |len|
        break if prefix_cache = cache[prefix[0, len]]
      end

      suffix_cache = nil
      0.upto(suffix.length - 1) do |idx|
        break if suffix_cache = cache[suffix[idx..-1]]
      end unless suffix.empty?

      partial_cache = [prefix_cache,
                       suffix_cache].compact.sort_by { |e| e.length }.first
      cache[q] ||= (partial_cache ?
                    partial_cache.map { |e| e.first } : list).map { |line|
        # Ignore errors: e.g. invalid byte sequence in UTF-8
        md = line.match(regexp) rescue nil
        md && [line, [md.offset(0)]]
      }.compact
    end
  end

  class ExtendedFuzzyMatcher < FuzzyMatcher
    def initialize rxflag
      super
      @regexps = {}
    end

    def empty? q
      parse(q).empty?
    end

    def parse q
      q = q.strip
      @regexps[q] ||= q.split(/\s+/).map { |w|
        invert =
          if w =~ /^!/
            w = w[1..-1]
            true
          end

        [ @regexp[w] ||=
            case w
            when ''
              nil
            when /^\^(.*)\$$/
              Regexp.new('^' << sanitize(Regexp.escape($1)) << '$', rxflag_for(w))
            when /^'/
              w.length > 1 ?
                Regexp.new(sanitize(Regexp.escape(w[1..-1])), rxflag_for(w)) : nil
            when /^\^/
              w.length > 1 ?
                Regexp.new('^' << sanitize(Regexp.escape(w[1..-1])), rxflag_for(w)) : nil
            when /\$$/
              w.length > 1 ?
                Regexp.new(sanitize(Regexp.escape(w[0..-2])) << '$', rxflag_for(w)) : nil
            else
              fuzzy_regex w
            end, invert ]
      }.select { |pair| pair.first }
    end

    def match list, q, prefix, suffix
      regexps = parse q
      # Look for prefix cache
      cache  = @caches[list.object_id]
      prefix = prefix.strip.sub(/\$\S*$/, '').sub(/(^|\s)!\S*$/, '')
      prefix_cache = nil
      (prefix.length - 1).downto(1) do |len|
        break if prefix_cache = cache[Set[@regexps[prefix[0, len]]]]
      end

      cache[Set[regexps]] ||= (prefix_cache ?
                               prefix_cache.map { |e| e.first } :
                               list).map { |line|
        offsets = []
        regexps.all? { |pair|
          regexp, invert = pair
          md = line.match(regexp) rescue nil
          if md && !invert
            offsets << md.offset(0)
          elsif !md && invert
            true
          end
        } && [line, offsets]
      }.select { |e| e }
    end
  end
end#FZF

FZF.new(ARGV, $stdin).start if ENV.fetch('FZF_EXECUTABLE', '1') == '1'

