--- _ignore_start_
local inspect = require("inspect")
--- _ignore_end_
local pardir = ".."
local app_name = "asefold"
local _module_item = '%s%s = "%s"'
local _module_item_escape = '%s["%s"] = "%s"'
local _shallow_module_template = [[
-- generated @ aseprite: asefold-export
local M = {
    -- ase-animations: begin
%s,
    -- ase-animations: end
    layers = {
%s
    },
    tags = {
%s
    }
}
return M
]]
local _deep_module_template = [[
-- generated @ aseprite: asefold-export
local M = {
    animations = {
%s
    },
    layers = {
%s
    },
    tags = {
%s
    }
}
return M
]]
local _tilesource_animation_template = [[
animations {
  id: "%s"
  start_tile: %d
  end_tile: %d
  playback: %s
  fps: %d
  flip_horizontal: %d
  flip_vertical: %d
}]]
local _tilesource_template = [[
image: "%s"
tile_width: %d
tile_height: %d
tile_margin: 0
tile_spacing: 0
collision: ""
material_tag: "tile"
collision_groups: "default"
%s
extrude_borders: 2
inner_padding: 0
sprite_trim_mode: SPRITE_TRIM_MODE_OFF]]

local LuaModuleType = { none = "none", shallow = "shallow", deep = "deep" }
local DialogWidgets = {
    Animation = "animation",
    SpriteSheetType = "sprite_sheet_type",
    GenerateModule = "generate_module",
    OutputFolder = "output_folder",
    FlattenVisible = "flatten_visible",
    SupressInfo = "supress_info",
}
local WidgetsValueField = {
    [DialogWidgets.Animation] = "option",
    [DialogWidgets.SpriteSheetType] = "option",
    [DialogWidgets.GenerateModule] = "option",
    [DialogWidgets.OutputFolder] = "text",
    [DialogWidgets.FlattenVisible] = "selected",
    [DialogWidgets.SupressInfo] = "selected",
}
local AnimationType = {
    FromTags = "tags",
    FromLayers = "layers",
}
local UserPlayback = {
    once = "once",
    loop = "loop",
    none = "none",
}
local type_map = {
    ROWS = SpriteSheetType.ROWS,
    PACKED = SpriteSheetType.PACKED,
    COLUMNS = SpriteSheetType.COLUMNS,
    VERTICAL = SpriteSheetType.VERTICAL,
    HORIZONTAL = SpriteSheetType.HORIZONTAL,
}
local SpriteSheetLabels = {
    "ROWS",
    "PACKED",
    "COLUMNS",
    "VERTICAL",
    "HORIZONTAL",
}
local commands = {
    AsefoldExportDialog = "AsefoldExportDialog",
    AsefoldRepeatExport = "AsefoldRepeatExport",
}
local empty_name = "__empty__"

local PINGPONG_REVERSE = "pingpong_reverse"
local function direction_loop(anidir, loop)
    return tostring(anidir) .. ":" .. tostring(loop)
end
-- animation + loop
local MapAniDir = {
    [direction_loop(AniDir.FORWARD, true)] = "PLAYBACK_LOOP_FORWARD",
    [direction_loop(AniDir.FORWARD, false)] = "PLAYBACK_ONCE_FORWARD",
    [direction_loop(AniDir.REVERSE, true)] = "PLAYBACK_LOOP_BACKWARD",
    [direction_loop(AniDir.REVERSE, false)] = "PLAYBACK_ONCE_BACKWARD",
    [direction_loop(AniDir.PING_PONG, true)] = "PLAYBACK_LOOP_PINGPONG",
    [direction_loop(AniDir.PING_PONG, false)] = "PLAYBACK_ONCE_PINGPONG",
    [direction_loop("forward", true)] = "PLAYBACK_LOOP_FORWARD",
    [direction_loop("forward", false)] = "PLAYBACK_ONCE_FORWARD",
    [direction_loop("reverse", true)] = "PLAYBACK_LOOP_BACKWARD",
    [direction_loop("reverse", false)] = "PLAYBACK_ONCE_BACKWARD",
    [direction_loop("pingpong", true)] = "PLAYBACK_LOOP_PINGPONG",
    [direction_loop("pingpong", false)] = "PLAYBACK_ONCE_PINGPONG",
    [direction_loop(PINGPONG_REVERSE, true)] = "PLAYBACK_LOOP_PINGPONG",
    [direction_loop(PINGPONG_REVERSE, false)] = "PLAYBACK_ONCE_PINGPONG",
    none = "PLAYBACK_NONE",
}

