-- The functions and variables in this file are only for use in the mod itself.
-- Those that do real work should be local and wrapped in public functions
local debug = yl_ticker.settings.debug or true

local function log(text)
    if debug then minetest.log("action", "[MOD] yl_ticker : " .. text) end
end

function yl_ticker.log(text) return log(text) end

-- Storage

local function get_savepath()
    local save_path = yl_ticker.settings.save_path
    local path = yl_ticker.worldpath .. DIR_DELIM .. save_path
    log("get_savepath : " .. dump(path))
    return path
end

local function get_filepath(filename)
    local path_to_file = get_savepath() .. DIR_DELIM .. filename
    log("get_filepath : " .. dump(filename) .. ":" .. dump(path_to_file))
    return path_to_file
end

local function save_json(filename, content)
    if type(filename) ~= "string" or type(content) ~= "table" then
        return false
    end
    local save_path = get_filepath(filename)
    local save_content = minetest.write_json(content)
    log("save_json : " .. dump(save_path) .. ":" .. dump(save_content))
    return minetest.safe_file_write(save_path, save_content)
end

local function load_json(path_to_file)
    local file = io.open(path_to_file, "r")
    if not file then return false, "Error opening file: " .. path_to_file end

    local content = file:read("*all")
    file:close()

    if not content then return false, "Error reading file: " .. path_to_file end
    log("load_json : " .. dump(path_to_file) .. ":" .. dump(content))
    return true, minetest.parse_json(content)
end

-- Public functions wrap the private ones, so they can be exchanged easily
function yl_ticker.load_json(filename, ...)
    return load_json(filename, ...)
end

function yl_ticker.save_json(filename, content, ...)
    return save_json(filename, content, ...)
end

-- load_all_data
--

local function is_visible(filename) return (string.sub(filename, 1, 1) ~= ".") end

local function is_json(filename) return (filename:match("%.json$")) end

local function validate_json(content, schema)

    -- Are all fields mentioned in the schema?
    for key, _ in pairs(content) do
        if schema[key] == nil then
            log("validate_json : Unexpected field in key = " .. dump(key))
            return false, "Unexpected field in " .. dump(key)
        end
    end

    -- Are all fields of the expected type?
    for key, expected_type in pairs(schema) do
        if type(content[key]) ~= expected_type then
            log("validate_json : Validation error in key = " .. dump(key))
            return false,
                   "Validation error in " .. dump(key) .. " not of type " ..
                       dump(expected_type)
        end
    end

    return true
end

local schema = {
    id = "number",
    creation_date = "number",
    message = "string",
    frequency = "number",
    runtime = "number",
    owner = "string"
}

local function load_all_data()
    -- Get all json files from savepath
    -- Excluding invisible
    -- Excluding non-json files
    local save_path = get_savepath()
    local files = minetest.get_dir_list(save_path, false) or {}
    local data = {}
    local total = 0
    local good = 0
    local bad = 0
    for key, filename in ipairs(files) do
        if is_visible(filename) and is_json(filename) then
            total = total + 1
            local filepath = get_filepath(filename)
            local success, content = load_json(filepath)

            if success and content.id and
                (validate_json(content, schema) == true) then
                good = good + 1
                data[content.id] = content
            else
                bad = bad + 1
            end

        end
    end

    yl_ticker.data = data

    if bad == 0 then
        minetest.log("action",
                     "[MOD] yl_ticker : bad = " .. tostring(bad) ..
                         ", good = " .. tostring(good) .. ", total = " ..
                         tostring(total))
        return true, good, bad
    else
        minetest.log("warning",
                     "[MOD] yl_ticker : bad = " .. tostring(bad) ..
                         ", good = " .. tostring(good) .. ", total = " ..
                         tostring(total))
        return false, good, bad
    end
end

function yl_ticker.load_all_data() return load_all_data() end

-- check privs
--

local function ends_with(str, suffix) return str:sub(-suffix:len()) == suffix end

local function split(str)
    local parts = {}
    for part in str:gmatch("[^,%s]+") do table.insert(parts, part) end
    return parts
end

local function priv_exists(priv)
    return (minetest.registered_privileges[priv] ~= nil) or false
end

