let s:save_cpo = &cpo
set cpo&vim

" SpaceVim API
let s:HI = SpaceVim#api#import('vim#highlight')

" constants
let s:ESC_CODE = char2nr("\<Esc>")
let s:HAS_TIMER = has('timers')

" configurations
let g:clever_f_across_no_line          = get(g:, 'clever_f_across_no_line', 0)
let g:clever_f_ignore_case             = get(g:, 'clever_f_ignore_case', 0)
let g:clever_f_use_migemo              = get(g:, 'clever_f_use_migemo', 0)
let g:clever_f_fix_key_direction       = get(g:, 'clever_f_fix_key_direction', 0)
let g:clever_f_show_prompt             = get(g:, 'clever_f_show_prompt', 0)
let g:clever_f_smart_case              = get(g:, 'clever_f_smart_case', 0)
let g:clever_f_chars_match_any_signs   = get(g:, 'clever_f_chars_match_any_signs', '')
let g:clever_f_mark_cursor             = get(g:, 'clever_f_mark_cursor', 1)
let g:clever_f_hide_cursor_on_cmdline  = get(g:, 'clever_f_hide_cursor_on_cmdline', 1)
let g:clever_f_timeout_ms              = get(g:, 'clever_f_timeout_ms', 0)
let g:clever_f_mark_char               = get(g:, 'clever_f_mark_char', 1)
let g:clever_f_repeat_last_char_inputs = get(g:, 'clever_f_repeat_last_char_inputs', ["\<CR>"])
let g:clever_f_mark_direct             = get(g:, 'clever_f_mark_direct', 0)
let g:clever_f_highlight_timeout_ms    = get(g:, 'clever_f_highlight_timeout_ms', 0)

" below variable must be set before loading this script
let g:clever_f_clean_labels_eagerly    = get(g:, 'clever_f_clean_labels_eagerly', 1)

" highlight labels
augroup plugin-clever-f-highlight
  autocmd!
  autocmd ColorScheme * highlight default CleverFDefaultLabel ctermfg=red ctermbg=NONE cterm=bold,underline guifg=red guibg=NONE gui=bold,underline
augroup END
highlight default CleverFDefaultLabel ctermfg=red ctermbg=NONE cterm=bold,underline guifg=red guibg=NONE gui=bold,underline

" Priority of highlight customization is:
"   High:   When g:clever_f_*_color
"   Middle: :highlight in a colorscheme
"   Low:    Default highlights
" When the variable is defined, it should be linked with :hi! since :hi does
" not overwrite existing highlight group. (#50)
if g:clever_f_mark_cursor
  if exists('g:clever_f_mark_cursor_color')
    execute 'highlight! link CleverFCursor' g:clever_f_mark_cursor_color
  else
    highlight link CleverFCursor Cursor
  endif
endif
if g:clever_f_mark_char
  if exists('g:clever_f_mark_char_color')
    execute 'highlight! link CleverFChar' g:clever_f_mark_char_color
  else
    highlight link CleverFChar CleverFDefaultLabel
  endif
endif
if g:clever_f_mark_direct
  if exists('g:clever_f_mark_direct_color')
    execute 'highlight! link CleverFDirect' g:clever_f_mark_direct_color
  else
    highlight link CleverFDirect CleverFDefaultLabel
  endif
endif

if g:clever_f_clean_labels_eagerly
  augroup plugin-clever-f-permanent-finalizer
    autocmd!
    autocmd WinEnter,WinLeave,CmdWinLeave * if g:clever_f_mark_char | call s:remove_highlight() | endif
  augroup END
endif
augroup plugin-clever-f-finalizer
  autocmd!
augroup END

" initialize the internal state
let s:last_mode = ''
let s:previous_map = {}
let s:previous_pos = {}
let s:first_move = {}
let s:migemo_dicts = {}
let s:previous_char_num = {}
let s:timestamp = [0, 0]
let s:highlight_timer = -1

" keys are mode string returned from mode()
function! clever_f#reset() abort
  let s:previous_map = {}
  let s:previous_pos = {}
  let s:first_move = {}
  let s:migemo_dicts = {}

  " Note:
  " [0, 0] may be invalid because the representation of return value of reltime() depends on implementation.
  let s:timestamp = [0, 0]

  call s:remove_highlight()

  return ''
endfunction

" hidden API for debug
function! clever_f#_reset_all() abort
  call clever_f#reset()
  let s:last_mode = ''
  let s:previous_char_num = {}
  autocmd! plugin-clever-f-finalizer
  unlet! s:moved_forward
endfunction

