vim9script

import autoload "kg8m/plugin.vim"
import autoload "kg8m/util/cursor.vim" as cursorUtil
import autoload "kg8m/util/logger.vim"

final cache = {
  type: "",
  input: "",
  pattern: "",
}

# 2-stroke `f` with migemo
export def LowerF(): void
  Prepare("f")
  RunSingleline()
enddef

# 2-stroke `F` with migemo
export def UpperF(): void
  Prepare("F")
  RunSingleline()
enddef

# 2-stroke `t` with migemo
export def LowerT(): void
  Prepare("t")
  RunSingleline()
enddef

# 2-stroke `T` with migemo
export def UpperT(): void
  Prepare("T")
  RunSingleline()
enddef

# `stargate#OKvim()` with migemo
export def Multiline(): void
  Prepare("multiline")
  RunMultiline()
enddef

export def Semi(): void
  if empty(cache.type)
    feedkeys(";", "n")
  else
    Repeat()
  endif
enddef

export def Comma(): void
  if empty(cache.type)
    feedkeys(",", "n")
  else
    Repeat()
  endif
enddef

def Repeat(): void
  if cache.type ==# "multiline"
    RunMultiline()
  else
    RunSingleline()
  endif
enddef

def Prepare(type: string): void
  Reset()
  cache.type = type
enddef

def Reset(): void
  cache.type    = ""
  cache.input   = ""
  cache.pattern = ""
enddef

def RunSingleline(): void
  const forward = cache.type =~# '\l'
  const cursor_position = getcurpos()

  const pattern  = BuildPattern()
  const flags    = (forward ? "" : "b") .. "n"
  const stopline = line(".")

  if cache.type ==# "t"
    cursorUtil.Move(cursor_position[1], cursor_position[2] + 1)
  elseif cache.type ==# "T"
    cursorUtil.Move(cursor_position[1], cursor_position[2] - 1)
  endif

  const position = TryWithPattern(pattern, (_pattern) => searchpos(_pattern, flags, stopline))

  if position ==# [0, 0]
    if getcurpos() !=# cursor_position
      cursorUtil.Move(cursor_position[1], cursor_position[2])
    endif

    const message = $"There are no matches for {string(cache.input)}"
    logger.Info(message, { save_history: false })
    Reset()
  else
    const line_number = position[0]
    var column_number = position[1]

    if cache.type ==# "f" && mode(1) =~# '^no'
      column_number += 1
    elseif cache.type ==# "t" && mode(1) !~# '^no'
      column_number -= 1
    elseif cache.type ==# "T"
      column_number += 1
    endif

    cursorUtil.Move(line_number, column_number)
    BlinkCursor()
  endif
enddef

def RunMultiline(): void
  const cursor_position = getcurpos()
  const pattern = BuildPattern()

  TryWithPattern(pattern, (_pattern) => stargate#OKvim(_pattern))

  if getcurpos() !=# cursor_position
    BlinkCursor()
  endif
enddef

def TryWithPattern(original_pattern: string, Callback: func(string): any): any
  try
    return Callback(original_pattern)
  # Catch invalid pattern errors.
  catch /\v:%(E554|E866|E870):/
    # Use a timer for overwriting other messages.
    timer_start(200, (_) => logger.Error($"[f2] Error: {v:exception}, Pattern: {cache.pattern}"))
  endtry

  const fallback_pattern = InputToSmartcasePattern(cache.input)
  return Callback(fallback_pattern)
enddef

def BuildPattern(): string
  if !empty(cache.pattern)
    return cache.pattern
  endif

  const input = getcharstr() .. getcharstr()
  cache.input = input

  # Don't check whether multibyte characters are contained in searching text. Always use migemo if available. Because
  # migemo targets are not only multibyte characters. For example, "do" matches with ".". It is too confusing if
  # searching behavior varies depending on multibyte characters existence.

  const smartcase_pattern = InputToSmartcasePattern(input)
  const migemo_pattern = InputToMimemoPattern(input)

  cache.pattern = $'\C\%({smartcase_pattern}\|{migemo_pattern}\)'

  return cache.pattern
enddef

def InputToSmartcasePattern(original_full_input: string): string
  const input1 = escape(original_full_input[0], "\\")
  const input2 = escape(original_full_input[1], "\\")
  const BuildFullPattern = (pattern1: string, pattern2: string) => printf('\V%s%s\m', pattern1, pattern2)

  if original_full_input =~# '\u'
    return BuildFullPattern(input1, input2)
  else
    const BuildCaseInsensitivePattern =
      (input: string) => input =~# '\l' ? printf('\[%s%s]', input, toupper(input)) : input

    return BuildFullPattern(
      BuildCaseInsensitivePattern(input1),
      BuildCaseInsensitivePattern(input2)
    )
  endif
enddef

def InputToMimemoPattern(input: string): string
  var left_space = ""
  var right_space = ""

  if input =~# '\s'
    if input[0] =~# '\s'
      left_space = input[0]
    endif

    if input[1] =~# '\s'
      right_space = input[1]
    endif
  endif

  # kensaku#query() removes whitespaces around its returning pattern.
  return left_space .. kensaku#query(input)->escape("~") .. right_space
enddef

def BlinkCursor(): void
  if &cursorcolumn
    return
  endif

  setlocal cursorcolumn
  timer_start(200, (_) => {
    setlocal nocursorcolumn
  })
enddef

plugin.EnsureSourced("kensaku.vim")