--[[--
ReaderLink is an abstraction for document-specific link interfaces.
]]

local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local InputContainer = require("ui/widget/container/inputcontainer")
local LinkBox = require("ui/widget/linkbox")
local Notification = require("ui/widget/notification")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
local Screen = Device.screen
local T = require("ffi/util").template

local ReaderLink = InputContainer:new{
    location_stack = {}
}

function ReaderLink:init()
    if Device:isTouchDevice() then
        self:initGesListener()
    end
    self.ui:registerPostInitCallback(function()
        self.ui.menu:registerToMainMenu(self)
    end)
end

function ReaderLink:onReadSettings(config)
    -- called when loading new document
    self.location_stack = {}
end

function ReaderLink:initGesListener()
    if Device:isTouchDevice() then
        self.ges_events = {
            Tap = {
                GestureRange:new{
                    ges = "tap",
                    range = Geom:new{
                        x = 0, y = 0,
                        w = Screen:getWidth(),
                        h = Screen:getHeight()
                    }
                }
            },
            Swipe = {
                GestureRange:new{
                    ges = "swipe",
                    range = Geom:new{
                        x = 0, y = 0,
                        w = Screen:getWidth(),
                        h = Screen:getHeight(),
                    }
                }
            },
        }
    end
end

local function isTapToFollowLinksOn()
    return not G_reader_settings:isFalse("tap_to_follow_links")
end

local function isSwipeToGoBackEnabled()
    return G_reader_settings:readSetting("swipe_to_go_back") == true
end

local function isSwipeToFollowFirstLinkEnabled()
    return G_reader_settings:readSetting("swipe_to_follow_first_link") == true
end

local function isSwipeToFollowNearestLinkEnabled()
    return G_reader_settings:readSetting("swipe_to_follow_nearest_link") == true
end

local function isSwipeToJumpToLatestBookmarkEnabled()
    return G_reader_settings:readSetting("swipe_to_jump_to_latest_bookmark") == true
end

function ReaderLink:addToMainMenu(menu_items)
    -- insert table to main reader menu
    menu_items.follow_links = {
        text = _("Links"),
        sub_item_table = {
            {
                text = _("Tap to follow links"),
                checked_func = isTapToFollowLinksOn,
                callback = function()
                    G_reader_settings:saveSetting("tap_to_follow_links",
                        not isTapToFollowLinksOn())
                end,
                separator = true,
            },

            {
                text = _("Swipe to go back"),
                checked_func = isSwipeToGoBackEnabled,
                callback = function()
                    G_reader_settings:saveSetting("swipe_to_go_back",
                        not isSwipeToGoBackEnabled())
                end,
            },
            {
                text = _("Swipe to follow first link on page"),
                checked_func = isSwipeToFollowFirstLinkEnabled,
                callback = function()
                    G_reader_settings:saveSetting("swipe_to_follow_first_link",
                        not isSwipeToFollowFirstLinkEnabled())
                    if isSwipeToFollowFirstLinkEnabled() then
                        G_reader_settings:delSetting("swipe_to_follow_nearest_link") -- can't have both
                    end
                end,
            },
            {
                text = _("Swipe to follow nearest link"),
                checked_func = isSwipeToFollowNearestLinkEnabled,
                callback = function()
                    G_reader_settings:saveSetting("swipe_to_follow_nearest_link",
                        not isSwipeToFollowNearestLinkEnabled())
                    if isSwipeToFollowNearestLinkEnabled() then
                        G_reader_settings:delSetting("swipe_to_follow_first_link") -- can't have both
                    end
                end,
                separator = true,
            },
            {
                text = _("Swipe to jump to latest bookmark"),
                checked_func = isSwipeToJumpToLatestBookmarkEnabled,
                callback = function()
                    G_reader_settings:saveSetting("swipe_to_jump_to_latest_bookmark",
                        not isSwipeToJumpToLatestBookmarkEnabled())
                end,
            },
        }
    }
    menu_items.go_to_previous_location = {
        text = _("Go back to previous location"),
        enabled_func = function() return #self.location_stack > 0 end,
        callback = function() self:onGoBackLink() end,
        hold_callback = function() UIManager:show(ConfirmBox:new{
            text = _("Clear location history?"),
            ok_text = _("Clear"),
            ok_callback = function()
                self.location_stack = {}
            end,
        })
        end,
    }