function! s:remove_highlight() abort
  if s:highlight_timer >= 0
    call timer_stop(s:highlight_timer)
    let s:highlight_timer = -1
  endif
  for h in filter(getmatches(), 'v:val.group ==# "CleverFChar"')
    call matchdelete(h.id)
  endfor
endfunction

function! s:is_timedout() abort
  let cur = reltime()
  let rel = reltimestr(reltime(s:timestamp, cur))
  let elapsed_ms = float2nr(str2float(rel) * 1000.0)
  let s:timestamp = cur
  return elapsed_ms > g:clever_f_timeout_ms
endfunction

function! s:on_highlight_timer_expired(timer) abort
  if s:highlight_timer != a:timer
    return
  endif
  let s:highlight_timer = -1
  call s:remove_highlight()
endfunction

" highlight characters to which the cursor can be moved directly
" Note: public function for test
function! clever_f#_mark_direct(forward, count) abort
  let line = getline('.')
  let [_, l, c, _] = getpos('.')

  if (a:forward && c >= len(line)) || (!a:forward && c == 1)
    " there is no matching characters
    return []
  endif

  if g:clever_f_ignore_case
    let line = tolower(line)
  endif

  let char_count = {}
  let matches = []
  let indices = a:forward ? range(c, len(line) - 1, 1) : range(c - 2, 0, -1)
  for i in indices
    let ch = line[i]
    " only matches to ASCII
    if ch !~# '^[\x00-\x7F]$' | continue | endif
    let ch_lower = tolower(ch)

    let char_count[ch] = get(char_count, ch, 0) + 1
    if g:clever_f_smart_case && ch =~# '\u'
      " uppercase characters are doubly counted
      let char_count[ch_lower] = get(char_count, ch_lower, 0) + 1
    endif

    if char_count[ch] == a:count ||
          \ (g:clever_f_smart_case && char_count[ch_lower] == a:count)
      " NOTE: should not use `matchaddpos(group, [...position])`,
      " because the maximum number of position is 8
      let m = matchaddpos('CleverFDirect', [[l, i + 1]])
      call add(matches, m)
    endif
  endfor
  return matches
endfunction

function! s:mark_char_in_current_line(map, char) abort
  let regex = '\%' . line('.') . 'l' . s:generate_pattern(a:map, a:char)
  call matchadd('CleverFChar', regex , 999)
endfunction

" Note:
" \x80\xfd` seems to be sent by a terminal.
" Below is a workaround for the sequence.
function! s:getchar() abort
  while 1
    let cn = getchar()
    if type(cn) != type('') || cn !=# "\x80\xfd`"
      return cn
    endif
  endwhile
endfunction

function! s:include_multibyte_char(str) abort
  return strlen(a:str) != clever_f#compat#strchars(a:str)
endfunction

