#include "assert.hh"
#include "backtrace.hh"
#include "buffer.hh"
#include "buffer_utils.hh"
#include "buffer_manager.hh"
#include "client_manager.hh"
#include "command_manager.hh"
#include "commands.hh"
#include "context.hh"
#include "debug.hh"
#include "event_manager.hh"
#include "face_registry.hh"
#include "file.hh"
#include "hook_manager.hh"
#include "highlighters.hh"
#include "insert_completer.hh"
#include "json_ui.hh"
#include "keymap_manager.hh"
#include "terminal_ui.hh"
#include "option_manager.hh"
#include "option_types.hh"
#include "parameters_parser.hh"
#include "profile.hh"
#include "ranges.hh"
#include "regex.hh"
#include "register_manager.hh"
#include "remote.hh"
#include "scope.hh"
#include "shared_string.hh"
#include "shell_manager.hh"
#include "string.hh"
#include "unit_tests.hh"
#include "window.hh"

#include <fcntl.h>
#include <locale.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>

namespace Kakoune
{

extern const char* version;

namespace
{

struct {
    unsigned int version;
    StringView notes;
} constexpr version_notes[] = { {
        0,
        "» local scope for command line evaluation\n"
    }, {
        20260521,
        "» support the {+b}\\N{} escape sequence in regex (matches {+b}[^\\n]{})\n"
        "» count and register forwarding to user modes\n"
        "» back switch added for the arrange-buffers command\n"
    }, {
        20260412,
        "» {+u}finaleol{} option to preserve files with no final end-of-line\n"
        "» {+b}%val\\{buffile}{} is now empty for scratch buffers\n"
        "» {+b}FocusIn{}/{+b}FocusOut{} events on suspend\n"
        "» {+u}number-lines -full-relative{} switch to keep a smaller line number gutter\n"
        "» {+b}<a-I>{} and {+b}<a-A>{} to select nested text objects\n"
        "» {+b}kak -C <session>{} to connect-or-create a session\n"
    }, {
        20250603,
        "» kak_* appearing in shell arguments will be added to the environment\n"
        "» {+U}double underline{} support\n"
        "» {+u}git apply{} can stage/revert selected changes to current buffer\n"
        "» {+u}exec/eval -client{} accepts '*' and comma separated list\n"
    }
};

static_assert(sizeof(version_notes) / sizeof(version_notes[0]) <= 4 - (version_notes[0].version != 0 ? 1 : 0),
              "Maximum 3 versions should be displayed in version notes, not including the development version");

void show_startup_info(Client* local_client, int last_version)
{
    const Face version_face{.attributes=Attribute::Bold};
    DisplayLineList info;
    for (auto [version, notes] : version_notes)
    {
        if (version and version <= last_version)
            continue;

        if (not version)
            info.push_back({"• Development version", version_face});
        else
        {
            const auto year = version / 10000;
            const auto month = (version / 100) % 100;
            const auto day = version % 100;
            info.push_back({format("• Kakoune v{}.{:02}.{:02}", year, month, day), version_face});
        }

        for (auto&& line : notes | split<StringView>('\n'))
            info.push_back(parse_display_line(line, GlobalScope::instance().faces()));
    }
    if (not info.empty())
        local_client->info_show({{{format("Kakoune {}", version), version_face},
                                  {", more info at ", {}},
                                  {":doc changelog", {.attributes=Attribute::Underline}}}},
                                 std::move(info), {}, InfoStyle::Prompt);
}

inline void write_stdout(StringView str) { try { write(STDOUT_FILENO, str); } catch (runtime_error&) {} }
inline void write_stderr(StringView str) { try { write(STDERR_FILENO, str); } catch (runtime_error&) {} }

String runtime_directory()
{
    if (const char* runtime_directory = getenv("KAKOUNE_RUNTIME"))
        return runtime_directory;

    char relpath[PATH_MAX+1];
    format_to(relpath, "{}../share/kak", split_path(get_kak_binary_path()).first);
    struct stat st;
    if (stat(relpath, &st) == 0 and S_ISDIR(st.st_mode))
        return real_path(relpath);

    return "/usr/share/kak";
}

String config_directory()
{
    if (StringView kak_cfg_dir = getenv("KAKOUNE_CONFIG_DIR"); not kak_cfg_dir.empty())
        return kak_cfg_dir.str();
    if (StringView xdg_cfg_home = getenv("XDG_CONFIG_HOME"); not xdg_cfg_home.empty())
        return format("{}/kak", xdg_cfg_home);
    return format("{}/.config/kak", homedir());
}

auto main_sel_first(const SelectionList& selections)
{
    auto beg = &*selections.begin(), end = &*selections.end();
    auto main = beg + selections.main_index();
    using View = ConstArrayView<Selection>;
    return concatenated(View{main, end}, View{beg, main});
}

const EnvVarDesc builtin_env_vars[] = { {
        "bufname", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {context.buffer().display_name()}; }
    }, {
        "buffile", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {context.buffer().filename()}; }
    }, {
        "buflist", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return BufferManager::instance() | transform(&Buffer::display_name) | gather<Vector<String>>(); }
    }, {
        "buf_line_count", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(context.buffer().line_count())}; }
    }, {
        "timestamp", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(context.buffer().timestamp())}; }
    }, {
        "history_id", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string((size_t)context.buffer().current_history_id())}; }
    }, {
        "selection", false,
        [](StringView name, const Context& context) -> Vector<String>
        { const Selection& sel = context.selections().main();
          return {content(context.buffer(), sel)}; }
    }, {
        "selections", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return context.selections_content(); }
    }, {
        "runtime", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {runtime_directory()}; }
    }, {
        "config", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {config_directory()}; }
    }, {
        "version", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {version}; }
    }, {
        "opt_", true,
        [](StringView name, const Context& context) -> Vector<String>
        { return context.options()[name.substr(4_byte)].get_as_strings(); }
    }, {
        "main_reg_", true,
        [](StringView name, const Context& context) -> Vector<String>
        { return {context.main_sel_register_value(name.substr(9_byte)).str()}; }
    }, {
        "reg_", true,
        [](StringView name, const Context& context)
        { return RegisterManager::instance()[name.substr(4_byte)].get(context) |
                     gather<Vector<String>>(); }
    }, {
        "client_env_", true,
        [](StringView name, const Context& context) -> Vector<String>
        { return {context.client().get_env_var(name.substr(11_byte)).str()}; }
    }, {
        "session", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {Server::instance().session()}; }
    }, {
        "client", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {context.name()}; }
    }, {
        "client_pid", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(context.client().pid())}; }
    }, {
        "client_list", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return ClientManager::instance() |
                      transform([](const UniquePtr<Client>& c) -> const String&
                                { return c->context().name(); }) | gather<Vector<String>>(); }
    }, {
        "modified", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {context.buffer().is_modified() ? "true" : "false"}; }
    }, {
        "cursor_line", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(context.selections().main().cursor().line + 1)}; }
    }, {
        "cursor_column", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(context.selections().main().cursor().column + 1)}; }
    }, {
        "cursor_char_value", false,
        [](StringView name, const Context& context) -> Vector<String>
        { auto coord = context.selections().main().cursor();
          auto& buffer = context.buffer();
          return {to_string((size_t)utf8::codepoint(buffer.iterator_at(coord), buffer.end()))}; }
    }, {
        "cursor_char_column", false,
        [](StringView name, const Context& context) -> Vector<String>
        { auto coord = context.selections().main().cursor();
          return {to_string(context.buffer()[coord.line].char_count_to(coord.column) + 1)}; }
    }, {
        "cursor_display_column", false,
        [](StringView name, const Context& context) -> Vector<String>
        { auto coord = context.selections().main().cursor();
          return {to_string(get_column(context.buffer(),
                                       context.options()["tabstop"].get<int>(),
                                       coord) + 1)}; }
    }, {
        "cursor_byte_offset", false,
        [](StringView name, const Context& context) -> Vector<String>
        { auto cursor = context.selections().main().cursor();
          return {to_string(context.buffer().distance({0,0}, cursor))}; }
    }, {
        "recording_register", false,
        [](StringView name, const Context& context) -> Vector<String>
        { const char reg = context.client().input_handler().recording_reg();
          return {reg ? String{static_cast<Codepoint>(reg)} : ""}; }
    }, {
        "selection_desc", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {selection_to_string(ColumnType::Byte, context.buffer(), context.selections().main())}; }
    }, {
        "selections_desc", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return main_sel_first(context.selections()) |
                     transform([&buffer=context.buffer()](const Selection& sel) {
                         return selection_to_string(ColumnType::Byte, buffer, sel);
                     }) | gather<Vector<String>>(); }
    }, {
        "selections_char_desc", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return main_sel_first(context.selections()) |
                     transform([&buffer=context.buffer()](const Selection& sel) {
                         return selection_to_string(ColumnType::Codepoint, buffer, sel);
                     }) | gather<Vector<String>>(); }
    }, {
        "selections_display_column_desc", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return main_sel_first(context.selections()) |
                     transform([&buffer=context.buffer(), tabstop=context.options()["tabstop"].get<int>()](const Selection& sel) {
                         return selection_to_string(ColumnType::DisplayColumn, buffer, sel, tabstop);
                     }) | gather<Vector<String>>(); }
    }, {
        "selection_length", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(char_length(context.buffer(), context.selections().main()))}; }
    }, {
        "selections_length", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return context.selections() |
                     transform([&](const Selection& s) -> String {
                         return to_string(char_length(context.buffer(), s));
                     }) | gather<Vector<String>>(); }
    }, {
        "selection_count", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(context.selections().size())}; }
    }, {
        "window_width", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(context.window().dimensions().column)}; }
    }, {
        "window_height", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return {to_string(context.window().dimensions().line)}; }
    }, {
        "user_modes", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return context.keymaps().user_modes(); }
    }, {
        "window_range", false,
        [](StringView name, const Context& context) -> Vector<String>
        {
            const auto& setup = context.window().last_display_setup();
            const auto width = context.window().dimensions().column - setup.widget_columns;
            return {to_string(setup.first_line), to_string(setup.first_column),
                    to_string(setup.line_count), to_string(width)};
        }
    }, {
        "history", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return history_as_strings(context.buffer().history()); }
    }, {
        "history_since_", true,
        [](StringView name, const Context& context) -> Vector<String>
        { return history_as_strings(
            ArrayView(context.buffer().history())
                .subrange(str_to_int(name.substr(14_byte)) + 1)
        ); }
    }, {
        "uncommitted_modifications", false,
        [](StringView name, const Context& context) -> Vector<String>
        { return undo_group_as_strings(context.buffer().current_undo_group()); }
    }
};

