#include <pantor/inja.hpp>
#include "mainwindow.hpp"

MainWindow::MainWindow(QApplication* a)
    : QMainWindow()
{
    app = a;
    main = new QStackedWidget(this);
    main->setObjectName("Central");
    setCentralWidget(main);

    setupSearchDock();
    setupFManDock();
    setupEditor();
    setupStatusBar();
    setupMenuBar();
    
    setWindowTitle(tr("qed"));
    setupStyle();
}

// File Menu Handlers
void MainWindow::newFile()
{
    TextEdit* ed = setupEditor();
    ed->setFocus(); // seem redundant but needed?
}

void MainWindow::openFile()
{
    Tool* tool = toolReg.toolInFocus();
    QString filename = tool->currentFilename();
    DEBUG("Open: " << filename.toStdString());
	if (filename.isEmpty()) {
        showFMan();
        return;
    }
    // check if the file is already open
    if (showIfOpen(filename)) {
        return;
    } 
    // check if there is a blank editor we can re-use
    if (openInBlank(filename)) {
        return;
    }
    // spawn a new editor
    TextEdit* ed = setupEditor();
    ed->openFile(filename);
    ed->setFocus(); // seem redundant but needed?
}

void MainWindow::saveFile() {
    Tool* tool = toolReg.toolInFocus();
    QString msg = tool->saveFile();
    if (!msg.isEmpty()) {
        statusBar()->showMessage(msg, 2000);
    }
}

void MainWindow::closeFile() {
    // TODO: allow closing/saving a buffer
    TextEdit* ed = toolReg.editorInFocus();
    if (ed == NULL) { return; }
    if (ed->isUnsaved()) {
        statusBar()->showMessage("File has unsaved changes. Save or discard?", 2000);
        return;
    }
    if (toolReg.remove(ed)) {
        main->removeWidget(ed);
        focus();
    }
}

void MainWindow::discardFile() {
    Tool* t = toolReg.toolInFocus();
    if (t->type == ToolType::FMan || t->type == ToolType::Search) { return; }
    if (toolReg.remove(t)) {
        t->hide(); // hides the dock if the tool is docked
        main->removeWidget(t); // removes from central, if in central
        focus(); // focus on new top widget
    }
    statusBar()->showMessage("File discarded.", 2000);
}

void MainWindow::quit()
{
    close();
}

void MainWindow::prevTool() {
    int newIndex = main->currentIndex() - 1;
    if (newIndex < 0) newIndex = main->count() - 1;
    main->setCurrentIndex(newIndex);
}

void MainWindow::nextTool() {
    int newIndex = main->currentIndex() + 1;
    if (newIndex == main->count()) newIndex = 0;
    main->setCurrentIndex(newIndex);
}

// View menu Handlers
void MainWindow::focus() {
    // focus on Main
    main->currentWidget()->setFocus();
}

void MainWindow::focusShelve() {
    // toggle the current tool between the central or dock
    Tool* tool = toolReg.toolInFocus();
    if (main->indexOf(tool) > -1) {
        // move to dock
        main->removeWidget(tool);
        tool->moveToShelf();
    } else {
        // move to central
        main->addWidget(tool);
        main->setCurrentWidget(tool);
        tool->moveToCenter();
    }
}

void MainWindow::showFMan() {
    if (fman->hasFocus()) {
        fman->hide();
    } else {
        fman->show();
    }
}

void MainWindow::showSearch() {
    if (search->hasFocus()) {
        search->hide();
    } else {
        search->show();
    }
}

// Help menu handlers
void MainWindow::about()
{
    QMessageBox::about(this, tr("About qed"), loadResource("haiku.txt"));
}

// Command Handler
void MainWindow::handleCommand(QString key) {
    DEBUG("Handle command: " << commands[key].label.toStdString());
    CommandRunner* cmd = new CommandRunner();
    cmd->start(commands[key].cli);
    subprocesses.push_back(cmd);
    TextBuffer* buffer = setupBuffer(); 
    buffer->show();
    // TODO: allow discarding output
    connect(cmd, &CommandRunner::resultsReady, this, 
            [this, buffer](QString result) { return buffer->append(result); });
    connect(cmd, &CommandRunner::finished, this,
            [this, cmd]() { return handleCommandFinished(cmd); });
}

void MainWindow::handleCommandFinished(CommandRunner* cmd) {
    subprocesses.removeOne(cmd);
    // but leave the buffer open
}

