/**
 * @file code.cpp
 * @brief Functionality for compiling and disassembling source code.
 *
 * Copyright (C) 2021 Clyne Sullivan
 *
 * Distributed under the GNU GPL v3 or later. You should have received a copy of
 * the GNU General Public License along with this program.
 * If not, see <https://www.gnu.org/licenses/>.
 */

#include "stmdsp.hpp"
#include "stmdsp_code.hpp"

#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>

extern std::shared_ptr<stmdsp::device> m_device;
void log(const std::string& str);

std::ifstream compileOpenBinaryFile();
void compileEditorCode(const std::string& code);
void disassembleCode();

static std::string tempFileName;
static std::string newTempFileName();
static bool codeExecuteCommand(
    const std::string& command,
    const std::string& file);
static void stringReplaceAll(
    std::string& str,
    const std::string& what,
    const std::string& with);

std::ifstream compileOpenBinaryFile()
{
    if (!tempFileName.empty())
        return std::ifstream(tempFileName + ".o");
    else
        return std::ifstream();
}

void compileEditorCode(const std::string& code)
{
    log("Compiling...");

    if (tempFileName.empty()) {
        tempFileName = newTempFileName();
    } else {
        std::filesystem::remove(tempFileName + ".o");
        std::filesystem::remove(tempFileName + ".orig.o");
    }

    const auto platform = m_device ? m_device->get_platform()
                                   : stmdsp::platform::L4;

    {
        std::ofstream file (tempFileName, std::ios::trunc | std::ios::binary);

        auto file_text =
            platform == stmdsp::platform::L4 ? stmdsp::file_header_l4
                                             : stmdsp::file_header_h7;
        const auto buffer_size = m_device ? m_device->get_buffer_size()
                                          : stmdsp::SAMPLES_MAX;

        stringReplaceAll(file_text, "$0", std::to_string(buffer_size));

        file << file_text << '\n' << code;
    }

    const auto scriptFile = tempFileName +
#ifndef STMDSP_WIN32
        ".sh";
#else
        ".bat";
#endif

    {
        std::ofstream makefile (scriptFile, std::ios::binary);
        auto make_text =
            platform == stmdsp::platform::L4 ? stmdsp::makefile_text_l4
                                             : stmdsp::makefile_text_h7;

        stringReplaceAll(make_text, "$0", tempFileName);
        stringReplaceAll(make_text, "$1",
                         std::filesystem::current_path().string());

        makefile << make_text;
    }

#ifndef STMDSP_WIN32
    system((std::string("chmod +x ") + scriptFile).c_str());
#endif

    const auto makeOutput = scriptFile + ".log";
    const auto makeCommand = scriptFile + " > " + makeOutput + " 2>&1";
    if (codeExecuteCommand(makeCommand, makeOutput))
        log("Compilation succeeded.");
    else
        log("Compilation failed.");

    std::filesystem::remove(tempFileName);
    std::filesystem::remove(scriptFile);
}

void disassembleCode()
{
    log("Disassembling...");

    //if (tempFileName.empty())
    //    compileEditorCode();

    const auto output = tempFileName + ".asm.log";
    const auto command =
        std::string("arm-none-eabi-objdump -d --no-show-raw-insn ") +
        tempFileName + ".orig.o > " + output + " 2>&1";

    if (codeExecuteCommand(command, output))
        log("Ready.");
    else
        log("Failed to load disassembly.");
}

std::string newTempFileName()
{
    const auto path = std::filesystem::temp_directory_path() / "stmdspgui_build";
    return path.string();
}

bool codeExecuteCommand(const std::string& command, const std::string& file)
{
    bool success = system(command.c_str()) == 0;
    if (success) {
        if (std::ifstream output (file); output.good()) {
            std::ostringstream sstr;
            sstr << output.rdbuf();
            log(sstr.str().c_str());
        } else {
            log("Could not read command output!");
        }

        std::filesystem::remove(file);
    }

    return success;
}

void stringReplaceAll(std::string& str, const std::string& what, const std::string& with)
{
    std::size_t i;
    while ((i = str.find(what)) != std::string::npos) {
        str.replace(i, what.size(), with);
        i += what.size();
    }
};