void register_registers()
{
    RegisterManager& register_manager = RegisterManager::instance();

    for (Codepoint c : StringView{"abcdefghijklmnopqrstuvwxyz\"^@"})
        register_manager.add_register(c, make_unique_ptr<StaticRegister>(String{c}));

    for (Codepoint c : StringView{"/|:\\"})
        register_manager.add_register(c, make_unique_ptr<HistoryRegister>(String{c}));

    using StringList = Vector<String, MemoryDomain::Registers>;

    register_manager.add_register('%', make_dyn_reg(
        "%",
        [](const Context& context)
        { return StringList{{context.buffer().display_name()}}; }));

    register_manager.add_register('.', make_dyn_reg(
        ".",
        [](const Context& context) {
            auto content = context.selections_content();
            return StringList{content.begin(), content.end()};
         }));

    register_manager.add_register('#', make_dyn_reg(
        "#",
        [](const Context& context) {
            const size_t count = context.selections().size();
            StringList res;
            res.reserve(count);
            for (size_t i = 1; i < count+1; ++i)
                res.push_back(to_string((int)i));
            return res;
        }));

    for (size_t i = 0; i < 10; ++i)
    {
        register_manager.add_register('0'+i, make_dyn_reg(
            String{Codepoint('0'+i)},
            [i](const Context& context) {
                StringList result;
                for (auto& sel : context.selections())
                    result.emplace_back(i < sel.captures().size() ? sel.captures()[i] : "");
                return result;
            },
            [i](Context& context, ConstArrayView<String> values) {
                if (values.empty())
                    return;

                auto& sels = context.selections();
                for (size_t sel_index = 0; sel_index < sels.size(); ++sel_index)
                {
                    auto& sel = sels[sel_index];
                    if (sel.captures().size() < i+1)
                        sel.captures().resize(i+1);
                    sel.captures()[i] = values[std::min(sel_index, values.size()-1)];
                }
            }));
    }

    register_manager.add_register('_', make_unique_ptr<NullRegister>());
}