local loc = {
    format_limitations = "nor layer or tag shall contain underscores",
    cancel = "cancel",
    reset = "reset",
    title_clear_preferences = "erases it all",
    clear_preferences = "reset your preferences?",
    clear_preferences_template = "%dx sprite configurations",
    clear_preferences_path = "or edit json settings in",
    clear_preferences_path_template = '"%s"',
    supress_info = "supress messages",
    supress_label = "skip sucess dialog",
    clear = "clear",
    clear_label = "clear preferences",
    invalid_sprite = "invalid sprite",
    no_repeat_settings = "no settings to repeat",
    flatten = "flatten visible",
    title_template = "Asefold: %s",
    titles = {

        "export",
        "thanks for exporting w/ us",
        "soon w/ tilemap support",
        "Defold rocks",
        "you are appreciated",
        "export sprite stacks",
        "playback options in userdata",
        "final fps is average of duration",
        "comment what to improve",
        "localization coming next",
    },
    export_title = "Export to defold",
    repeat_title = "Repeat last",
    success = "success",
    sprite_tilesource_info = "export sprites to tilesource",
    sprite_tileset_info = "export tiles to tileset",
    path_info = "where to export",
    run_export = "export",
    import_animations = "get animations from",
    sorry = "sorry",
    not_implemented = "not implemented",
    ok = "ok",
    filepath_label = "where to export",
    output_folder_label = "output folder",
    run_label = "run export",
    sheet_type_label = "sheet type",
    generate_module = "generate lua module",
    generate_module_label = "to use w/ scripts",
    save_printout = 'saving %s at "%s"',
    pingpong_reverse_warning = {
        'There is no equivalent to "Ping-Pong Reverse" animation playback in Defold.',
        '    Using "Ping-Pong" instead.',
    },
    -- static, nonchanging
    underscore = "_",
    temporary_export = "asefold_temporary",
    tab1 = "\t",
    tab2 = "\t\t",
    empty_space = " ",
    id_format = "%s_%s",
    module_filename_form = "%s_tilesource.lua",
    defold_sprite = "defold_sprite",
    extension_tilesource = "tilesource",
    filename = "%s.%s",
    extension_png = "png",
    data_filename_form = "{layer}|{frame}",
    data_filename_parse = "([%w%s_. ]*)|([%w%s_. ]+)",
    filename_parse_separator = "|",
    __index = function(table, key)
        if table[key] == nil then
            error(("missing loc string named `%s`"):format(key))
        end
        return table[key]
    end,
}
function math.round(x)
    return math.floor(x + 0.5)
end

