" Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com>
" Description: Code Fix support for tsserver and LSP servers

let s:codefix_map = {}

" Used to get the codefix map in tests.
function! ale#codefix#GetMap() abort
    return deepcopy(s:codefix_map)
endfunction

" Used to set the codefix map in tests.
function! ale#codefix#SetMap(map) abort
    let s:codefix_map = a:map
endfunction

function! ale#codefix#ClearLSPData() abort
    let s:codefix_map = {}
endfunction

function! s:message(message) abort
    call ale#util#Execute('echom ' . string(a:message))
endfunction

function! ale#codefix#ApplyTSServerCodeAction(data, item) abort
    if has_key(a:item, 'changes')
        let l:changes = a:item.changes

        call ale#code_action#HandleCodeAction(
        \   {
        \       'description': 'codefix',
        \       'changes': l:changes,
        \   },
        \   {},
        \)
    else
        let l:message = ale#lsp#tsserver_message#GetEditsForRefactor(
        \   a:data.buffer,
        \   a:data.line,
        \   a:data.column,
        \   a:data.end_line,
        \   a:data.end_column,
        \   a:item.id[0],
        \   a:item.id[1],
        \)

        let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)

        let s:codefix_map[l:request_id] = a:data
    endif
endfunction

function! ale#codefix#HandleTSServerResponse(conn_id, response) abort
    if !has_key(a:response, 'request_seq')
    \ || !has_key(s:codefix_map, a:response.request_seq)
        return
    endif

    let l:data = remove(s:codefix_map, a:response.request_seq)
    let l:MenuCallback = get(l:data, 'menu_callback', v:null)

    if get(a:response, 'command', '') is# 'getCodeFixes'
        if get(a:response, 'success', v:false) is v:false
        \&& l:MenuCallback is v:null
            let l:message = get(a:response, 'message', 'unknown')
            call s:message('Error while getting code fixes. Reason: ' . l:message)

            return
        endif

        let l:result = get(a:response, 'body', [])
        call filter(l:result, 'has_key(v:val, ''changes'')')

        if l:MenuCallback isnot v:null
            call l:MenuCallback(
            \   l:data,
            \   map(copy(l:result), '[''tsserver'', v:val]')
            \)

            return
        endif

        if len(l:result) == 0
            call s:message('No code fixes available.')

            return
        endif

        let l:code_fix_to_apply = 0

        if len(l:result) == 1
            let l:code_fix_to_apply = 1
        else
            let l:codefix_no = 1
            let l:codefixstring = "Code Fixes:\n"

            for l:codefix in l:result
                let l:codefixstring .= l:codefix_no . ') '
                \   . l:codefix.description . "\n"
                let l:codefix_no += 1
            endfor

            let l:codefixstring .= 'Type number and <Enter> (empty cancels): '

            let l:code_fix_to_apply = ale#util#Input(l:codefixstring, '')
            let l:code_fix_to_apply = str2nr(l:code_fix_to_apply)

            if l:code_fix_to_apply == 0
                return
            endif
        endif

        call ale#codefix#ApplyTSServerCodeAction(
        \   l:data,
        \   l:result[l:code_fix_to_apply - 1],
        \)
    elseif get(a:response, 'command', '') is# 'getApplicableRefactors'
        if get(a:response, 'success', v:false) is v:false
        \&& l:MenuCallback is v:null
            let l:message = get(a:response, 'message', 'unknown')
            call s:message('Error while getting applicable refactors. Reason: ' . l:message)

            return
        endif

        let l:result = get(a:response, 'body', [])

        if len(l:result) == 0
            call s:message('No applicable refactors available.')

            return
        endif

        let l:refactors = []

        for l:item in l:result
            for l:action in l:item.actions
                call add(l:refactors, {
                \   'name': l:action.description,
                \   'id': [l:item.name, l:action.name],
                \})
            endfor
        endfor

        if l:MenuCallback isnot v:null
            call l:MenuCallback(
            \   l:data,
            \   map(copy(l:refactors), '[''tsserver'', v:val]')
            \)

            return
        endif

        let l:refactor_no = 1
        let l:refactorstring = "Applicable refactors:\n"

        for l:refactor in l:refactors
            let l:refactorstring .= l:refactor_no . ') '
            \   . l:refactor.name . "\n"
            let l:refactor_no += 1
        endfor

        let l:refactorstring .= 'Type number and <Enter> (empty cancels): '

        let l:refactor_to_apply = ale#util#Input(l:refactorstring, '')
        let l:refactor_to_apply = str2nr(l:refactor_to_apply)

        if l:refactor_to_apply == 0
            return
        endif

        let l:id = l:refactors[l:refactor_to_apply - 1].id

        call ale#codefix#ApplyTSServerCodeAction(
        \   l:data,
        \   l:refactors[l:refactor_to_apply - 1],
        \)
    elseif get(a:response, 'command', '') is# 'getEditsForRefactor'
        if get(a:response, 'success', v:false) is v:false
            let l:message = get(a:response, 'message', 'unknown')
            call s:message('Error while getting edits for refactor. Reason: ' . l:message)

            return
        endif

        call ale#code_action#HandleCodeAction(
        \   {
        \       'description': 'editsForRefactor',
        \       'changes': a:response.body.edits,
        \   },
        \   {},
        \)
    endif