void register_keymaps()
{
    auto& keymaps = GlobalScope::instance().keymaps();
    keymaps.map_key(Key::Left, KeymapMode::Normal, {'h'}, "");
    keymaps.map_key(Key::Right, KeymapMode::Normal, {'l'}, "");
    keymaps.map_key(Key::Down, KeymapMode::Normal, {'j'}, "");
    keymaps.map_key(Key::Up, KeymapMode::Normal, {'k'}, "");

    keymaps.map_key(shift(Key::Left), KeymapMode::Normal, {'H'}, "");
    keymaps.map_key(shift(Key::Right), KeymapMode::Normal, {'L'}, "");
    keymaps.map_key(shift(Key::Down), KeymapMode::Normal, {'J'}, "");
    keymaps.map_key(shift(Key::Up), KeymapMode::Normal, {'K'}, "");

    keymaps.map_key(Key::End, KeymapMode::Normal, {alt('l')}, "");
    keymaps.map_key(Key::Home, KeymapMode::Normal, {alt('h')}, "");
    keymaps.map_key(shift(Key::End), KeymapMode::Normal, {alt('L')}, "");
    keymaps.map_key(shift(Key::Home), KeymapMode::Normal, {alt('H')}, "");
}

void check_tabstop(const int& val)
{
    if (val < 1) throw runtime_error{"tabstop should be strictly positive"};
}

void check_indentwidth(const int& val)
{
    if (val < 0) throw runtime_error{"indentwidth should be positive or zero"};
}

void check_scrolloff(const DisplayCoord& so)
{
    if (so.line < 0 or so.column < 0)
        throw runtime_error{"scroll offset must be positive or zero"};
}

void check_timeout(const int& timeout)
{
    if (timeout < 50)
        throw runtime_error{"the minimum acceptable timeout is 50 milliseconds"};
}

void check_extra_word_chars(const Vector<Codepoint, MemoryDomain::Options>& extra_chars)
{
    if (any_of(extra_chars, is_blank))
        throw runtime_error{"blanks are not accepted for extra completion characters"};
}

void check_matching_pairs(const Vector<Codepoint, MemoryDomain::Options>& pairs)
{
    if ((pairs.size() % 2) != 0)
        throw runtime_error{"matching pairs should have a pair number of element"};
    if (not all_of(pairs, [](Codepoint cp) { return is_punctuation(cp); }))
        throw runtime_error{"matching pairs can only be punctuation"};
}

