local DataStorage = require("datastorage")
local InputContainer = require("ui/widget/container/inputcontainer")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local ltn12 = require("ltn12")
local DEBUG = require("dbg")
local _ = require("gettext")

local ffi = require("ffi")
local C = ffi.C
ffi.cdef[[
int remove(const char *);
int rmdir(const char *);
]]

require("ffi/zeromq_h")
local ZSync = InputContainer:new{
    name = "zsync",
    is_doc_only = true,
}

function ZSync:init()
    self.ui.menu:registerToMainMenu(self)
    self.outbox = DataStorage:getFullDataDir().."/zsync-outbox"
    self.server_config = self.path.."/server.cfg"
    self.client_config = self.path.."/client.cfg"
end

function ZSync:addToMainMenu(menu_items)
    menu_items.zsync = {
        text = _("ZSync"),
        sub_item_table = {
            {
                text_func = function()
                    return not self.filemq_server
                        and _("Share this document")
                        or _("Stop sharing books")
                end,
                enabled_func = function()
                    return self.filemq_client == nil
                end,
                callback = function()
                    local NetworkMgr = require("ui/network/manager")
                    local connect_callback = function()
                        if not self.filemq_server then
                            self:publish()
                        else
                            self:unpublish()
                        end
                    end
                    NetworkMgr:runWhenOnline(connect_callback)
                end
            },
            {
                text_func = function()
                    return not self.filemq_client
                        and _("Subscribe to book share")
                        or _("Unsubscribe from book share")
                end,
                enabled_func = function()
                    return self.filemq_server == nil
                end,
                callback = function()
                    local NetworkMgr = require("ui/network/manager")
                    local connect_callback = function()
                        if not self.filemq_client then
                            self:subscribe()
                        else
                            self:unsubscribe()
                        end
                    end
                    NetworkMgr:runWhenOnline(connect_callback)
                end
            }
        }
    }
end

function ZSync:initServerZyreMQ()
    local ZyreMessageQueue = require("ui/message/zyremessagequeue")
    if self.zyre_messagequeue == nil then
        self.server_zyre = ZyreMessageQueue:new{
            header = {["FILEMQ-SERVER"] = tostring(self.fmq_port)},
        }
        self.server_zyre:start()
        self.zyre_messagequeue = UIManager:insertZMQ(self.server_zyre)
    end
end

function ZSync:initClientZyreMQ()
    local ZyreMessageQueue = require("ui/message/zyremessagequeue")
    if self.zyre_messagequeue == nil then
        self.client_zyre = ZyreMessageQueue:new{}
        self.client_zyre:start()
        self.zyre_messagequeue = UIManager:insertZMQ(self.client_zyre)
    end
end

function ZSync:initServerFileMQ(outboxes)
    local FileMessageQueue = require("ui/message/filemessagequeue")
    local filemq = ffi.load("libs/libfmq.so.1")
    if self.file_messagequeue == nil then
        self.filemq_server = filemq.fmq_server_new()
        self.file_messagequeue = UIManager:insertZMQ(FileMessageQueue:new{
            server = self.filemq_server
        })
        self.fmq_port = filemq.fmq_server_bind(self.filemq_server, "tcp://*:*")
        filemq.fmq_server_configure(self.filemq_server, self.server_config)
        filemq.fmq_server_set_anonymous(self.filemq_server, true)
    end
    UIManager:scheduleIn(1, function()
        for _, outbox in ipairs(outboxes) do
            DEBUG("publish", outbox.path, outbox.alias)
            filemq.fmq_server_publish(self.filemq_server, outbox.path, outbox.alias)
        end
    end)
end

function ZSync:initClientFileMQ(inbox)
    local FileMessageQueue = require("ui/message/filemessagequeue")
    local filemq = ffi.load("libs/libfmq.so.1")
    if self.file_messagequeue == nil then
        self.filemq_client = filemq.fmq_client_new()
        self.file_messagequeue = UIManager:insertZMQ(FileMessageQueue:new{
            client = self.filemq_client
        })
        filemq.fmq_client_configure(self.filemq_client, self.client_config)
    end
    UIManager:scheduleIn(1, function()
        filemq.fmq_client_set_inbox(self.filemq_client, inbox)
    end)
end

local function clearDirectory(dir, rmdir)
    for f in lfs.dir(dir) do
        local path = dir.."/"..f
        local mode = lfs.attributes(path, "mode")
        if mode == "file" then
            C.remove(path)
        elseif mode == "directory" and f ~= "." and f ~= ".." then
            clearDirectory(path, true)
        end
    end
    if rmdir then
        C.rmdir(dir)
    end
end

local function mklink(path, filename)
    local basename = filename:match(".*/(.*)") or filename
    local linkname = path .. "/" .. basename .. ".ln"
    local linkfile = io.open(linkname, "w")
    if linkfile then
        linkfile:write(filename .. "\n")
        linkfile:close()
    end
