-- texdoclib-view.tlu: view a document and/or display the list of results in texdoc
--
-- The TeX Live Team, GPLv3, see texdoclib.tlu for details

-- dependencies
local texdoc = {
    const = require('texdoclib-const'),
    util = require('texdoclib-util'),
    config = require('texdoclib-config'),
    search = require('texdoclib-search'),
}

-- shortcuts
local M = {}
local C = texdoc.const
local err_print = texdoc.util.err_print
local dbg_print = texdoc.util.dbg_print

-----------------------------   view a document   -----------------------------

-- view a document
-- see texdoclib-search.tlu for the structure of the argument
local function view_doc(docfile)
    M.view_file(docfile.realpath)
end

-- view a file, if possible
local function try_viewing(view_command, viewer_replacement)
    if string.match (view_command, C.place_holder) then
        view_command = string.gsub(
            view_command, C.place_holder, viewer_replacement)
    else
        view_command = view_command .. ' ' .. viewer_replacement
    end

    err_print('info', 'View command: ' .. view_command)

    -- See long comment below this function for the LC_CTYPE story.
    -- We only want to reset the environment if we have the value
    -- to reset it to. In older versions of luatex, status.lc_* will be nil.
    local env_lc_ctype = status.lc_ctype
    local luatex_lc_ctype = os.setlocale(nil, 'ctype')
    if (env_lc_ctype) then
        err_print('info', 'Setting environment LC_CTYPE to: ' .. env_lc_ctype)
        os.setenv('LC_CTYPE', env_lc_ctype)
    end

    -- the big casino: run the external command.
    if os.execute(view_command) > 0 then
        err_print('error', 'Failed to execute: ' .. view_command)
        -- try to catch problems with missing DISPLAY on Unix
        if os.type == 'unix' and not (os.name == 'macosx')
                and os.getenv('DISPLAY') == nil then
            err_print('error',
                'Maybe your viewer failed because DISPLAY is not set.')
        end
        os.exit(C.exit_error)
    end

    -- reset back to "C" (should always be C and always happen, but in case...)
    if (luatex_lc_ctype) then
        os.setenv('LC_CTYPE', luatex_lc_ctype)
    end
end

-- get viewer and viewer_replacement before calling try_viewing
-- returns false of failure, true on success viewer_replacement is either:
--
--     1. the filename, quoted with ""
--     2. the filename, quoted with "" and followed by some rm commands
--
-- The second case happens when the doc is zipped. In the case, this function
-- unzips it in a tmpdir so that the viewer command can use the unzipped file.
function M.view_file(filename)
    local viewer, viewer_replacement

    -- check if the file is zipped
    local nozipname, zipext = texdoc.util.parse_zip(filename)

    -- determine viewer_replacement
    if zipext then
        local unzip_cmd = texdoc.config.get_value('unzip_' .. zipext)

        if not unzip_cmd then
            err_print('error', 'No unzip command for ".%s" files.', zipext)
            os.exit(C.exit_error)
        end

        local tmpdir = os.tmpdir('/tmp/texdoc.XXXXXX')
        if not tmpdir then
            err_print('error', 'Failed to create tempdir to unzip.')
            os.exit(C.exit_error)
        end

        local basename = string.match(nozipname, '.*/(.*)$') or nozipname
        local tmpfile = '"' .. tmpdir .. '/' .. basename .. '"'
        local unzip = unzip_cmd .. ' "' .. filename .. '">' .. tmpfile
        dbg_print('view', 'Unzip command: ' .. unzip)

        if not os.execute(unzip) then
            err_print('error', 'Failed to unzip ' .. filename)
            os.remove(tmpfile)
            os.remove(tmpdir)
            os.exit(C.exit_error)
        end

        -- it is necessary to sleep a few secounds. Otherwise, the temporary
        -- file could be removed before opening it.
        viewer_replacement = tmpfile .. '; sleep 2; ' ..
            texdoc.config.get_value('rm_file') .. ' ' ..tmpfile .. '; ' ..
            texdoc.config.get_value('rm_dir') .. ' ' .. tmpdir
        filename = nozipname
    else
        viewer_replacement = '"' .. texdoc.util.w32_path(filename) .. '"'
    end

    -- files without extension are assumed to be text
    local viewext = (filename:match('.*%.([^/]*)$') or 'txt'):lower()

    if filename:match('^http') then
        viewext = 'html'
    end

    -- special case : sty files use txt viewer
    -- FIXME: hardcoding such cases this way is not very clean
    if viewext == 'sty' then viewext = 'txt' end
    if viewext == 'texlive' then viewext = 'txt' end
    if viewext == 'htm' then viewext = 'html' end

    -- get a viewer from built-in defaults if not already set
    if not texdoc.config.get_value('viewer_' .. viewext) then
        texdoc.config.get_default_viewers()
    end

    -- still no viewers? use txt as a fallback
    if not texdoc.config.get_value('viewer_' .. viewext) then
        err_print('warning',
            'No viewer defined for ".%s" files, using "viewer_txt" instead.',
            viewext)
        viewext = 'txt'
    end

    -- finally, check validity of the viewer
    viewer = texdoc.config.get_value('viewer_' .. viewext)
    assert(viewer, 'Internal error: no viewer found.')
    dbg_print('view', 'Using "viewer_%s" to open the file.', viewext )

    return try_viewing(viewer, viewer_replacement)
