local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
local Dispatcher = require("dispatcher")
local DocumentRegistry = require("document/documentregistry")
local Font = require("ui/font")
local QRMessage = require("ui/widget/qrmessage")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local LuaSettings = require("luasettings")
local Notification = require("ui/widget/notification")
local PathChooser = require("ui/widget/pathchooser")
local Trapper = require("ui/trapper")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local ffiUtil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
local Screen = require("device").screen
local T = ffiUtil.template

local TextEditor = WidgetContainer:extend{
    name = "texteditor",
    fullname = _("Text editor"),
    settings_file = DataStorage:getSettingsDir() .. "/text_editor.lua",
    settings = nil, -- loaded only when needed
    -- how many to display in menu (10x3 pages minus our 3 default menu items):
    history_menu_size = 27,
    history_keep_size = 60, -- hom many to keep in settings
    normal_font = "x_smallinfofont",
    monospace_font = "infont",
    default_font_size = 20, -- x_smallinfofont default size
    min_file_size_warn = 200000, -- warn/ask when opening files bigger than this
}

function TextEditor:onDispatcherRegisterActions()
    Dispatcher:registerAction("edit_last_edited_file", { category = "none", event = "OpenLastEditedFile", title = _("Text editor: open last file"), general=true, separator = true})
end

function TextEditor:init()
    self:onDispatcherRegisterActions()
    self.ui.menu:registerToMainMenu(self)
    self:registerDocumentRegistryAuxProvider()
end

function TextEditor:registerDocumentRegistryAuxProvider()
    DocumentRegistry:addAuxProvider({
        provider_name = self.fullname,
        provider = self.name,
        order = 30, -- order in OpenWith dialog
        disable_file = true,
        disable_type = false,
    })
end

function TextEditor:isFileTypeSupported(file)
    return true
end

function TextEditor:openFile(file, caller_callback)
    self:checkEditFile(file, nil, nil, caller_callback)
end

function TextEditor:loadSettings()
    if self.settings then
        return
    end
    self.settings = LuaSettings:open(self.settings_file)
    -- NOTE: addToHistory assigns a new object
    self.history = self.settings:readSetting("history") or {}
    self.last_view_pos = self.settings:readSetting("last_view_pos") or {}
    self.last_path = self.settings:readSetting("last_path") or ffiUtil.realpath(DataStorage:getDataDir())
    self.font_face = self.settings:readSetting("font_face") or self.normal_font
    self.font_size = self.settings:readSetting("font_size") or self.default_font_size
    -- The font settings could be saved in G_reader_setting if we want them
    -- to be re-used by default by InputDialog (on certain conditaions,
    -- when fullscreen or condensed or add_nav_bar...)
    --
    -- Allow users to set their preferred font manually in text_editor.lua
    -- (sadly, not via TextEditor itself, as they would be overridden on close)
    if self.settings:has("normal_font") then
        self.normal_font = self.settings:readSetting("normal_font")
    end
    if self.settings:has("monospace_font") then
        self.monospace_font = self.settings:readSetting("monospace_font")
    end
    self.auto_para_direction = self.settings:nilOrTrue("auto_para_direction")
    self.force_ltr_para_direction = self.settings:isTrue("force_ltr_para_direction")
    self.qr_code_export = self.settings:nilOrTrue("qr_code_export")
    self.show_keyboard_on_start = self.settings:nilOrTrue("show_keyboard_on_start")
end

function TextEditor:onFlushSettings()
    if self.settings then
        self.settings:saveSetting("history", self.history)
        self.settings:saveSetting("last_view_pos", self.last_view_pos)
        self.settings:saveSetting("last_path", self.last_path)
        self.settings:saveSetting("font_face", self.font_face)
        self.settings:saveSetting("font_size", self.font_size)
        self.settings:saveSetting("auto_para_direction", self.auto_para_direction)
        self.settings:saveSetting("force_ltr_para_direction", self.force_ltr_para_direction)
        self.settings:saveSetting("qr_code_export", self.qr_code_export)
        self.settings:saveSetting("show_keyboard_on_start", self.show_keyboard_on_start)
        self.settings:flush()
    end