function math.choice(list)
    if #list == 0 then
        return nil
    end
    local index = math.random(1, #list)
    return list[index]
end

function table.values(self)
    local list = {}
    for _, v in ipairs(self) do
        table.insert(list, v)
    end
    return list
end

local function ternary(cond, a, b)
    if cond then
        return a
    end
    return b
end

local function get_temporary_file(filename)
    filename = filename or app_name
    local temp_name = app.fs.joinPath(app.fs.tempPath, filename)
    return temp_name
end

---formats
---@param layer string
---@param tag string
local function id_formatting(layer, tag)
    -- assert(not layer:find(loc.underscore), loc.please_no_underscore_template:format(loc.layer_underscore, ))
    local empty_tag = tag == empty_name
    local id = ternary(empty_tag, layer, loc.id_format:format(layer, tag))
    local err = ternary(
        (layer:find(loc.underscore) or tag:find(loc.underscore)) and not empty_tag,
        loc.format_limitations,
        false
    )
    return id, err
end

---@param dialog Dialog
---@return boolean
local function is_export_lua_module(dialog)
    return dialog.data[DialogWidgets.GenerateModule] and dialog.data[DialogWidgets.GenerateModule] ~= LuaModuleType.none
end

---@param dialog Dialog
---@return boolean
local function is_from_tags(dialog)
    return dialog.data[DialogWidgets.Animation] == AnimationType.FromTags
end

---table of strings into dialog new rows
---@param dialog Dialog
---@param messages table<string>
---@return Dialog
local function dialog_concat(dialog, messages)
    if messages and type(messages) == "string" then
        dialog = dialog:label({ text = messages }):newrow()
    elseif messages and type(messages) == "table" then
        for i, text in ipairs(messages) do
            dialog = dialog:label({ text = text })
            if i < #messages then
                dialog = dialog:newrow()
            end
        end
    end
    return dialog
end
local function get_obj_from_temp(temp_json_path)
    local temp_file = io.open(temp_json_path, "r")
    if not temp_file then
        error("temp_file not generated")
    end
    local temp_data = temp_file:read("a")
    print(temp_data)
    json = json or { decode = function(...) end }
    local temp_object = json.decode(temp_data)
    temp_file:close()
    return temp_object
end

local function get_is_success_supressed(plugin, dialog)
    plugin = plugin or {}
    plugin.preferences = plugin.preferences or {}
    local sprite = app.activeSprite or {}
    local setting = plugin.preferences[sprite.filename]
    return (setting and setting[DialogWidgets.SupressInfo]) or (dialog and dialog.data[DialogWidgets.SupressInfo])
end

local function dialog_verb_cancel(title, message, verb, callback)
    local dialog = Dialog({ title = title })
    dialog = dialog_concat(dialog, message)
    dialog = dialog:button({
        text = verb,
        onclick = function()
            if callback then
                callback()
            end
            dialog:close()
        end,
    })
    dialog = dialog:button({
        text = loc.cancel,
        onclick = function()
            dialog:close()
        end,
    })
    return dialog
end

local function format_animation(id, start_tile, end_tile, playback, fps, flip_horizontal, flip_vertical)
    return _tilesource_animation_template:format(
        id,
        start_tile,
        end_tile,
        playback,
        fps,
        flip_horizontal,
        flip_vertical
    )
end

local function success_dialog(parent, messages)
    local _inner = Dialog({ title = loc.success, parent = parent })
    _inner = dialog_concat(_inner, messages)
    _inner:button({
        text = loc.ok,
        onclick = function()
            _inner:close()
        end,
    })
    _inner:show()
end
local function error_dialog(parent, reason)
    local _inner = Dialog({ title = loc.sorry, parent = parent })
    _inner = dialog_concat(_inner, reason)
    _inner = _inner:button({
        text = loc.ok,
        onclick = function()
            _inner:close()
        end,
    })
    _inner:show()
end
local function not_implemented_dialog(parent)
    local _inner = Dialog({ title = loc.sorry, parent = parent }):label({ text = loc.not_implemented })
    _inner:button({
        text = loc.ok,
        onclick = function()
            _inner:close()
        end,
    })
    _inner:show()
end
local function get_minmax(list, selector)
    selector = selector or function(self)
        return self
    end
    local _max = -math.huge
    local _min = math.huge
    for _, v in ipairs(list) do
        local value = selector(v)
        if value > _max then
            _max = value
        end
        if value < _min then
            _min = value
        end
    end

    return {
        max = _max,
        min = _min,
    }
end
local function average(list, selector)
    selector = selector or function(self)
        return self
    end
    local sum = 0
    for _, v in ipairs(list) do
        local value = selector(v)
        sum = sum + value
    end
    return sum / #list
end
local function get_animations_from_tags(export_data)
    local animations = {}
    local err = false
    -- TODO: get from input
    local animation_ids = {}
    -- print(inspect(export_data))

    local sheet_width = export_data.meta.size.w
    local sheet_height = export_data.meta.size.h

    local tile_size
    local sheet_size
    if #export_data.meta.frameTags == 0 then
        export_data.meta.frameTags = {
            {
                name = empty_name,
                from = 0,
                to = #app.activeSprite.frames,
                direction = "forward",
                color = "#000000ff",
            },
        }
    end
    --TODO: check exported type
    -- if _temporary_type == SpriteSheetDataFormat.JSON_HASH then
    -- end
    local frame_tags = export_data.meta.frameTags
    local frame_data = {}
    for frame_name, frame in pairs(export_data.frames) do
        print("frame_name") --, frame_name)
        print(frame_name)
        print(frame)
        print(loc.data_filename_parse)
        local layer, frame_number = string.match(frame_name, loc.data_filename_parse)
        print(frame_number, layer)
        frame_number = tonumber(frame_number)
        for _, tag_data in ipairs(export_data.meta.frameTags) do
            local tag = tag_data.name
            local start_frame = tag_data.from
            local end_frame = tag_data.to

            -- print(inspect(tag_data))
            -- print("frame frame_number", frame_number, start_frame, end_frame)
            if frame_number >= start_frame and frame_number <= end_frame then
                -- print(layer, tag, frame_number, "~~~~~~~~~~~~~~~~~", frame_name)
                layer = layer or string.sub(frame_name, 1, string.find(frame_name, loc.filename_parse_separator) - 1)
                tile_size = tile_size or frame.sourceSize
                sheet_size = sheet_size
                    or {
                        w = math.round(sheet_width / tile_size.w),
                        h = math.round(sheet_height / tile_size.h),
                    }
                -- + .5 for rounding
                local tilex = math.round(frame.frame.x / sheet_width * sheet_size.w)
                local tiley = math.round(frame.frame.y / sheet_height * sheet_size.h)
                local tile_index = tilex + tiley * sheet_size.w
                local id, format_err = id_formatting(layer, tag)
                if format_err then
                    return {}, {}, format_err
                end

                if layer then
                    frame_data[id] = frame_data[id]
                        or {
                            layer = layer,
                            tag = tag,
                            frame_number = frame_number,
                            durations = {},
                            tiles = {},
                        }
                    table.insert(frame_data[id].tiles, { tile_index, tilex, tiley })
                    table.insert(frame_data[id].durations, frame.duration)
                end
            end
        end
    end

    for data_id, frame in pairs(frame_data) do
        local minmax = get_minmax(frame.tiles, function(table)
            return table[1]
        end)
        local avg_duration_ms = average(frame.durations)
        local avg_fps = 1000 / avg_duration_ms
        frame_data[data_id].fps = avg_fps
        frame_data[data_id].from = minmax.min
        frame_data[data_id].to = minmax.max
    end

    -- TODO: for some reason we're getteing double ids...
    -- this is best workaround
    local consumed_ids = {}
    print("frame data")
    print(inspect(frame_data))
    local pingpong_reverse_warning = true

    for _, layer in ipairs(export_data.meta.layers) do
        for _, tag in ipairs(frame_tags) do
            tag = tag or { name = empty_name }
            assert(layer.name)
            local id, format_err = id_formatting(layer.name, tag.name)
            if format_err then
                return {}, {}, format_err
            end
            local data = frame_data[id]
            local tag_data = tag.data or UserPlayback.loop
            -- print("here I am", id)
            -- print(inspect(data))
            if data and not consumed_ids[id] then
                --- _ignore_start_
                print(
                    ("Layer '%s', tag '%s', tag_direction '%s' and data '%s'"):format(
                        layer.name,
                        tag.name,
                        tag.direction,
                        tag_data
                    )
                )
                --- _ignore_end_
                local playback_match = {
                    once = MapAniDir[direction_loop(tag.direction or AniDir.FORWARD, false)],
                    none = MapAniDir.none,
                    loop = MapAniDir[direction_loop(tag.direction or AniDir.FORWARD, true)],
                }
                local resulting_match = playback_match.loop
                for key, v in pairs(playback_match) do
                    if tag_data:find(key) then
                        resulting_match = v
                        break
                    end
                end

                if tag.direction == PINGPONG_REVERSE and pingpong_reverse_warning then
                    error_dialog(nil, loc.pingpong_reverse_warning)
                    pingpong_reverse_warning = false
                end

                local animation = format_animation(id, data.from + 1, data.to + 1, resulting_match, data.fps, 0, 0)
                table.insert(animations, animation)
                table.insert(animation_ids, id)
                consumed_ids[id] = true
            end
        end
    end

    return animations, animation_ids, err