end

-- Explanation of locale madness:
-- LuaTeX resets LC_CTYPE, LC_COLLATE, LC_NUMERIC to "C". That is good for
-- inside luatex, but when we run an external program, if the user's
-- environment is something else, we want to switch back to it. As of
-- TL 2017 LuaTeX, we can inspect the user's locale with status.lc_ctype, etc.
--
-- For texdoc purposes, what matters is LC_CTYPE (so we don't bother with
-- the others). For example, with the less pager, when LC_CTYPE=C,
-- non-ASCII bytes are displayed as "<xx>", where xx is the two hex
-- digits for the byte.

-----------------------------   display results   -----------------------------

-- length of the list to be shown
local function count_list_length(doclist, showall)
    local res = 0
    for _, doc in pairs(doclist) do
        if doc:get_quality() == 'good' or
                (showall and doc:get_quality() ~= 'killed') then
            res = res + 1
        end
    end
    return res
end

-- parse user response and view the file
local function view_selected_doc(doclist, num_str, last_i)
    -- That returns the empty string on an empty line, nil on EOF.
    -- We only want to default to viewing 1 on an empty line.
    -- Use Lua's faked ternary operator for fun and brevity:
    num = (num_str == '' and 1 or tonumber(num_str))
    if num and doclist[num] and num <= last_i then
        view_doc(doclist[num])
    end
end

-- print a list of docfile objects (see texdoclib-search.tlu) as a menu
-- if showall is false, stop as soon as a bad result is encountered
local function print_doclist(name, doclist, showall)
    local max_lines = tonumber(texdoc.config.get_value('max_lines'))
    local interact = texdoc.config.get_value('interact_switch')

    local last_i = 0
    local prompt_more = false

    local len = count_list_length(doclist, showall)
    if interact and len > max_lines then
        local msg = '%d results. Only the top %d are shown below.'
        print(msg:format(len, max_lines))
        prompt_more = true
    elseif len == 0 then -- nothing to show
        return last_i
    end

    for i, doc in ipairs(doclist) do
        if doc:get_quality() == 'killed' then break end
        if doc:get_quality() ~= 'good' and not showall then break end

        last_i = i -- save for testing
        if texdoc.config.get_value('machine_switch') == true then
            -- for machine-readable output, list all results in a certain format
            print(name, doc.score, texdoc.util.w32_path(doc.realpath),
                  doc.lang or '', doc.details or '')
        else
            print(string.format('%2d %s', i, texdoc.util.w32_path(doc.realpath)))
            if doc.details or doc.lang then
                local line = '   = '
                if doc.lang then line = line .. '[' .. doc.lang .. '] ' end
                if doc.details then line = line .. doc.details end
                print(line)
            end

            if i >= max_lines and prompt_more then
                io.write('Enter number of file to view, RET to view 1, S to show all results, ' ..
                         'or any other key to exit: ')
                local ans = io.read('*line')
                if ans == 'S' then
                    prompt_more = false  -- and continue this loop
                else
                    view_selected_doc(doclist, ans, last_i)
                    return last_i  -- which should be always positive
                end
            end
        end
    end

    if interact then
        io.write('Enter number of file to view, RET to view 1, anything else to skip: ')
        view_selected_doc(doclist, io.read('*line'), last_i)
    end

    return last_i