local function check_privs()
    for key, value in pairs(yl_ticker.settings) do
        if ends_with(key, "_privs") then
            local parts = split(value)
            for _, part in ipairs(parts) do
                assert(priv_exists(part),
                       "yl_ticker : configured priv " .. dump(part) ..
                           " does not exist.")
            end
        end
    end
    log("PASS priv check")
end

function yl_ticker.check_privs() return check_privs() end

-- Remove file
--

local function remove_file(filename)
    local filepath = get_filepath(filename)
    return os.remove(filepath)
end

function yl_ticker.remove_file(filename) return remove_file(filename) end

-- Help
--

help_texts = {}

local function register_help(chatcommand_cmd, chatcommand_definition)
    local definition = {
        chatcommand = chatcommand_cmd,
        params = chatcommand_definition.params,
        description = chatcommand_definition.description,
        privs = chatcommand_definition.privs
    }
    help_texts[chatcommand_cmd] = definition
end

function yl_ticker.register_help(chatcommand_cmd, chatcommand_definition)
    return register_help(chatcommand_cmd, chatcommand_definition)
end

local function display_help()
    if (type(help_texts) ~= "table") then
        return false, "Help texts not a table"
    end
    if (next(help_texts) == nil) then return false, "Help text no content" end

    local message = {}

    for chatcommand, definition in pairs(help_texts) do

        local privs = ""

        if (definition.privs and (type(definition.privs) == "table")) then
            for priv, _ in pairs(definition.privs) do
                privs = privs .. priv .. ", "
            end
        end

        table.insert(message, minetest.colorize("#FF6700", "/" .. chatcommand))

        if (definition.params and (type(definition.params) == "string")) then
            table.insert(message, minetest.colorize("#FFFF00", "Params:" ..
                                                        definition.params))
        end

        if (definition.description and
            (type(definition.description) == "string")) then
            table.insert(message, minetest.colorize("#FFFF00", "Description:" ..
                                                        definition.description))
        end

        table.insert(message,
                     minetest.colorize("#FFFF00", "Privs:" .. privs) .. "\n")

    end

    local s_message = table.concat(message, "\n")

    return true, s_message
end

function yl_ticker.display_help() return display_help() end

-- Chatcommands
--

function yl_ticker.chatcommand_ticker_add(name, param) -- param is a string containing a message and more

    -- defense
    local player = minetest.get_player_by_name(name)
    if not player then return false, "Player not online" end
    if (not param) or (type(param) ~= "string") or (param == "") then
        return false, "Requirements not met"
    end
    -- Create ticker
    local ticker = string.split(param, "$", true)

    local message = ticker[1] or ""
    local frequency = tonumber(ticker[2]) or
                          yl_ticker.settings.frequency or 3600
    local runtime = tonumber(ticker[3]) or 7257600 -- 12 weeks
    local owner = name

    local success, ticker_id = yl_ticker.set(message, frequency,
                                                          runtime, owner)
    return success, "Ticker ID " .. tostring(ticker_id)
end

function yl_ticker.chatcommand_ticker_copy(name, param) -- param is a numerical a_id

    -- defense
    local player = minetest.get_player_by_name(name)
    if not player then return false, "Player not online" end
    if param == "" then return false, "Ticker ID missing" end
    local ticker_id = tonumber(param)
    if type(ticker_id) ~= "number" then
        return false, "Ticker ID is not a number"
    end
    if (ticker_id <= 0) then
        return false, "Ticker ID cannot be zero or negative"
    end

    local success, formspecstring = yl_ticker.formspec(ticker_id)

    if (success == false) then return false, formspecstring end

    -- Send the formspec
    minetest.show_formspec(name, "yl_ticker:copy", formspecstring)
    -- Report
    return true, "Copied ticker ID " .. tostring(ticker_id)
end

function yl_ticker.chatcommand_ticker_delete(name, param) -- param is a numerical a_id
    -- defense
    local player = minetest.get_player_by_name(name)
    if not player then return false, "Player not online" end
    if param == "" then return false, "Ticker ID missing" end
    local ticker_id = tonumber(param)
    if type(ticker_id) ~= "number" then
        return false, "Ticker ID not a number"
    end

    local success, ticker = yl_ticker.delete(ticker_id)

    if success == false then
        return false, ticker
    else
        return true, "Deleted ticker ID " .. tostring(ticker_id)
    end
end

-- List ticker

-- taken from yl_cinema
-- TODO: Should we API-fy this?