void register_options()
{
    OptionsRegistry& reg = GlobalScope::instance().option_registry();

    reg.declare_option<int, check_tabstop>("tabstop", "size of a tab character", 8);
    reg.declare_option<int, check_indentwidth>("indentwidth", "indentation width", 4);
    reg.declare_option<DisplayCoord, check_scrolloff>(
        "scrolloff", "number of lines and columns to keep visible main cursor when scrolling",
        {0,0});
    reg.declare_option("eolformat", "end of line format", EolFormat::Lf);
    reg.declare_option("finaleol", "write and end of line at end of file", FinalEol::Present);
    reg.declare_option("BOM", "byte order mark to use when writing buffer",
                       ByteOrderMark::None);
    reg.declare_option("incsearch",
                       "incrementally apply search/select/split regex",
                       true);
    reg.declare_option("autoinfo",
                       "automatically display contextual help",
                       AutoInfo::Command | AutoInfo::OnKey);
    reg.declare_option("autocomplete",
                       "automatically display possible completions",
                       AutoComplete::Insert | AutoComplete::Prompt);
    reg.declare_option("aligntab",
                       "use tab characters when possible for alignment",
                       false);
    reg.declare_option("ignored_files",
                       "patterns to ignore when completing filenames",
                       Regex{R"(^(\..*|.*\.(o|so|a))$)"});
    reg.declare_option("disabled_hooks",
                      "patterns to disable hooks whose group is matched",
                      Regex{});
    reg.declare_option("filetype", "buffer filetype", ""_str);
    reg.declare_option("path", "path to consider when trying to find a file",
                   Vector<String, MemoryDomain::Options>({ "./", "%/", "/usr/include" }));
    reg.declare_option("completers", "insert mode completers to execute.",
                       InsertCompleterDescList({
                           InsertCompleterDesc{ InsertCompleterDesc::Filename, {} },
                           InsertCompleterDesc{ InsertCompleterDesc::Word, "all"_str }
                       }), OptionFlags::None);
    reg.declare_option("static_words", "list of words to always consider for insert word completion",
                   Vector<String, MemoryDomain::Options>{});
    reg.declare_option("autoreload",
                       "autoreload buffer when a filesystem modification is detected",
                       Autoreload::Ask);
    reg.declare_option("writemethod",
                       "how to write buffer to files",
                       WriteMethod::Overwrite);
    reg.declare_option<int, check_timeout>(
        "idle_timeout", "timeout, in milliseconds, before idle hooks are triggered", 50);
    reg.declare_option<int, check_timeout>(
        "fs_check_timeout", "timeout, in milliseconds, between file system buffer modification checks",
        500);
    reg.declare_option("ui_options",
                       "space separated list of <key>=<value> options that are "
                       "passed to and interpreted by the user interface\n"
                       "\n"
                       "The terminal ui supports the following options:\n"
                       "    <key>:                        <value>:\n"
                       "    terminal_assistant             clippy|cat|dilbert|none|off\n"
                       "    terminal_status_on_top         bool\n"
                       "    terminal_set_title             bool\n"
                       "    terminal_title                 str\n"
                       "    terminal_enable_mouse          bool\n"
                       "    terminal_synchronized          bool\n"
                       "    terminal_wheel_scroll_amount   int\n"
                       "    terminal_shift_function_key    int\n"
                       "    terminal_padding_char          codepoint\n"
                       "    terminal_padding_fill          bool\n"
                       "    terminal_cursor_native         bool\n"
                       "    terminal_info_max_width        int\n",
                       UserInterface::Options{});
    reg.declare_option("modelinefmt", "format string used to generate the modeline",
                       "%val{bufname} %val{cursor_line}:%val{cursor_char_column} {{context_info}} {{mode_info}} - %val{client}@[%val{session}]"_str);

    reg.declare_option("debug", "various debug flags", DebugFlags::None);
    reg.declare_option("readonly", "prevent buffers from being modified", false);
    reg.declare_option<Vector<Codepoint, MemoryDomain::Options>, check_extra_word_chars>(
        "extra_word_chars",
        "Additional characters to be considered as words for insert completion",
        { '_' });
    reg.declare_option<Vector<Codepoint, MemoryDomain::Options>, check_matching_pairs>(
        "matching_pairs",
        "set of pair of characters to be considered as matching pairs",
        { '(', ')', '{', '}', '[', ']', '<', '>' });
    reg.declare_option<int>("startup_info_version", "version up to which startup info changes should be hidden", 0);
}

Client* local_client = nullptr;
bool convert_to_client_pending = false;

enum class UIType
{
    Terminal,
    Json,
    Dummy,
};

UIType parse_ui_type(StringView ui_name)
{
    if (ui_name == "terminal") return UIType::Terminal;
    if (ui_name == "json") return UIType::Json;
    if (ui_name == "dummy") return UIType::Dummy;

    throw parameter_error(format("error: unknown ui type: '{}'", ui_name));
}

UniquePtr<UserInterface> make_ui(UIType ui_type)
{
    struct DummyUI : UserInterface
    {
        DummyUI() { set_signal_handler(SIGINT, SIG_DFL); }
        bool is_ok() const override { return true; }
        void menu_show(ConstArrayView<DisplayLine>, DisplayCoord,
                       Face, Face, MenuStyle) override {}
        void menu_select(int) override {}
        void menu_hide() override {}

        void info_show(const DisplayLine&, const DisplayLineList&, DisplayCoord, Face, InfoStyle) override {}
        void info_hide() override {}

        void draw(const DisplayBuffer&, DisplayCoord, const Face&, const Face&, ColumnCount) override {}
        void draw_status(const DisplayLine&, const DisplayLine&, const ColumnCount, const DisplayLine&, const Face&, StatusStyle) override {}
        DisplayCoord dimensions() override { return {24,80}; }
        void refresh(bool) override {}
        void set_on_key(OnKeyCallback) override {}
        void set_on_paste(OnPasteCallback) override {}
        void set_ui_options(const Options&) override {}
    };

    switch (ui_type)
    {
        case UIType::Terminal: return make_unique_ptr<TerminalUI>();
        case UIType::Json: return make_unique_ptr<JsonUI>();
        case UIType::Dummy: return make_unique_ptr<DummyUI>();
    }
    throw logic_error{};
}