// Setup Widgets
void MainWindow::setupStyle() {
    setObjectName("MainGui");
    inja::json fields = inja::json::parse(loadResource("style.json").toStdString());
    fields["back_image"] = findConfigFile(QString::fromStdString(fields["back_image"])).toStdString();
    QString style = QString::fromStdString(inja::render(loadResource("style.qss").toStdString(), fields));
    DEBUG(style.toStdString());
    setStyleSheet(style);
    setWindowIcon(QIcon(findConfigFile("workbench.png")));

    // Manually style the menus b/c stylesheets are broken
    // 1. The menus don't resize to fit a new font
    // 2. submenus don't get styled at all.
    QFont font(QString::fromStdString(fields["menu_font"]), fields["menu_font_size"]);
    for (const auto& [mnuKey, mnu]: menus) {
        mnu->setFont(font);
        mnu->setStyleSheet(style);
    }
}

TextBuffer* MainWindow::setupBuffer() {
    TextBuffer* buffer = new TextBuffer;
    buffer->spawn(this);
    toolReg.add(buffer);
    return buffer;
}

TextEdit* MainWindow::setupEditor()
{
    TextEdit* ed = new TextEdit;
    ed->spawn(this);
    ed->moveToCenter();
    main->addWidget(ed);
    main->setCurrentWidget(ed);
    toolReg.add(ed);
    connect(search, &Search::search, ed, &TextEdit::findNext);
    connect(search, &Search::searchChanged, ed, &TextEdit::findFirst);
    connect(search, &Search::searchWithCase, ed, &TextEdit::findNextWithCase);
    connect(search, &Search::searchWithCaseChanged, ed, &TextEdit::findFirstWithCase);
    return ed;
    // TODO: sort by filename
}

void MainWindow::setupStatusBar()
{
    stts = statusBar();
    stts->showMessage("Ready...", 5000);
}

void MainWindow::setupMenuBar()
{
    menu = menuBar();
    QMenu* fileMenu = menu->addMenu("&Meta");
    QMenu* editMenu = menu->addMenu("&Edit");
    QMenu* navigateMenu = menu->addMenu("&Navigate");
    QMenu* modeMenu = menu->addMenu("&Mode");
    QMenu* commandMenu = menu->addMenu("&Commands");
    QMenu* viewMenu = menu->addMenu("&View");
    QMenu* helpMenu = menu->addMenu("&Help");

    menus["meta"] = fileMenu;
    menus["edit"] = editMenu;
    menus["navigate"] = navigateMenu;
    menus["mode"] = modeMenu;
    menus["command"] = commandMenu;
    menus["view"] = viewMenu;
    menus["help"] = helpMenu;

    // New: Starts a new TextEditor with no content
    //
    // Open: If the selected item is a filename(s) open a TextEditor for each
    // else open a List of Files in the CWD in Filter mode

    // Save: If the current tool holds a file, save it to disk
    // else Save As
    //
    // Close: warn of unsaved changes and closes the tool/file
    // Quit: warn of unsaved files and close the application
    //
    // Focus: Moves the focus back to the main tool area

    // Focus Shelve: If the main area has focsu, moves the current tool to the shelf
    // else: moves the current tool from the shelf to the main area
    // focus stays with tool
    //
    // Show File Manager: Starts the file manager tool (a list showing files in the CWD)
    
    std::map<std::string, void (MainWindow::*)()> handlers {
        {"newFile", &MainWindow::newFile},
        {"openFile", &MainWindow::openFile},
        {"saveFile", &MainWindow::saveFile},
        {"closeFile", &MainWindow::closeFile},
        {"discardFile", &MainWindow::discardFile},
        {"quit", &MainWindow::quit},
        {"previousTool", &MainWindow::prevTool},
        {"nextTool", &MainWindow::nextTool},
        {"focus", &MainWindow::focus},
        {"focusShelve", &MainWindow::focusShelve},
        {"showFMan", &MainWindow::showFMan},
        {"showFind", &MainWindow::showSearch},
        {"about", &MainWindow::about},
    };

    inja::json actions = inja::json::parse(loadResource("actions.json").toStdString());

    // TODO: fix cutoff shortcut text
    for (const auto& [mnuKey, mnu]: menus) {
        for (inja::json::iterator it = actions[mnuKey].begin(); 
                it != actions[mnuKey].end(); ++it) {
            std::string key = it.value()[0];
            QString label = QString::fromStdString(it.value()[1]);
            if (label.startsWith("-")) {
                mnu->addSeparator();
                continue;
            }
            QAction* act = new QAction(label, this);
            QString shortcut = QString::fromStdString(it.value()[2]);
            if (shortcut.length() > 0) {
                act->setShortcut(QKeySequence(shortcut));
            }
            void (MainWindow::*h)() = handlers[key];
            if (h == NULL) {
                ERROR("Invalid Action: " << key);
            } else {
                connect(act, &QAction::triggered, this, h); 
                mnu->addAction(act);
            }
        }
    }

    
    // ***Meta
    // SaveAs: request a filename and Save
    // Create Project: confirm CWD will be a new project
    //


    // ***Select
    // Deselect
    // Select One (toggle)
    // Start Range/End Range

    // ***Edit
    // Copy
    // Cut
    // Paste
    // Comment/Uncomment
    // Indent
    // Unindent
    // Wrap lines
    // Join lines
    // Autocomplete
    
    // ***Navigate
    // Up
    // Down
    // Left
    // Right
    // Page Up
    // Page Down
    // Home
    // End
    // Beginning
    // EOF
    // Goto line
    // Change Directory
    // Goto Doc
    // Goto Definition
    
    // ***Mode
    // Text
    // List
    // Table
    //
    // Filter
    // Find
    // Find/Replace
    //
    // Insert
    // Replace
    //
    // Char
    // Word
    // Line/Row
    // Column
    // Cell
    //
    // ***Command
    // Add: Opens the new command template for modification
    // Run: Run the command open in the current tool
    // [Command1] -> Variants/Add Variant

    // ***View

    //->setShortcut(QKeySequence("F11"));
    //->setShortcut(QKeySequence("F10"));


    // Hide Simple Actions: Hides most common actions from the menu

    // ***Help

    setupCommands(commandMenu);
    setMenuBar(menu);
}

