" Facilities for moving around Julia blocks (e.g. if/end, function/end etc.)
" (AKA a collection of horrible hacks)

let s:default_mappings = {
  \  "moveblock_n" : "]]",
  \  "moveblock_N" : "][",
  \  "moveblock_p" : "[[",
  \  "moveblock_P" : "[]",
  \
  \  "move_n" : "]j",
  \  "move_N" : "]J",
  \  "move_p" : "[j",
  \  "move_P" : "[J",
  \
  \  "select_a" : "aj",
  \  "select_i" : "ij",
  \
  \  "whereami" : "",
  \  }

function! s:getmapchars(function)
  if exists("g:julia_blocks_mappings") && has_key(g:julia_blocks_mappings, a:function)
    return s:escape(g:julia_blocks_mappings[a:function])
  else
    return s:escape(s:default_mappings[a:function])
  endif
endfunction

function! s:map_move(function, toend, backwards)
  let chars = s:getmapchars(a:function)
  if empty(chars)
    return
  endif
  let fn = "julia_blocks#" . a:function
  let lhs = "<buffer> <nowait> <silent> " . chars . " "
  let cnt = ":<C-U>let b:jlblk_count=v:count1"
  exe "nnoremap " . lhs . cnt
    \ . " <Bar> call " . fn . "()<CR>"
  exe "onoremap " . lhs . cnt
    \ . "<CR><Esc>:call julia_blocks#owrapper_move(v:operator, \"" . fn . "\", " . a:toend . ", " . a:backwards . ")<CR>"
  exe "xnoremap " . lhs . cnt
    \ . "<CR>gv<Esc>:call julia_blocks#vwrapper_move(\"" . fn . "\")<CR>"
  let b:jlblk_mapped[a:function] = 1
endfunction

function! julia_blocks#owrapper_move(oper, function, toend, backwards)
  let F = function(a:function)

  let save_redraw = &lazyredraw
  let save_select = &selection

  let restore_cmds = "\<Esc>"
    \ . ":let &l:selection = \"" . save_select . "\"\<CR>"
    \ . ":let &l:lazyredraw = " . save_redraw . "\<CR>"
    \ . ":\<BS>"

  setlocal lazyredraw

  let start_pos = getpos('.')
  let b:jlblk_abort_calls_esc = 0
  call F()
  let b:jlblk_abort_calls_esc = 1
  let end_pos = getpos('.')
  if start_pos == end_pos
    call feedkeys(restore_cmds, 'n')
  endif

  let &l:selection = "inclusive"
  if a:backwards || !a:toend
    let &l:selection = "exclusive"
  endif
  if a:toend && a:backwards
    let end_pos[2] += 1
  endif

  if s:compare_pos(start_pos, end_pos) > 0
    let [start_pos, end_pos] = [end_pos, start_pos]
  endif

  call setpos("'<", start_pos)
  call setpos("'>", end_pos)

  " NOTE: the 'c' operator behaves differently, for mysterious reasons. We
  "       simulate it with 'd' followed by 'i' instead
  call feedkeys("gv" . (a:oper == "c" ? "d" : a:oper) . restore_cmds . (a:oper == "c" ? "i" : ""), 'n')
endfunction

function! julia_blocks#vwrapper_move(function)
  let F = function(a:function)

  let s = getpos('.')
  let b1 = getpos("'<")
  let b2 = getpos("'>")

  let b = b1 == s ? b2 : b1
  call setpos('.', s)
  let b:jlblk_abort_calls_esc = 0
  call F()
  let b:jlblk_abort_calls_esc = 1
  let e = getpos('.')
  call setpos('.', b)
  exe "normal " . visualmode()
  call setpos('.', e)
endfunction

function! s:unmap(function)
  if !get(b:jlblk_mapped, a:function, 0)
    return
  endif
  let chars = s:getmapchars(a:function)
  if empty(chars)
    " shouldn't happen
    return
  endif
  let mapids = a:function =~# "^move" ? ["n", "x", "o"] :
	\      a:function =~# "^select" ? ["x", "o"] :
	\      ["n"]
  let fn = "julia_blocks#" . a:function
  let cmd = "<buffer> " . chars
  for m in mapids
    exe m . "unmap " . cmd
  endfor
  let b:jlblk_mapped[a:function] = 0
endfunction

function! s:escape(chars)
  let c = a:chars
  let c = substitute(c, '|', '<Bar>', 'g')
  return c
endfunction

function! s:map_select(function)
  let chars = s:getmapchars(a:function)
  if empty(chars)
    return
  endif
  let fn = "julia_blocks#" . a:function
  let lhs = "<buffer> <nowait> <silent> " . chars . " "
  let cnt = ":<C-U>let b:jlblk_inwrapper=1<CR>:let b:jlblk_count=max([v:prevcount,1])<CR>"
  exe "onoremap " . lhs . "<Esc>" . cnt
    \ . ":call julia_blocks#owrapper_select(v:operator, \"" . fn . "\")<CR>"
  exe "xnoremap " . lhs . cnt
    \ . ":call julia_blocks#vwrapper_select(\"" . fn . "\")<CR>"
  let b:jlblk_mapped[a:function] = 1
endfunction

function! julia_blocks#owrapper_select(oper, function) ", toend, backwards)
  let F = function(a:function)

  let save_redraw = &lazyredraw
  let save_select = &selection

  let restore_cmds = "\<Esc>"
    \ . ":let &l:selection = \"" . save_select . "\"\<CR>"
    \ . ":let &l:lazyredraw = " . save_redraw . "\<CR>"
    \ . ":\<BS>"

  setlocal lazyredraw

  let b:jlblk_abort_calls_esc = 0
  let retF = F()
  let b:jlblk_abort_calls_esc = 1
  if empty(retF)
    let b:jlblk_inwrapper = 0
    call feedkeys(restore_cmds, 'n')
    return
  end
  let [start_pos, end_pos] = retF

  if start_pos == end_pos
    call feedkeys(restore_cmds, 'n')
  endif

  let &l:selection = "inclusive"

  call setpos("'<", start_pos)
  call setpos("'>", end_pos)

  let b:jlblk_inwrapper = 0
  " NOTE: the 'c' operator behaves differently, for mysterious reasons. We
  "       simulate it with 'd' followed by 'i' instead
  call feedkeys("gv" . (a:oper == "c" ? "d" : a:oper) . restore_cmds . (a:oper == "c" ? "i" : ""), 'n')
endfunction

function! julia_blocks#vwrapper_select(function)
  let F = function(a:function)

  let b:jlblk_abort_calls_esc = 0
  let retF = F()
  let b:jlblk_abort_calls_esc = 1
  if empty(retF)
    let b:jlblk_inwrapper = 0
    return
  end
  let [start_pos, end_pos] = retF
  call setpos("'<", start_pos)
  call setpos("'>", end_pos)
  normal! gv
  let b:jlblk_inwrapper = 0
endfunction

function! s:map_aux(function)
  let chars = s:getmapchars(a:function)
  if empty(chars)
    return
  endif
  let fn = "julia_blocks#" . a:function
  let lhs = "<buffer> <nowait> <silent> " . chars . " "
  exe "nnoremap " . lhs . ":<C-U>echo " . fn . "()<CR>"
  let b:jlblk_mapped[a:function] = 1
endfunction

let s:julia_blocks_functions = {
      \  "moveblock_N": [1, 0],
      \  "moveblock_n": [0, 0],
      \  "moveblock_p": [0, 1],
      \  "moveblock_P": [1, 1],
      \
      \  "move_N": [1, 0],
      \  "move_n": [0, 0],
      \  "move_p": [0, 1],
      \  "move_P": [1, 1],
      \
      \  "select_a": [],
      \  "select_i": [],
      \
      \  "whereami": [],
      \  }

function! julia_blocks#init_mappings()
  let b:jlblk_mapped = {}
  for f in keys(s:julia_blocks_functions)
    if f =~# "^move"
      let [te, bw] = s:julia_blocks_functions[f]
      call s:map_move(f, te, bw)
    elseif f =~# "^select"
      call s:map_select(f)
    else
      call s:map_aux(f)
    endif
  endfor
  call julia_blocks#select_reset()
  augroup JuliaBlocks
    au!
    au InsertEnter <buffer> call julia_blocks#select_reset()
    au CursorMoved <buffer> call s:cursor_moved()
  augroup END

  " we would need some autocmd event associated with exiting from
  " visual mode, but there isn't any, so we resort to this crude
  " hack
  " ACTUALLY this creates more problems than it solves, so the crude hack
  " is just disabled
  "vnoremap <buffer><silent><unique> <Esc> <Esc>:call julia_blocks#select_reset()<CR>
endfunction

function! julia_blocks#remove_mappings()
  if exists("b:jlblk_mapped")
    for f in keys(s:julia_blocks_functions)
      call s:unmap(f)
    endfor
  endif
  unlet! b:jlblk_save_pos b:jlblk_view b:jlblk_count b:jlblk_abort_calls_esc
  unlet! b:jlblk_inwrapper b:jlblk_did_select b:jlblk_doing_select
  unlet! b:jlblk_last_start_pos b:jlblk_last_end_pos b:jlblk_last_mode
  augroup JuliaBlocks
    au!
  augroup END
  augroup! JuliaBlocks
  let md = maparg("<Esc>", "x", 0, 1)
  if !empty(md) && md["buffer"]
    vunmap <buffer> <Esc>
  endif
endfunction

function! s:restore_view()
  "redraw! " would ensure correct behaviour, but is annoying
  let pos = getpos('.')
  if pos == b:jlblk_save_pos
    call winrestview(b:jlblk_view)
    return
  endif
  let oldtopline = b:jlblk_view["topline"]
  let newtopline = winsaveview()["topline"]
  let l = pos[1]
  if l >= oldtopline + &l:scrolloff && l <= oldtopline + winheight(0) - 1 - &l:scrolloff
    if newtopline > oldtopline
      exe ":normal! " . (newtopline - oldtopline) . "\<C-Y>"
    elseif newtopline < oldtopline
      exe ":normal! " . (oldtopline - newtopline) . "\<C-E>"
    endif
  " these reduce the scrolling to the minimum (which is maybe not
  " standard ViM behaviour?)
  elseif newtopline < oldtopline && (l - newtopline - &l:scrolloff) > 0
    exe ":normal! " . (l - newtopline - &l:scrolloff) . "\<C-E>"
  elseif newtopline > oldtopline && (newtopline + &l:scrolloff - l) > 0
    exe ":normal! " . (l - newtopline - &l:scrolloff) . "\<C-E>"
  endif
  call setpos('.', pos) " make sure we didn't screw up
                        " (since winsaveview may not be up to date)
endfunction

function! s:abort()
  call setpos('.', b:jlblk_save_pos)
  call s:restore_view()
  if get(b:, "jlblk_abort_calls_esc", 1)
    call feedkeys("\<Esc>", 'n')
  endif
  return 0
endfunction

function! s:set_mark_tick(...)
  " This could be a one-liner:
  "   call setpos("''", b:jlblk_save_pos)
  " but we want to append to the jumplist,
  " which setpos doesn't do
  let p = getpos('.')
  call setpos('.', b:jlblk_save_pos)
  normal! m'
  call setpos('.', p)
endfunction

function! s:get_save_pos(...)
  if !exists("b:jlblk_save_pos") || (a:0 == 0) || (a:0 > 0 && a:1)
    let b:jlblk_save_pos = getpos('.')
  endif
  let b:jlblk_view = winsaveview()
endfunction

function! s:on_end()
  return getline('.')[col('.')-1] =~# '\k' && expand("<cword>") =~# b:julia_end_keywords
endfunction

function! s:on_begin()
  let [l,c] = [line('.'), col('.')]
  normal! ^
  let patt = '\%<'.(c+1).'c\(' . b:julia_begin_keywordsm . '\)\%>'.(c-1).'c'
  let n = search(patt, 'Wnc', l)
  call cursor(l, c)
  return n > 0
endfunction

function! s:matchit()
  let lkj = exists(":lockjumps") == 2 ? "lockjumps " : ""
  exe lkj . "normal %"
endfunction

function! s:move_before_begin()
  call search(b:julia_begin_keywordsm, 'Wbc')
  normal! h
endfunction

function! s:cycle_until_end()
  let pos = getpos('.')
  while !s:on_end()
    call s:matchit()
    let c = 0
    if getpos('.') == pos || c > 1000
      " shouldn't happen, but let's avoid infinite loops anyway
      return 0
    endif
    let c += 1
  endwhile
  return 1
endfunction

function! s:moveto_block_delim(toend, backwards, ...)
  let pattern = a:toend ? b:julia_end_keywords : b:julia_begin_keywordsm
  let flags = a:backwards ? 'Wb' : 'W'
  let cnt = a:0 > 0 ? a:1 : b:jlblk_count
  if !a:toend && a:backwards && s:on_begin()
    call s:move_before_begin()
  endif
  let ret = 0
  for c in range(cnt)
    if a:toend && a:backwards && s:on_end()
      normal! l
      normal! bh
    endif
    while 1
      let searchret = search(pattern, flags)
      if !searchret
	return ret
      endif
      exe "let skip = " . b:match_skip
      if !skip
	let ret = 1
	break
      endif
    endwhile
  endfor
  return ret
endfunction

function! s:compare_pos(pos1, pos2)
  if a:pos1[1] < a:pos2[1]
    return -1
  elseif a:pos1[1] > a:pos2[1]
    return 1
  elseif a:pos1[2] < a:pos2[2]
    return -1
  elseif a:pos1[2] > a:pos2[2]
    return 1
  else
    return 0
  endif
endfunction

function! julia_blocks#move_N()
  call s:get_save_pos()

  let ret = s:moveto_block_delim(1, 0)
  if !ret
    return s:abort()
  endif

  normal! e
  call s:set_mark_tick()

  return 1
endfunction

function! julia_blocks#move_n()
  call s:get_save_pos()

  let ret = s:moveto_block_delim(0, 0)
  if !ret
    return s:abort()
  endif

  call s:set_mark_tick()

  return 1
endfunction

function! julia_blocks#move_p()
  call s:get_save_pos()

  let ret = s:moveto_block_delim(0, 1)
  if !ret
    return s:abort()
  endif

  call s:set_mark_tick()

  return 1
endfunction

function! julia_blocks#move_P()
  call s:get_save_pos()

  let ret = s:moveto_block_delim(1, 1)
  if !ret
    return s:abort()
  endif

  normal! e
  call s:set_mark_tick()

  return 1
endfunction

function! s:moveto_currentblock_end()
  let flags = 'W'
  if s:on_end()
    let flags .= 'c'
    " NOTE: using "normal! lb" fails at the end of the file (?!)
    normal! l
    normal! b
  endif

  let ret = searchpair(b:julia_begin_keywordsm, '', b:julia_end_keywords, flags, b:match_skip)
  if ret <= 0
    return s:abort()
  endif

  normal! e
  return 1
endfunction

function! julia_blocks#moveblock_N()
  call s:get_save_pos()

  let ret = 0
  for c in range(b:jlblk_count)
    let last_seen_pos = getpos('.')
    if s:on_end()
      normal! hel
      let save_pos = getpos('.')
      let ret_start = s:moveto_block_delim(0, 0, 1)
      let start1_pos = ret_start ? getpos('.') : [0,0,0,0]
      call setpos('.', save_pos)
      if s:on_end()
	normal! h
      endif
      let ret_end = s:moveto_block_delim(1, 0, 1)
      let end1_pos = ret_end ? getpos('.')  : [0,0,0,0]

      if ret_start && (!ret_end || s:compare_pos(start1_pos, end1_pos) < 0)
	call setpos('.', start1_pos)
      else
	call setpos('.', save_pos)
      endif
    endif

    let moveret = s:moveto_currentblock_end()
    if !moveret && c == 0
      let moveret = s:moveto_block_delim(0, 0, 1) && s:cycle_until_end()
      if moveret
        normal! e
      endif
    endif
    if !moveret
      call setpos('.', last_seen_pos)
      break
    endif

    let ret = 1
  endfor
  if !ret
    return s:abort()
  endif

  call s:set_mark_tick()

  return 1
endfunction

function! julia_blocks#moveblock_n()
  call s:get_save_pos()

  let ret = 0
  for c in range(b:jlblk_count)
    let last_seen_pos = getpos('.')

    call s:moveto_currentblock_end()
    if s:moveto_block_delim(0, 0, 1)
      let ret = 1
    else
      call setpos('.', last_seen_pos)
      break
    endif
  endfor

  if !ret
    return s:abort()
  endif

  call s:set_mark_tick()

  return 1
endfunction

function! julia_blocks#moveblock_p()
  call s:get_save_pos()

  let ret = 0
  for c in range(b:jlblk_count)
    let last_seen_pos = getpos('.')
    if s:on_begin()
      call s:move_before_begin()
      if s:on_end()
	normal! l
      endif
      let save_pos = getpos('.')
      let ret_start = s:moveto_block_delim(0, 1, 1)
      let start1_pos = ret_start ? getpos('.') : [0,0,0,0]
      call setpos('.', save_pos)
      let ret_end = s:moveto_block_delim(1, 1, 1)
      let end1_pos = ret_end ? getpos('.') : [0,0,0,0]

      if ret_end && (!ret_start || s:compare_pos(start1_pos, end1_pos) < 0)
	call setpos('.', end1_pos)
      else
	call setpos('.', save_pos)
      endif
    endif

    let moveret = s:moveto_currentblock_end()
    if !moveret && c == 0
      let moveret = s:moveto_block_delim(1, 1, 1)
    endif
    if !moveret
      call setpos('.', last_seen_pos)
      break
    endif

    call s:matchit()
    let ret = 1
  endfor
  if !ret
    return s:abort()
  endif

  call s:set_mark_tick()
  call s:restore_view()

  return 1
endfunction

function! julia_blocks#moveblock_P()
  call s:get_save_pos()

  let ret = 0
  for c in range(b:jlblk_count)
    let last_seen_pos = getpos('.')

    call s:moveto_currentblock_end()
    if s:on_end()
      call s:matchit()
    endif

    if s:moveto_block_delim(1, 1, 1)
      " NOTE: normal! he does not work unless &whichwrap inlcudes h
      normal! h
      normal! e
      let ret = 1
    else
      call setpos('.', last_seen_pos)
    endif
  endfor

  if !ret
    return s:abort()
  endif

  call s:set_mark_tick()
  call s:restore_view()

  return 1
endfunction

function! julia_blocks#whereami()
  let b:jlblk_count = v:count1
  let save_redraw = &lazyredraw
  setlocal lazyredraw
  let pos = getpos('.')
  let ret = julia_blocks#select_a('w')
  if empty(ret)
    call setpos('.', pos)
    let &l:lazyredraw = save_redraw
    return ""
  end
  let [start_pos, end_pos] = ret
  let m = getline(start_pos[1])[start_pos[2]-1:]

  " If cursor_moved was not forced from select_a, we force it now
  " (TODO: this is *really* ugly)
  if end_pos != pos
    call s:cursor_moved(1)
  endif
  call setpos('.', pos)
  call s:restore_view()
  let &l:lazyredraw = save_redraw
  return m
endfunction

" Block text objects

function! s:find_block(current_mode)

  let flags = 'W'

  if b:jlblk_did_select
    call setpos('.', b:jlblk_last_start_pos)
    if !s:cycle_until_end()
      return s:abort()
    endif
    if !(a:current_mode[0] == 'a' && a:current_mode == b:jlblk_last_mode)
      let flags .= 'c'
    endif
  elseif s:on_end()
    let flags .= 'c'
    " NOTE: using "normal! lb" fails at the end of the file (?!)
    normal! l
    normal! b
  endif
  let searchret = searchpair(b:julia_begin_keywordsm, '', b:julia_end_keywords, flags, b:match_skip)
  if searchret <= 0
    if !b:jlblk_did_select
      return s:abort()
    else
     call setpos('.', b:jlblk_last_end_pos)
    endif
  endif

  let end_pos = getpos('.')
  " Jump to match
  call s:matchit()
  let start_pos = getpos('.')

  let b:jlblk_last_start_pos = copy(start_pos)
  let b:jlblk_last_end_pos = copy(end_pos)

  return [start_pos, end_pos]
endfunction

function! s:repeated_find(ai_mode)
  let repeat = b:jlblk_count + (a:ai_mode == 'i' && v:count1 > 1 ? 1 : 0)
  for c in range(repeat)
    let current_mode = (c < repeat - 1 ? 'a' : a:ai_mode)
    let ret_find_block = s:find_block(current_mode)
    if empty(ret_find_block)
      return 0
    endif
    let [start_pos, end_pos] = ret_find_block
    call setpos('.', end_pos)
    let b:jlblk_last_mode = current_mode
    if c < repeat - 1
      let b:jlblk_doing_select = 0
      let b:jlblk_did_select = 1
    endif
  endfor
  return [start_pos, end_pos]
endfunction

function! julia_blocks#select_a(...)
  let mode_flag = a:0 > 0 ? a:1 : ''
  call s:get_save_pos(!b:jlblk_did_select)
  let current_pos = getpos('.')
  let ret_find_block = s:repeated_find('a' . mode_flag)
  if empty(ret_find_block)
    return 0
  endif
  let [start_pos, end_pos] = ret_find_block

  call setpos('.', end_pos)
  normal! e
  let end_pos = getpos('.')

  let b:jlblk_doing_select = 1

  " CursorMove is only triggered if end_pos
  " end_pos is different than the staring position;
  " so when starting from the 'd' in 'end' we need to
  " force it
  if current_pos == end_pos
    call s:cursor_moved(1)
  endif

  call s:set_mark_tick()
  return [start_pos, end_pos]
endfunction

function! julia_blocks#select_i()
  call s:get_save_pos(!b:jlblk_did_select)
  let current_pos = getpos('.')
  let ret_find_block = s:repeated_find('i')
  if empty(ret_find_block)
    return 0
  endif
  let [start_pos, end_pos] = ret_find_block

  if end_pos[1] <= start_pos[1]+1
    return s:abort()
  endif

  call setpos('.', end_pos)

  let b:jlblk_doing_select = 1

  let start_pos[1] += 1
  call setpos('.', start_pos)
  normal! ^
  let start_pos = getpos('.')
  let end_pos[1] -= 1
  let end_pos[2] = len(getline(end_pos[1]))

  " CursorMove is only triggered if end_pos
  " end_pos is different than the staring position;
  " so when starting from the 'd' in 'end' we need to
  " force it
  if current_pos == end_pos
    call s:cursor_moved(1)
  endif

  call s:set_mark_tick()
  return [start_pos, end_pos]
endfunction

function julia_blocks#select_reset()
  let b:jlblk_did_select = 0
  let b:jlblk_doing_select = 0
  let b:jlblk_inwrapper = 0
  let b:jlblk_last_mode = ""
endfunction

function! s:cursor_moved(...)
  if b:jlblk_inwrapper && !(a:0 > 0 && a:1)
    return
  endif
  let b:jlblk_did_select = b:jlblk_doing_select
  let b:jlblk_doing_select = 0
endfunction