local function format_table(t)
    -- Format of t must be {{row1,row2,row3, ...},{row1,row2,row3, ...},...}
    local blanks_between_rows = 3
    local max_row_length = {}
    for linenumber = 1, #t do
        for rownumber = 1, #t[linenumber] do
            local row_length = #tostring(t[linenumber][rownumber])
            if (max_row_length[rownumber] or 0) < row_length then
                max_row_length[rownumber] = row_length
            end
        end
    end

    local ret = {}

    for linenumber = 1, #t do
        local line_s = ""
        for rownumber = 1, #t[linenumber] do
            local text = t[linenumber][rownumber]
            local text_length = #tostring(text)
            local add_blanks = max_row_length[rownumber] - text_length
            local newtext = t[linenumber][rownumber]
            for add = 1, (add_blanks + blanks_between_rows) do
                newtext = newtext .. " "
            end
            line_s = line_s .. newtext
        end
        table.insert(ret, line_s)
    end
    return table.concat(ret, "\n")
end

function yl_ticker.chatcommand_ticker_list_all(name, param) -- param must be empty
    -- defense
    local player = minetest.get_player_by_name(name)
    if not player then return false, "Player not online" end
    if param ~= "" then
        return false, "This command lists all tickers. " ..
                   "Do /ticker_list <ticker_id> if you want to have only one."
    end

    local success, data = yl_ticker.list()

    if (success == false) then return false, data end

    local f_ticker = {
        {"ID", "message", "created", "owner", "runtime", "frequency"}
    }
    for _, ticker in pairs(data) do

        local id = tostring(ticker.id) or "N/A"
        local message = ticker.message or "N/A"
        local created = os.date("!%Y-%m-%d %H:%M:%S",
                                (ticker.creation_date or 0)) or "N/A"
        local owner = ticker.owner or "N/A"
        local runtime = tostring(ticker.runtime) or "N/A"
        local frequency = tostring(ticker.frequency) or "N/A"

        local t = {id, message, created, owner, runtime, frequency}
        table.insert(f_ticker, t)
    end

    return true, format_table(f_ticker)
end

function yl_ticker.chatcommand_ticker_list(name, param) -- param is a numerical a_id
    -- defense
    local player = minetest.get_player_by_name(name)
    if not player then return false, "Player not online" end
    if param == "" then return false, "Ticker ID missing" end
    local ticker_id = tonumber(param)
    if type(ticker_id) ~= "number" then
        return false, "Ticker ID not a number"
    end

    local success, ticker = yl_ticker.get(ticker_id)

    if ((success == false) or (success == nil)) then
        return false, "Ticker not found"
    end

    return true, dump(ticker)

end

function yl_ticker.chatcommand_ticker_say_all(name, param) -- param must be empty
    -- defense
    local player = minetest.get_player_by_name(name)
    if not player then return false, "Player not online" end
    if param ~= "" then
        return false,
               "This command sends all ticker to the main chat. " ..
                   "Do /ticker_say <ticker_id> if you want to send only one."
    end

    local success, data = yl_ticker.list()

    if (success == false) then return false, "No data" end

    local n = 0
    for _, ticker in pairs(data) do
        local s_success, s_message = yl_ticker.say(ticker.id, "*")
        if (s_success == false) then return false, s_message end
        n = n + 1
    end

    return true, "Sent " .. tostring(n) .. " ticker to public."

end

function yl_ticker.chatcommand_ticker_say(name, param) -- param is a numerical a_id
    -- defense
    local player = minetest.get_player_by_name(name)
    if not player then return false, "Player not online" end
    if param == "" then return false, "Ticker ID missing" end
    local ticker_id = tonumber(param)
    if type(ticker_id) ~= "number" then
        return false, "Ticker ID not a number"
    end

    local s_success, s_message = yl_ticker.say(ticker_id, "*")
    if (s_success == false) then return false, s_message end

    return true,
           "Sent ticker " .. tostring(ticker_id) .. " to public."
end

function yl_ticker.chatcommand_ticker_help(name, param) -- param must be empty
    -- defense
    local player = minetest.get_player_by_name(name)
    if not player then return false, "Player not online" end
    if param ~= "" then
        return false,
               "This command displays the help for the ticker. " ..
                   "Do /ticker_help without parameters."
    end

    local success, message = yl_ticker.display_help()

    if (success == false) then return false, message end

    return true, message
end
