local BD = require("ui/bidi")
local InputContainer = require("ui/widget/container/inputcontainer")
local LoginDialog = require("ui/widget/logindialog")
local InfoMessage = require("ui/widget/infomessage")
local NetworkMgr = require("ui/network/manager")
local DataStorage = require("datastorage")
local DocSettings = require("docsettings")
local UIManager = require("ui/uimanager")
local Screen = require("device").screen
local logger = require("logger")
local util = require("ffi/util")
local Device = require("device")
local JoplinClient = require("JoplinClient")
local T = util.template
local _ = require("gettext")
local N_ = _.ngettext
local slt2 = require('slt2')
local MyClipping = require("clip")
local json = require("json")

local EvernoteExporter = InputContainer:new{
    name = "evernote",
    login_title = _("Login to Evernote"),
    notebook_name = _("KOReader Notes"),
    evernote_domain = nil,
    notemarks = _("Note: "),
    clipping_dir = DataStorage:getDataDir() .. "/clipboard",

    evernote_token = nil,
    notebook_guid = nil,
}

function EvernoteExporter:init()
    self.text_clipping_file = self.clipping_dir .. "/KOReaderClipping.txt"
    self.json_clipping_file = self.clipping_dir .. "/KOReaderClipping.json"
    local settings = G_reader_settings:readSetting("evernote") or {}
    self.evernote_domain = settings.domain
    self.evernote_username = settings.username or ""
    self.evernote_token = settings.token
    self.notebook_guid = settings.notebook
    self.joplin_IP = settings.joplin_IP or "localhost"
    self.joplin_port = settings.joplin_port or 41185
    self.joplin_token = settings.joplin_token -- or your token
    self.joplin_notebook_guid = settings.joplin_notebook_guid or nil
    self.html_export = settings.html_export or false
    self.joplin_export = settings.joplin_export or false
    self.txt_export = settings.txt_export or false
    self.json_export = settings.json_export or false
    --- @todo Is this if block necessary? Nowhere in the code they are assigned both true.
    -- Do they check against external modifications to settings file?

    if self.html_export then
        self.txt_export = false
        self.joplin_export = false
        self.json_export = false
    elseif self.txt_export then
        self.joplin_export = false
        self.json_export = false
    elseif self.json_export then
        self.joplin_export = false
    end

    self.parser = MyClipping:new{
        my_clippings = "/mnt/us/documents/My Clippings.txt",
        history_dir = "./history",
    }
    self.template = slt2.loadfile(self.path.."/note.tpl")
    self:migrateClippings()
    self.config = DocSettings:open(util.joinPath(self.clipping_dir, "evernote.sdr"))

    self.ui.menu:registerToMainMenu(self)
end

function EvernoteExporter:isDocless()
    return self.ui == nil or self.ui.document == nil or self.view == nil
end

function EvernoteExporter:readyToExport()
    return self.evernote_token ~= nil or
            self.html_export ~= false or
            self.txt_export ~= false or
            self.json_export ~= false or
            self.joplin_export ~= false
end

function EvernoteExporter:migrateClippings()
    if jit.os == "OSX" then return end
    local old_dir = util.joinPath(util.realpath(util.joinPath(self.path, "..")),
        "evernote.sdr")
    if lfs.attributes(old_dir, "mode") == "directory" then
        local mv_bin = Device:isAndroid() and "/system/bin/mv" or "/bin/mv"
        return util.execute(mv_bin, old_dir, self.clipping_dir) == 0
    end
end

