local DataStorage = require("datastorage")
local DocumentRegistry = require("document/documentregistry")
local LuaSettings = require("luasettings")
local ffiUtil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")

local collection_file = DataStorage:getSettingsDir() .. "/collection.lua"

local ReadCollection = {
    coll = nil, -- hash table
    coll_settings = nil, -- hash table
    last_read_time = 0,
    default_collection_name = "favorites",
}

-- read, write

local function buildEntry(file, order, attr)
    file = ffiUtil.realpath(file)
    if file then
        attr = attr or lfs.attributes(file)
        if attr and attr.mode == "file" then
            return {
                file  = file,
                text  = file:gsub(".*/", ""),
                order = order,
                attr  = attr,
            }
        end
    end
end

function ReadCollection:_read()
    local collection_file_modification_time = lfs.attributes(collection_file, "modification")
    if collection_file_modification_time then
        if collection_file_modification_time <= self.last_read_time then return end
        self.last_read_time = collection_file_modification_time
    end
    local collections = LuaSettings:open(collection_file)
    if collections:hasNot(self.default_collection_name) then
        collections:saveSetting(self.default_collection_name, {})
    end
    logger.dbg("ReadCollection: reading from collection file")
    self.coll = {}
    self.coll_settings = {}
    local updated_collections = {}
    for coll_name, collection in pairs(collections.data) do
        local coll = {}
        for _, v in ipairs(collection) do
            local item = buildEntry(v.file, v.order)
            if item then -- exclude deleted files
                coll[item.file] = item
            end
        end
        self.coll[coll_name] = coll
        self.coll_settings[coll_name] = collection.settings or { order = 1 } -- favorites, first run
        if self:updateCollectionFromFolder(coll_name) > 0 then
            updated_collections[coll_name] = true
        end
    end
    if next(updated_collections) ~= nil then
        self:write(updated_collections)
    end
end

function ReadCollection:write(updated_collections)
    local collections = LuaSettings:open(collection_file)
    for coll_name in pairs(collections.data) do
        if not self.coll[coll_name] then
            collections:delSetting(coll_name)
        end
    end
    for coll_name, coll in pairs(self.coll) do
        if updated_collections == nil or updated_collections[1] or updated_collections[coll_name] then
            local is_manual_collate = not self.coll_settings[coll_name].collate or nil
            local data = { settings = self.coll_settings[coll_name] }
            for _, item in pairs(coll) do
                table.insert(data, { file = item.file, order = is_manual_collate and item.order })
            end
            collections:saveSetting(coll_name, data)
        end
    end
    logger.dbg("ReadCollection: writing to collection file")
    collections:flush()
end

function ReadCollection:updateLastBookTime(file)
    file = ffiUtil.realpath(file)
    if file then
        local now = os.time()
        for _, coll in pairs(self.coll) do
            if coll[file] then
                coll[file].attr.access = now
            end
        end
    end
end

-- info

function ReadCollection:isFileInCollection(file, collection_name)
    file = ffiUtil.realpath(file) or file
    return self.coll[collection_name] and self.coll[collection_name][file] and true or false
end

function ReadCollection:isFileInCollections(file, ignore_show_mark_setting)
    if ignore_show_mark_setting or G_reader_settings:nilOrTrue("collection_show_mark") then
        file = ffiUtil.realpath(file) or file
        for _, coll in pairs(self.coll) do
            if coll[file] then
                return true
            end
        end
    end
    return false
end

function ReadCollection:getCollectionsWithFile(file)
    file = ffiUtil.realpath(file) or file
    local collections = {}
    for coll_name, coll in pairs(self.coll) do
        if coll[file] then
            collections[coll_name] = true
        end
    end
    return collections
end

function ReadCollection:getCollectionNextOrder(collection_name)
    if self.coll_settings[collection_name].collate then return end
    local max_order = 0
    for _, item in pairs(self.coll[collection_name]) do
        if max_order < item.order then
            max_order = item.order
        end
    end
    return max_order + 1
end

-- manage items

function ReadCollection:addItem(file, collection_name, attr)
    local item = buildEntry(file, self:getCollectionNextOrder(collection_name), attr)
    self.coll[collection_name][item.file] = item
end

function ReadCollection:addRemoveItemMultiple(file, collections_to_add)
    file = ffiUtil.realpath(file) or file
    local attr
    for coll_name, coll in pairs(self.coll) do
        if collections_to_add[coll_name] then
            if not coll[file] then
                attr = attr or lfs.attributes(file)
                coll[file] = buildEntry(file, self:getCollectionNextOrder(coll_name), attr)
            end
        else
            if coll[file] then
                coll[file] = nil
            end
        end
    end
end

function ReadCollection:addItemsMultiple(files, collections_to_add)
    local count = 0
    for file in pairs(files) do
        file = ffiUtil.realpath(file) or file
        local attr
        for coll_name in pairs(collections_to_add) do
            local coll = self.coll[coll_name]
            if not coll[file] then
                attr = attr or lfs.attributes(file)
                coll[file] = buildEntry(file, self:getCollectionNextOrder(coll_name), attr)
                count = count + 1
            end
        end
    end
    return count
end