function! clever_f#find_with(map) abort
  if a:map !~# '^[fFtT]$'
    throw "clever-f: Invalid mapping '" . a:map . "'"
  endif

  if &foldopen =~# '\<\%(all\|hor\)\>'
    while foldclosed(line('.')) >= 0
      foldopen
    endwhile
  endif

  let current_pos = getpos('.')[1 : 2]
  let mode = s:mode()
  let highlight_timer_enabled = g:clever_f_mark_char && g:clever_f_highlight_timeout_ms > 0 && s:HAS_TIMER
  let in_macro = clever_f#compat#reg_executing() !=# ''

  " When 'f' is run while executing a macro, do not repeat previous
  " character. See #59 for more details
  if current_pos != get(s:previous_pos, mode, [0, 0]) || in_macro
    let should_redraw = !in_macro
    let back = 0
    if g:clever_f_mark_cursor
      let cursor_marker = matchadd('CleverFCursor', '\%#', 999)
      if should_redraw
        redraw
      endif
    endif
    " block-NONE does not work on Neovim
    if g:clever_f_hide_cursor_on_cmdline
      let save_tve = &t_ve
      setlocal t_ve=
      let cursor_hi = {}
      let cursor_hi = s:HI.group2dict('Cursor')
      let lcursor_hi = s:HI.group2dict('lCursor')
      let guicursor = &guicursor
      call s:HI.hide_in_normal('Cursor')
      call s:HI.hide_in_normal('lCursor')
      if has('nvim')
        set guicursor+=a:Cursor/lCursor
      endif
    endif
    try
      if g:clever_f_mark_direct && should_redraw
        let direct_markers = clever_f#_mark_direct(a:map =~# '\l', v:count1)
        redraw
      endif
      if g:clever_f_show_prompt
        echon 'clever-f: '
      endif
      let s:previous_map[mode] = a:map
      let s:first_move[mode] = 1
      let cn = s:getchar()
      if cn == s:ESC_CODE
        return "\<Esc>"
      endif
      if index(map(deepcopy(g:clever_f_repeat_last_char_inputs), 'char2nr(v:val)'), cn) == -1
        let s:previous_char_num[mode] = cn
      else
        if has_key(s:previous_char_num, s:last_mode)
          let s:previous_char_num[mode] = s:previous_char_num[s:last_mode]
        else
          echohl ErrorMsg | echo 'Previous input not found.' | echohl None
          return ''
        endif
      endif
      let s:last_mode = mode

      if g:clever_f_timeout_ms > 0
        let s:timestamp = reltime()
      endif

      if g:clever_f_mark_char
        call s:remove_highlight()
        if mode ==# 'n' || mode ==? 'v' || mode ==# "\<C-v>" ||
              \ mode ==# 'ce' || mode ==? 's' || mode ==# "\<C-s>"
          augroup plugin-clever-f-finalizer
            autocmd CursorMoved <buffer> call s:maybe_finalize()
            autocmd InsertEnter <buffer> call s:finalize()
          augroup END
          call s:mark_char_in_current_line(s:previous_map[mode], s:previous_char_num[mode])
        endif
      endif

      if g:clever_f_show_prompt && should_redraw
        redraw!
      endif
    finally
      if g:clever_f_mark_cursor | call matchdelete(cursor_marker) | endif
      if g:clever_f_mark_direct && exists('l:direct_markers')
        for m in direct_markers
          call matchdelete(m)
        endfor
      endif
      if g:clever_f_hide_cursor_on_cmdline
        let &t_ve = save_tve
        call s:HI.hi(cursor_hi)
        call s:HI.hi(lcursor_hi)
        let &guicursor = guicursor
      endif
    endtry
  else
    " When repeated

    let back = a:map =~# '\u'
    if g:clever_f_fix_key_direction && s:previous_map[mode] =~# '\u'
      let back = !back
    endif

    " reset and retry if timed out
    if g:clever_f_timeout_ms > 0 && s:is_timedout()
      call clever_f#reset()
      return clever_f#find_with(a:map)
    endif

    " Restore highlights which were removed by timeout
    if highlight_timer_enabled && s:highlight_timer < 0
      call s:remove_highlight()
      if mode ==# 'n' || mode ==? 'v' || mode ==# "\<C-v>" ||
            \ mode ==# 'ce' || mode ==? 's' || mode ==# "\<C-s>"
        call s:mark_char_in_current_line(s:previous_map[mode], s:previous_char_num[mode])
      endif
    endif
  endif

  if highlight_timer_enabled
    if s:highlight_timer >= 0
      call timer_stop(s:highlight_timer)
    endif
    let s:highlight_timer = timer_start(g:clever_f_highlight_timeout_ms, funcref('s:on_highlight_timer_expired'))
  endif

  return clever_f#repeat(back)
endfunction

function! clever_f#repeat(back) abort
  let mode = s:mode()
  let pmap = get(s:previous_map, mode, '')
  let prev_char_num = get(s:previous_char_num, mode, 0)

  if pmap ==# ''
    return ''
  endif

  " ignore special characters like \<Left>
  if type(prev_char_num) == type('') && char2nr(prev_char_num) == 128
    return ''
  endif

  if a:back
    let pmap = s:swapcase(pmap)
  endif

  if mode[0] ==? 'v' || mode[0] ==# "\<C-v>"
    let cmd = s:move_cmd_for_visualmode(pmap, prev_char_num)
  else
    let inclusive = mode ==# 'no' && pmap =~# '\l'
    let cmd = printf("%s:\<C-u>call clever_f#find(%s, %s)\<CR>",
          \    inclusive ? 'v' : '',
          \    string(pmap), prev_char_num)
  endif

  return cmd
endfunction

" absolutely moved forward?
function! s:moves_forward(p, n) abort
  if a:p[0] != a:n[0]
    return a:p[0] < a:n[0]
  endif

  if a:p[1] != a:n[1]
    return a:p[1] < a:n[1]
  endif

  return 0
endfunction