end

-- add directory directly into outboxes
function ZSync:outboxesAddDirectory(outboxes, dir)
    if lfs.attributes(dir, "mode") == "directory" then
        local basename = dir:match(".*/(.*)") or dir
        table.insert(outboxes, {
            path = dir,
            alias = "/"..basename,
        })
    end
end

-- link file in root outbox
function ZSync:outboxAddFileLink(filename)
    local mode = lfs.attributes(filename, "mode")
    if mode == "file" then
        mklink(self.outbox, filename)
    end
end

-- copy directory content into root outbox(no recursively)
function ZSync:outboxCopyDirectory(dir)
    local basename = dir:match(".*/(.*)") or dir
    local newdir = self.outbox.."/"..basename
    lfs.mkdir(newdir)
    if pcall(lfs.dir, dir) then
        for f in lfs.dir(dir) do
            local filename = dir.."/"..f
            if lfs.attributes(filename, "mode") == "file" then
                local newfile = newdir.."/"..f
                ltn12.pump.all(
                    ltn12.source.file(assert(io.open(filename, "rb"))),
                    ltn12.sink.file(assert(io.open(newfile, "wb")))
                )
            end
        end
    end
end

function ZSync:publish()
    DEBUG("publish document", self.view.document.file)
    lfs.mkdir(self.outbox)
    clearDirectory(self.outbox)
    local file = self.view.document.file
    local sidecar = file:match("(.*)%.")..".sdr"
    self:outboxAddFileLink(file)
    self:outboxCopyDirectory(sidecar)
    local outboxes = {}
    table.insert(outboxes, {
        path = self.outbox,
        alias = "/",
    })
    -- init filemq first to get filemq port
    self:initServerFileMQ(outboxes)
    self:initServerZyreMQ()
end

function ZSync:unpublish()
    DEBUG("ZSync unpublish")
    clearDirectory(self.outbox)
    self:stopZyreMQ()
    self:stopFileMQ()
end

function ZSync:onChooseInbox(inbox)
    DEBUG("choose inbox", inbox)
    self.inbox = inbox
    -- init zyre first for filemq endpoint
    self:initClientZyreMQ()
    self:initClientFileMQ(inbox)
    return true
end

function ZSync:subscribe()
    DEBUG("subscribe documents")
    self.received = {}
    local zsync = self
    require("ui/downloadmgr"):new{
        show_hidden = G_reader_settings:readSetting("show_hidden"),
        onConfirm = function(inbox)
            G_reader_settings:saveSetting("inbox_dir", inbox)
            zsync:onChooseInbox(inbox)
        end,
    }:chooseDir()
end

function ZSync:unsubscribe()
    DEBUG("ZSync unsubscribe")
    self.received = {}
    self:stopFileMQ()
    self:stopZyreMQ()
end

function ZSync:onZyreEnter(id, name, header, endpoint)
    local filemq = ffi.load("libs/libfmq.so.1")
    if header and endpoint and header["FILEMQ-SERVER"] then
        self.server_zyre_endpoint = endpoint
        local port = header["FILEMQ-SERVER"]
        local host = endpoint:match("(.*:)") or "*:"
        local fmq_server_endpoint = host..port
        DEBUG("connect filemq server at", fmq_server_endpoint)
        -- wait for filemq server setup befor connecting
        UIManager:scheduleIn(2, function()
            filemq.fmq_client_set_resync(self.filemq_client, true)
            filemq.fmq_client_subscribe(self.filemq_client, "/")
            filemq.fmq_client_connect(self.filemq_client, fmq_server_endpoint)
        end)
    end
    return true
end

function ZSync:onFileDeliver(filename, fullname)
    -- sometimes several FileDelever msgs are sent from filemq
    if self.received[filename] then return end
    UIManager:show(InfoMessage:new{
        text = _("Received file:") .. "\n" .. filename,
        timeout = 1,
    })
    self.received[filename] = true
end

--[[
-- We assume that ZSync is running in either server mode or client mode
-- but never both. The zyre_messagequeue may be a server_zyre or client_zyre.
-- And the file_messagequeue may be a filemq_server or filemq_client.
--]]
function ZSync:stopZyreMQ()
    if self.zyre_messagequeue then
        self.zyre_messagequeue:stop()
        UIManager:removeZMQ(self.zyre_messagequeue)
        self.zyre_messagequeue = nil
        self.server_zyre = nil
        self.client_zyre = nil
    end
end

function ZSync:stopFileMQ()
    if self.file_messagequeue then
        self.file_messagequeue:stop()
        UIManager:removeZMQ(self.file_messagequeue)
        self.file_messagequeue = nil
        self.filemq_server = nil
        self.filemq_client = nil
    end
end

function ZSync:onCloseReader()
    self:stopZyreMQ()
    self:stopFileMQ()
end

return ZSync