end

--- Check if a xpointer to <a> node really points to itself
function ReaderLink:isXpointerCoherent(a_xpointer)
    -- Get screen coordinates of xpointer
    local doc_margins = self.ui.document._document:getPageMargins()
    local doc_y, doc_x = self.ui.document:getPosFromXPointer(a_xpointer)
    local top_y = self.ui.document:getCurrentPos()
    -- (strange, but using doc_margins.top is accurate even in scroll mode)
    local screen_y = doc_y - top_y + doc_margins["top"]
    local screen_x = doc_x + doc_margins["left"]
    -- Get again link and a_xpointer from this position
    local re_link_xpointer, re_a_xpointer = self.ui.document:getLinkFromPosition({x = screen_x, y = screen_y}) -- luacheck: no unused
    -- We should get the same a_xpointer. If not, crengine has messed up
    -- and we should not trust this xpointer to get back to this link.
    if re_a_xpointer ~= a_xpointer then
        logger.info("not coherent a_xpointer:", a_xpointer)
        return false
    end
    return true
end

--- Gets link from gesture.
-- `Document:getLinkFromPosition()` behaves differently depending on
-- document type, so this function provides a wrapper.
function ReaderLink:getLinkFromGes(ges)
    if self.ui.document.info.has_pages then
        local pos = self.view:screenToPageTransform(ges.pos)
        if pos then
            -- link box in native page
            local link, lbox = self.ui.document:getLinkFromPosition(pos.page, pos)
            if link and lbox then
                return {
                    link = link,
                    lbox = lbox,
                    pos = pos,
                }
            end
        end
    else
        local link_xpointer, a_xpointer = self.ui.document:getLinkFromPosition(ges.pos)
        logger.dbg("getLinkFromPosition link_xpointer:", link_xpointer)
        logger.dbg("getLinkFromPosition a_xpointer:",  a_xpointer)

        -- On some documents, crengine may sometimes give a wrong a_xpointer
        -- (in some Wikipedia saved as EPUB, it would point to some other <A>
        -- element in the same paragraph). If followed then back, we could get
        -- to a different page. So, we check here how valid it is, and if not,
        -- we just discard it so that addCurrentLocationToStack() is used.
        if a_xpointer and not self:isXpointerCoherent(a_xpointer) then
            a_xpointer = nil
        end

        if link_xpointer ~= "" then
            -- This link's target xpointer is more precise than a classic
            -- xpointer to top of a page: we can show a marker at its
            -- y-position in target page
            return {
                xpointer = link_xpointer,
                marker_xpointer = link_xpointer,
                from_xpointer = a_xpointer,
            }
        end
    end
end

--- Highlights a linkbox if available and goes to it.
function ReaderLink:showLinkBox(link)
    if link and link.lbox then -- pdfdocument
        -- screen box that holds the link
        local sbox = self.view:pageToScreenTransform(link.pos.page,
            self.ui.document:nativeToPageRectTransform(link.pos.page, link.lbox))
        if sbox then
            UIManager:show(LinkBox:new{
                box = sbox,
                timeout = FOLLOW_LINK_TIMEOUT,
                callback = function() self:onGotoLink(link.link) end
            })
            return true
        end
    elseif link and link.xpointer ~= "" then -- credocument
        return self:onGotoLink(link)
    end
end

function ReaderLink:onSetDimensions(dimen)
    -- update listening according to new screen dimen
    if Device:isTouchDevice() then
        self:initGesListener()
    end
end

function ReaderLink:onTap(_, ges)
    if not isTapToFollowLinksOn() then return end
    local link = self:getLinkFromGes(ges)
    if link then
        return self:showLinkBox(link)
    end
end

--- Remember current location so we can go back to it
function ReaderLink:addCurrentLocationToStack()
    if self.ui.document.info.has_pages then
        table.insert(self.location_stack, self.ui.paging:getBookLocation())
    else
        table.insert(self.location_stack, {
            xpointer = self.ui.rolling:getBookLocation(),
        })
    end
end

