local DocSettings = require("docsettings")
local LuaSettings = require("luasettings")
local Notification = require("ui/widget/notification")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local random = require("random")
local _ = require("gettext")
local T = require("ffi/util").template

local ReaderAnnotation = WidgetContainer:extend{
    annotations = nil, -- array sorted by annotation position order, ascending
}

-- build, read, save

function ReaderAnnotation:buildAnnotation(bm, highlights, init)
    -- bm: associated single bookmark ; highlights: tables with all highlights
    if self.ui.rolling then
        local is_invalid
        if not self.document:isXPointerInDocument(bm.page) then
            logger.warn("Skipping old bookmark, invalid start xpointer:", bm.page)
            is_invalid = true
        end
        if bm.pos1 and not self.document:isXPointerInDocument(bm.pos1) then
            logger.warn("Skipping old bookmark, invalid end xpointer:", bm.page)
            is_invalid = true
        end
        if is_invalid then return end
    end
    local note = bm.text
    if note == "" then
        note = nil
    end
    local chapter = bm.chapter
    local hl, pageno = self:getHighlightForBookmark(highlights, bm)
    local pageref
    if init then
        if note and self.ui.bookmark:isBookmarkAutoText(bm) then
            note = nil
        end
        if chapter == nil then
            chapter = self.ui.toc:getTocTitleByPage(bm.page)
        end
        pageno = self.ui.rolling and self.document:getPageFromXPointer(bm.page) or bm.page
        pageref = self:getPageRef(bm.page, pageno)
    end
    if self.ui.paging and bm.pos0 and not bm.pos0.page then
        -- old single-page reflow highlights do not have page in position
        bm.pos0.page = bm.page
        bm.pos1.page = bm.page
    end
    if hl == nil then -- page bookmark or orphaned bookmark
        hl = {}
        if bm.highlighted then -- orphaned bookmark
            hl.drawer = self.view.highlight.saved_drawer
            hl.color = self.view.highlight.saved_color
            if self.ui.paging then
                if bm.pos0.page == bm.pos1.page then
                    hl.pboxes = self.document:getPageBoxesFromPositions(bm.page, bm.pos0, bm.pos1)
                else -- multi-page highlight, restore the first box only
                    hl.pboxes = self.document:getPageBoxesFromPositions(bm.page, bm.pos0, bm.pos0)
                end
            end
        end
    end
    return { -- annotation
        datetime         = bm.datetime, -- creation time, not changeable
        datetime_updated = nil,         -- last modification time
        drawer           = hl.drawer,   -- highlight drawer
        color            = hl.color,    -- highlight color
        text             = bm.notes,    -- highlighted text, editable
        text_edited      = hl.edited,   -- true if highlighted text has been edited
        note             = note,        -- user's note, editable
        chapter          = chapter,     -- book chapter title
        pageno           = pageno,      -- book page number (continuous numbering, used by KOHighlights)
        pageref          = pageref,     -- book page number (iff: reference pages or hidden flows)
        page             = bm.page,     -- highlight location, xPointer or number (pdf)
        pos0             = bm.pos0,     -- highlight start position, xPointer (== page) or table (pdf)
        pos1             = bm.pos1,     -- highlight end position, xPointer or table (pdf)
        pboxes           = hl.pboxes,   -- pdf pboxes, used only and changeable by addMarkupAnnotation
        ext              = hl.ext,      -- pdf multi-page highlight
    }
end

function ReaderAnnotation:getHighlightForBookmark(highlights, bookmark)
    if not bookmark.highlighted then return end -- page bookmark
    local doesMatch = self:getMatchFunc()
    for pageno, page_highlights in pairs(highlights) do
        for _, highlight in ipairs(page_highlights) do
            if doesMatch(highlight, bookmark) then
                return highlight, pageno
            end
        end
    end
end

function ReaderAnnotation:getAnnotationsFromBookmarksHighlights(bookmarks, highlights, init)
    local annotations = {}
    for i = #bookmarks, 1, -1 do
        local annotation = self:buildAnnotation(bookmarks[i], highlights, init)
        if annotation then
            table.insert(annotations, annotation)
        end
    end
    if init then
        self:sortItems(annotations)
    end
    return annotations
