" MIT License. Copyright (c) 2013-2021 Bailey Ling et al.
" vim: et ts=2 sts=2 sw=2

scriptencoding utf-8

let s:prototype = {}

" Set the point in the tabline where the builder should insert the titles.
"
" Subsequent calls will overwrite the previous ones, so only the last call
" determines to location to insert titles.
"
" NOTE: The titles are not inserted until |build| is called, so that the
" remaining contents of the tabline can be taken into account.
"
" Callers should define at least |get_title| and |get_group| on the host
" object before calling |build|.
function! s:prototype.insert_titles(current, first, last) dict
  let self._first_title = a:first " lowest index
  let self._last_title = a:last " highest index
  let self._left_title = a:current " next index to add on the left
  let self._right_title = a:current + 1 " next index to add on the right
  let self._left_position = self.get_position() " left end of titles
  let self._right_position = self._left_position " right end of the titles
endfunction

" Insert a title for entry number |index|, of group |group| at position |pos|,
" if there is space for it. Returns 1 if it is inserted, 0 otherwise
"
" |force| inserts the title even if there isn't enough space left for it.
" |sep_size| adjusts the size change that the title is considered to take up,
"            to account for changes to the separators
"
" The title is defined by |get_title| on the hosting object, called with
" |index| as its only argument.
" |get_pretitle| and |get_posttitle| may be defined on the host object to
" insert some formatting before or after the title. These should be 0-width.
"
" This method updates |_right_position| and |_remaining_space| on the host
" object, if the title is inserted.
function! s:prototype.try_insert_title(index, group, pos, sep_size, force) dict
  let title = self.get_title(a:index)
  let title_size = s:tabline_evaluated_length(title) + a:sep_size
  if a:force || self._remaining_space >= title_size
    let pos = a:pos
    if has_key(self, "get_pretitle")
      call self.insert_raw(self.get_pretitle(a:index), pos)
      let self._right_position += 1
      let pos += 1
    endif

    call self.insert_section(a:group, title, pos)
    let self._right_position += 1
    let pos += 1

    if has_key(self, "get_posttitle")
      call self.insert_raw(self.get_posttitle(a:index), pos)
      let self._right_position += 1
      let pos += 1
    endif

    let self._remaining_space -= title_size
    return 1
  endif
  return 0
endfunction

function! s:get_separator_change(new_group, old_group, end_group, sep_size, alt_sep_size)
  return s:get_separator_change_with_end(a:new_group, a:old_group, a:end_group, a:end_group, a:sep_size, a:alt_sep_size)
endfunction

" Compute the change in size of the tabline caused by separators
"
" This should be kept up-to-date with |s:get_transitioned_separator| and
" |s:get_separator| in autoload/airline/builder.vim
function! s:get_separator_change_with_end(new_group, old_group, new_end_group, old_end_group, sep_size, alt_sep_size)
  let sep_change = 0
  if !empty(a:new_end_group) " Separator between title and the end
    let sep_change += airline#builder#should_change_group(a:new_group, a:new_end_group) ? a:sep_size : a:alt_sep_size
  endif
  if !empty(a:old_group) " Separator between the title and the one adjacent
    let sep_change += airline#builder#should_change_group(a:new_group, a:old_group) ? a:sep_size : a:alt_sep_size
    if !empty(a:old_end_group) " Remove mis-predicted separator
      let sep_change -= airline#builder#should_change_group(a:old_group, a:old_end_group) ? a:sep_size : a:alt_sep_size
    endif
  endif
  return sep_change
endfunction