end

-- wrapper function for print_doclist(): we could call it twice
local function print_list_menu(name, doclist, showall)
    local last_i = print_doclist(name, doclist, showall)

    if not showall and last_i == 0 then
        err_print('warning', 'No good result found, showing all results.')
        last_i = print_doclist(name, doclist, true)
    end

    if last_i == 0 then
        local msg = string.gsub(C.notfound_msg, C.notfound_msg_ph, name)
        io.stderr:write(msg .. '\n') -- get rid of gsub's 2nd value
        os.exit(C.exit_notfound)
    end
end

local function search_online(name, doclist)
    if not texdoc.config.get_value('interact_switch') or
        texdoc.config.get_value('machine_switch')
    then
        view_doc(doclist[1])
        return
    end

    local function write(msg)
        io.stderr:write(msg .. '') -- cast to string
    end
    -- Fuzzy heuristic to see if the user has _any_ local documentation
    local kpathsea_docs = texdoc.search.get_doclist("kpathsea")
    local docs_installed = kpathsea_docs[1] and
                           kpathsea_docs[1].basename == "kpathsea.pdf" and
                           not os.getenv("TEXDOC_NO_LOCAL_DOCS")

    if docs_installed and doclist[1] then
        write(C.badmatch_msg:gsub(C.badmatch_msg_ph, name))

        for i=1,3 do
            local doc = (doclist[i] and doclist[i].normname) or '<empty>'
            write('    ' .. i .. ' ' .. doc .. '\n')
        end
        write('\n')
    elseif docs_installed then
        write(C.nomatch_msg:gsub(C.nomatch_msg_ph, name))
    else
        write(C.nolocaldocs_msg)
    end

    local url = texdoc.config.get_value('online_url')
                :gsub(C.online_baseurl_ph, name)

    write(C.online_msg:gsub(C.online_msg_url, url)
                      :gsub(C.online_msg_ph, name))

    if docs_installed and doclist[1] then
        write(C.badmatch_prompt)
    else
        write(C.nolocaldocs_prompt)
    end

    local ans = io.read('*line')

    if ans:match('^\r?[yY]') then
        M.view_file(url)
    elseif tonumber(ans) then
        view_doc(doclist[tonumber(ans)])
    end
end

-----------------------   deliver results based on mode   ---------------------

function M.deliver_results(name, doclist, many)
    -- ensure that results were found or apologize
    if (not doclist[1] or doclist[1]:get_quality() == 'killed') and
        not texdoc.config.get_value('interact_switch')
    then
        local msg = string.gsub(C.notfound_msg, C.notfound_msg_ph, name)
        io.stderr:write(msg .. '\n') -- get rid of gsub's 2nd value
        os.exit(C.exit_notfound)
    end

    -- shall we show all of them or only the "good" ones?
    local mode = texdoc.config.get_value('mode')

    -- view result or show menu based on mode and number of results
    if (mode == 'view' and doclist[1] and doclist[1]:get_quality() == 'good') or
        (mode == 'mixed' and
            (not doclist[2] or doclist[2]:get_quality() ~= 'good')
        )
    then
        view_doc(doclist[1])
    elseif mode ~= 'view' then
        if many and not texdoc.config.get_value('machine_switch') then
            print('*** Results for: ' .. name .. ' ***')
        end
        print_list_menu(name, doclist, mode == 'showall')
    else
        search_online(name, doclist)
    end
end

return M

-- vim: ft=lua:
