#include <monoformat_fontreader.hpp>
#include <monoformat_structured.hpp>
#include <monoformat_parseonly.hpp>

#include <sstream>
#include <fstream>
#include <iostream>
#include <vector>
#include <cstdint>
#include <filesystem>
#include <stdexcept>
#include <atomic>
#include <csignal>
#include <thread>
#include <chrono>

using namespace monoformat;

constexpr static std::uint16_t const ScreenWidth = 120;
constexpr static std::uint16_t const ScreenHeight = 60;

static std::atomic<bool> g_ctrlCPressed;

static void handleCtrlC(int) {
    g_ctrlCPressed.store(true, std::memory_order_relaxed);
}

template<typename Container>
static Container readEntireFile(std::filesystem::path const& fileName) {
    std::ifstream fileReader(fileName, std::ios::binary | std::ios::ate | std::ios::in);
    if (!fileReader) {
        std::ostringstream buf;
        buf << "Could not open \"" << fileName << "\" for reading";
        throw std::runtime_error(buf.str());
    }
    auto fileSize = fileReader.tellg();
    fileReader.seekg(std::ios::beg);
    typename Container::size_type scalarSize = sizeof(typename Container::value_type);
    Container content((static_cast<typename Container::size_type>(fileSize) + scalarSize - 1) / scalarSize,
                      static_cast<typename Container::value_type>(0));
    fileReader.read(reinterpret_cast<char*>(&content[0]), static_cast<std::size_t>(fileSize));
    return content;
}

static void clearScreen() {
    std::cout << "\x1b[1J\x1b[1;1H" << std::flush;
}

static void renderScreeen(monoformat::OneBitBufferInterface const& imageBuffer) {
    std::uint16_t const w = ScreenWidth;
    std::uint16_t const h = ScreenHeight / 2;

    std::ostringstream buf;
    buf << "\xE2\x94\x8C";
    for (std::uint16_t i = 0; i < w; ++i) {
        buf << "\xE2\x94\x80";
    }
    buf << "\xE2\x94\x90\n";
    for (std::uint16_t y = 0; y < h; ++y) {
        buf << "\xE2\x94\x82";
        for (std::uint16_t x = 0; x < w; ++x) {
            bool const pxUpperSet = imageBuffer.isPixelSet(x, 2 * y + 0);
            bool const pxLowerSet = imageBuffer.isPixelSet(x, 2 * y + 1);
            if (pxUpperSet && pxLowerSet) {
                buf << "\xE2\x96\x88";
            } else if (pxUpperSet) {
                buf << "\xF0\x9F\xAC\x8E";
            } else if (pxLowerSet) {
                buf << "\xE2\x96\x84";
            } else {
                buf << " ";
            }
        }
        buf << "\xE2\x94\x82\n";
    }
    if (ScreenHeight % 1) {
        buf << "\xE2\x94\x82";
        for (std::uint16_t x = 0; x < w; ++x) {
            bool const pxUpperSet = imageBuffer.isPixelSet(x, ScreenHeight - 1);
            if (pxUpperSet) {
                buf << "\xF0\x9F\xAC\x8E";
            } else {
                buf << " ";
            }
        }
        buf << "\xE2\x94\x82\n";
    }
    buf << "\xE2\x94\x94";
    for (std::uint16_t i = 0; i < w; ++i) {
        buf << "\xE2\x94\x80";
    }
    buf << "\xE2\x94\x98\n";
    std::cout << buf.str();
}

static void showContents(monoformat::File const& file, std::size_t tick, std::int64_t timestamp, std::span<monoformat::CustomFontSection const*> customFonts) {
    monoformat::ImageElement imageBufferElement{0, 0, ScreenWidth, ScreenHeight};
    auto imageBuffer = imageBufferElement.image();

    for (auto const& section : file.sections) {
        if (auto sec = dynamic_cast<monoformat::AlwaysDrawnSection*>(section.get())) {
            for (std::size_t i = 0; i < sec->elementCount(); ++i) {
                sec->elementAt(i)->drawTo(&imageBuffer, tick, timestamp, customFonts);
            }
        } else if (auto sec = dynamic_cast<monoformat::TimeBasedDrawnSection*>(section.get())) {
            for (std::size_t i = 0; i < sec->elementCount(); ++i) {
                sec->elementAt(i)->drawTo(&imageBuffer, tick, timestamp, customFonts);
            }
        }
    }

    renderScreeen(imageBuffer);
}

static void showContents2(std::span<std::byte const> data, std::size_t tick, std::int64_t timestamp) {
    std::vector<std::byte> buffer;
    buffer.resize((ScreenWidth * ScreenHeight + 8 - 1) / 8);
    MemoryOneBitBuffer imageBuffer{buffer, ScreenWidth, ScreenHeight};

    auto ret = parseAndApply(data, &imageBuffer, ScreenWidth, ScreenHeight, true, tick, timestamp);
    if (!ret) {
        std::cerr << "Parse  & apply error\n";
    }

    renderScreeen(imageBuffer);
}

int main(int argc, char** argv) {
    if (argc < 2 || !argv[1]) {
        std::ostringstream buf;
        buf << "Usage: " << argv[0] << " filename [timestamp]\n";
        std::cerr << buf.str();
        return 1;
    }
    if (std::string_view{argv[1]} == "--help") {
        std::ostringstream buf;
        buf << "Usage: " << argv[0] << " filename [timestamp]\n";
        std::cout << buf.str() << std::flush;
        return 0;
    }

    std::int64_t timestamp = -1;
    if (argc > 2) {
        char* endptr = nullptr;
        timestamp = strtoll(argv[2], &endptr, 10);
        if (!endptr || *endptr) {
            std::ostringstream buf;
            buf << "Usage: " << argv[0] << " filename [timestamp]\n";
            std::cerr << buf.str();
            return 1;
        }
    }

    try {
        auto contents = readEntireFile<std::vector<std::byte>>(argv[1]);
        auto file = monoformat::parseFile(contents);
        if (!file) {
            std::ostringstream buf;
            buf << "Parse error while processing the file \"" << argv[1] << "\"";
            throw std::runtime_error(buf.str());
        }

        std::vector<monoformat::CustomFontSection const*> customFonts;
        for (auto const& section : file->sections) {
            if (auto fs = dynamic_cast<monoformat::CustomFontSection const*>(section.get())) {
                customFonts.push_back(fs);
            }
        }

        std::signal(SIGINT, &handleCtrlC);
        std::signal(SIGTERM, &handleCtrlC);
        std::size_t tick = 0;
        while (!g_ctrlCPressed.load(std::memory_order_relaxed)) {
            std::int64_t currentTime = timestamp == -1 ? time(nullptr) : timestamp;
            clearScreen();
            //showContents(*file, tick, currentTime, customFonts);
            showContents2(contents, tick, currentTime);
            std::ostringstream buf;
            buf << "Animation Tick: " << tick << ", Timestamp = " << currentTime << "\n";
            std::cout << buf.str() << std::flush;
            std::this_thread::sleep_for(std::chrono::milliseconds{40});
            ++tick;
        }
    } catch (std::exception& e) {
        std::ostringstream buf;
        buf << argv[0] << ": " << e.what() << "\n";
        std::cerr << buf.str();
        return 2;
    }
    return 0;
}