pid_t fork_server_to_background()
{
    if (pid_t pid = fork())
        return pid;

    setsid();
    if (fork()) // double fork to orphan the server
        exit(0);

    write_stderr(format("Kakoune forked server to background ({}), for session '{}'\n",
                        getpid(), Server::instance().session()));
    return 0;
}

UniquePtr<UserInterface> create_local_ui(UIType ui_type)
{
    auto ui = make_ui(ui_type);

    static SignalHandler old_handler = set_signal_handler(SIGTSTP, [](int sig) {
        if (ClientManager::instance().count() == 1 and
            *ClientManager::instance().begin() == local_client and
            not Server::instance().is_daemon())
            old_handler(sig);
        else
        {
            convert_to_client_pending = true;
            set_signal_handler(SIGTSTP, old_handler);
        }
    });
    return ui;
}

int run_client(StringView session, StringView name, StringView client_init,
               Optional<BufferCoord> init_coord, UIType ui_type,
               bool suspend)
{
    try
    {
        Optional<int> stdin_fd;
        // json-ui (or dummy) is not intended to be user interactive.
        // So only worry about making the tty your stdin if:
        // (a) ui_type is Terminal, *and*
        // (b) fd 0 is not interactive.
        if (ui_type == UIType::Terminal and not isatty(0))
        {
            // move stdin to another fd, and restore tty as stdin
            stdin_fd = dup(0);
            int tty = open("/dev/tty", O_RDONLY);
            dup2(tty, 0);
            close(tty);
        }

        EventManager event_manager;
        RemoteClient client{session, name, make_ui(ui_type), getpid(), get_env_vars(),
                            client_init, std::move(init_coord), stdin_fd};
        stdin_fd.map(close);

        if (suspend)
            raise(SIGTSTP);
        while (not client.exit_status() and client.is_ui_ok())
            event_manager.handle_next_events(EventMode::Normal);
        return client.exit_status().value_or(-1);
    }
    catch (disconnected& e)
    {
        write_stderr(format("{}\ndisconnecting\n", e.what()));
        return -1;
    }
}

struct convert_to_client_mode
{
    String session;
    String client_name;
    String buffer_name;
    String selections;
};

enum class ServerFlags
{
    None        = 0,
    IgnoreKakrc = 1 << 0,
    Daemon      = 1 << 1,
    ReadOnly    = 1 << 2,
    StartupInfo = 1 << 3,
};
constexpr bool with_bit_ops(Meta::Type<ServerFlags>) { return true; }