end

function ReaderAnnotation:onReadSettings(config)
    local annotations = config:readSetting("annotations")
    -- KOHighlights may set this key when it has merged annotations from different sources:
    -- we want to make sure they are updated and sorted
    local needs_update = annotations and config:isTrue("annotations_externally_modified")
    local needs_sort -- if incompatible annotations were built of old highlights/bookmarks
    if annotations then
        -- Annotation formats in crengine and mupdf are incompatible.
        local has_annotations = #annotations > 0
        local annotations_type = has_annotations and type(annotations[1].page)
        if self.ui.rolling and annotations_type ~= "string" then -- incompatible format loaded, or empty
            if has_annotations then -- backup incompatible format if not empty
                config:saveSetting("annotations_paging", annotations)
            end
             -- load compatible format
            annotations = config:readSetting("annotations_rolling") or {}
            config:delSetting("annotations_rolling")
            needs_sort = true
        elseif self.ui.paging and annotations_type ~= "number" then
            if has_annotations then
                config:saveSetting("annotations_rolling", annotations)
            end
            annotations = config:readSetting("annotations_paging") or {}
            config:delSetting("annotations_paging")
            needs_sort = true
        end
        self.annotations = annotations
    else -- first run
        if self.ui.rolling then
            self.ui:registerPostInitCallback(function()
                self:migrateToAnnotations(config)
            end)
        else
            self:migrateToAnnotations(config)
        end
    end
    if self:importAnnotations() then
        needs_update = true
    end
    if needs_update or needs_sort then
        self.ui:registerPostReaderReadyCallback(function()
            self:updateAnnotations(needs_update, needs_sort)
        end)
        config:delSetting("annotations_externally_modified")
    end
end

function ReaderAnnotation:migrateToAnnotations(config)
    local bookmarks = config:readSetting("bookmarks") or {}
    local highlights = config:readSetting("highlight") or {}

    if config:hasNot("highlights_imported") then
        -- before 2014, saved highlights were not added to bookmarks when they were created.
        for page, hls in pairs(highlights) do
            for _, hl in ipairs(hls) do
                local hl_page = self.ui.paging and page or hl.pos0
                -- highlights saved by some old versions don't have pos0 field
                -- we just ignore those highlights
                if hl_page then
                    local item = {
                        datetime    = hl.datetime,
                        highlighted = true,
                        notes       = hl.text,
                        page        = hl_page,
                        pos0        = hl.pos0,
                        pos1        = hl.pos1,
                    }
                    if self.ui.paging then
                        item.pos0.page = page
                        item.pos1.page = page
                    end
                    table.insert(bookmarks, item)
                end
            end
        end
    end

    -- Bookmarks/highlights formats in crengine and mupdf are incompatible.
    local has_bookmarks = #bookmarks > 0
    local bookmarks_type = has_bookmarks and type(bookmarks[1].page)
    if self.ui.rolling then
        if bookmarks_type == "string" then -- compatible format loaded, check for incompatible old backup
            if config:has("bookmarks_paging") then -- save incompatible old backup
                local bookmarks_paging = config:readSetting("bookmarks_paging")
                local highlights_paging = config:readSetting("highlight_paging")
                local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks_paging, highlights_paging)
                config:saveSetting("annotations_paging", annotations)
                config:delSetting("bookmarks_paging")
                config:delSetting("highlight_paging")
            end
        else -- incompatible format loaded, or empty
            if has_bookmarks then -- save incompatible format if not empty
                local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights)
                config:saveSetting("annotations_paging", annotations)
            end
            -- load compatible format
            bookmarks = config:readSetting("bookmarks_rolling") or {}
            highlights = config:readSetting("highlight_rolling") or {}
            config:delSetting("bookmarks_rolling")
            config:delSetting("highlight_rolling")
        end
    else -- self.ui.paging
        if bookmarks_type == "number" then
            if config:has("bookmarks_rolling") then
                local bookmarks_rolling = config:readSetting("bookmarks_rolling")
                local highlights_rolling = config:readSetting("highlight_rolling")
                local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks_rolling, highlights_rolling)
                config:saveSetting("annotations_rolling", annotations)
                config:delSetting("bookmarks_rolling")
                config:delSetting("highlight_rolling")
            end
        else
            if has_bookmarks then
                local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights)
                config:saveSetting("annotations_rolling", annotations)
            end
            bookmarks = config:readSetting("bookmarks_paging") or {}
            highlights = config:readSetting("highlight_paging") or {}
            config:delSetting("bookmarks_paging")
            config:delSetting("highlight_paging")
        end
    end

    self.annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights, true)
