# Copyright 2010 Wincent Colaiuta. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

require 'command-t/finder'
require 'command-t/match_window'
require 'command-t/prompt'

module CommandT
  class Controller
    def initialize
      @prompt = Prompt.new
      @max_height = get_number('g:CommandTMaxHeight') || 0
      @finder = CommandT::Finder.new nil,
        :max_files              => get_number('g:CommandTMaxFiles'),
        :max_depth              => get_number('g:CommandTMaxDepth'),
        :always_show_dot_files  => get_bool('g:CommandTAlwaysShowDotFiles'),
        :never_show_dot_files   => get_bool('g:CommandTNeverShowDotFiles'),
        :scan_dot_directories   => get_bool('g:CommandTScanDotDirectories'),
        :excludes               => get_string('&wildignore')
    end

    def show
      @finder.path    = VIM::pwd
      @initial_window = $curwin
      @initial_buffer = $curbuf
      @match_window   = MatchWindow.new \
        :prompt               => @prompt,
        :match_window_at_top  => get_bool('g:CommandTMatchWindowAtTop')
      @focus          = @prompt
      @prompt.focus
      register_for_key_presses
      clear # clears prompt and list matches
    end

    def hide
      @match_window.close
      if @initial_window.select
        VIM::command "silent b #{@initial_buffer.number}"
      end
    end

    def flush
      @finder.flush
    end

    def key_pressed
      key = VIM::evaluate('a:arg').to_i.chr
      if @focus == @prompt
        @prompt.add! key
        list_matches
      else
        @match_window.find key
      end
    end

    def backspace_pressed
      if @focus == @prompt
        @prompt.backspace!
        list_matches
      end
    end

    def delete_pressed
      if @focus == @prompt
        @prompt.delete!
        list_matches
      end
    end

    def accept_selection options = {}
      selection = @match_window.selection
      hide
      open_selection(selection, options) unless selection.nil?
    end

    def toggle_focus
      @focus.unfocus # old focus
      if @focus == @prompt
        @focus = @match_window
      else
        @focus = @prompt
      end
      @focus.focus # new focus
    end

    def cancel
      hide
    end

    def select_next
      @match_window.select_next
    end

    def select_prev
      @match_window.select_prev
    end

    def clear
      @prompt.clear!
      list_matches
    end

    def cursor_left
      @prompt.cursor_left if @focus == @prompt
    end

    def cursor_right
      @prompt.cursor_right if @focus == @prompt
    end

    def cursor_end
      @prompt.cursor_end if @focus == @prompt
    end

    def cursor_start
      @prompt.cursor_start if @focus == @prompt
    end

  private

    def get_number name
      return nil if VIM::evaluate("exists(\"#{name}\")").to_i == 0
      VIM::evaluate("#{name}").to_i
    end

    def get_bool name
      return nil if VIM::evaluate("exists(\"#{name}\")").to_i == 0
      VIM::evaluate("#{name}").to_i != 0
    end

    def get_string name
      return nil if VIM::evaluate("exists(\"#{name}\")").to_i == 0
      VIM::evaluate("#{name}").to_s
    end

    # Backslash-escape space, \, |, %, #, "
    def sanitize_path_string str
      # for details on escaping command-line mode arguments see: :h :
      # (that is, help on ":") in the Vim documentation.
      str.gsub(/[ \\|%#"]/, '\\\\\0')
    end

    def default_open_command
      if !get_bool('&hidden') && get_bool('&modified')
        'sp'
      else
        'e'
      end
    end

    def open_selection selection, options = {}
      command = options[:command] || default_open_command
      selection = sanitize_path_string selection
      VIM::command "silent #{command} #{selection}"
    end

    def map key, function, param = nil
      VIM::command "noremap <silent> <buffer> #{key} " \
        ":call CommandT#{function}(#{param})<CR>"
    end

    def register_for_key_presses
      # "normal" keys (interpreted literally)
      numbers     = ('0'..'9').to_a.join
      lowercase   = ('a'..'z').to_a.join
      uppercase   = lowercase.upcase
      punctuation = '<>`@#~!"$%&/()=+*-_.,;:?\\\'{}[] ' # and space
      (numbers + lowercase + uppercase + punctuation).each_byte do |b|
        map "<Char-#{b}>", 'KeyPressed', b
      end

      # "special" keys
      map '<BS>',     'BackspacePressed'
      map '<Del>',    'DeletePressed'
      map '<CR>',     'AcceptSelection'
      map '<C-CR>',   'AcceptSelectionSplit'
      map '<C-s>',    'AcceptSelectionSplit'
      map '<C-t>',    'AcceptSelectionTab'
      map '<C-v>',    'AcceptSelectionVSplit'
      map '<Tab>',    'ToggleFocus'
      map '<Esc>',    'Cancel'
      map '<C-c>',    'Cancel'
      map '<C-n>',    'SelectNext'
      map '<C-p>',    'SelectPrev'
      map '<C-j>',    'SelectNext'
      map '<C-k>',    'SelectPrev'
      map '<Down>',   'SelectNext'
      map '<Up>',     'SelectPrev'
      map '<C-u>',    'Clear'
      map '<Left>',   'CursorLeft'
      map '<Right>',  'CursorRight'
      map '<C-h>',    'CursorLeft'
      map '<C-l>',    'CursorRight'
      map '<C-e>',    'CursorEnd'
      map '<C-a>',    'CursorStart'
    end

    # Returns the desired maximum number of matches, based on available
    # vertical space and the g:CommandTMaxHeight option.
    def match_limit
      limit = VIM::Screen.lines - 5
      limit = 1 if limit < 0
      limit = [limit, @max_height].min if @max_height > 0
      limit
    end

    def list_matches
      matches = @finder.sorted_matches_for @prompt.abbrev, :limit => match_limit
      @match_window.matches = matches
    end
  end # class Controller
end # module commandT