int run_server(StringView session, StringView server_init,
               StringView client_init, StringView init_buffer, Optional<BufferCoord> init_coord,
               ServerFlags flags, UIType ui_type, DebugFlags debug_flags,
               ConstArrayView<StringView> files)
{
    static bool terminate = false;
    set_signal_handler(SIGTERM, [](int) { terminate = true; });
    set_signal_handler(SIGINT, [](int) { terminate = true; });
    if ((flags & ServerFlags::Daemon) and session.empty())
    {
        write_stderr("-d needs a session name to be specified with -s\n");
        return -1;
    }

    EventManager        event_manager;
    Server              server{session.empty() ? to_string(getpid()) : session.str(),
                               (bool)(flags & ServerFlags::Daemon)};

    StringRegistry      string_registry;
    GlobalScope         global_scope;
    ShellManager        shell_manager{builtin_env_vars};
    CommandManager      command_manager;
    RegisterManager     register_manager;
    HighlighterRegistry highlighter_registry;
    SharedHighlighters  defined_highlighters;
    ClientManager       client_manager;
    BufferManager       buffer_manager;

    register_options();
    register_registers();
    register_keymaps();
    register_commands();
    register_highlighters();

    global_scope.options()["debug"].set(debug_flags);

    write_to_debug_buffer("*** This is the debug buffer, where debug info will be written ***");

#ifdef KAK_DEBUG
    {
        ProfileScope profile{debug_flags, [&](std::chrono::microseconds duration) {
            write_to_debug_buffer(format("running the unit tests took {} ms", duration.count()));
        }};
        UnitTest::run_all_tests();
    }
#endif

    bool startup_error = false;
    if (not (flags & ServerFlags::IgnoreKakrc)) try
    {
        Context init_context{Context::EmptyContextFlag{}};
        command_manager.execute(format("source {}/kakrc", runtime_directory()),
                                init_context);
    }
    catch (runtime_error& error)
    {
        startup_error = true;
        write_to_debug_buffer(format("error while parsing kakrc:\n"
                                     "    {}", error.what()));
    }

    {
        Context empty_context{Context::EmptyContextFlag{}};
        global_scope.hooks().run_hook(Hook::EnterDirectory, real_path("."), empty_context);
        global_scope.hooks().run_hook(Hook::KakBegin, session, empty_context);
    }

    if (not server_init.empty()) try
    {
        Context init_context{Context::EmptyContextFlag{}};
        command_manager.execute(server_init, init_context);
    }
    catch (const kill_session& kill)
    {
        Context empty_context{Context::EmptyContextFlag{}};
        global_scope.hooks().run_hook(Hook::KakEnd, "", empty_context);
        return kill.exit_status;
    }
    catch (runtime_error& error)
    {
        startup_error = true;
        write_to_debug_buffer(format("error while running server init commands:\n"
                                     "    {}", error.what()));
    }

    if (not files.empty()) try
    {
        for (auto& file : files)
        {
            try
            {
                Buffer *buffer = open_or_create_file_buffer(file);
                if (flags & ServerFlags::ReadOnly)
                {
                    buffer->flags() |= Buffer::Flags::ReadOnly;
                    buffer->options().get_local_option("readonly").set(true);
                }
            }
            catch (runtime_error& error)
            {
                startup_error = true;
                write_to_debug_buffer(format("error while opening file '{}':\n"
                                             "    {}", file, error.what()));
            }
        }
    }
    catch (runtime_error& error)
    {
         write_to_debug_buffer(format("error while opening command line files: {}", error.what()));
    }

    int exit_status = 0;
    try
    {
        if (ui_type == UIType::Terminal and not isatty(0))
        {
            // move stdin to another fd, and restore tty as stdin
            int fd = dup(0);
            int tty = open("/dev/tty", O_RDONLY);
            dup2(tty, 0);
            close(tty);
            create_fifo_buffer("*stdin*", fd, Buffer::Flags::None, AutoScroll::NotInitially);
        }

        if (not server.is_daemon())
        {
            local_client = client_manager.create_client(
                 create_local_ui(ui_type), getpid(), {}, get_env_vars(), client_init, init_buffer, std::move(init_coord),
                 [&](int status) { exit_status = status; });

            if (startup_error and local_client)
                local_client->print_status({}, {
                    "error during startup, see `:buffer *debug*` for details",
                    local_client->context().faces()["Error"]
                }, -1, StatusStyle::Status);

            if (flags & ServerFlags::StartupInfo and local_client)
                show_startup_info(local_client, global_scope.options()["startup_info_version"].get<int>());
        }

        while (not terminate and
               (not client_manager.empty() or server.negotiating() or server.is_daemon()))
        {
            client_manager.redraw_clients();

            // Loop so that eventual inputs happening during the processing are handled as
            // well, avoiding unneeded redraws.
            Optional<std::chrono::nanoseconds> timeout;
            if (client_manager.has_pending_inputs())
                timeout = std::chrono::nanoseconds{};
            try
            {
                while (event_manager.handle_next_events(EventMode::Normal, nullptr, timeout))
                {
                    if (client_manager.process_pending_inputs())
                        break;
                    timeout = std::chrono::nanoseconds{};
                }
            }
            catch (const cancel&) {}

            client_manager.process_pending_inputs();

            client_manager.clear_client_trash();
            client_manager.clear_window_trash();
            buffer_manager.clear_buffer_trash();
            global_scope.option_registry().clear_option_trash();

            if (local_client and not contains(client_manager, local_client))
            {
                local_client = nullptr;
                if ((not client_manager.empty() or server.is_daemon()) and fork_server_to_background())
                    exit(exit_status); // We do not want to run destructors and hooks here
            }
            else if (convert_to_client_pending)
            {
                kak_assert(local_client);
                auto& local_context = local_client->context();
                String client_name = local_context.name();
                String buffer_name = local_context.buffer().name();
                String selections = selection_list_to_string(ColumnType::Byte, local_context.selections());

                ClientManager::instance().remove_client(*local_client, true, 0);
                client_manager.clear_client_trash();
                local_client = nullptr;
                convert_to_client_pending = false;

                if (fork_server_to_background())
                {
                    ClientManager::instance().clear(false);
                    String session = server.session();
                    server.close_session(false);
                    throw convert_to_client_mode{ std::move(session), std::move(client_name), std::move(buffer_name), std::move(selections) };
                }
            }
        }
    }
    catch (const kill_session& kill)
    {
        exit_status = kill.exit_status;
    }

    {
        Context empty_context{Context::EmptyContextFlag{}};
        global_scope.hooks().run_hook(Hook::KakEnd, "", empty_context);
    }

    return exit_status;
}

int run_filter(StringView keystr, ConstArrayView<StringView> files, bool quiet, StringView suffix_backup)
{
    StringRegistry  string_registry;
    GlobalScope     global_scope;
    EventManager    event_manager;
    ShellManager    shell_manager{builtin_env_vars};
    RegisterManager register_manager;
    BufferManager   buffer_manager;

    register_options();
    register_registers();

    try
    {
        auto keys = parse_keys(keystr);

        auto apply_to_buffer = [&](Buffer& buffer)
        {
            try
            {
                InputHandler input_handler{
                    { buffer, Selection{{0,0}, buffer.back_coord()} },
                    Context::Flags::Draft
                };

                for (auto& key : keys)
                    input_handler.handle_key(key);
            }
            catch (runtime_error& err)
            {
                if (not quiet)
                    write_stderr(format("error while applying keys to buffer '{}': {}\n",
                                        buffer.display_name(), err.what()));
            }
        };

        for (auto& file : files)
        {
            Buffer* buffer = open_file_buffer(file, Buffer::Flags::NoHooks);
            if (not suffix_backup.empty())
                write_buffer_to_file(*buffer, buffer->filename() + suffix_backup,
                                     WriteMethod::Overwrite, WriteFlags::None);
            apply_to_buffer(*buffer);
            write_buffer_to_file(*buffer, buffer->filename(),
                                 WriteMethod::Overwrite, WriteFlags::None);
            buffer_manager.delete_buffer(*buffer);
        }
        if (not isatty(0))
        {
            Buffer& buffer = *create_buffer_from_string(
                "*stdin*", Buffer::Flags::NoHooks, read_fd(0));
            apply_to_buffer(buffer);
            write_buffer_to_fd(buffer, 1);
            buffer_manager.delete_buffer(buffer);
        }
    }
    catch (runtime_error& err)
    {
        write_stderr(format("error: {}\n", err.what()));
    }

    buffer_manager.clear_buffer_trash();
    return 0;
}