" This replaces the build function of the |airline#builder#new| object, to
" insert titles as specified by the last call to |insert_titles| before
" passing to the original build function.
"
" Callers should define at least |get_title| and |get_group| on the host
" object if |insert_titles| has been called on it.
function! s:prototype.build() dict
  if has_key(self, '_left_position') && self._first_title <= self._last_title
    let self._remaining_space = &columns - s:tabline_evaluated_length(self._build())

    let center_active = get(g:, 'airline#extensions#tabline#center_active', 0)

    let sep_size = s:tabline_evaluated_length(self._context.left_sep)
    let alt_sep_size = s:tabline_evaluated_length(self._context.left_alt_sep)

    let outer_left_group = airline#builder#get_prev_group(self._sections, self._left_position)
    let outer_right_group = airline#builder#get_next_group(self._sections, self._right_position)

    let overflow_marker = get(g:, 'airline#extensions#tabline#overflow_marker', g:airline_symbols.ellipsis)
    let overflow_marker_size = s:tabline_evaluated_length(overflow_marker)
    " Allow space for the markers before we begin filling in titles.
    if self._left_title > self._first_title
      let self._remaining_space -= overflow_marker_size +
        \ s:get_separator_change(self.overflow_group, "", outer_left_group, sep_size, alt_sep_size)
    endif
    if self._left_title < self._last_title
      let self._remaining_space -= overflow_marker_size +
        \ s:get_separator_change(self.overflow_group, "", outer_right_group, sep_size, alt_sep_size)
    endif

    " Add the current title
    let group = self.get_group(self._left_title)
    if self._left_title == self._first_title
      let sep_change = s:get_separator_change(group, "", outer_left_group, sep_size, alt_sep_size)
    else
      let sep_change = s:get_separator_change(group, "", self.overflow_group, sep_size, alt_sep_size)
    endif
    if self._left_title == self._last_title
      let sep_change += s:get_separator_change(group, "", outer_right_group, sep_size, alt_sep_size)
    else
      let sep_change += s:get_separator_change(group, "", self.overflow_group, sep_size, alt_sep_size)
    endif
    let left_group = group
    let right_group = group
    let self._left_title -=
      \ self.try_insert_title(self._left_title, group, self._left_position, sep_change, 1)

    if get(g:, 'airline#extensions#tabline#current_first', 0)
      " always have current title first
      let self._left_position += 1
    endif

    if !center_active && self._right_title <= self._last_title
      " Add the title to the right
      let group = self.get_group(self._right_title)
      if self._right_title == self._last_title
        let sep_change = s:get_separator_change_with_end(group, right_group, outer_right_group, self.overflow_group, sep_size, alt_sep_size) - overflow_marker_size
      else
        let sep_change = s:get_separator_change(group, right_group, self.overflow_group, sep_size, alt_sep_size)
      endif
      let right_group = group
      let self._right_title +=
      \ self.try_insert_title(self._right_title, group, self._right_position, sep_change, 1)
    endif

    while self._remaining_space > 0
      let done = 0
      if self._left_title >= self._first_title
        " Insert next title to the left
        let group = self.get_group(self._left_title)
        if self._left_title == self._first_title
          let sep_change = s:get_separator_change_with_end(group, left_group, outer_left_group, self.overflow_group, sep_size, alt_sep_size) - overflow_marker_size
        else
          let sep_change = s:get_separator_change(group, left_group, self.overflow_group, sep_size, alt_sep_size)
        endif
        let left_group = group
        let done = self.try_insert_title(self._left_title, group, self._left_position, sep_change, 0)
        let self._left_title -= done
      endif
      " If center_active is set, this |if| operates as an independent |if|,
      " otherwise as an |elif|.
      if self._right_title <= self._last_title && (center_active || !done)
        " Insert next title to the right
        let group = self.get_group(self._right_title)
        if self._right_title == self._last_title
          let sep_change = s:get_separator_change_with_end(group, right_group, outer_right_group, self.overflow_group, sep_size, alt_sep_size) - overflow_marker_size
        else
          let sep_change = s:get_separator_change(group, right_group, self.overflow_group, sep_size, alt_sep_size)
        endif
        let right_group = group
        let done = self.try_insert_title(self._right_title, group, self._right_position, sep_change, 0)
        let self._right_title += done
      endif
      if !done
        break
      endif
    endwhile

    if self._left_title >= self._first_title
      if get(g:, 'airline#extensions#tabline#current_first', 0)
        let self._left_position -= 1
      endif
      call self.insert_section(self.overflow_group, overflow_marker, self._left_position)
      let self._right_position += 1
    endif

    if self._right_title <= self._last_title
      call self.insert_section(self.overflow_group, overflow_marker, self._right_position)
    endif
  endif

  return self._build()
endfunction

let s:prototype.overflow_group = 'airline_tab'

" Extract the text content a tabline will render. (Incomplete).
"
" See :help 'statusline' for the list of fields.
function! s:evaluate_tabline(tabline)
  let tabline = a:tabline
  let tabline = substitute(tabline, '%{\([^}]\+\)}', '\=eval(submatch(1))', 'g')
  let tabline = substitute(tabline, '%#[^#]\+#', '', 'g')
  let tabline = substitute(tabline, '%(\([^)]\+\)%)', '\1', 'g')
  let tabline = substitute(tabline, '%\d\+[TX]', '', 'g')
  let tabline = substitute(tabline, '%=', '', 'g')
  let tabline = substitute(tabline, '%\d*\*', '', 'g')
  if has('tablineat')
    let tabline = substitute(tabline, '%@[^@]\+@', '', 'g')
  endif
  return tabline
endfunction

function! s:tabline_evaluated_length(tabline)
  return airline#util#strchars(s:evaluate_tabline(a:tabline))
endfunction

function! airline#extensions#tabline#builder#new(context)
  let builder = airline#builder#new(a:context)
  let builder._build = builder.build
  call extend(builder, s:prototype, 'force')
  return builder
endfunction