function EvernoteExporter:addToMainMenu(menu_items)
    menu_items.evernote = {
        text = _("Evernote"),
        sub_item_table = {
            {
                text_func = function()
                    local domain
                    if self.evernote_domain == "sandbox" then
                        domain = "Sandbox"
                    elseif self.evernote_domain == "yinxiang" then
                        domain = "Yinxiang"
                    else
                        domain = "Evernote"
                    end
                    return self.evernote_token and (_("Logout") .. " " .. domain)
                            or _("Login")
                end,
                callback_func = function()
                    return self.evernote_token and function() self:logout() end
                            or nil
                end,
                sub_item_table_func = function()
                    return not self.evernote_token and {
                        {
                            text = "Evernote",
                            callback = function()
                                self.evernote_domain = nil
                                self:login()
                            end
                        },
                        {
                            text = "印象笔记",
                            callback = function()
                                self.evernote_domain = "yinxiang"
                                self:login()
                            end
                        }
                    } or nil
                end,
            },
            {
                text = _("Joplin") ,
                checked_func = function() return self.joplin_export end,
                separator = true,
                sub_item_table ={
                    {
                        text = _("Set Joplin IP and Port"),
                        keep_menu_open = true,
                        callback = function()
                            local MultiInputDialog = require("ui/widget/multiinputdialog")
                            local url_dialog
                            url_dialog = MultiInputDialog:new{
                                title = _("Set Joplin IP and port number"),
                                fields = {
                                    {
                                        text = self.joplin_IP,
                                        input_type = "string"
                                    },
                                    {
                                        text = self.joplin_port,
                                        input_type = "number"
                                    }
                                },
                                buttons = {
                                    {
                                        {
                                            text = _("Cancel"),
                                            callback = function()
                                                UIManager:close(url_dialog)
                                            end
                                        },
                                        {
                                            text = _("OK"),
                                            callback = function()
                                                local fields = url_dialog:getFields()
                                                local ip = fields[1]
                                                local port = tonumber(fields[2])
                                                if ip ~= "" then
                                                    if port and port < 65355 then
                                                        self.joplin_IP = ip
                                                        self.joplin_port = port
                                                    end
                                                    self:saveSettings()
                                                end
                                                UIManager:close(url_dialog)
                                            end
                                        }
                                    }
                                }
                            }
                            UIManager:show(url_dialog)
                            url_dialog:onShowKeyboard()
                        end
                    },
                    {
                        text = _("Set authorization token"),
                        keep_menu_open = true,
                        callback = function()
                            local MultiInputDialog = require("ui/widget/multiinputdialog")
                            local auth_dialog
                            auth_dialog = MultiInputDialog:new{
                                title = _("Set authorization token for Joplin"),
                                fields = {
                                    {
                                        text = self.joplin_token,
                                        input_type = "string"
                                    }
                                },
                                buttons = {
                                    {
                                        {
                                            text = _("Cancel"),
                                            callback = function()
                                                UIManager:close(auth_dialog)
                                            end
                                        },
                                        {
                                            text = _("Set token"),
                                            callback = function()
                                                local auth_field = auth_dialog:getFields()
                                                self.joplin_token = auth_field[1]
                                                self:saveSettings()
                                                UIManager:close(auth_dialog)
                                            end
                                        }
                                    }
                                }
                            }
                            UIManager:show(auth_dialog)
                            auth_dialog:onShowKeyboard()
                        end
                    },
                    {
                        text = _("Export to Joplin"),
                        checked_func = function() return self.joplin_export end,
                        callback = function()
                            self.joplin_export = not self.joplin_export
                            if self.joplin_export then
                                self.html_export = false
                                self.txt_export = false
                                self.json_export = false
                            end
                            self:saveSettings()
                        end
                    },
                    {
                        text = _("Help"),
                        keep_menu_open = true,
                        callback = function()
                            UIManager:show(InfoMessage:new{
                                text = T(_([[You can enter your auth token on your computer by saving an empty token. Then quit KOReader, edit the evernote.joplin_token field in %1/settings.reader.lua after creating a backup, and restart KOReader once you're done.

To export to Joplin, you must forward the IP and port used by this plugin to the localhost:port on which Joplin is listening. This can be done with socat or a similar program. For example:

For Windows: netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=41185 connectaddress=localhost connectport=41184

For Linux: $socat tcp-listen:41185,reuseaddr,fork tcp:localhost:41184

For more information, please visit https://github.com/koreader/koreader/wiki/Evernote-export.]])
                            , BD.dirpath(DataStorage:getDataDir()))
                            })
                        end
                    }
                }
            },
            {
                text = _("Export all notes in this book"),
                enabled_func = function()
                    return not self:isDocless() and self:readyToExport() and not self.txt_export
                end,
                callback = function()
                    UIManager:scheduleIn(0.5, function()
                        self:exportCurrentNotes(self.view)
                    end)

                    UIManager:show(InfoMessage:new{
                        text = _("Exporting may take several seconds…"),
                        timeout = 1,
                    })
                end
            },
            {
                text = _("Export all notes in your library"),
                enabled_func = function()
                    return self:readyToExport()
                end,
                callback = function()
                    UIManager:scheduleIn(0.5, function()
                        self:exportAllNotes()
                    end)

                    UIManager:show(InfoMessage:new{
                        text = _("Exporting may take several minutes…"),
                        timeout = 1,
                    })
                end,
                separator = true,
            },
            {
                text = _("Export to local JSON files"),
                checked_func = function() return self.json_export end,
                callback = function()
                    self.json_export = not self.json_export
                    if self.json_export then
                        self.txt_export = false
                        self.html_export = false
                        self.joplin_export = false
                    end
                    self:saveSettings()
                end
            },
            {
                text = _("Export to local HTML files"),
                checked_func = function() return self.html_export end,
                callback = function()
                    self.html_export = not self.html_export
                    if self.html_export then
                        self.txt_export = false
                        self.json_export = false
                        self.joplin_export = false
                    end
                    self:saveSettings()
                end
            },
            {
                text = _("Export to local clipping text file"),
                checked_func = function() return self.txt_export end,
                callback = function()
                    self.txt_export = not self.txt_export
                    if self.txt_export then
                        self.html_export = false
                        self.json_export = false
                        self.joplin_export = false
                    end
                    self:saveSettings()
                end,
                separator = true,
            },
            {
                text = _("Purge history records"),
                callback = function()
                    self.config:purge()
                    UIManager:show(InfoMessage:new{
                        text = _("History records have been purged.\nAll notes will be exported again next time.\n"),
                        timeout = 2,
                    })
                end
            }
        }
    }
end

function EvernoteExporter:login()
    if NetworkMgr:willRerunWhenOnline(function() self:login() end) then
        return
    end

    self.login_dialog = LoginDialog:new{
        title = self.login_title,
        username = self.evernote_username or "",
        buttons = {
            {
                {
                    text = _("Cancel"),
                    enabled = true,
                    callback = function()
                        self:closeDialog()
                    end,
                },
                {
                    text = _("Login"),
                    enabled = true,
                    callback = function()
                        local username, password = self:getCredential()
                        self:closeDialog()
                        UIManager:scheduleIn(0.5, function()
                            self:doLogin(username, password)
                        end)

                        UIManager:show(InfoMessage:new{
                            text = _("Logging in. Please wait…"),
                            timeout = 1,
                        })
                    end,
                },
            },
        },
        width = math.floor(Screen:getWidth() * 0.8),
        height = math.floor(Screen:getHeight() * 0.4),
    }

    UIManager:show(self.login_dialog)
    self.login_dialog:onShowKeyboard()
end

function EvernoteExporter:closeDialog()
    self.login_dialog:onClose()
    UIManager:close(self.login_dialog)
end

function EvernoteExporter:getCredential()
    return self.login_dialog:getCredential()
end

function EvernoteExporter:doLogin(username, password)
    local EvernoteOAuth = require("EvernoteOAuth")
    local EvernoteClient = require("EvernoteClient")

    local oauth = EvernoteOAuth:new{
        domain = self.evernote_domain,
        username = username,
        password = password,
        logger = logger.dbg,
    }
    self.evernote_username = username
    local ok, token = pcall(oauth.getToken, oauth)
    -- prompt users to turn on Wi-Fi if network is unreachable
    if not ok and token then
        UIManager:show(InfoMessage:new{
            text = _("An error occurred while logging in:") .. "\n" .. token,
        })
        return
    end

    local client = EvernoteClient:new{
        domain = self.evernote_domain,
        authToken = token,
    }
    local guid
    ok, guid = pcall(self.getExportNotebook, self, client)
    if not ok and guid and guid:find("Transport not open") then
        --- @note: No recursive callback because it feels fishy here...
        NetworkMgr:beforeWifiAction()
        return
    elseif not ok and guid then
        UIManager:show(InfoMessage:new{
            text = _("An error occurred while logging in:") .. "\n" .. guid,
        })
    elseif ok and guid then
        self.evernote_token = token
        self.notebook_guid = guid
        UIManager:show(InfoMessage:new{
            text = _("Logged in to Evernote."),
        })
    end

    self:saveSettings()
end

function EvernoteExporter:logout()
    self.evernote_token = nil
    self.notebook_guid = nil
    self.evernote_domain = nil
    self:saveSettings()
end

function EvernoteExporter:saveSettings()
    local settings = {
        domain = self.evernote_domain,
        username = self.evernote_username,
        token = self.evernote_token,
        notebook = self.notebook_guid,
        html_export = self.html_export,
        txt_export = self.txt_export,
        json_export = self.json_export,
        joplin_IP = self.joplin_IP,
        joplin_port = self.joplin_port,
        joplin_token = self.joplin_token,
        joplin_notebook_guid = self.joplin_notebook_guid,
        joplin_export = self.joplin_export
    }
    G_reader_settings:saveSetting("evernote", settings)
end

function EvernoteExporter:getExportNotebook(client)
    local name = self.notebook_name
    return client:findNotebookByTitle(name) or client:createNotebook(name).guid
end

function EvernoteExporter:exportCurrentNotes(view)
    local clippings = self.parser:parseCurrentDoc(view)
    self:exportClippings(clippings)
end

function EvernoteExporter:updateHistoryClippings(clippings, new_clippings)
    -- update clippings from history clippings
    for title, booknotes in pairs(new_clippings) do
        for chapter_index, chapternotes in ipairs(booknotes) do
            for note_index, note in ipairs(chapternotes) do
                if clippings[title] == nil or clippings[title][chapter_index] == nil
                    or clippings[title][chapter_index][note_index] == nil
                    or clippings[title][chapter_index][note_index].page ~= note.page
                    or clippings[title][chapter_index][note_index].time ~= note.time
                    or clippings[title][chapter_index][note_index].text ~= note.text
                    or clippings[title][chapter_index][note_index].note ~= note.note then
                    logger.dbg("found new notes in history", booknotes.title)
                    clippings[title] = booknotes
                end
            end
        end
    end
    return clippings
end

function EvernoteExporter:updateMyClippings(clippings, new_clippings)
    -- only new titles or new notes in My clippings are updated to clippings
    -- since appending is the only way to modify notes in My Clippings
    for title, booknotes in pairs(new_clippings) do
        if clippings[title] == nil or #clippings[title] < #booknotes then
            logger.dbg("found new notes in MyClipping", booknotes.title)
            clippings[title] = booknotes
        end
    end
    return clippings
end

--[[--
Parses highlights and calls exporter functions.

Entry point for exporting highlights. User interface calls this function.
Parses current document and documents from history, passes them to exportClippings().
Highlight: Highlighted text or image in document, stored in "highlights" table in
documents sidecar file. Parser uses this table. If highlight._._.text field is empty parser uses
highlight._._.pboxes field to get an image instead.
Bookmarks: Data in bookmark explorer. Stored in "bookmarks" table of documents sidecar file. Every
field in bookmarks._ has "text" and "notes" fields When user edits a highlight or "renames" bookmark,
text field is created or updated. Parser looks to bookmarks._.text field for edited notes. bookmarks._.notes isn't used for exporting operations.
https://github.com/koreader/koreader/blob/605f6026bbf37856ee54741b8a0697337ca50039/plugins/evernote.koplugin/clip.lua#L229
Clippings: Parsed form of highlights, stored in clipboard/evernote.sdr/metadata.sdr.lua
for all documents. Used only for exporting bookmarks. Internal highlight or bookmark functions
does not use this table.
Booknotes: Every table in clippings table. clippings = {"title" = booknotes}
--]]
function EvernoteExporter:exportAllNotes()
    -- Flush highlights of current document.
    if not self:isDocless() then
        self.ui:saveSettings()
    end
    local clippings = self.config:readSetting("clippings") or {}
    clippings = self:updateHistoryClippings(clippings, self.parser:parseHistory())
    clippings = self:updateMyClippings(clippings, self.parser:parseMyClippings())
    -- remove blank entries
    for title, booknotes in pairs(clippings) do
        -- chapter number is zero
        if #booknotes == 0 then
            clippings[title] = nil
        end
    end
    --logger.dbg("clippings", clippings)
    self:exportClippings(clippings)
    self.config:saveSetting("clippings", clippings)
    self.config:flush()
end

function EvernoteExporter:exportClippings(clippings)
    local client = nil
    local exported_stamp
    local joplin_client
    if not (self.html_export or self.txt_export or self.joplin_export or self.json_export) then
        client = require("EvernoteClient"):new{
            domain = self.evernote_domain,
            authToken = self.evernote_token,
        }
        exported_stamp = self.notebook_guid
    elseif self.html_export then
        exported_stamp= "html"
    elseif self.json_export then
        exported_stamp= "json"
    elseif self.txt_export then
        os.remove(self.text_clipping_file)
        exported_stamp = "txt"
    elseif self.joplin_export then
        exported_stamp = "joplin"
        joplin_client = JoplinClient:new{
            server_ip = self.joplin_IP,
            server_port = self.joplin_port,
            auth_token = self.joplin_token
        }
        ---@todo Check if user deleted our notebook, in that case note
        -- will end up in random folder in Joplin.
        if not self.joplin_notebook_guid then
            self.joplin_notebook_guid = joplin_client:createNotebook(self.notebook_name)
            self:saveSettings()
        end
    else
        assert("an exported_stamp is expected for a new export type")
    end

    local export_count, error_count = 0, 0
    local export_title, error_title
    for title, booknotes in pairs(clippings) do
        if type(booknotes.exported) ~= "table" then
            booknotes.exported = {}
        end
        -- check if booknotes are exported in this notebook
        -- so that booknotes will still be exported after switching user account
        -- Don't respect exported_stamp on txt export since it isn't possible to delete(update) prior clippings.
        if booknotes.exported[exported_stamp] ~= true or self.txt_export or self.json_export then
            local ok, err
            if self.html_export then
                ok, err = pcall(self.exportBooknotesToHTML, self, title, booknotes)
            elseif self.txt_export then
                ok, err = pcall(self.exportBooknotesToTXT, self, title, booknotes)
            elseif self.json_export then
                ok, err = pcall(self.exportBooknotesToJSON, self, title, booknotes)
            elseif self.joplin_export then
                ok, err = pcall(self.exportBooknotesToJoplin, self, joplin_client, title, booknotes)
            else
                ok, err = pcall(self.exportBooknotesToEvernote, self, client, title, booknotes)
            end
            -- Error reporting
            if not ok and err and err:find("Transport not open") then
                --- @note: No recursive callback because it feels fishy here...
                NetworkMgr:beforeWifiAction()
                return
            elseif not ok and err then
                logger.dbg("Error while exporting book", title, err)
                error_count = error_count + 1
                error_title = title
            elseif ok then
                logger.dbg("Exported notes in book:", title)
                export_count = export_count + 1
                export_title = title
                booknotes.exported[exported_stamp] = true
            end
        end
    end

    local msg = "Nothing was exported."
    local all_count = export_count + error_count
    if export_count > 0 and error_count == 0 then
        msg = T(
            N_("Exported notes from the book:\n%1",
               "Exported notes from the book:\n%1\nand %2 others.",
               all_count-1),
            export_title,
            all_count-1
        )
    elseif error_count > 0 then
        msg = T(
            N_("An error occurred while trying to export notes from the book:\n%1",
               "Multiple errors occurred while trying to export notes from the book:\n%1\nand %2 others.",
               error_count-1),
            error_title,
            error_count-1
        )
    end
    if (self.html_export or self.txt_export) and export_count > 0 then
        msg = msg .. T(_("\nNotes can be found in %1/."), BD.dirpath(util.realpath(self.clipping_dir)))
    end
    UIManager:show(InfoMessage:new{ text = msg })
end

function EvernoteExporter:exportBooknotesToEvernote(client, title, booknotes)
    local content = slt2.render(self.template, {
        booknotes = booknotes,
        notemarks = self.notemarks,
    })
    --logger.dbg("content", content)
    local note_guid = client:findNoteByTitle(title, self.notebook_guid)
    local resources = {}
    for _, chapter in ipairs(booknotes) do
        for _, clipping in ipairs(chapter) do
            if clipping.image then
                table.insert(resources, {
                    image = clipping.image
                })
                -- nullify clipping image after passing it to evernote client
                clipping.image = nil
            end
        end
    end
    if not note_guid then
        client:createNote(title, content, resources, {}, self.notebook_guid)
    else
        client:updateNote(note_guid, title, content, resources, {}, self.notebook_guid)
    end
end

function EvernoteExporter:exportBooknotesToHTML(title, booknotes)
    local content = slt2.render(self.template, {
        booknotes = booknotes,
        notemarks = self.notemarks,
    })
    --logger.dbg("content", content)
    local html = io.open(self.clipping_dir .. "/" .. title .. ".html", "w")
    if html then
        html:write(content)
        html:close()
    end
end

function EvernoteExporter:exportBooknotesToJSON(title, booknotes)
    local file = io.open(self.json_clipping_file, "a")
    if file then
        file:write(json.encode(booknotes))
        file:write("\n")
        file:close()
    end
end

function EvernoteExporter:exportBooknotesToTXT(title, booknotes)
    -- Use wide_space to avoid crengine to treat it specially.
    local wide_space = "\227\128\128"
    local file = io.open(self.text_clipping_file, "a")
    if file then
        file:write(title .. "\n" .. wide_space .. "\n")
        for _ignore1, chapter in ipairs(booknotes) do
            if chapter.title then
                file:write(wide_space .. chapter.title .. "\n" .. wide_space .. "\n")
            end
            for _ignore2, clipping in ipairs(chapter) do
                file:write(wide_space .. wide_space ..
                            T(_("-- Page: %1, added on %2\n"),
                                clipping.page, os.date("%c", clipping.time)))
                if clipping.text then
                    file:write(clipping.text)
                end
                if clipping.image then
                    file:write(_("<An image>"))
                end
                file:write("\n-=-=-=-=-=-\n")
            end
        end

        file:write("\n")
        file:close()
    end
end

function EvernoteExporter:exportBooknotesToJoplin(client, title, booknotes)
    if not client:ping() then
        error("Cannot reach Joplin server")
    end

    local note_guid = client:findNoteByTitle(title, self.joplin_notebook_guid)
    local note = ""
    for _, chapter in ipairs(booknotes) do
        if chapter.title then
            note = note .. "\n\t*" .. chapter.title .. "*\n\n * * *"
        end

        for _, clipping in ipairs(chapter) do
            note = note .. os.date("%Y-%m-%d %H:%M:%S \n", clipping.time)
            note = note .. clipping.text .. "\n * * *\n"
        end
    end

    if note_guid then
        client:updateNote(note_guid, note)
    else
        client:createNote(title, note, self.joplin_notebook_guid)
    end

end

return EvernoteExporter