int run_pipe(StringView session)
{
    try
    {
        send_command(session, read_fd(0));
    }
    catch (disconnected& e)
    {
        write_stderr(format("{}\ndisconnecting\n", e.what()));
        return -1;
    }
    return 0;
}

void signal_handler(int signal)
{
    TerminalUI::restore_terminal();
    const char* text = nullptr;
    switch (signal)
    {
        case SIGSEGV: text = "SIGSEGV"; break;
        case SIGFPE:  text = "SIGFPE";  break;
        case SIGQUIT: text = "SIGQUIT"; break;
        case SIGPIPE: text = "SIGPIPE"; break;
    }
    auto msg = format("Received {}, exiting.\nPid: {}\nCallstack:\n{}",
                      text, getpid(), Backtrace{}.desc());
    write_stderr(msg);
    notify_fatal_error(msg);

    if (Server::has_instance())
        Server::instance().close_session();
    if (BufferManager::has_instance())
        BufferManager::instance().backup_modified_buffers();

    if (signal == SIGSEGV)
    {
        // generate core dump
        ::signal(SIGSEGV, SIG_DFL);
        ::kill(getpid(), SIGSEGV);
    }
    else
        abort();
}

}

}

int main(int argc, char* argv[])
{
    using namespace Kakoune;

    setlocale(LC_ALL, "");

    set_signal_handler(SIGSEGV, signal_handler);
    set_signal_handler(SIGFPE,  signal_handler);
    set_signal_handler(SIGQUIT, signal_handler);
    set_signal_handler(SIGTERM, signal_handler);
    set_signal_handler(SIGPIPE, [](int){});
    set_signal_handler(SIGINT, [](int){});
    set_signal_handler(SIGCHLD, [](int){});
    set_signal_handler(SIGTTOU, SIG_IGN);

    const ParameterDesc param_desc{
        SwitchMap{ { "c", { ArgCompleter{},  "connect to given session" } },
                   { "C", { ArgCompleter{},  "connect to given session, create if it does not exist" } },
                   { "e", { ArgCompleter{},  "execute argument on client initialisation" } },
                   { "E", { ArgCompleter{},  "execute argument on server initialisation" } },
                   { "n", { {}, "do not source kakrc files on startup" } },
                   { "s", { ArgCompleter{},  "set session name" } },
                   { "d", { {}, "run as a headless session (requires -s)" } },
                   { "p", { ArgCompleter{},  "just send stdin as commands to the given session" } },
                   { "f", { ArgCompleter{},  "filter: for each file, select the entire buffer and execute the given keys" } },
                   { "i", { ArgCompleter{}, "backup the files on which a filter is applied using the given suffix" } },
                   { "q", { {}, "in filter mode, be quiet about errors applying keys" } },
                   { "ui", { ArgCompleter{}, "set the type of user interface to use (terminal, dummy, or json)" } },
                   { "l", { {}, "list existing sessions" } },
                   { "clear", { {}, "clear dead sessions" } },
                   { "debug", { ArgCompleter{}, "initial debug option value" } },
                   { "version", { {}, "display kakoune version and exit" } },
                   { "ro", { {}, "readonly mode" } },
                   { "help", { {}, "display a help message and quit" } } }
    };

    try
    {
        auto show_usage = [&]() {
            write_stdout(format("Usage: {} [options] [file]... [+<line>[:<col>]|+:]\n\n"
                    "Options:\n"
                    "{}\n"
                    "Prefixing a positional argument with a plus (`+`) sign will place the\n"
                    "cursor at a given set of coordinates, or the end of the buffer if the plus\n"
                    "sign is followed only by a colon (`:`)\n",
                    argv[0], generate_switches_doc(param_desc.switches)));
            return 0;
        };

        const auto params = ArrayView<char*>{argv+1, argv + argc}
                          | transform([](auto* s) { return String{s}; })
                          | gather<Vector<String>>();

        if (contains(params, "--help"_sv))
            return show_usage();

        ParametersParser parser{params, param_desc};

        const bool show_help_message = (bool)parser.get_switch("help");
        if (show_help_message)
            return show_usage();

        if (parser.get_switch("version"))
        {
            write_stdout(format("Kakoune {}\n", Kakoune::version));
            return 0;
        }

        const bool list_sessions = (bool)parser.get_switch("l");
        const bool clear_sessions = (bool)parser.get_switch("clear");
        if (list_sessions or clear_sessions)
        {
            list_files(session_directory(), [&](StringView session, auto&) {
                if (session.substr(0_byte, 1_byte) == ".")
                    return;

                const bool valid = check_session(session);
                if (list_sessions)
                    write_stdout(format("{}{}\n", session, valid ? "" : " (dead)"));
                if (not valid and clear_sessions)
                    unlink(session_path(session).c_str());
            });
            return 0;
        }

        if (auto session = parser.get_switch("p"))
        {
            for (auto opt : { "c", "n", "s", "d", "e", "E", "ro" })
            {
                if (parser.get_switch(opt))
                {
                    write_stderr(format("error: -{} is incompatible with -p\n", opt));
                    return -1;
                }
            }
            return run_pipe(*session);
        }

        auto client_init = parser.get_switch("e").value_or(StringView{});
        auto server_init = parser.get_switch("E").value_or(StringView{});
        const UIType ui_type = parse_ui_type(parser.get_switch("ui").value_or("terminal"));

        if (auto keys = parser.get_switch("f"))
        {
            if (parser.get_switch("ro"))
            {
                write_stderr("error: -ro is incompatible with -f\n");
                return -1;
            }

            Vector<StringView> files;
            for (size_t i = 0; i < parser.positional_count(); ++i)
                files.emplace_back(parser[i]);

            return run_filter(*keys, files, (bool)parser.get_switch("q"),
                              parser.get_switch("i").value_or(StringView{}));
        }

        Vector<StringView> files;
        Optional<BufferCoord> init_coord;
        for (auto& name : parser)
        {
            if (not name.empty() and name[0_byte] == '+')
            {
                if (name == "+" or name == "+:")
                {
                    client_init = client_init + "; exec gj";
                    continue;
                }
                auto colon = find(name, ':');
                if (auto line = str_to_int_ifp({name.begin()+1, colon}))
                {
                    init_coord = std::max<BufferCoord>({0,0}, {
                        *line - 1,
                        colon != name.end() ?
                            str_to_int_ifp({colon+1, name.end()}).value_or(1) - 1
                          : 0
                    });
                    continue;
                }
            }

            files.emplace_back(name);
        }

        if ((bool)parser.get_switch("c") + (bool)parser.get_switch("C") + (bool)parser.get_switch("s") > 1)
        {
            write_stderr("error: -s, -c and -C are mutually exclusive\n");
            return -1;
        }
        if (parser.get_switch("c") or (parser.get_switch("C") and check_session(*parser.get_switch("C"))))
        {
            for (auto opt : { "n", "d", "E", "ro" })
            {
                if (parser.get_switch(opt))
                {
                    write_stderr(format("error: -{} incompatible with connecting to an existing session\n", opt));
                    return -1;
                }
            }
            StringView session = parser.get_switch("c") ? *parser.get_switch("c") : *parser.get_switch("C");
            String new_files;
            for (auto name : files) {
                new_files += format("edit '{}'", escape(real_path(name), "'", '\''));
                if (init_coord) {
                    new_files += format(" {} {}", init_coord->line + 1, init_coord->column + 1);
                    init_coord.reset();
                }
                new_files += ";";
            }

            return run_client(session, {}, new_files + client_init, init_coord, ui_type, false);
        }
        else
        {
            StringView session = parser.get_switch("C").value_or(parser.get_switch("s").value_or(StringView{}));
            try
            {
                auto ignore_kakrc = (bool)parser.get_switch("n");
                auto flags = (ignore_kakrc                                ? ServerFlags::IgnoreKakrc : ServerFlags::None) |
                             (parser.get_switch("d")                      ? ServerFlags::Daemon      : ServerFlags::None) |
                             (parser.get_switch("ro")                     ? ServerFlags::ReadOnly    : ServerFlags::None) |
                             ((argc == 1 or (ignore_kakrc and argc == 2))
                              and isatty(0)                               ? ServerFlags::StartupInfo : ServerFlags::None);
                auto debug_flags = option_from_string(Meta::Type<DebugFlags>{}, parser.get_switch("debug").value_or(""));
                return run_server(session, server_init, client_init, files.empty() ? StringView{} : files[0], init_coord, flags, ui_type, debug_flags, files);
            }
            catch (convert_to_client_mode& convert)
            {
                return run_client(convert.session, convert.client_name,
                                  format("try %^buffer '{}'; select '{}'^; echo converted to client only mode",
                                         escape(convert.buffer_name, "'^", '\\'), convert.selections), {}, ui_type, true);
            }
        }
    }
    catch (parameter_error& error)
    {
        write_stderr(format("Error while parsing parameters: {}\n"
                            "Valid switches:\n"
                            "{}", error.what(),
                            generate_switches_doc(param_desc.switches)));
       return -1;
    }
    catch (Kakoune::exception& error)
    {
        write_stderr(format("Fatal error: {}\n", error.what()));
        return -1;
    }
    catch (std::exception& error)
    {
        write_stderr(format("uncaught exception ({}):\n{}\n", typeid(error).name(), error.what()));
        return -1;
    }
    catch (...)
    {
        write_stderr("uncaught exception");
        return -1;
    }
    return 0;
}

#if defined(__ELF__)
#ifdef __arm__
# define PROGBITS "%progbits"
#else
# define PROGBITS "@progbits"
#endif
asm(".pushsection \".debug_gdb_scripts\", \"MS\"," PROGBITS ",1" R"(
.byte 4
.ascii "kakoune-inline-gdb.py\n"
.ascii "import os.path\n"
.ascii "sys.path.insert(0, os.path.dirname(gdb.current_objfile().filename) + '/../share/kak/gdb/')\n"
.ascii "import gdb.printing\n"
.ascii "import kakoune\n"
.ascii "gdb.printing.register_pretty_printer(gdb.current_objfile(), kakoune.build_pretty_printer())\n\0"
.popsection
)");
#endif