end

function ReaderAnnotation:setNeedsUpdateFlag()
    self.needs_update = true
end

ReaderAnnotation.onDocumentRerendered = ReaderAnnotation.setNeedsUpdateFlag
ReaderAnnotation.onUsePageLabelsUpdated = ReaderAnnotation.setNeedsUpdateFlag

function ReaderAnnotation:onCloseDocument()
    self:updatePageNumbers()
end

function ReaderAnnotation:onSaveSettings()
    self:updatePageNumbers()
    self.ui.doc_settings:saveSetting("annotations", self.annotations)
    self:onExportAnnotations(true)
end

function ReaderAnnotation:getExportAnnotationsFilepath()
    local dir = G_reader_settings:readSetting("annotations_export_folder") or DocSettings:getSidecarDir(self.document.file)
    local filename = self.document.file:gsub(".*/", "")
    return dir .. "/" .. filename .. ".annotations.lua"
end

function ReaderAnnotation:onExportAnnotations(on_closing)
    local do_export = not on_closing or G_reader_settings:isTrue("annotations_export_on_closing")
    if do_export and self:hasAnnotations() then
        local file = self:getExportAnnotationsFilepath()
        local anno = LuaSettings:open(file)
        local device_id = G_reader_settings:readSetting("device_id", random.uuid())
        anno:saveSetting("device_id", device_id)
        anno:saveSetting("datetime", os.date("%Y-%m-%d %H:%M:%S"))
        anno:saveSetting("paging", self.ui.paging and true)
        anno:saveSetting("annotations", self.annotations)
        anno:flush()
        os.remove(file .. ".old")
        if not on_closing then
            Notification:notify(_"Annotations exported")
        end
    end
end

