#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
import argcomplete, argparse
import os     # to execute nvim
from neovim import attach
import operator
# from argcomplete import warn # logging

# convert string into a (char -> list) map
# the list contains the indices of char position in string
# we use this map to find the location of corresponding char from user_input
def index_map(user_input, string):
  indices = {}
  for ch in user_input:
    if not indices.has_key(ch):
      indices[ch] = []

  for i, ch in enumerate(string):
    if indices.has_key(ch):
      index = indices[ch]
      index.append(i)

  return indices

# for file name partial matching
# we want to give substring the lowest score
# 0: 'abc', '123abc456' <= exact substring
# 1: 'abc', '12a34bc56' <= two substring 'a', 'bc'
# 2: 'abc', '1a34b5c6' <= three substring 'a', 'b', 'c'
# None: 'abc', '123ab456' <= 'abc' is not a subsequence of '123ab456'
def match_fuzzy_substring(user_input, string):
  if len(user_input) <= 0:
    # are you kidding me?
    return 0

  max_cost = len(user_input) + 1
  indices = index_map(user_input, string)

  # first line
  distances = []
  for i in indices[user_input[0]]:
    distances.append((0, i)) # cost, index

  for ch in user_input[1:]:
    if len(distances) <= 0:
      # user_input is not a subsequence of string
      break
    distances_ = []
    for i in indices[ch]:
      min_cost = max_cost
      for t in distances:
        # TODO: binary search?
        if (t[1] >= i):
          continue

        if t[1] != i - 1:
          # non-consecutive matching
          # cost++
          # e.g. compare 'aa' against 'aba'
          cost = t[0] + 1
        else:
          # consecutive matching
          # e.g. compare 'aa' against 'aa' or 'baa' or 'aab'
          cost = t[0]
        if cost < min_cost:
          min_cost = cost

      if min_cost != max_cost: # at least we got something
        distances_.append((min_cost, i))
    distances = distances_

  if len(distances) > 0:
    return [x for x, _ in sorted(distances)][0] # return the final cost
  else:
    return None # user_input is not a subsequence of string

# for file name partial matching
def fuzzyfinder(user_input, collection, is_case_sensitive = True):
    if not is_case_sensitive:
      user_input = user_input.lower()

    suggestions = []
    for i, item in enumerate(collection):
      if is_case_sensitive:
        score = match_fuzzy_substring(user_input, item) # Checks if user_input is a subsequence of theItem
      else:
        score = match_fuzzy_substring(user_input, item.lower()) # Checks if user_input is a subsequence of theItem
        if score != None:
          # print (score, item)
          suggestions.append((score, i, item))

    # sort the result based on score and i
    # smaller i means more recent files
    return [x for _, _, x in sorted(suggestions, key = operator.itemgetter(0, 1))]

# no need to validate
def novalidator(completion, prefix):
    # return TRUE
    return completion.startwith(prefix)

# only return existed files
def exist_filter(files, max_count):
  res = []
  for i, path in enumerate(files):
    if os.path.exists(path):
      res.append(path)
    if len(res) >= max_count:
      break

  return res

def file_completer(prefix, cwd):
  res = []

  # if user typed a complete dir name, search for the files in it instead
  prefix = os.path.join(prefix, '') if os.path.isdir(prefix) else prefix

  (prefix_path, last_item) = os.path.split(prefix)

  target_dir = os.path.join(cwd, prefix_path)

  for i in os.listdir(target_dir):
    if i.startswith(last_item):
      res.append(os.path.join(target_dir, i))

  return res

def nvim_recent_files(prefix, parsed_args, **kwargs):
    # handle ~/my_file or ~abc cases
    prefix = os.path.expanduser(os.path.normpath(prefix))

    # get nvim recent files
    nvim = attach('child', argv=["nvim", "--embed"])
    nvim.command('let nfasd_history = v:oldfiles')
    history = nvim.vars['nfasd_history']

    cwd = os.getcwd()
    files = file_completer(prefix, cwd)

    is_case_sensitive = True
    if prefix == prefix.lower():
      # smart case: no uppercase char, so we just ignore cases
      is_case_sensitive = False

    matched = []
    if len(prefix):
      matched = fuzzyfinder(prefix, history, is_case_sensitive)
    else:
      matched = history

    # give local files higher priority over nvim history (my intuition?)
    collection = files + matched

    # remove duplicated files
    seen = {}
    collection = [seen.setdefault(x, x) for x in collection if x not in seen]

    # limit choices to 8, otherwise the screen might be cluttered with lines
    res = exist_filter(collection, 8)

    # if the result is a subdir of current working dir
    # remove the common prefix
    res = [os.path.relpath(x, cwd) if x.startswith(cwd) else x for x in res]

    # add trailing slash for directories
    res = [os.path.join(x, '') if os.path.isdir(x) else x for x in res]

    return res

parser = argparse.ArgumentParser()
parser.add_argument("-e", help='the executable name of neovim')
parser.add_argument("filepath", help='the path of the recent file').completer = nvim_recent_files

def my_validator(current_input, keyword_to_check_against):
    # warn('my_validator'+current_input+keyword_to_check_against)
    # Pass through ALL options even if they don't all start with 'current_input'
    return True

argcomplete.autocomplete(parser, validator=my_validator, always_complete_options=False) # don't complete annoying --help

# args = parser.parse_args()
args, unknown = parser.parse_known_args()

# print args.filepath
# print args.e
# print unknown

if len(unknown):
  path = unknown[0]
else:
  path = args.filepath

if not os.path.exists(path):
  # the user might forget to press tab (like nfasd -e nvim mysomet...)
  # help the user press "the tab"
  res = nvim_recent_files(path, None)

  if len(res):
    path = res[0][len(path)+1:] # remove prefix

# don't do anything when the file doesn't exist
# we don't want user to accidentally create some file with strange partial file name
if os.path.exists(path):
  exe_name = 'nvim'
  if args.e and len(args.e):
    exe_name = args.e # use the user-defined executable name
  exec_str = exe_name + ' ' + path

  os.system(exec_str)

# if __name__ == "__main__":
  # print match_fuzzy_substring('drop', 'Dropbox/p')         # =>None
  # print match_fuzzy_substring('drop', 'dropbox/p')         # =>(3, 0)
  # print match_fuzzy_substring('aa', 'aa')                  # =>(1, 0)
  # print match_fuzzy_substring('aa', 'aab')                 # =>(1, 0)
  # print match_fuzzy_substring('aa', 'baa')                 # =>(2, 0)
  # print match_fuzzy_substring('aa', 'bbaa')                # =>(3, 0)
  # print match_fuzzy_substring('aa', 'aabb')                # =>(1, 0)
  # print match_fuzzy_substring('aa', 'abba')                # =>(3, 1)
  # print match_fuzzy_substring('aaa', 'ababa')              # =>(4, 2)
  # print match_fuzzy_substring('init', 'ing/install.sh')    # =>(7, 2)
  # print match_fuzzy_substring('init', 'ing/nvim/init.vim') # =>(12, 0)
  # print match_fuzzy_substring('init', 'ing/insitall.sh')   # =>(8, 1)
  # print match_fuzzy_substring("foo",  "foobar")            # =>(2, 0)
  # print match_fuzzy_substring("foo",  "FooBar")            # =>None
  # print match_fuzzy_substring("fb",  "foobar")             # =>(3, 1)
  # print match_fuzzy_substring("",  "foo bar")              # =>0
  # print match_fuzzy_substring("",  "")                     # =>0
  # print match_fuzzy_substring("f",  "")                    # =>None