end

---@param name string
local function format_module_line(name, tab)
    tab = tab or loc.tab2
    local need_escape = name:find(loc.empty_space)
    if need_escape == nil then
        return _module_item:format(tab, name, name)
    end
    return _module_item_escape:format(tab, name, name)
end
local function save_module(dialog, filepath, animation_ids, export_data)
    if not is_export_lua_module(dialog) then
        return
    end

    local is_shallow = dialog.data[DialogWidgets.GenerateModule] == LuaModuleType.shallow

    --- get animations
    local animations = {}
    for _, id in ipairs(animation_ids) do
        local line
        if is_shallow then
            line = table.insert(animations, format_module_line(id, loc.tab1))
        else
            line = table.insert(animations, format_module_line(id))
        end
        table.insert(animations, line)
    end

    local layers = {}
    for _, layer in ipairs(export_data.meta.layers) do
        table.insert(layers, format_module_line(layer.name))
    end

    local tags = {}
    for _, tag in ipairs(export_data.meta.frameTags) do
        table.insert(tags, format_module_line(tag.name))
    end

    local animations_string = table.concat(animations, ",\n")
    local layers_string = table.concat(layers, ",\n")
    local tags_string = table.concat(tags, ",\n")

    -- _tilesource_template
    local module_file = io.open(filepath, "w+")
    if not module_file then
        error("not able to save module")
    end
    local template = ternary(is_shallow, _shallow_module_template, _deep_module_template)
    module_file:write(template:format(animations_string, layers_string, tags_string))
    module_file:close()