function! clever_f#find(map, char_num) abort
  let before_pos = getpos('.')[1 : 2]
  let next_pos = s:next_pos(a:map, a:char_num, v:count1)
  if next_pos == [0, 0]
    return
  endif

  let moves_forward = s:moves_forward(before_pos, next_pos)

  " update highlight when cursor moves across lines
  let mode = s:mode()
  if g:clever_f_mark_char
    if next_pos[0] != before_pos[0]
          \ || (a:map ==? 't' && !s:first_move[mode] && clever_f#compat#xor(s:moved_forward, moves_forward))
      call s:remove_highlight()
      call s:mark_char_in_current_line(a:map, a:char_num)
    endif
  endif

  let s:moved_forward = moves_forward
  let s:previous_pos[mode] = next_pos
  let s:first_move[mode] = 0
endfunction

function! s:finalize() abort
  autocmd! plugin-clever-f-finalizer
  call s:remove_highlight()
  let s:previous_pos = {}
  let s:moved_forward = 0
endfunction

function! s:maybe_finalize() abort
  let pp = get(s:previous_pos, s:last_mode, [0, 0])
  if getpos('.')[1 : 2] != pp
    call s:finalize()
  endif
endfunction

function! s:move_cmd_for_visualmode(map, char_num) abort
  let next_pos = s:next_pos(a:map, a:char_num, v:count1)
  if next_pos == [0, 0]
    return ''
  endif

  let m = s:mode()
  call setpos("''", [0] + next_pos + [0])
  let s:previous_pos[m] = next_pos
  let s:first_move[m] = 0

  return '``'
endfunction

function! s:search(pat, flag) abort
  if g:clever_f_across_no_line
    return search(a:pat, a:flag, line('.'))
  else
    return search(a:pat, a:flag)
  endif
endfunction

function! s:should_use_migemo(char) abort
  if !g:clever_f_use_migemo || a:char !~# '^\a$'
    return 0
  endif

  if !g:clever_f_across_no_line
    return 1
  endif

  return s:include_multibyte_char(getline('.'))
endfunction

function! s:load_migemo_dict() abort
  let enc = &l:encoding
  if enc ==# 'utf-8'
    return clever_f#migemo#utf8#load_dict()
  elseif enc ==# 'cp932'
    return clever_f#migemo#cp932#load_dict()
  elseif enc ==# 'euc-jp'
    return clever_f#migemo#eucjp#load_dict()
  else
    let g:clever_f_use_migemo = 0
    throw "clever-f: Encoding '" . enc . "' is not supported. Migemo is disabled"
  endif
endfunction

function! s:generate_pattern(map, char_num) abort
  let char = type(a:char_num) == type(0) ? nr2char(a:char_num) : a:char_num
  let regex = char

  let should_use_migemo = s:should_use_migemo(char)
  if should_use_migemo
    if !has_key(s:migemo_dicts, &l:encoding)
      let s:migemo_dicts[&l:encoding] = s:load_migemo_dict()
    endif
    let regex = s:migemo_dicts[&l:encoding][regex] . '\&\%(' . char . '\|\A\)'
  elseif stridx(g:clever_f_chars_match_any_signs, char) != -1
    let regex = '\[!"#$%&''()=~|\-^\\@`[\]{};:+*<>,.?_/]'
  elseif char ==# '\'
    let regex = '\\'
  endif

  let is_exclusive_visual = &selection ==# 'exclusive' && s:mode()[0] ==? 'v'
  if a:map ==# 't' && !is_exclusive_visual
    let regex = '\_.\ze\%(' . regex . '\)'
  elseif is_exclusive_visual && a:map ==# 'f'
    let regex = '\%(' . regex . '\)\zs\_.'
  elseif a:map ==# 'T'
    let regex = '\%(' . regex . '\)\@<=\_.'
  endif

  if !should_use_migemo
    let regex = '\V'.regex
  endif

  return ((g:clever_f_smart_case && char =~# '\l') || g:clever_f_ignore_case ? '\c' : '\C') . regex
endfunction

function! s:next_pos(map, char_num, count) abort
  let mode = s:mode()
  let search_flag = a:map =~# '\l' ? 'W' : 'bW'
  let cnt = a:count
  let pattern = s:generate_pattern(a:map, a:char_num)

  if a:map ==? 't' && get(s:first_move, mode, 1)
    if !s:search(pattern, search_flag . 'c')
      return [0, 0]
    endif
    let cnt -= 1
  endif

  while 0 < cnt
    if !s:search(pattern, search_flag)
      return [0, 0]
    endif
    let cnt -= 1
  endwhile

  return getpos('.')[1 : 2]
endfunction

function! s:swapcase(char) abort
  return a:char =~# '\u' ? tolower(a:char) : toupper(a:char)
endfunction

" Drop forced visual mode character ('nov' -> 'no')
function! s:mode() abort
  let mode = mode(1)
  if mode =~# '^no'
    let mode = mode[0 : 1]
  endif
  return mode
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo
