function! gitgutter#hunk#set_hunks(bufnr, hunks) abort
  call gitgutter#utility#setbufvar(a:bufnr, 'hunks', a:hunks)
  call s:reset_summary(a:bufnr)
endfunction

function! gitgutter#hunk#hunks(bufnr) abort
  return gitgutter#utility#getbufvar(a:bufnr, 'hunks', [])
endfunction

function! gitgutter#hunk#reset(bufnr) abort
  call gitgutter#utility#setbufvar(a:bufnr, 'hunks', [])
  call s:reset_summary(a:bufnr)
endfunction


function! gitgutter#hunk#summary(bufnr) abort
  return gitgutter#utility#getbufvar(a:bufnr, 'summary', [0,0,0])
endfunction

function! s:reset_summary(bufnr) abort
  call gitgutter#utility#setbufvar(a:bufnr, 'summary', [0,0,0])
endfunction

function! gitgutter#hunk#increment_lines_added(bufnr, count) abort
  let summary = gitgutter#hunk#summary(a:bufnr)
  let summary[0] += a:count
  call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary)
endfunction

function! gitgutter#hunk#increment_lines_modified(bufnr, count) abort
  let summary = gitgutter#hunk#summary(a:bufnr)
  let summary[1] += a:count
  call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary)
endfunction

function! gitgutter#hunk#increment_lines_removed(bufnr, count) abort
  let summary = gitgutter#hunk#summary(a:bufnr)
  let summary[2] += a:count
  call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary)
endfunction


function! gitgutter#hunk#next_hunk(count) abort
  let bufnr = bufnr('')
  if gitgutter#utility#is_active(bufnr)
    let current_line = line('.')
    let hunk_count = 0
    for hunk in gitgutter#hunk#hunks(bufnr)
      if hunk[2] > current_line
        let hunk_count += 1
        if hunk_count == a:count
          execute 'normal!' hunk[2] . 'Gzv'
          return
        endif
      endif
    endfor
    call gitgutter#utility#warn('No more hunks')
  endif
endfunction