function ReadCollection:removeItem(file, collection_name, no_write) -- FM: delete file; FMColl: remove file
    file = ffiUtil.realpath(file) or file
    if collection_name then
        if self.coll[collection_name][file] then
            self.coll[collection_name][file] = nil
            if not no_write then
                self:write({ collection_name = true })
            end
            return true
        end
    else
        local do_write
        for _, coll in pairs(self.coll) do
            if coll[file] then
                coll[file] = nil
                do_write = true
            end
        end
        if do_write then
            if not no_write then
                self:write()
            end
            return true
        end
    end
end

function ReadCollection:removeItems(files) -- FM: delete files
    local do_write
    for file in pairs(files) do
        if self:removeItem(file, nil, true) then
            do_write = true
        end
    end
    if do_write then
        self:write()
    end
end

function ReadCollection:removeItemsByPath(path) -- FM: delete folder
    local do_write
    for coll_name, coll in pairs(self.coll) do
        for file_name in pairs(coll) do
            if util.stringStartsWith(file_name, path) then
                coll[file_name] = nil
                do_write = true
            end
        end
    end
    if do_write then
        self:write()
    end
end

function ReadCollection:_updateItem(coll_name, file_name, new_filepath, new_path)
    local coll = self.coll[coll_name]
    local item_old = coll[file_name]
    new_filepath = new_filepath or new_path .. "/" .. item_old.text
    local item = buildEntry(new_filepath, item_old.order, item_old.attr) -- no lfs call
    coll[item.file] = item
    coll[file_name] = nil
end

function ReadCollection:updateItem(file, new_filepath) -- FM: rename file, move file
    file = ffiUtil.realpath(file) or file
    local do_write
    for coll_name, coll in pairs(self.coll) do
        if coll[file] then
            self:_updateItem(coll_name, file, new_filepath)
            do_write = true
        end
    end
    if do_write then
        self:write()
    end
end

function ReadCollection:updateItems(files, new_path) -- FM: move files
    local do_write
    for file in pairs(files) do
        file = ffiUtil.realpath(file) or file
        for coll_name, coll in pairs(self.coll) do
            if coll[file] then
                self:_updateItem(coll_name, file, nil, new_path)
                do_write = true
            end
        end
    end
    if do_write then
        self:write()
    end
end

function ReadCollection:updateItemsByPath(path, new_path) -- FM: rename folder, move folder
    local len = #path
    local do_write
    for coll_name, coll in pairs(self.coll) do
        for file_name in pairs(coll) do
            if file_name:sub(1, len) == path then
                self:_updateItem(coll_name, file_name, new_path .. file_name:sub(len + 1))
                do_write = true
            end
        end
    end
    if do_write then
        self:write()
    end
end

function ReadCollection:updateCollectionFromFolder(collection_name, folders, is_showing)
    folders = folders or self.coll_settings[collection_name].folders
    local count = 0
    if folders then
        local coll = self.coll[collection_name]
        local filetypes
        local str = util.tableGetValue(self.coll_settings[collection_name], "filter", "add", "filetype")
        if str then -- string of comma separated file types
            filetypes = {}
            for filetype in util.gsplit(str, ",") do
                filetypes[util.trim(filetype)] = true
            end
        end
        local function add_item_callback(file, f, attr)
            file = ffiUtil.realpath(file)
            local does_match = coll[file] == nil and not util.stringStartsWith(f, "._")
                and (filetypes or DocumentRegistry:hasProvider(file))
            if does_match then
                if filetypes then
                    local _, fileext = require("apps/filemanager/filemanagerutil").splitFileNameType(file)
                    does_match = filetypes[fileext]
                end
                if does_match then
                    self:addItem(file, collection_name, attr)
                    count = count + 1
                end
            end
        end
        for folder, folder_settings in pairs(folders) do
            if not is_showing or folder_settings.scan_on_show then
                logger.dbg("ReadCollection: scanning folder", folder)
                util.findFiles(folder, add_item_callback, folder_settings.subfolders)
            end
        end
    end
    return count
end

function ReadCollection:getOrderedCollection(collection_name)
    local ordered_coll = {}
    for _, item in pairs(self.coll[collection_name]) do
        table.insert(ordered_coll, item)
    end
    table.sort(ordered_coll, function(v1, v2) return v1.order < v2.order end)
    return ordered_coll
end

function ReadCollection:updateCollectionOrder(collection_name, ordered_coll)
    local coll = self.coll[collection_name]
    for i, item in ipairs(ordered_coll) do
        coll[item.file].order = i
    end
end

-- manage collections

function ReadCollection:addCollection(coll_name)
    local max_order = 0
    for _, settings in pairs(self.coll_settings) do
        if max_order < settings.order then
            max_order = settings.order
        end
    end
    self.coll_settings[coll_name] = { order = max_order + 1 }
    self.coll[coll_name] = {}
end

function ReadCollection:renameCollection(coll_name, new_name)
    self.coll_settings[new_name] = self.coll_settings[coll_name]
    self.coll[new_name] = self.coll[coll_name]
    self.coll_settings[coll_name] = nil
    self.coll[coll_name] = nil
end

function ReadCollection:removeCollection(coll_name)
    self.coll_settings[coll_name] = nil
    self.coll[coll_name] = nil
end

function ReadCollection:updateCollectionListOrder(ordered_coll)
    for i, item in ipairs(ordered_coll) do
        self.coll_settings[item.name].order = i
    end
end

ReadCollection:_read()

return ReadCollection