end
local function save_tilesource(dialog, export_data, image_filepath, filepath, module_filename)
    local file = io.open(filepath, "w+")
    print(filepath)
    if not file then
        error("not able to save tilesource")
    end

    local animations, animation_ids, err = {}, {}, false
    if is_from_tags(dialog) then
        animations, animation_ids, err = get_animations_from_tags(export_data)
    end
    if err then
        return err
    end
    local out_data = _tilesource_template:format(
        image_filepath,
        app.activeSprite.width,
        app.activeSprite.height,
        table.concat(animations, "\n")
    )

    file:write(out_data)
    file:close()
    save_module(dialog, module_filename, animation_ids, export_data)
    return false
end
local function _export_tilesource(dialog)
    local ran_transaction = false
    if dialog.data[DialogWidgets.FlattenVisible] then
        app.transaction(function()
            local layer_name = app.activeLayer.name
            for _, layer in ipairs(app.activeSprite.layers) do
                if not layer.isVisible then
                    app.activeSprite:deleteLayer(layer)
                    ran_transaction = true
                end
            end
            app.activeSprite:flatten()
            ran_transaction = true
            app.activeLayer.name = layer_name
            ran_transaction = true
        end)
    end

    local sprite_name = app.fs.fileTitle(app.activeSprite.filename) or loc.defold_sprite
    local sprite_filename = loc.filename:format(sprite_name, loc.extension_png)
    local curr_folder = app.fs.filePath(app.activeSprite.filename) or "/"
    ---@type string
    local reference_folder = dialog.data[DialogWidgets.OutputFolder]
    reference_folder = reference_folder:sub(1, 1) == app.fs.pathSeparator and reference_folder:sub(2)
        or reference_folder
    local relative_folder = app.fs.joinPath(curr_folder, app.fs.joinPath(pardir, reference_folder))
    relative_folder = app.fs.normalizePath(relative_folder)

    if not app.fs.isDirectory(relative_folder) then
        error_dialog(ternary(dialog.is_synthetic, nil, dialog), "directory does not exist")
        return {}
    end

    -- print("current folder", curr_folder)
    -- local temp_json_path = app.fs.joinPath(curr_folder, "temp_export_defold")
    local temporary_export_path = get_temporary_file(loc.temporary_export)

    local export_texture_path = app.fs.joinPath(relative_folder, sprite_filename)
    local internal_image_filename = app.fs.pathSeparator
        .. app.fs.joinPath(reference_folder, loc.filename:format(sprite_name, loc.extension_png))
    local tilesource_filename =
        app.fs.joinPath(relative_folder, loc.filename:format(sprite_name, loc.extension_tilesource))
    local module_filepath = app.fs.joinPath(relative_folder, loc.module_filename_form:format(sprite_name))

    app.command.ExportSpriteSheet({
        ui = false,
        dataFilename = temporary_export_path,
        askOverwrite = false,
        splitLayers = true,
        -- dataFormat = SpriteSheetDataFormat.JSON_ARRAY

        type = type_map[dialog.data[DialogWidgets.SpriteSheetType]],
        textureFilename = export_texture_path,
        filenameFormat = loc.data_filename_form,
    })

    if ran_transaction then
        app.undo()
    end

    local export_data = get_obj_from_temp(temporary_export_path)
    local err = save_tilesource(dialog, export_data, internal_image_filename, tilesource_filename, module_filepath)
    if err then
        error_dialog(dialog, { err })
        return {}
    end

    local success_information = {}
    table.insert(success_information, loc.save_printout:format("Temporary JSON", temporary_export_path))
    table.insert(success_information, loc.save_printout:format("Texture", export_texture_path))
    table.insert(success_information, loc.save_printout:format("Tile Source", tilesource_filename))
    if is_export_lua_module(dialog) then
        table.insert(success_information, loc.save_printout:format("Lua module", module_filepath))
    end
    (dialog or { close = function(...) end }):close()
    return success_information