void MainWindow::setupCommands(QMenu* cmdMenu) {
    QMap<QString, QMenu*> cmdCategories;
    QStringList filters;
    filters << "*.json";
    // TODO: don't hard code this!
    QStringList filenames = QDir("./data/cmds").entryList(filters);
    for (int i=0; i<filenames.size(); i++) {
        QString filename = filenames[i];
        inja::json fields = inja::json::parse(
                loadResource("cmds/"+filename).toStdString());
        QMenu* mnu;
        QString cat = QString::fromStdString(fields["category"]);
        QString key = QString::fromStdString(fields["key"]);
        if (!cmdCategories.contains(cat)) {
            mnu = new QMenu(cat);
            // Save menu for manual styling (workaround to broken stylesheets)
            menus[QString("commands_" + cat).toStdString()] = mnu;
            cmdCategories[cat] = mnu;
            cmdMenu->addMenu(mnu);
        } else {
            mnu = cmdCategories[cat];
        }

        QAction* act = new QAction(
                QString::fromStdString(fields["label"]), this);
        QString shortcut = QString::fromStdString(fields["shortcut"]);
        if ((shortcut != NULL) && (shortcut[0] != '\0')) {
            act->setShortcut(QKeySequence(shortcut));
        }
        // TODO: configure Buffer or Null
        OutputDestination out = OutputDestination::Buffer;        
        OutputDestination err = OutputDestination::Buffer;        
        QStringList commandLine;
        for (std::string s : fields["cli"] ) {
            commandLine << QString::fromStdString(s);
        }

        commands[key] = Command(key, commandLine,
                cat, QString::fromStdString(fields["label"]),
                out, err);

        // sweet! Lambdas work as action handlers.
        connect(act, &QAction::triggered, this, [this, key]() { 
                return handleCommand(key); });
        mnu->addAction(act);
    }
}

void MainWindow::setupFManDock() {
    fman = new FMan();
    fman->spawn(this);
    toolReg.add(fman);
}

void MainWindow::setupSearchDock() {
    search = new Search();
    search->spawn(this);
    toolReg.add(search);
}

// Other helpers
bool MainWindow::showIfOpen(QString filename) {
    if (auto r = toolReg.isThereAnEditorWithFilename(filename)) {
        TextEdit* ed = *r;
        ed->show();
        if (!ed->isDocked()) {
            main->setCurrentWidget(toolReg.tool(ed));
        }
        ed->setFocus();
        return true;
    }
    return false;
}

bool MainWindow::openInBlank(QString filename) {
    if (auto r = toolReg.isThereABlankEditor()) {
        TextEdit* ed = *r;
        ed->openFile(filename);
        ed->show();
        if (!ed->isDocked()) {
            main->setCurrentWidget(toolReg.tool(ed));
        }
        ed->setFocus(); // seem redundant but needed?
        return true;
    }
    return false;
}