endfunction

function! ale#codefix#ApplyLSPCodeAction(data, item) abort
    if has_key(a:item, 'command')
    \&& type(a:item.command) == v:t_dict
        let l:command = a:item.command
        let l:message = ale#lsp#message#ExecuteCommand(
        \   l:command.command,
        \   l:command.arguments,
        \)

        let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
    elseif has_key(a:item, 'command') && has_key(a:item, 'arguments')
    \&& type(a:item.command) == v:t_string
        let l:message = ale#lsp#message#ExecuteCommand(
        \   a:item.command,
        \   a:item.arguments,
        \)

        let l:request_id = ale#lsp#Send(a:data.connection_id, l:message)
    elseif has_key(a:item, 'edit') || has_key(a:item, 'arguments')
        if has_key(a:item, 'edit')
            let l:topass = a:item.edit
        else
            let l:topass = a:item.arguments[0]
        endif

        let l:changes_map = ale#code_action#GetChanges(l:topass)

        if empty(l:changes_map)
            return
        endif

        let l:changes = ale#code_action#BuildChangesList(l:changes_map)

        call ale#code_action#HandleCodeAction(
        \   {
        \       'description': 'codeaction',
        \       'changes': l:changes,
        \   },
        \   {},
        \)
    endif
endfunction

function! ale#codefix#HandleLSPResponse(conn_id, response) abort
    if has_key(a:response, 'method')
    \ && a:response.method is# 'workspace/applyEdit'
    \ && has_key(a:response, 'params')
        let l:params = a:response.params

        let l:changes_map = ale#code_action#GetChanges(l:params.edit)

        if empty(l:changes_map)
            return
        endif

        let l:changes = ale#code_action#BuildChangesList(l:changes_map)

        call ale#code_action#HandleCodeAction(
        \   {
        \       'description': 'applyEdit',
        \       'changes': l:changes,
        \   },
        \   {}
        \)
    elseif has_key(a:response, 'id')
    \&& has_key(s:codefix_map, a:response.id)
        let l:data = remove(s:codefix_map, a:response.id)
        let l:MenuCallback = get(l:data, 'menu_callback', v:null)

        let l:result = get(a:response, 'result')

        if type(l:result) != v:t_list
            let l:result = []
        endif

        " Send the results to the menu callback, if set.
        if l:MenuCallback isnot v:null
            call l:MenuCallback(
            \   l:data,
            \   map(copy(l:result), '[''lsp'', v:val]')
            \)

            return
        endif

        if len(l:result) == 0
            call s:message('No code actions received from server')

            return
        endif

        let l:codeaction_no = 1
        let l:codeactionstring = "Code Fixes:\n"

        for l:codeaction in l:result
            let l:codeactionstring .= l:codeaction_no . ') '
            \   . l:codeaction.title . "\n"
            let l:codeaction_no += 1
        endfor

        let l:codeactionstring .= 'Type number and <Enter> (empty cancels): '

        let l:codeaction_to_apply = ale#util#Input(l:codeactionstring, '')
        let l:codeaction_to_apply = str2nr(l:codeaction_to_apply)

        if l:codeaction_to_apply == 0
            return
        endif

        let l:item = l:result[l:codeaction_to_apply - 1]

        call ale#codefix#ApplyLSPCodeAction(l:data, l:item)
    endif
endfunction