--- Goes to link.
function ReaderLink:onGotoLink(link, neglect_current_location)
    logger.dbg("onGotoLink:", link)
    if self.ui.document.info.has_pages then
        -- internal pdf links have a "page" attribute, while external ones have an "uri" attribute
        if link.page then -- Internal link
            logger.dbg("Internal link:", link)
            if not neglect_current_location then
                self:addCurrentLocationToStack()
            end
            self.ui:handleEvent(Event:new("GotoPage", link.page + 1))
            return true
        end
        link = link.uri -- external link
    else
        -- For crengine, internal links may look like :
        --   #_doc_fragment_0_Organisation (link from anchor)
        --   /body/DocFragment/body/ul[2]/li[5]/text()[3].16 (xpointer from full-text search)
        -- If the XPointer does not exist (or is a full url), we will jump to page 1
        -- Best to check that this link exists in document with the following,
        -- which accepts both of the above legitimate xpointer as input.
        if self.ui.document:isXPointerInDocument(link.xpointer) then
            logger.dbg("Internal link:", link)
            if not neglect_current_location then
                if link.from_xpointer then
                    -- We have a more precise xpointer than the xpointer to top of
                    -- current page that addCurrentLocationToStack() would give, and
                    -- we may be able to show a marker when back
                    local saved_location
                    if self.view.view_mode == "scroll" then
                        -- In scroll mode, we still use the top of page as the
                        -- xpointer to go back to, so we get back to the same view.
                        -- We can still show the marker at the link position
                        saved_location = {
                            xpointer = self.ui.rolling:getBookLocation(),
                            marker_xpointer = link.from_xpointer,
                        }
                    else
                        -- In page mode, we use the same for go to and for marker,
                        -- as 'page mode' ensures we get back to the same view.
                        saved_location = {
                            xpointer = link.from_xpointer,
                            marker_xpointer = link.from_xpointer,
                        }
                    end
                    table.insert(self.location_stack, saved_location)
                else
                    self:addCurrentLocationToStack()
                end
            end
            self.ui:handleEvent(Event:new("GotoXPointer", link.xpointer, link.marker_xpointer))
            return true
        end
    end
    logger.dbg("External link:", link)
    -- Check if it is a wikipedia link
    local wiki_lang, wiki_page = link.xpointer:match([[https?://([^%.]+).wikipedia.org/wiki/([^/]+)]])
    if wiki_lang and wiki_page then
        logger.dbg("Wikipedia link:", wiki_lang, wiki_page)
        -- Ask for user confirmation before launching lookup (on a
        -- wikipedia page saved as epub, full of wikipedia links, it's
        -- too easy to click on links when wanting to change page...)
        -- But first check if this wikipedia article has been saved as EPUB
        local epub_filename = util.replaceInvalidChars(wiki_page:gsub("_", " ")) .. "."..string.upper(wiki_lang)..".epub"
        local epub_fullpath
        -- either in current book directory
        local last_file = G_reader_settings:readSetting("lastfile")
        if last_file then
            local current_book_dir = last_file:match("(.*)/")
            local epub_path = current_book_dir .. "/" .. epub_filename
            if util.pathExists(epub_path) then
                epub_fullpath = epub_path
            end
        end
        -- or in wikipedia save directory
        if not epub_fullpath then
            local dir = G_reader_settings:readSetting("wikipedia_save_dir")
            if not dir then dir = G_reader_settings:readSetting("home_dir") end
            if not dir then dir = require("apps/filemanager/filemanagerutil").getDefaultDir() end
            if dir then
                local epub_path = dir .. "/" .. epub_filename
                if util.pathExists(epub_path) then
                    epub_fullpath = epub_path
                end
            end
        end
        if epub_fullpath then
            local MultiConfirmBox = require("ui/widget/multiconfirmbox")
            UIManager:show(MultiConfirmBox:new{
                text = T(_("Would you like to read this Wikipedia %1 article?\n\n%2\n\nThis article has previously been saved as EPUB. You may wish to read the saved EPUB instead."), wiki_lang:upper(), wiki_page:gsub("_", " "), epub_fullpath),
                choice1_text = _("Read online"),
                choice1_callback = function()
                    UIManager:nextTick(function()
                        self.ui:handleEvent(Event:new("LookupWikipedia", wiki_page, false, true, wiki_lang))
                    end)
                end,
                choice2_text = _("Read EPUB"),
                choice2_callback = function()
                    -- close current ReaderUI, and create a new one
                    UIManager:scheduleIn(0.1, function()
                        local ReaderUI = require("apps/reader/readerui")
                        local reader = ReaderUI:_getRunningInstance()
                        if reader then
                            reader:onClose()
                        end
                        ReaderUI:showReader(epub_fullpath)
                    end)
                end,
            })
        else
            UIManager:show(ConfirmBox:new{
                text = T(_("Would you like to read this Wikipedia %1 article?\n\n%2\n"), wiki_lang:upper(), wiki_page:gsub("_", " ")),
                ok_callback = function()
                    UIManager:nextTick(function()
                        self.ui:handleEvent(Event:new("LookupWikipedia", wiki_page, false, true, wiki_lang))
                    end)
                end
            })
        end
    else
        local InfoMessage = require("ui/widget/infomessage")
        UIManager:show(InfoMessage:new{
            text = T(_("Invalid or external link:\n%1"), link.xpointer),
            -- no timeout to allow user to type that link in his web browser
        })
    end
    -- don't propagate, user will notice and tap elsewhere if he wants to change page
    return true
end

--- Goes back to previous location.
function ReaderLink:onGoBackLink()
    local saved_location = table.remove(self.location_stack)
    if saved_location then
        logger.dbg("GoBack: restoring:", saved_location)
        self.ui:handleEvent(Event:new('RestoreBookLocation', saved_location))
        return true
    end
end

function ReaderLink:onSwipe(arg, ges)
    if ges.direction == "east" then
        if isSwipeToGoBackEnabled() then
            if #self.location_stack > 0 then
                -- Remember if location stack is going to be empty, so we
                -- can stop the propagation of next swipe back: so the user
                -- knows it is empty and that next swipe back will get him
                -- to previous page (and not to previous location)
                self.swipe_back_resist = #self.location_stack == 1
                return self:onGoBackLink()
            elseif self.swipe_back_resist then
                self.swipe_back_resist = false
                -- Make that gesture don't do anything, and show a Notification
                -- so the user knows why
                UIManager:show(Notification:new{
                    text = _("Location history is empty"),
                    timeout = 1.0,
                })
                return true
            end
        end
    elseif ges.direction == "west" then
        local ret = false
        if isSwipeToFollowFirstLinkEnabled() then
            ret = self:onGoToPageLink(ges, true)
        elseif isSwipeToFollowNearestLinkEnabled() then
            ret = self:onGoToPageLink(ges)
        end
        -- If no link found, or no follow link option enabled,
        -- jump to latest bookmark (if enabled)
        if not ret and isSwipeToJumpToLatestBookmarkEnabled() then
            ret = self:onGoToLatestBookmark(ges)
        end
        return ret
    end
end

--- Goes to link nearest to the gesture (or first link in page)
function ReaderLink:onGoToPageLink(ges, use_page_first_link)
    local selected_link = nil
    if self.ui.document.info.has_pages then
        local pos = self.view:screenToPageTransform(ges.pos)
        if not pos then
            return
        end
        local links = self.ui.document:getPageLinks(pos.page)
        if not links or #links == 0 then
            return
        end
        -- DEBUG("PDF Page links : ", links)
        -- We may get multiple links: internal ones (with "page" key)
        -- that we're interested in, but also external links (no "page", but
        -- a "uri" key) that we don't care about.
        --     [2] = {
        --         ["y1"] = 107.88977050781,
        --         ["x1"] = 176.60360717773,
        --         ["y0"] = 97.944396972656,
        --         ["x0"] = 97,
        --         ["page"] = 347
        --     },
        local pos_x, pos_y = pos.x, pos.y
        local shortest_dist = nil
        local first_y0 = nil
        for _, link in ipairs(links) do
            if link["page"] then
                if use_page_first_link then
                    -- Links may not be in the order they are in the page, so let's
                    -- find the one with the smallest y0.
                    if first_y0 == nil or link["y0"] < first_y0 then
                        selected_link = link
                        first_y0 = link["y0"]
                    end
                else
                    local start_dist = math.pow(link.x0 - pos_x, 2) + math.pow(link.y0 - pos_y, 2)
                    local end_dist = math.pow(link.x1 - pos_x, 2) + math.pow(link.y1 - pos_y, 2)
                    local min_dist = math.min(start_dist, end_dist)
                    if shortest_dist == nil or min_dist < shortest_dist then
                        -- onGotoLink()'s GotoPage event needs the link
                        -- itself, and will use its "page" value
                        selected_link = link
                        shortest_dist = min_dist
                    end
                end
            end
        end
    else
        local links = self.ui.document:getPageLinks()
        if not links or #links == 0 then
            return
        end
        -- DEBUG("CRE Page links : ", links)
        -- We may get multiple links: internal ones (they have a "section" key)
        -- that we're interested in, but also external links (no "section", but
        -- a "uri" key) that we don't care about.
        --     [1] = {
        --         ["end_x"] = 825,
        --         ["uri"] = "",
        --         ["end_y"] = 333511,
        --         ["start_x"] = 90,
        --         ["start_y"] = 333511
        --     },
        --     [2] = {
        --         ["end_x"] = 366,
        --         ["section"] = "#_doc_fragment_19_ftn_fn6",
        --         ["end_y"] = 1201,
        --         ["start_x"] = 352,
        --         ["start_y"] = 1201
        --         ["a_xpointer"] = "/body/DocFragment/body/div/p[12]/sup[3]/a[3].0",
        --     },
        -- Note: with some documents and some links, crengine may give wrong
        -- coordinates, and our code below may miss or give the wrong first
        -- or nearest link...
        local pos_x, pos_y = ges.pos.x, ges.pos.y
        local shortest_dist = nil
        local first_start_y = nil
        for _, link in ipairs(links) do
            if link["section"] then
                if use_page_first_link then
                    -- links may not be in the order they are in the page, so let's
                    -- find the one with the smallest start_y.
                    if first_start_y == nil or link["start_y"] < first_start_y then
                        selected_link = link
                        first_start_y = link["start_y"]
                    end
                else
                    local start_dist = math.pow(link.start_x - pos_x, 2) + math.pow(link.start_y - pos_y, 2)
                    local end_dist = math.pow(link.end_x - pos_x, 2) + math.pow(link.end_y - pos_y, 2)
                    local min_dist = math.min(start_dist, end_dist)
                    if shortest_dist == nil or min_dist < shortest_dist then
                        selected_link = link
                        shortest_dist = min_dist
                    end
                end
            end
        end
        -- cre.cpp getPageLinks() does highlight found links :
        --   sel.add( new ldomXRange(*links[i]) ); // highlight
        -- and we'll find them highlighted when back from link.
        -- So let's clear them now.
        self.ui.document:clearSelection()
        -- (Comment out previous line to visually see which links on the
        -- page are not coherent: those not highlighted)

        if selected_link then
            logger.dbg("original selected_link", selected_link)
            -- Make it a link as expected by onGotoLink
            selected_link = {
                xpointer = selected_link["section"],
                marker_xpointer = selected_link["section"],
                from_xpointer = selected_link["a_xpointer"],
            }
            logger.dbg("selected_link", selected_link)
            -- Check from_xpointer is coherent, and unset it if not
            if selected_link.from_xpointer and not self:isXpointerCoherent(selected_link.from_xpointer) then
                selected_link.from_xpointer = nil
            end

        end
    end
    if selected_link then
        return self:onGotoLink(selected_link)
    end
end

function ReaderLink:onGoToLatestBookmark(ges)
    local latest_bookmark = self.ui.bookmark:getLatestBookmark()
    if latest_bookmark then
        if self.ui.document.info.has_pages then
            -- self:onGotoLink() needs something with a page attribute.
            -- we need to substract 1 to bookmark page, as links start from 0
            -- and onGotoLink will add 1 - we need a fake_link (with a single
            -- page attribute) so we don't touch the bookmark itself
            local fake_link = {}
            fake_link.page = latest_bookmark.page - 1
            return self:onGotoLink(fake_link)
        else
            -- Make it a link as expected by onGotoLink
            local link
            if latest_bookmark.pos0 then -- text highlighted, precise xpointer
                link = {
                    xpointer = latest_bookmark.pos0,
                    marker_xpointer = latest_bookmark.pos0,
                }
            else -- page bookmarked, 'page' is a xpointer to top of page
                link = {
                    xpointer = latest_bookmark.page,
                }
            end
            return self:onGotoLink(link)
        end
    end
end

return ReaderLink
