#!/usr/bin/env ruby
require File.dirname(__FILE__) + "/../current_word"
require ENV['TM_SUPPORT_PATH'] + '/lib/ui'
require ENV['TM_SUPPORT_PATH'] + '/lib/escape'
require ENV['TM_SUPPORT_PATH'] + '/lib/osx/plist'
require 'rubygems'
require 'json'
require 'shellwords'

module TextMate
  class Complete
    IMAGES_FOLDER_NAME = 'icons'
    IMAGES = {
      "C"   => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Commands.png",
      "D"   => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Drag Commands.png",
      "L"   => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Languages.png",
      "M"   => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Macros.png",
      "P"   => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Preferences.png",
      "S"   => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Snippets.png",
      "T"   => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Templates.png",
      "Doc" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Template Files.png",
    }
    
    def initialize
    end
    
    # 0-config completion command using environment variables for everything
    def complete!
      return choices unless choices
      TextMate::UI.complete(choices, {:images => images, :extra_chars => extra_chars})
    end
    
    def tip!
      # If there is no current_word, then check the next current_word
      #   If there really is no current_word, then show a menu of choices
      
      chars = "a-zA-Z0-9" # Hard-coded into D2
      chars += Regexp.escape(extra_chars) if extra_chars
      current_word ||= Word.current_word chars, :both
      
      result = nil
      menu_choices = nil
      choices      = nil
      choice       = 0
      
      [current_word, current_method_name, current_collection_name].each do |initial_filter|
        next unless initial_filter and not initial_filter.empty?
        # p initial_filter
        
        choices = self.choices.select { |c| (c['match'] || c['display']) =~ /^#{Regexp.quote(initial_filter)}/ }
        
        # p choices
        break if choices and not choices.empty?
      end
      choices ||= self.choices
      
      menu_choices = choices.map { |c| c['display'] }
      choice = TextMate::UI.menu(menu_choices) if menu_choices and menu_choices.length > 1
      if choice
        result = choices[choice]
      end
      
      TextMate::UI.tool_tip( result['tool_tip'], {:format => result['tool_tip_format'] || :text }) if result
    end
    
    def choices
      @choices ||= data['suggestions']
    end
    def choices= choice_array
      @choices = array_to_suggestions(choice_array)
    end
    
    def images
      @images = data['images'] || IMAGES
      
      data['images']||{}.each_pair do |name,path|
        @images[name] = path
        next if File.exists? @images[name]
        @images[name] = ENV['TM_BUNDLE_SUPPORT'] + "/#{IMAGES_FOLDER_NAME}/" + path
        next if File.exists? @images[name]
        @images[name] = ENV['TM_SUPPORT_PATH']   + "/#{IMAGES_FOLDER_NAME}/" + path
        next if File.exists? @images[name]
      end
      
      @images
    end
    
    def tool_tip_prefix
      @tool_tip_prefix ||= data['tool_tip_prefix']
    end
    
    def extra_chars
      ENV['TM_COMPLETIONS_EXTRACHARS'] || data['extra_chars']
    end
    
    def chars
      "a-zA-Z0-9"
    end
    
    private
    def data(raw_data=nil)
      fix_legacy
      
      raw_data ||= read_data
      return {} unless raw_data and not raw_data.empty?
      
      @data = parse_data raw_data
      return @data
    end
    
    def read_data
      raw_data = read_file
      raw_data ||= read_string
      raw_data
    end
    
    def read_file
      paths = [ENV['TM_COMPLETIONS_FILE']] if ENV['TM_COMPLETIONS_FILE']
      paths ||= Shellwords.shellwords( ENV['TM_COMPLETIONS_FILES'] ) if ENV.has_key?('TM_COMPLETIONS_FILES')
      return nil unless paths
      
      paths.map do |path|
        next unless path and not path.empty?
        path = ENV['TM_BUNDLE_SUPPORT'] + '/' + path unless File.exists? path
        next unless File.exists? path
        
        { :data   => File.read(path),
          :format => path.scan(/\.([^\.]+)$/).last.last
        }
      end
    end
    
    def read_string
      [{:data   => ENV['TM_COMPLETIONS'],
        :format => ENV['TM_COMPLETIONS_SPLIT']
      }]
    end
    
    attr_accessor :filepath
    
    def parse_data(raw_datas)
      return @parsed if @parsed
      parsed = {"suggestions"=>[]}
      
      raw_datas.each do |raw_data|
        suggestions = parsed['suggestions']
        
        case raw_data[:format]
        when 'plist'
          par = parse_plist(raw_data)
        when 'json'
          par = parse_json(raw_data)
        when "txt"
          raw_data[:format] = "\n"
          par = parse_string(raw_data)
        when nil
          raw_data[:format] = ","
          par = parse_string(raw_data)
        else
          par = parse_string(raw_data)
        end
        
        if par['tool_tip_prefix']
          par['suggestions'] = par['suggestions'].map do |suggestion|
            suggestion['tool_tip'] = par['tool_tip_prefix'] + suggestion['tool_tip']
            suggestion
          end
        end
        
        parsed.merge! par
        parsed['suggestions'] = suggestions + parsed['suggestions']
      end
      
      @parsed = parsed
    end
    def parse_string(raw_data)
      return {} unless raw_data and raw_data[:data]
      return raw_data[:data] unless raw_data[:data].respond_to? :to_str
      raw_data[:data] = raw_data[:data].to_str
      
      data = {}
      data['suggestions'] = array_to_suggestions(raw_data[:data].split(raw_data[:format]))
      data
    end
    def parse_plist(raw_data)
      OSX::PropertyList.load(raw_data[:data])
    end
    def parse_json(raw_data)
      JSON.parse(raw_data[:data])
    end
    
    def array_to_suggestions(suggestions)
      suggestions.delete('')
      
      suggestions.map! do |c|
        {'display' => c}
      end
      
      suggestions
    end
    def current_method_name
      # Regex for finding a method or function name that's close to your caret position using Word.current_word
      # TODO: Allow completion prefs to define their own Complete.tip! method_name
      
      characters = "a-zA-Z0-9" # Hard-coded into D2
      characters += Regexp.escape(extra_chars) if extra_chars
      
      regex = %r/
      (?> [^\(\)]+ | \)   (?> [^\(\)]+ | \)   (?> [^\(\)]+ | \)   (?> [^\(\)]+ | \)   (?> [^\(\)]+ | \)   (?> [^\(\)]* )   \( | )+   \( | )+   \( | )+   \( | )+   \( | )+
      (?: \(([#{characters}]+) )?/ix
      
      Word.current_word(regex,:left)
    end
    def current_collection_name
      characters = "a-zA-Z0-9" # Hard-coded into D2
      characters += Regexp.escape(extra_chars) if extra_chars
      
      regex = %r/
      (?> [^\[\]]+ | \]   (?> [^\[\]]+ | \]   (?> [^\[\]]+ | \]   (?> [^\[\]]+ | \]   (?> [^\[\]]+ | \]   (?> [^\[\]]* )   \[ | )+   \[ | )+   \[ | )+   \[ | )+   \[ | )+
      (?: \[([#{characters}]+) )?/ix
      
      Word.current_word(regex,:left)
    end
    
    def fix_legacy
      ENV['TM_COMPLETIONS_SPLIT'] ||= ENV['TM_COMPLETIONS_split'] 
    end
  end
end

if __FILE__ == $0

`open "txmt://open?url=file://$TM_FILEPATH"` #For testing purposes, make this document the topmost so that the complete popup works
ENV['WEB_PREVIEW_RUBY']='NO-RUN'
require "test/unit"
# require "complete"

class TestComplete < Test::Unit::TestCase
  def setup
    @string_raw = 'ad(),adipisicing,aliqua,aliquip,amet,anim,aute,cillum,commodo,consectetur,consequat,culpa,cupidatat,deserunt,do,dolor,dolore,Duis,ea,eiusmod,elit,enim,esse,est,et,eu,ex,Excepteur,exercitation,fugiat,id,in,incididunt,ipsum,irure,labore,laboris,laborum,Lorem,magna,minim,mollit,nisi,non,nostrud,nulla,occaecat,officia,pariatur,proident,qui,quis,reprehenderit,sed,sint,sit,sunt,tempor,ullamco,Ut,ut,velit,veniam,voluptate,'
    
    @plist_raw = <<-'PLIST'
    { suggestions = ( 
        { display = moo; image = Drag;    insert = "(${1:one}, ${2:one}, ${3:three}${4:, ${5:five}, ${6:six}})";         tool_tip = "moo(one, two, four[, five])\n This method does something or other maybe.\n Insert longer description of it here."; }, 
        { display = foo; image = Macro;   insert = "(${1:one}, \"${2:one}\", ${3:three}${4:, ${5:five}, ${6:six}})";     tool_tip = "foo(one, two)\n This method does something or other maybe.\n Insert longer description of it here."; }, 
        { display = bar; image = Command; insert = "(${1:one}, ${2:one}, \"${3:three}\"${4:, \"${5:five}\", ${6:six}})"; tool_tip = "bar(one, two[, three])\n This method does something or other maybe.\n Insert longer description of it here."; } 
      ); 
      extra_chars = '.';
      images = { 
        Command    = "Commands.png"; 
        Drag       = "Drag Commands.png"; 
        Language   = "Languages.png"; 
        Macro      = "Macros.png"; 
        Preference = "Preferences.png"; 
        Snippet    = "Snippets.png"; 
        Template   = "Template Files.png"; 
        Templates  = "Templates.png"; 
      }; 
    }
    PLIST
    
    @json_raw = <<-'JSON'
    {
    	"extra_chars": "-_$.",
    	"suggestions": [
    		{ "display": ".moo", "image": "", "insert": "(${1:one}, ${2:one}, ${3:three}${4:, ${5:five}, ${6:six}})",         "tool_tip": "moo(one, two, four[, five])\n This method does something or other maybe.\n Insert longer description of it here." },
    		{ "display": "foo",  "image": "", "insert": "(${1:one}, \"${2:one}\", ${3:three}${4:, ${5:five}, ${6:six}})",     "tool_tip": "foo(one, two)\n This method does something or other maybe.\n Insert longer description of it here." },
    		{ "display": "bar",  "image": "", "insert": "(${1:one}, ${2:one}, \"${3:three}\"${4:, \"${5:five}\", ${6:six}})", "tool_tip": "bar(one, two[, three])\n This method does something or other maybe.\n Insert longer description of it here." }
    	],
    	"images": {
    		"String"  : "String.png",
    		"RegExp"  : "RegExp.png",
    		"Number"  : "Number.png",
    		"Array"   : "Array.png",
    		"Function": "Function.png",
    		"Object"  : "Object.png",
    		"Node"    : "Node.png",
    		"NodeList": "NodeList.png"
    	}
    }
    JSON
  end
  
  def test_basic_complete
    ENV['TM_COMPLETIONS'] = @string_raw
    
    assert_equal ENV['TM_COMPLETIONS'].split(','), TextMate::Complete.new.choices.map{|c| c['display']}
    assert_equal TextMate::Complete::IMAGES, TextMate::Complete.new.images
    
    TextMate::Complete.new.complete!
  end
  # 
  def test_should_support_plist
    ENV['TM_COMPLETIONS_SPLIT']='plist'
    ENV['TM_COMPLETIONS'] = @plist_raw
    TextMate::Complete.new.complete!
  end
  # 
  def test_should_support_json
    ENV.delete 'TM_COMPLETIONS'
    assert_nil(ENV['TM_COMPLETIONS'])
    ENV.delete 'TM_COMPLETIONS_SPLIT'
    assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
    
    ENV['TM_COMPLETIONS_SPLIT']='json'
    ENV['TM_COMPLETIONS'] = @json_raw
    fred = TextMate::Complete.new
    assert_equal(3, fred.choices.length)
  end
  # 
  def test_should_be_able_to_modify_the_choices
    ENV['TM_COMPLETIONS'] = @string_raw
    
    fred = TextMate::Complete.new
    
    assert_not_nil fred.choices
    assert_equal ENV['TM_COMPLETIONS'].split(','), fred.choices.map{|c| c['display']}
    fred.choices.reject!{|choice| choice['display'] !~ /^a/ }
    assert_equal ENV['TM_COMPLETIONS'].split(',').grep(/^a/), fred.choices.map{|c| c['display']}
    
    fred.choices=%w[fred is not my name]
    assert_equal %w[fred is not my name], fred.choices.map{|c| c['display']}
  end
  # 
  def test_should_parse_files_based_on_extension_plist
    ENV['TM_COMPLETIONS_FILE'] = '/tmp/completions_test.plist'
    
    File.open(ENV['TM_COMPLETIONS_FILE'],'w'){|file| file.write @plist_raw }
    assert File.exists?(ENV['TM_COMPLETIONS_FILE'])
    
    fred = TextMate::Complete.new
    assert_equal(['moo', 'foo', 'bar'], fred.choices.map{|c| c['display']})
  end
  # 
  def test_should_parse_files_based_on_extension_txt
    ENV.delete 'TM_COMPLETIONS'
    assert_nil(ENV['TM_COMPLETIONS'])
    ENV.delete 'TM_COMPLETIONS_SPLIT'
    assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
    
    ENV['TM_COMPLETIONS_FILE'] = '/tmp/completions_test.txt'
    
    File.open(ENV['TM_COMPLETIONS_FILE'],'w'){|file| file.write @string_raw.gsub(',',"\n") }
    assert File.exists?(ENV['TM_COMPLETIONS_FILE'])
    
    fred = TextMate::Complete.new
    
    assert_equal(@string_raw.split(','), fred.choices.map{|c| c['display']})
  end
  # 
  def test_should_parse_multiple_files
    ENV.delete 'TM_COMPLETIONS'
    assert_nil(ENV['TM_COMPLETIONS'])
    ENV.delete 'TM_COMPLETIONS_SPLIT'
    assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
    ENV.delete 'TM_COMPLETIONS_FILE'
    assert_nil(ENV['TM_COMPLETIONS_FILE'])
    
    ENV['TM_COMPLETIONS_FILES'] = "'/tmp/completions_test.txt' '/tmp/completions_test1.txt' '/tmp/completions_test2.txt'"
    
    require 'shellwords'
    Shellwords.shellwords( ENV['TM_COMPLETIONS_FILES'] ).each_with_index do |filepath,i|
      File.open(filepath,'w'){|file| file.write @string_raw.gsub(',',"#{i}\n") }
      assert File.exists?(filepath)
    end
    
    fred = TextMate::Complete.new
    
    assert_equal(@string_raw.split(',').uniq.length * 3, fred.choices.map{|c| c['display']}.length )
  end
  # 
  def test_should_override_split_with_extension
    ENV['TM_COMPLETIONS_SPLIT'] = ','
    ENV['TM_COMPLETIONS_FILE'] = '/tmp/completions_test.plist'
    
    File.open(ENV['TM_COMPLETIONS_FILE'],'w'){|file| file.write @plist_raw }
    assert File.exists?(ENV['TM_COMPLETIONS_FILE'])
    
    fred = TextMate::Complete.new
    assert_equal(['moo', 'foo', 'bar'], fred.choices.map{|c| c['display']})
  end
  # 
  def test_should_get_extra_chars_from_var
    ENV['TM_COMPLETIONS_SPLIT']=','
    ENV['TM_COMPLETIONS'] = @string_raw
    ENV['TM_COMPLETIONS_EXTRACHARS'] = '.'
    
    fred = TextMate::Complete.new
    assert_equal('.', fred.extra_chars)
  end
  # 
  def test_should_get_extra_chars_from_plist
    ENV['TM_COMPLETIONS_SPLIT']='plist'
    ENV['TM_COMPLETIONS'] = @plist_raw
    
    assert_nil(ENV['TM_COMPLETIONS_EXTRACHARS'])
    
    fred = TextMate::Complete.new
    assert_equal('.', fred.extra_chars)
  end
  # TODO: should_fix_image_paths
=begin
  def test_should_fix_image_paths
    ENV['TM_COMPLETIONS_SPLIT'] = 'plist'
    ENV['TM_COMPLETIONS']       = @plist_raw
    ENV['TM_BUNDLE_SUPPORT']    = '/tmp'
    ENV['TM_SUPPORT_PATH']      = '/tmp'
    
    images = OSX::PropertyList.load(@plist_raw)['images']
    
    FileUtils.mkdir_p "#{ENV['TM_SUPPORT_PATH']}/#{TextMate::Complete::IMAGES_FOLDER_NAME}"
    images.each_pair do |name,path|
      File.open("#{ENV['TM_SUPPORT_PATH']}/#{TextMate::Complete::IMAGES_FOLDER_NAME}/#{path}", 'w'){ |file| file.write('') }
    end
    
    TextMate::Complete.new.images.each_pair do |name,path|
      assert File.exists?(path)
    end
    
  end
=end
  def test_should_apply_prefix
    ENV.delete 'TM_COMPLETIONS'
    assert_nil(ENV['TM_COMPLETIONS'])
    ENV.delete 'TM_COMPLETIONS_SPLIT'
    assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
    
    @json_raw = <<-'JSON'
    {
    	"extra_chars": "-_$.",
    	"tool_tip_prefix":"prefix",
    	"suggestions": [
    		{ "display": ".moo", "image": "", "insert": "(${1:one}, ${2:one}, ${3:three}${4:, ${5:five}, ${6:six}})",         "tool_tip": "moo(one, two, four[, five])\n This method does something or other maybe.\n Insert longer description of it here." },
    		{ "display": "foo",  "image": "", "insert": "(${1:one}, \"${2:one}\", ${3:three}${4:, ${5:five}, ${6:six}})",     "tool_tip": "foo(one, two)\n This method does something or other maybe.\n Insert longer description of it here." },
    		{ "display": "bar",  "image": "", "insert": "(${1:one}, ${2:one}, \"${3:three}\"${4:, \"${5:five}\", ${6:six}})", "tool_tip": "bar(one, two[, three])\n This method does something or other maybe.\n Insert longer description of it here." }
    	],
    	"images": {
    		"String"  : "String.png",
    		"RegExp"  : "RegExp.png",
    		"Number"  : "Number.png",
    		"Array"   : "Array.png",
    		"Function": "Function.png",
    		"Object"  : "Object.png",
    		"Node"    : "Node.png",
    		"NodeList": "NodeList.png"
    	}
    }
    JSON
    
    ENV['TM_COMPLETIONS_SPLIT']='json'
    ENV['TM_COMPLETIONS'] = @json_raw
    fred = TextMate::Complete.new
    assert_equal(3, fred.choices.length)
    assert fred.choices.first['tool_tip'].match(/^prefix/)
  end
  # 
  def test_should_show_tooltip_without_inserting_anything
    # This method passes if it shows a tooltip when selecting a menu-item 
    # and DOESN'T insert anything or cause the document think anything has changed
    ENV.delete 'TM_COMPLETIONS'
    assert_nil(ENV['TM_COMPLETIONS'])
    ENV.delete 'TM_COMPLETIONS_SPLIT'
    assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
    
    ENV['TM_COMPLETIONS_SPLIT']='json'
    ENV['TM_COMPLETIONS'] = @json_raw
    fred = TextMate::Complete.new
    assert_equal(3, fred.choices.length)
    
    TextMate::Complete.new.tip!
  end
  # 
  def test_tip_should_look_for_the_current_word_and_then_try_the_closest_function_name
    ENV.delete 'TM_COMPLETIONS'
    assert_nil(ENV['TM_COMPLETIONS'])
    ENV.delete 'TM_COMPLETIONS_SPLIT'
    assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
    
    ENV['TM_COMPLETIONS_SPLIT']='json'
    ENV['TM_COMPLETIONS'] = @json_raw
    fred = TextMate::Complete.new
    assert_equal(3, fred.choices.length)
    
    TextMate::Complete.new.tip!
    # 
    # This test passes if, when run, you see the tooltip for closest function
    #   Be sure to more your caret around and try a few times
    # Showing a menu is a fail
    # 
    # foo( bar(  ), '.moo' ) 
    #    ^ Caret here should give the tip for 'foo'
    #          ^ Caret here should give the tip for 'bar'
    #                     ^ Caret here should give the tip for 'bar'
    #                ^ Caret here should give the tip for '.moo'
    # foo( bar(one,two,foo), '.moo' ) 
    # foo['bar']
  end
  #
end

end#if