end

---@param dialog Dialog
local function dialog_export_tilesource(dialog)
    -- sheet_type_label = "sheet type",
    return dialog
        :check({ text = loc.flatten, id = DialogWidgets.FlattenVisible })
        :combobox({
            options = { SpriteSheetLabels[1], SpriteSheetLabels[4], SpriteSheetLabels[5] },
            id = DialogWidgets.SpriteSheetType,
            label = loc.sheet_type_label,
            onchange = function()
                if
                    dialog.data[DialogWidgets.SpriteSheetType] ~= SpriteSheetLabels[3]
                    and dialog.data[DialogWidgets.SpriteSheetType] ~= SpriteSheetLabels[2]
                then
                    return
                end
                not_implemented_dialog(dialog)
                dialog:modify({
                    id = DialogWidgets.SpriteSheetType,
                    option = SpriteSheetLabels[1],
                })
            end,
        })
        :combobox({
            id = DialogWidgets.Animation,
            option = AnimationType.FromTags,
            options = { AnimationType.FromTags, AnimationType.FromLayers },
            label = loc.import_animations,
            onchange = function()
                if dialog.data[DialogWidgets.Animation] ~= AnimationType.FromLayers then
                    return
                end
                not_implemented_dialog(dialog)
                dialog:modify({
                    id = DialogWidgets.Animation,
                    option = AnimationType.FromTags,
                })
            end,
        })
        :combobox({
            options = { LuaModuleType.none, LuaModuleType.shallow, LuaModuleType.deep },
            selected = LuaModuleType.none,
            label = loc.generate_module_label,
            id = DialogWidgets.GenerateModule,
            text = loc.generate_module,
        })
        :check({
            id = DialogWidgets.SupressInfo,
            text = loc.supress_info,
            label = loc.supress_label,
        })
end

---@param dialog Dialog
local function basic_dialog(dialog, overrides)
    return dialog
        :button({
            text = loc.run_export,
            label = loc.run_label,
            onclick = overrides.run_export or function(...) end,
        })
        :entry({
            text = "/assets",
            save = true,
            label = loc.output_folder_label,
            id = DialogWidgets.OutputFolder,
        })
end