function ReaderAnnotation:importAnnotations()
    local file = self:getExportAnnotationsFilepath()
    if lfs.attributes(file, "mode") ~= "file" then return end -- no import file
    local anno = LuaSettings:open(file)
    if anno:readSetting("device_id") == G_reader_settings:readSetting("device_id") then return end -- same device
    local new_annotations = anno:readSetting("annotations")
    if (self.ui.paging and true) ~= anno:readSetting("paging") then return end -- incompatible annotations type
    local new_datetime = G_reader_settings:isTrue("annotations_export_keep_all_on_import") and "" or anno:readSetting("datetime")
    os.remove(file)
    if #self.annotations == 0 then
        self.annotations = new_annotations
        return true
    end
    local doesMatch
    if self.ui.rolling then
        doesMatch = function(a, b)
            if (not a.drawer) ~= (not b.drawer) or a.page ~= b.page or a.pos1 ~= b.pos1 then
                return false
            end
            return true
        end
    else
        doesMatch = function(a, b)
            if (not a.drawer) ~= (not b.drawer) or a.page ~= b.page
                or (a.pos0 and (a.pos0.x ~= b.pos0.x or a.pos1.x ~= b.pos1.x
                             or a.pos0.y ~= b.pos0.y or a.pos1.y ~= b.pos1.y)) then
                return false
            end
            return true
        end
    end
    for i = #self.annotations, 1, -1 do
        local item = self.annotations[i]
        local item_datetime = item.datetime_updated or item.datetime
        local found
        for j = #new_annotations, 1, -1 do
            local new_item = new_annotations[j]
            if doesMatch(item, new_item) then
                if item_datetime < (new_item.datetime_updated or new_item.datetime) then
                    table.remove(self.annotations, i) -- new is newer, replace old with it
                else
                    table.remove(new_annotations, j) -- old is newer, keep it
                end
                found = true
                break
            end
        end
        if not found then
            if item_datetime < new_datetime then
                table.remove(self.annotations, i) -- export is newer, remove old
            end
        end
    end
    if #new_annotations > 0 then
        table.move(new_annotations, 1, #new_annotations, #self.annotations + 1, self.annotations)
    end
    return true
end

-- items handling

function ReaderAnnotation:updatePageNumbers(force_update)
    if force_update or self.needs_update then
        for _, item in ipairs(self.annotations) do
            item.pageno = self.ui.rolling and self.document:getPageFromXPointer(item.page) or item.page
            item.pageref = self:getPageRef(item.page, item.pageno)
        end
    end
    self.needs_update = nil
end

function ReaderAnnotation:sortItems(items)
    if #items > 1 then
        local sort_func = self.ui.rolling and function(a, b) return self:isItemInPositionOrderRolling(a, b) end
                                           or function(a, b) return self:isItemInPositionOrderPaging(a, b) end
        table.sort(items, sort_func)
    end
end

function ReaderAnnotation:updateAnnotations(needs_update, needs_sort)
    if needs_update then
        self.needs_update = true
        self:updatePageNumbers()
        needs_sort = true
    end
    if needs_sort then
        self:sortItems(self.annotations)
    end
end

function ReaderAnnotation:updateItemByXPointer(item)
    -- called by ReaderRolling:checkXPointersAndProposeDOMVersionUpgrade()
    local chapter = self.ui.toc:getTocTitleByPage(item.page)
    if chapter == "" then
        chapter = nil
    end
    if not item.drawer then -- page bookmark
        item.text = chapter and T(_("in %1"), chapter) or nil
    end
    item.chapter = chapter
    item.pageno = self.document:getPageFromXPointer(item.page)
    item.pageref = self:getPageRef(item.page, item.pageno)
end

function ReaderAnnotation:isItemInPositionOrderRolling(a, b)
    local a_page = self.document:getPageFromXPointer(a.page)
    local b_page = self.document:getPageFromXPointer(b.page)
    if a_page == b_page then -- both items in the same page
        if (not a.drawer) ~= (not b.drawer) then -- comparing a page bookmark and a highlight
            return not a.drawer -- have page bookmarks before highlights
        end
        local compare_xp = self.document:compareXPointers(a.page, b.page)
        if compare_xp then
            if a.drawer and compare_xp == 0 then -- both highlights with the same start, compare ends
                compare_xp = self.document:compareXPointers(a.pos1, b.pos1)
                if compare_xp then
                    return compare_xp > 0
                end
                logger.warn("Invalid xpointer in highlight:", a.pos1, b.pos1)
                return true
            end
            return compare_xp > 0
        end
        -- if compare_xp is nil, some xpointer is invalid and "a" will be sorted first to page 1
        logger.warn("Invalid xpointer in highlight:", a.page, b.page)
        return true
    end
    return a_page < b_page
end

function ReaderAnnotation:isItemInPositionOrderPaging(a, b)
    if a.page == b.page then -- both items in the same page
        if a.drawer and b.drawer then -- both items are highlights, compare positions
            local is_reflow = self.document.configurable.text_wrap -- save reflow mode
            self.document.configurable.text_wrap = 0 -- native positions
            -- sort start and end positions of each highlight
            local a_start, a_end, b_start, b_end, result
            if self.document:comparePositions(a.pos0, a.pos1) > 0 then
                a_start, a_end = a.pos0, a.pos1
            else
                a_start, a_end = a.pos1, a.pos0
            end
            if self.document:comparePositions(b.pos0, b.pos1) > 0 then
                b_start, b_end = b.pos0, b.pos1
            else
                b_start, b_end = b.pos1, b.pos0
            end
            -- compare start positions
            local compare_pos = self.document:comparePositions(a_start, b_start)
            if compare_pos == 0 then -- both highlights with the same start, compare ends
                result = self.document:comparePositions(a_end, b_end) > 0
            else
                result = compare_pos > 0
            end
            self.document.configurable.text_wrap = is_reflow -- restore reflow mode
            return result
        end
        return not a.drawer -- have page bookmarks before highlights
    end
    return a.page < b.page
end

function ReaderAnnotation:getMatchFunc()
    local doesMatch
    if self.ui.rolling then
        doesMatch = function(a, b)
            if (a.datetime ~= nil and b.datetime ~= nil and a.datetime ~= b.datetime)
                    or a.pos0 ~= b.pos0
                    or a.pos1 ~= b.pos1 then
                return false
            end
            return true
        end
    else
        doesMatch = function(a, b)
            if (a.datetime ~= nil and b.datetime ~= nil and a.datetime ~= b.datetime)
                    or a.page ~= b.page
                    or (a.pos0 and (a.pos0.x ~= b.pos0.x or a.pos1.x ~= b.pos1.x
                                 or a.pos0.y ~= b.pos0.y or a.pos1.y ~= b.pos1.y)) then
                return false
            end
            return true
        end
    end
    return doesMatch
end

function ReaderAnnotation:getItemIndex(item, no_binary)
    local doesMatch = self:getMatchFunc()

    if not no_binary then
        local isInOrder = self.ui.rolling and self.isItemInPositionOrderRolling or self.isItemInPositionOrderPaging
        local _start, _end, _middle = 1, #self.annotations
        while _start <= _end do
            _middle = bit.rshift(_start + _end, 1)
            local v = self.annotations[_middle]
            if doesMatch(item, v) then
                return _middle
            elseif isInOrder(self, item, v) then
                _end = _middle - 1
            else
                _start = _middle + 1
            end
        end
    end

    for i, v in ipairs(self.annotations) do
        if doesMatch(item, v) then
            return i
        end
    end
end

function ReaderAnnotation:getInsertionIndex(item)
    local isInOrder = self.ui.rolling and self.isItemInPositionOrderRolling or self.isItemInPositionOrderPaging
    local _start, _end, _middle, direction = 1, #self.annotations, 1, 0
    while _start <= _end do
        _middle = bit.rshift(_start + _end, 1)
        if isInOrder(self, item, self.annotations[_middle]) then
            _end, direction = _middle - 1, 0
        else
            _start, direction = _middle + 1, 1
        end
    end
    return _middle + direction
end

function ReaderAnnotation:addItem(item)
    item.datetime = item.datetime or os.date("%Y-%m-%d %H:%M:%S")
    item.pageno = self.ui.rolling and self.document:getPageFromXPointer(item.page) or item.page
    item.pageref = self:getPageRef(item.page, item.pageno)
    local index = self:getInsertionIndex(item)
    table.insert(self.annotations, index, item)
    return index
end

function ReaderAnnotation:onAnnotationsModified(items)
    if items.index_modified == nil or items.modify_datetime then -- not needed when annotation added or removed
        items[1].datetime_updated = os.date("%Y-%m-%d %H:%M:%S")
    end
end

-- info

function ReaderAnnotation:getPageRef(pn_or_xp, pn)
    -- same as ReaderBookmark:getBookmarkPageString(page)
    -- but gets pn (page number already calculated in the caller)
    -- and returns nil if there are no reference pages and hidden flows
    if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
        return self.ui.pagemap:getXPointerPageLabel(pn_or_xp, true)
    end
    if self.document:hasHiddenFlows() then
        local page = self.document:getPageNumberInFlow(pn)
        local flow = self.document:getPageFlow(pn)
        if flow > 0 then
            return T("[%1]%2", page, flow)
        end
        return tostring(page)
    end
end

function ReaderAnnotation:hasAnnotations()
    return #self.annotations > 0
end

function ReaderAnnotation:getNumberOfAnnotations()
    return #self.annotations
end

function ReaderAnnotation:getNumberOfHighlightsAndNotes() -- for Statistics plugin
    local highlights = 0
    local notes = 0
    for _, item in ipairs(self.annotations) do
        if item.drawer then
            if item.note then
                notes = notes + 1
            else
                highlights = highlights + 1
            end
        end
    end
    return highlights, notes
end

return ReaderAnnotation