end

function TextEditor:addToMainMenu(menu_items)
    menu_items.text_editor = {
        text = self.fullname,
        sub_item_table_func = function()
            return self:getSubMenuItems()
        end,
    }
end

function TextEditor:getSubMenuItems()
    self:loadSettings()
    self.whenDoneFunc = nil -- discard reference to previous TouchMenu instance
    local sub_item_table
    sub_item_table = {
        {
            text = _("Settings"),
            sub_item_table = {
                {
                    text_func = function()
                        return T(_("Text font size: %1"), self.font_size)
                    end,
                    keep_menu_open = true,
                    callback = function(touchmenu_instance)
                        local SpinWidget = require("ui/widget/spinwidget")
                        local font_size = self.font_size
                        UIManager:show(SpinWidget:new{
                            value = font_size,
                            value_min = 8,
                            value_max = 26,
                            default_value = self.default_font_size,
                            title_text =  _("Text font size"),
                            callback = function(spin)
                                self.font_size = spin.value
                                touchmenu_instance:updateItems()
                            end,
                        })
                    end,
                },
                {
                    text = _("Use monospace font"),
                    checked_func = function()
                        return self.font_face == self.monospace_font
                    end,
                    callback = function()
                        if self.font_face == self.monospace_font then
                            self.font_face = self.normal_font
                        else
                            self.font_face = self.monospace_font
                        end
                    end,
                    separator = true,
                },
                {
                    text = _("Auto paragraph direction"),
                    help_text = _([[
Detect the direction of each paragraph in the text: align to the right paragraphs in languages such as Arabic and Hebrew…, while keeping other paragraphs aligned to the left.
If disabled, paragraphs align according to KOReader's language default direction.]]),
                    checked_func = function()
                        return self.auto_para_direction
                    end,
                    callback = function()
                        self.auto_para_direction = not self.auto_para_direction
                    end,
                },
                {
                    text = _("Force paragraph direction LTR"),
                    help_text = _([[
Force all text to be displayed Left-To-Right (LTR) and aligned to the left.
Enable this if you are mostly editing code, HTML, CSS…]]),
                    enabled_func = BD.rtlUIText, -- only useful for RTL users editing code
                    checked_func = function()
                        return BD.rtlUIText() and self.force_ltr_para_direction
                    end,
                    callback = function()
                        self.force_ltr_para_direction = not self.force_ltr_para_direction
                    end,
                    separator = true,
                },
                {
                    text = _("Show keyboard on start"),
                    checked_func = function()
                        return self.show_keyboard_on_start
                    end,
                    callback = function()
                        self.show_keyboard_on_start = not self.show_keyboard_on_start
                    end,
                },
                {
                    text = _("Enable QR code export"),
                    help_text = _([[
Export text to QR code, that can be scanned, for example, by a phone.]]),
                    checked_func = function()
                        return self.qr_code_export
                    end,
                    callback = function()
                        self.qr_code_export = not self.qr_code_export
                    end,
                    separator = true,
                },
                {
                    text = _("Clean text editor history"),
                    enabled_func = function()
                        return #self.history > 0
                    end,
                    keep_menu_open = true,
                    callback = function(touchmenu_instance)
                        UIManager:show(ConfirmBox:new{
                            text = _("Clean text editor history?"),
                            ok_text = _("Clean"),
                            ok_callback = function()
                                self.history = {}
                                self.last_view_pos = {}
                                -- remove history items from the parent menu
                                for j = #sub_item_table, 1, -1 do
                                    if sub_item_table[j]._texteditor_id then
                                        table.remove(sub_item_table)
                                    end
                                end
                                touchmenu_instance:updateItems()
                            end,
                        })
                end,
                },
            },
            separator = true,
        },
        {
            text = _("New file"),
            keep_menu_open = true,
            callback = function(touchmenu_instance)
                self:setupWhenDoneFunc(touchmenu_instance)
                self:newFile()
            end,
        },
        {
            text = _("Open file"),
            keep_menu_open = true,
            callback = function(touchmenu_instance)
                self:setupWhenDoneFunc(touchmenu_instance)
                self:chooseFile()
            end,
            separator = true,
        },
    }
    for i=1, math.min(#self.history, self.history_menu_size) do
        local file_path = self.history[i]
        local directory, filename = util.splitFilePathName(file_path) -- luacheck: no unused
        table.insert(sub_item_table, {
            text = T("\u{f016} %1", BD.filename(filename)), -- file symbol
            keep_menu_open = true,
            callback = function(touchmenu_instance)
                self:setupWhenDoneFunc(touchmenu_instance)
                self:checkEditFile(file_path, true)
            end,
            _texteditor_id = file_path, -- for removal from menu itself
            hold_callback = function(touchmenu_instance)
                -- Show full path and some info, and propose to remove from history
                local text
                local attr = lfs.attributes(file_path)
                if attr then
                    local filesize = util.getFormattedSize(attr.size)
                    local lastmod = os.date("%Y-%m-%d %H:%M", attr.modification)
                    text = T(_("File path:\n%1\n\nFile size: %2 bytes\nLast modified: %3\n\nRemove this file from text editor history?"),
                                BD.filepath(file_path), filesize, lastmod)
                else
                    text = T(_("File path:\n%1\n\nThis file does not exist anymore.\n\nRemove it from text editor history?"),
                                BD.filepath(file_path))
                end
                UIManager:show(ConfirmBox:new{
                    text = text,
                    ok_text = _("Remove"),
                    ok_callback = function()
                        self:removeFromHistory(file_path)
                        -- Also remove from menu itself
                        for j=1, #sub_item_table do
                            if sub_item_table[j]._texteditor_id == file_path then
                                table.remove(sub_item_table, j)
                                break
                            end
                        end
                        touchmenu_instance:updateItems()
                    end,
                })
            end,
        })
    end
    return sub_item_table
end

function TextEditor:setupWhenDoneFunc(touchmenu_instance)
    -- This will keep a reference to the TouchMenu instance, that may not
    -- get released if file opening is aborted while in the file selection
    -- widgets and dialogs (quite complicated to call a resetWhenDoneFunc()
    -- in every abort case). But :getSubMenuItems() will release it when
    -- the TextEditor menu is opened again.
    self.whenDoneFunc = function()
        touchmenu_instance.item_table = self:getSubMenuItems()
        touchmenu_instance.page = 1
        touchmenu_instance:updateItems()
    end
end

function TextEditor:execWhenDoneFunc()
    if self.whenDoneFunc then
        self.whenDoneFunc()
        self.whenDoneFunc = nil
    end
end

function TextEditor:removeFromHistory(file_path)
    for i = #self.history, 1, -1 do
        if self.history[i] == file_path then
            table.remove(self.history, i)
        end
    end
    self.last_view_pos[file_path] = nil
end

function TextEditor:addToHistory(file_path)
    local new_history = {}
    table.insert(new_history, file_path)
    -- Trim history and cleanup duplicates
    local seen = {}
    seen[file_path] = true
    while #self.history > 0 and #new_history < self.history_keep_size do
        local item = table.remove(self.history, 1)
        if not seen[item] then
            table.insert(new_history, item)
            seen[item] = true
        end
    end
    self.history = new_history
end

function TextEditor:newFile(new_path, caller_callback)
    self:loadSettings()
    new_path = new_path or (self.last_path == "/" and "/" or self.last_path .. "/")
    local file_input
    file_input = InputDialog:new{
        title =  _("Enter filename"),
        input = new_path,
        buttons = {
            {
                {
                    text = _("Choose folder"),
                    callback = function()
                        UIManager:close(file_input) -- need to close keyboard
                        local path_chooser = PathChooser:new{
                            select_file = false,
                            path = new_path:match("(.*)/"),
                            onConfirm = function(dir_path)
                                self:newFile(dir_path .. "/", caller_callback)
                            end,
                        }
                        UIManager:show(path_chooser)
                    end,
                },
            },
            {
                {
                    text = _("Cancel"),
                    id = "close",
                    callback = function()
                        UIManager:close(file_input)
                    end,
                },
                {
                    text = _("Edit"),
                    callback = function()
                        local file_path = file_input:getInputText()
                        UIManager:close(file_input)
                        -- Remember last_path
                        self.last_path = file_path:match("(.*)/")
                        if self.last_path == "" then self.last_path = "/" end
                        self:checkEditFile(file_path, false, true, caller_callback)
                    end,
                },
            },
        },
    }
    UIManager:show(file_input)
    file_input:onShowKeyboard()
end

function TextEditor:chooseFile()
    self:loadSettings()
    local path_chooser = PathChooser:new{
        select_directory = false,
        path = self.last_path,
        onConfirm = function(file_path)
            -- Remember last_path only when we select a file from it
            self.last_path = file_path:match("(.*)/")
            if self.last_path == "" then self.last_path = "/" end
            self:checkEditFile(file_path)
        end
    }
    UIManager:show(path_chooser)
end

function TextEditor:checkEditFile(file_path, from_history, possibly_new_file, caller_callback)
    self:loadSettings()
    local attr = lfs.attributes(file_path)
    if not possibly_new_file and not attr then
        UIManager:show(ConfirmBox:new{
            text = T(_("This file does not exist:\n\n%1\n\nDo you want to create it and start editing it?"), BD.filepath(file_path)),
            ok_text = _("Create"),
            ok_callback = function()
                -- go again thru there with possibly_new_file=true
                self:checkEditFile(file_path, from_history, true, caller_callback)
            end,
        })
        return
    end
    if attr then
        -- File exists: get its real path with symlink and ../ resolved
        file_path = ffiUtil.realpath(file_path)
        attr = lfs.attributes(file_path)
    end
    if attr then -- File exists
        if attr.mode ~= "file" then
            UIManager:show(InfoMessage:new{
                text = T(_("This file is not a regular file:\n\n%1"), BD.filepath(file_path))
            })
            return
        end
        -- Check if file is writable ("r+b" checks that, and does not
        -- update the last mod timestamp, unlike "wb")
        -- No need to warn if readonly, the user will know it when we open
        -- without keyboard and the Save button says "Read only".
        local readonly = true
        local file = io.open(file_path, "r+b")
        if file then
            file:close()
            readonly = false
        end
        -- Don't check size if coming from history: user had already confirmed it
        if not from_history and attr.size > self.min_file_size_warn then
            UIManager:show(ConfirmBox:new{
                text = T(_("This file is %2:\n\n%1\n\nAre you sure you want to open it?\n\nOpening big files may take some time."),
                    BD.filepath(file_path), util.getFriendlySize(attr.size)),
                ok_text = _("Open"),
                ok_callback = function()
                    self:editFile(file_path, readonly, caller_callback)
                end,
            })
        else
            self:editFile(file_path, readonly, caller_callback)
        end
    else -- File does not exist
        -- Try to create it just to check if writing to it later is possible
        local file, err = io.open(file_path, "wb")
        if file then
            -- Clean it, we'll create it again on Save, and allow closing
            -- without saving in case the user has changed his mind.
            file:close()
            os.remove(file_path)
            self:editFile(file_path, nil, caller_callback)
        else
            UIManager:show(InfoMessage:new{
                text = T(_("This file can not be created:\n\n%1\n\nReason: %2"), BD.filepath(file_path), err)
            })
            return
        end
    end
end

function TextEditor:saveFileContent(file_path, content, caller_callback)
    local ok, err = util.writeToFile(content, file_path)
    if ok then
        if self.ui.file_chooser then
            self.ui.file_chooser:refreshPath()
        end
        self.caller_callback = caller_callback -- will be called in self.input.close_callback
        logger.info("TextEditor: saved file", file_path)
        return true
    end
    logger.info("TextEditor: failed saving file", file_path, ":", err)
    return false, err
end

function TextEditor:deleteFile(file_path)
    local ok, err = os.remove(file_path)
    if ok then
        logger.info("TextEditor: deleted file", file_path)
        return true
    end
    logger.info("TextEditor: failed deleting file", file_path, ":", err)
    return false, err
end

function TextEditor:editFile(file_path, readonly, caller_callback)
    self:addToHistory(file_path)
    local directory, filename = util.splitFilePathName(file_path) -- luacheck: no unused
    local filename_without_suffix, filetype = util.splitFileNameSuffix(filename) -- luacheck: no unused
    local is_lua = filetype:lower() == "lua"
    local para_direction_rtl = nil -- use UI language direction
    if self.force_ltr_para_direction then
        para_direction_rtl = false -- force LTR
    end
    local buttons_first_row = {}  -- First button on first row, that will be filled with Reset|Save|Close
    if is_lua then
        table.insert(buttons_first_row, {
            text = _("Lua check"),
            callback = function()
                local parse_error = util.checkLuaSyntax(self.input:getInputText())
                if parse_error then
                    UIManager:show(InfoMessage:new{
                        text = T(_("Lua syntax check failed:\n\n%1"), parse_error)
                    })
                else
                    UIManager:show(Notification:new{
                        text = T(_("Lua syntax OK")),
                    })
                end
            end,
        })
    end
    if self.qr_code_export then
        table.insert(buttons_first_row, {
            text = _("QR"),
            callback = function()
                UIManager:show(QRMessage:new{
                    text = self.input:getInputText(),
                    height = Screen:getHeight(),
                    width = Screen:getWidth()
                })
            end,
        })
    end
    self.input = InputDialog:new{
        title =  filename,
        input = util.readFromFile(file_path, "rb"),
        input_face = Font:getFace(self.font_face, self.font_size),
        para_direction_rtl = para_direction_rtl,
        auto_para_direction = self.auto_para_direction,
        fullscreen = true,
        condensed = true,
        allow_newline = true,
        cursor_at_end = false,
        readonly = readonly,
        add_nav_bar = true,
        title_bar_left_icon = "appbar.menu",
        title_bar_left_icon_tap_callback = function() self:showMenu() end,
        rotation_enabled = true,
        keyboard_visible = self.show_keyboard_on_start, -- InputDialog will enforce false if readonly
        scroll_by_pan = true,
        buttons = {buttons_first_row},
        -- Store/retrieve view and cursor position callback
        view_pos_callback = function(top_line_num, charpos)
            -- This same callback is called with no arguments on init to retrieve the stored initial position,
            -- and with arguments to store the final position on close.
            if top_line_num and charpos then
                self.last_view_pos[file_path] = {top_line_num, charpos}
            else
                local prev_pos = self.last_view_pos[file_path]
                if type(prev_pos) == "table" and prev_pos[1] and prev_pos[2] then
                    return prev_pos[1], prev_pos[2]
                end
                return nil, nil -- no previous position known
            end
        end,
        -- File restoring callback
        reset_callback = function(content) -- Will add a Reset button
            return util.readFromFile(file_path, "rb") or "", _("Text reset to last saved content")
        end,
        -- Close callback
        close_callback = function()
            if self.input.rotation_mode_backup and self.input.rotation_mode_backup ~= Screen:getRotationMode() then
                Screen:setRotationMode(self.input.rotation_mode_backup)
            end
            if self.caller_callback then
                self.caller_callback(file_path)
            end
            self:execWhenDoneFunc()
        end,
        -- File saving callback
        save_callback = function(content, closing) -- Will add Save/Close buttons
            if readonly then
                -- We shouldn't be called if read-only, but just in case
                return false, _("File is read only")
            end
            if content and #content > 0 then
                if not is_lua then
                    local ok, err = self:saveFileContent(file_path, content, caller_callback)
                    if ok then
                        return true, _("File saved")
                    else
                        return false, T(_("Failed saving file: %1"), err)
                    end
                end
                local parse_error = util.checkLuaSyntax(content)
                if not parse_error then
                    local ok, err = self:saveFileContent(file_path, content, caller_callback)
                    if ok then
                        return true, _("Lua syntax OK, file saved")
                    else
                        return false, T(_("Failed saving file: %1"), err)
                    end
                end
                local save_anyway = Trapper:confirm(T(_([[
Lua syntax check failed:

%1

KOReader may crash if this is saved.
Do you really want to save to this file?

%2]]), parse_error, BD.filepath(file_path)),  _("Do not save"), _("Save anyway"))
                -- we'll get the safer "Do not save" on tap outside
                if save_anyway then
                    local ok, err = self:saveFileContent(file_path, content, caller_callback)
                    if ok then
                        return true, _("File saved")
                    else
                        return false, T(_("Failed saving file: %1"), err)
                    end
                else
                    return false, false -- no need for more InfoMessage
                end
            else -- If content is empty, propose to delete the file
                local delete_file = Trapper:confirm(T(_([[
Text content is empty.
Do you want to keep this file as empty, or do you prefer to delete it?

%1]]), BD.filepath(file_path)), _("Keep empty file"), _("Delete file"))
                -- we'll get the safer "Keep empty file" on tap outside
                if delete_file then
                    local ok, err = self:deleteFile(file_path)
                    if ok then
                        return true, _("File deleted")
                    else
                        return false, T(_("Failed deleting file: %1"), err)
                    end
                else
                    local ok, err = self:saveFileContent(file_path, content, caller_callback)
                    if ok then
                        return true, _("File saved")
                    else
                        return false, T(_("Failed saving file: %1"), err)
                    end
                end
            end
        end,

    }
    UIManager:show(self.input)
    if self.show_keyboard_on_start and not readonly then
        self.input:onShowKeyboard()
    end
    -- Note about readonly:
    -- We might have liked to still show keyboard even if readonly, just
    -- to use the arrow keys for line by line scrolling with cursor.
    -- But it's easier to just let InputDialog and InputText do their
    -- own readonly prevention (and on devices where we run as root, we
    -- will hardly ever be readonly).
end

-- reopen last edited file. Invokeable with gesture:
function TextEditor:onOpenLastEditedFile()
    self:loadSettings()
    if #self.history > 0 then
        local file_path = self.history[1]
        self:checkEditFile(file_path, true)
    else
        self:chooseFile()
    end
end

-- quickly open and edit a file
-- calls the done_callback function on close
function TextEditor:quickEditFile(file_path, done_callback, possible_new_file)
    if done_callback then
        self.whenDoneFunc = done_callback
    end
    self:checkEditFile(file_path, possible_new_file or false)
end

-- TitleBar left button tap
function TextEditor:showMenu()
    local dialog
    local buttons = {}
    local optionsutil = require("ui/data/optionsutil")
    for i, mode in ipairs(optionsutil.rotation_modes) do
        buttons[i] = {{
            text = optionsutil.rotation_labels[i],
            enabled_func = function()
                return optionsutil.rotation_modes[i] ~= Screen:getRotationMode()
            end,
            callback = function()
                UIManager:close(dialog)
                self.input:onSetRotationMode(optionsutil.rotation_modes[i])
            end,
        }}
    end
    dialog = ButtonDialog:new{
        shrink_unneeded_width = true,
        buttons = buttons,
        anchor = function()
            return self.input.title_bar.left_button.image.dimen
        end,
        modal = true,
    }
    UIManager:show(dialog)
end

return TextEditor