function! s:FindError(buffer, line, column, end_line, end_column, linter_name) abort
    let l:nearest_error = v:null

    if a:line == a:end_line
    \&& a:column == a:end_column
    \&& has_key(g:ale_buffer_info, a:buffer)
        let l:nearest_error_diff = -1

        for l:error in get(g:ale_buffer_info[a:buffer], 'loclist', [])
            if has_key(l:error, 'code')
            \  && (a:linter_name is v:null || l:error.linter_name is# a:linter_name)
            \  && l:error.lnum == a:line
                let l:diff = abs(l:error.col - a:column)

                if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff
                    let l:nearest_error_diff = l:diff
                    let l:nearest_error = l:error
                endif
            endif
        endfor
    endif

    return l:nearest_error
endfunction

function! s:OnReady(
\   line,
\   column,
\   end_line,
\   end_column,
\   MenuCallback,
\   linter,
\   lsp_details,
\) abort
    let l:id = a:lsp_details.connection_id

    if !ale#lsp#HasCapability(l:id, 'code_actions')
        return
    endif

    let l:buffer = a:lsp_details.buffer

    if a:linter.lsp is# 'tsserver'
        let l:nearest_error =
        \   s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column, a:linter.lsp)

        if l:nearest_error isnot v:null
            let l:message = ale#lsp#tsserver_message#GetCodeFixes(
            \   l:buffer,
            \   a:line,
            \   a:column,
            \   a:line,
            \   a:column,
            \   [l:nearest_error.code],
            \)
        else
            let l:message = ale#lsp#tsserver_message#GetApplicableRefactors(
            \   l:buffer,
            \   a:line,
            \   a:column,
            \   a:end_line,
            \   a:end_column,
            \)
        endif
    else
        " Send a message saying the buffer has changed first, otherwise
        " completions won't know what text is nearby.
        call ale#lsp#NotifyForChanges(l:id, l:buffer)

        let l:diagnostics = []
        let l:nearest_error =
        \   s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column, v:null)

        if l:nearest_error isnot v:null
            let l:diagnostics = [
            \   {
            \       'code': l:nearest_error.code,
            \       'message': l:nearest_error.text,
            \       'range': {
            \           'start': {
            \               'line': l:nearest_error.lnum - 1,
            \               'character': l:nearest_error.col - 1,
            \           },
            \           'end': {
            \               'line': get(l:nearest_error, 'end_lnum', 1) - 1,
            \               'character': get(l:nearest_error, 'end_col', 0)
            \           },
            \       },
            \   },
            \]
        endif

        let l:message = ale#lsp#message#CodeAction(
        \   l:buffer,
        \   a:line,
        \   a:column,
        \   a:end_line,
        \   a:end_column,
        \   l:diagnostics,
        \)
    endif

    let l:Callback = a:linter.lsp is# 'tsserver'
    \   ? function('ale#codefix#HandleTSServerResponse')
    \   : function('ale#codefix#HandleLSPResponse')

    call ale#lsp#RegisterCallback(l:id, l:Callback)

    let l:request_id = ale#lsp#Send(l:id, l:message)

    let s:codefix_map[l:request_id] = {
    \   'connection_id': l:id,
    \   'buffer': l:buffer,
    \   'line': a:line,
    \   'column': a:column,
    \   'end_line': a:end_line,
    \   'end_column': a:end_column,
    \   'menu_callback': a:MenuCallback,
    \}
endfunction

function! s:ExecuteGetCodeFix(linter, range, MenuCallback) abort
    let l:buffer = bufnr('')

    if a:range == 0
        let [l:line, l:column] = getpos('.')[1:2]
        let l:end_line = l:line
        let l:end_column = l:column

        " Expand the range to cover the current word, if there is one.
        let l:cword = expand('<cword>')

        if !empty(l:cword)
            let l:search_pos = searchpos('\V' . l:cword, 'bn', l:line)

            if l:search_pos != [0, 0]
                let l:column = l:search_pos[1]
                let l:end_column = l:column + len(l:cword) - 1
            endif
        endif
    elseif mode() is# 'v' || mode() is# "\<C-V>"
        " You need to get the start and end in a different way when you're in
        " visual mode.
        let [l:line, l:column] = getpos('v')[1:2]
        let [l:end_line, l:end_column] = getpos('.')[1:2]
    else
        let [l:line, l:column] = getpos("'<")[1:2]
        let [l:end_line, l:end_column] = getpos("'>")[1:2]
    endif

    let l:column = min([l:column, len(getline(l:line))])
    let l:end_column = min([l:end_column, len(getline(l:end_line))])

    let l:Callback = function(
    \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column, a:MenuCallback]
    \)

    call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback)
endfunction

function! ale#codefix#Execute(range, ...) abort
    if a:0 > 1
        throw 'Too many arguments'
    endif

    let l:MenuCallback = get(a:000, 0, v:null)
    let l:lsp_linters = []

    for l:linter in ale#linter#Get(&filetype)
        if !empty(l:linter.lsp)
            call add(l:lsp_linters, l:linter)
        endif
    endfor

    if empty(l:lsp_linters)
        if l:MenuCallback is v:null
            call s:message('No active LSPs')
        else
            call l:MenuCallback({}, [])
        endif

        return
    endif

    for l:lsp_linter in l:lsp_linters
        call s:ExecuteGetCodeFix(l:lsp_linter, a:range, l:MenuCallback)
    endfor
endfunction