function! gitgutter#hunk#prev_hunk(count) abort
  let bufnr = bufnr('')
  if gitgutter#utility#is_active(bufnr)
    let current_line = line('.')
    let hunk_count = 0
    for hunk in reverse(copy(gitgutter#hunk#hunks(bufnr)))
      if hunk[2] < current_line
        let hunk_count += 1
        if hunk_count == a:count
          let target = hunk[2] == 0 ? 1 : hunk[2]
          execute 'normal!' target . 'Gzv'
          return
        endif
      endif
    endfor
    call gitgutter#utility#warn('No previous hunks')
  endif
endfunction

" Returns the hunk the cursor is currently in or an empty list if the cursor
" isn't in a hunk.
function! s:current_hunk() abort
  let bufnr = bufnr('')
  let current_hunk = []

  for hunk in gitgutter#hunk#hunks(bufnr)
    if gitgutter#hunk#cursor_in_hunk(hunk)
      let current_hunk = hunk
      break
    endif
  endfor

  return current_hunk
endfunction

function! gitgutter#hunk#cursor_in_hunk(hunk) abort
  let current_line = line('.')

  if current_line == 1 && a:hunk[2] == 0
    return 1
  endif

  if current_line >= a:hunk[2] && current_line < a:hunk[2] + (a:hunk[3] == 0 ? 1 : a:hunk[3])
    return 1
  endif

  return 0
endfunction

function! gitgutter#hunk#text_object(inner) abort
  let hunk = s:current_hunk()

  if empty(hunk)
    return
  endif

  let [first_line, last_line] = [hunk[2], hunk[2] + hunk[3] - 1]

  if ! a:inner
    let lnum = last_line
    let eof = line('$')
    while lnum < eof && empty(getline(lnum + 1))
      let lnum +=1
    endwhile
    let last_line = lnum
  endif

  execute 'normal! 'first_line.'GV'.last_line.'G'
endfunction


function! gitgutter#hunk#stage() abort
  call s:hunk_op(function('s:stage'))
  silent! call repeat#set("\<Plug>GitGutterStageHunk", -1)<CR>
endfunction

function! gitgutter#hunk#undo() abort
  call s:hunk_op(function('s:undo'))
  silent! call repeat#set("\<Plug>GitGutterUndoHunk", -1)<CR>
endfunction

function! gitgutter#hunk#preview() abort
  call s:hunk_op(function('s:preview'))
  silent! call repeat#set("\<Plug>GitGutterPreviewHunk", -1)<CR>
endfunction


function! s:hunk_op(op)
  let bufnr = bufnr('')

  if gitgutter#utility#is_active(bufnr)
    " Get a (synchronous) diff.
    let [async, g:gitgutter_async] = [g:gitgutter_async, 0]
    let diff = gitgutter#diff#run_diff(bufnr, 1)
    let g:gitgutter_async = async

    call gitgutter#hunk#set_hunks(bufnr, gitgutter#diff#parse_diff(diff))

    if empty(s:current_hunk())
      call gitgutter#utility#warn('cursor is not in a hunk')
    else
      call a:op(gitgutter#diff#hunk_diff(bufnr, diff))
    endif
  endif
endfunction


function! s:stage(hunk_diff)
  let bufnr = bufnr('')
  let diff = s:adjust_header(bufnr, a:hunk_diff)
  " Apply patch to index.
  call gitgutter#utility#system(
        \ gitgutter#utility#cd_cmd(bufnr, g:gitgutter_git_executable.' apply --cached --unidiff-zero - '),
        \ diff)

  " Refresh gitgutter's view of buffer.
  call gitgutter#process_buffer(bufnr, 1)
endfunction


function! s:undo(hunk_diff)
  " Apply reverse patch to buffer.
  let hunk  = gitgutter#diff#parse_hunk(split(a:hunk_diff, '\n')[4])
  let lines = map(split(a:hunk_diff, '\n')[5:], 'v:val[1:]')
  let lnum  = hunk[2]
  let added_only   = hunk[1] == 0 && hunk[3]  > 0
  let removed_only = hunk[1]  > 0 && hunk[3] == 0

  if removed_only
    call append(lnum, lines)
  elseif added_only
    execute lnum .','. (lnum+len(lines)-1) .'d'
  else
    call append(lnum-1, lines[0:hunk[1]])
    execute (lnum+hunk[1]) .','. (lnum+hunk[1]+hunk[3]) .'d'
  endif
endfunction


function! s:preview(hunk_diff)
  let hunk_lines = split(s:discard_header(a:hunk_diff), "\n")
  let hunk_lines_length = len(hunk_lines)
  let previewheight = min([hunk_lines_length, &previewheight])

  silent! wincmd P
  if !&previewwindow
    noautocmd execute 'bo' previewheight 'new'
    set previewwindow
  else
    execute 'resize' previewheight
  endif

  setlocal noreadonly modifiable filetype=diff buftype=nofile bufhidden=delete noswapfile
  execute "%delete_"
  call append(0, hunk_lines)
  normal! gg
  setlocal readonly nomodifiable

  noautocmd wincmd p
endfunction


function! s:adjust_header(bufnr, hunk_diff)
  let filepath = gitgutter#utility#repo_path(a:bufnr, 0)
  return s:adjust_hunk_summary(s:fix_file_references(filepath, a:hunk_diff))
endfunction


" Replaces references to temp files with the actual file.
function! s:fix_file_references(filepath, hunk_diff)
  let lines = split(a:hunk_diff, '\n')

  let left_prefix  = matchstr(lines[2], '[abciow12]').'/'
  let right_prefix = matchstr(lines[3], '[abciow12]').'/'
  let quote        = lines[0][11] == '"' ? '"' : ''

  let left_file  = quote.left_prefix.a:filepath.quote
  let right_file = quote.right_prefix.a:filepath.quote

  let lines[0] = 'diff --git '.left_file.' '.right_file
  let lines[2] = '--- '.left_file
  let lines[3] = '+++ '.right_file

  return join(lines, "\n")."\n"
endfunction

if $VIM_GITGUTTER_TEST
  function! gitgutter#hunk#fix_file_references(filepath, hunk_diff)
    return s:fix_file_references(a:filepath, a:hunk_diff)
  endfunction
endif


function! s:adjust_hunk_summary(hunk_diff) abort
  let line_adjustment = s:line_adjustment_for_current_hunk()
  let diff = split(a:hunk_diff, '\n', 1)
  let diff[4] = substitute(diff[4], '+\@<=\(\d\+\)', '\=submatch(1)+line_adjustment', '')
  return join(diff, "\n")
endfunction


function! s:discard_header(hunk_diff)
  return join(split(a:hunk_diff, '\n', 1)[5:], "\n")
endfunction


" Returns the number of lines the current hunk is offset from where it would
" be if any changes above it in the file didn't exist.
function! s:line_adjustment_for_current_hunk() abort
  let bufnr = bufnr('')
  let adj = 0
  for hunk in gitgutter#hunk#hunks(bufnr)
    if gitgutter#hunk#cursor_in_hunk(hunk)
      break
    else
      let adj += hunk[1] - hunk[3]
    endif
  endfor
  return adj
endfunction