---persistesting
---@param plugin any
---@param dialog Dialog
local function dialog_persistence(plugin, dialog)
    dialog:button({
        text = loc.clear,
        label = loc.clear_label,
        onclick = function()
            local counter = 0
            for _, _ in pairs(plugin.preferences or {}) do
                counter = counter + 1
            end
            dialog_verb_cancel(
                loc.title_clear_preferences,
                {
                    loc.clear_preferences_template:format(counter),
                    loc.clear_preferences_path,
                    loc.clear_preferences_path_template:format(get_temporary_file(loc.temporary_export)),
                    loc.clear_preferences,
                },
                loc.reset,
                function()
                    plugin.preferences = {}
                end
            ):show()
        end,
    })
    local data = plugin.preferences[app.activeSprite.filename]
    if data then
        for _, key in pairs(DialogWidgets) do
            local update = {
                id = key,
            }
            update[WidgetsValueField[key]] = data[key]
            print(inspect(update))
            print(key)
            dialog:modify(update)
        end
    end
    print(app.activeSprite.filename, inspect.inspect(plugin.preferences))
    return dialog
end

local function repeat_export(plugin)
    if not app.activeSprite then
        error_dialog(nil, loc.invalid_sprite)
        return
    end
    if not plugin.preferences or (plugin.preferences and not plugin.preferences[app.activeSprite.filename]) then
        error_dialog(nil, loc.no_repeat_settings)
        return
    end
    print(inspect.inspect(plugin.preferences))
    print(inspect.inspect(plugin.preferences[app.activeSprite.filename]))

    local success_information = _export_tilesource({
        is_synthetic = true,
        data = plugin.preferences[app.activeSprite.filename],
        close = function(...) end,
    })
    if not get_is_success_supressed(plugin) then
        success_dialog(nil, success_information)
    end
end

local function _read_plugin_preferences()
    if not app.fs.isFile(get_temporary_file()) then
        print("warn: no file")
        return {}
    end

    local file, err = io.open(get_temporary_file(), "r")
    if err or not file then
        return {}
    end
    local data = file:read()
    file:close()
    return json.decode(data)
end
local function _write_plugin_preferences(plugin_preferences)
    local file, err = io.open(get_temporary_file(), "w+")
    if err or not file then
        return
    end
    print("written")
    print(inspect(plugin_preferences))
    file:write(json.encode(plugin_preferences))
    file:close()
end
---@param plugin any
---@param dialog Dialog
local function _export_persistence(plugin, dialog)
    local data = dialog.data
    local new_data = {}
    for _, key in pairs(DialogWidgets) do
        new_data[key] = data[key]
    end
    plugin.preferences[app.activeSprite.filename] = new_data
    _write_plugin_preferences(plugin.preferences)
end

local function show_dialog(plugin)
    plugin = plugin or {}
    if not app.activeSprite then
        error_dialog(nil, loc.invalid_sprite)
        return
    end
    local dialog = Dialog({
        title = loc.title_template:format(math.choice(loc.titles) or app_name),
        hexpand = true,
        vexpand = true,
    })

    dialog = basic_dialog(dialog, {
        run_export = function()
            local success_information = _export_tilesource(dialog)
            if #success_information ~= 0 then
                _export_persistence(plugin, dialog)
            end
            if not get_is_success_supressed(plugin, dialog) then
                success_dialog(dialog, success_information)
            end
        end,
    })
    dialog = dialog_export_tilesource(dialog)
    dialog = dialog_persistence(plugin, dialog)
    dialog:show()
end

function init(plugin)
    local group = "asefold_file_export"
    plugin:newMenuGroup({
        id = group,
        title = loc.export_title,
        group = "file_export",
    })
    plugin:newCommand({
        id = commands.AsefoldExportDialog,
        title = loc.export_title,
        group = group,
        onclick = function()
            show_dialog(plugin)
        end,
    })
    plugin:newCommand({
        id = commands.AsefoldRepeatExport,
        title = loc.repeat_title,
        group = group,
        onclick = function()
            repeat_export(plugin)
        end,
    })
    plugin = plugin or {}
    plugin.preferences = _read_plugin_preferences()
end

function exit(plugin)
    -- no use
end
