#include "datafile.hh"
#include "importtools.hh"
#include "bdf_import.hh"
#include "freetype_import.hh"
#include "export_rlefont.hh"
#include "encode_rlefont.hh"
#include "optimize_rlefont.hh"
#include "export_bwfont.hh"
#include <vector>
#include <string>
#include <set>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <cstdlib>
#include <ctime>
#include <map>
#include "ccfixes.hh"

using namespace mcufont;

static std::string strip_extension(std::string filename)
{
    size_t pos = filename.find_last_of('.');
    
    if (pos == std::string::npos)
    {
        return filename;
    }
    else
    {
        return filename.substr(0, pos);
    }
}

static std::unique_ptr<DataFile> load_dat(std::string src)
{
    std::ifstream infile(src);
    
    if (!infile.good())
    {
        std::cerr << "Could not open " << src << std::endl;
        return nullptr;
    }
    
    std::unique_ptr<DataFile> f = DataFile::Load(infile);
    if (!f)
    {
        std::cerr << "Invalid format for .dat file: " << src << std::endl;
        return nullptr;
    }
    
    return f;
}

static bool save_dat(std::string dest, DataFile *f)
{
    std::ofstream outfile(dest);
    
    if (!outfile.good())
    {
        std::cerr << "Could not open " << dest << std::endl;
        return false;
    }
    
    f->Save(outfile);
    
    if (!outfile.good())
    {
        std::cerr << "Could not write to " << dest << std::endl;
        return false;
    }
    
    return true;
}

enum status_t
{
    STATUS_OK = 0, // All good
    STATUS_INVALID = 1, // Invalid command or args
    STATUS_ERROR = 2 // Error when executing command
};

static status_t cmd_import_ttf(const std::vector<std::string> &args)
{
    if (args.size() != 3 && args.size() != 4)
        return STATUS_INVALID;
    
    std::string src = args.at(1);
    int size = std::stoi(args.at(2));
    bool bw = (args.size() == 4 && args.at(3) == "bw");
    std::string dest = strip_extension(src) + std::to_string(size) + (bw ? "bw" : "") + ".dat";
    std::ifstream infile(src, std::ios::binary);
    
    if (!infile.good())
    {
        std::cerr << "Could not open " << src << std::endl;
        return STATUS_ERROR;
    }
    
    std::cout << "Importing " << src << " to " << dest << std::endl;
    
    std::unique_ptr<DataFile> f = LoadFreetype(infile, size, bw);
    
    mcufont::rlefont::init_dictionary(*f);
    
    if (!save_dat(dest, f.get()))
        return STATUS_ERROR;
    
    std::cout << "Done: " << f->GetGlyphCount() << " unique glyphs." << std::endl;
    return STATUS_OK;
}

static status_t cmd_import_bdf(const std::vector<std::string> &args)
{
    if (args.size() != 2)
        return STATUS_INVALID;
    
    std::string src = args.at(1);
    std::string dest = strip_extension(args.at(1)) + ".dat";
    std::ifstream infile(src, std::ios::binary);
    
    if (!infile.good())
    {
        std::cerr << "Could not open " << src << std::endl;
        return STATUS_ERROR;
    }
    
    std::cout << "Importing " << src << " to " << dest << std::endl;
    
    std::unique_ptr<DataFile> f = LoadBDF(infile);
    
    mcufont::rlefont::init_dictionary(*f);
    
    if (!save_dat(dest, f.get()))
        return STATUS_ERROR;
    
    std::cout << "Done: " << f->GetGlyphCount() << " unique glyphs." << std::endl;
    return STATUS_OK;
}

static status_t cmd_filter(const std::vector<std::string> &args)
{
    if (args.size() < 3)
        return STATUS_INVALID;
    
    std::set<int> allowed;
    
    // Parse arguments
    for (size_t i = 2; i < args.size(); i++)
    {
        std::string s = args.at(i);
        size_t pos = s.find('-');
        if (pos == std::string::npos)
        {
            // Single char
            allowed.insert(std::stoi(s, nullptr, 0));
        }
        else
        {
            // Range
            int start = std::stoi(s.substr(0, pos), nullptr, 0);
            int end = std::stoi(s.substr(pos + 1), nullptr, 0);
            
            for (int j = start; j <= end; j++)
            {
                allowed.insert(j);
            }
        }
    }
    
    std::string src = args.at(1);
    std::unique_ptr<DataFile> f = load_dat(src);
    if (!f)
        return STATUS_ERROR;
    
    std::cout << "Font originally had " << f->GetGlyphCount() << " glyphs." << std::endl;
    
    // Filter the glyphs
    std::vector<DataFile::glyphentry_t> newglyphs;
    for (size_t i = 0; i < f->GetGlyphCount(); i++)
    {
        DataFile::glyphentry_t g = f->GetGlyphEntry(i);
        
        for (size_t j = 0; j < g.chars.size(); j++)
        {
            if (!allowed.count(g.chars.at(j)))
            {
                g.chars.erase(g.chars.begin() + j);
                j--;
            }
        }
        
        if (g.chars.size())
        {
            newglyphs.push_back(g);
        }
    }
    
    DataFile::fontinfo_t fontinfo = f->GetFontInfo();
    crop_glyphs(newglyphs, fontinfo);
    detect_flags(newglyphs, fontinfo);
    
    f.reset(new DataFile(f->GetDictionary(), newglyphs, fontinfo));
    std::cout << "After filtering, " << f->GetGlyphCount() << " glyphs remain." << std::endl;
    
    if (!save_dat(src, f.get()))
        return STATUS_ERROR;
    
    return STATUS_OK;
}

static status_t cmd_show_glyph(const std::vector<std::string> &args)
{
    if (args.size() != 3)
        return STATUS_INVALID;
    
    std::string src = args.at(1);
    std::unique_ptr<DataFile> f = load_dat(src);
    
    if (!f)
        return STATUS_ERROR;
    
    size_t index = 0;
    if (args.at(2) == "largest")
    {
        std::unique_ptr<mcufont::rlefont::encoded_font_t> e =
            mcufont::rlefont::encode_font(*f, false);
        size_t maxlen = 0;
        size_t i = 0;
        for (mcufont::rlefont::encoded_font_t::refstring_t g : e->glyphs)
        {
            if (g.size() > maxlen)
            {
                maxlen = g.size();
                index = i;
            }
            i++;
        }
        
        std::cout << "Index " << index << ", length " << maxlen << std::endl;
    }
    else
    {
        index = strtol(args.at(2).c_str(), nullptr, 0);
    }
    
    if (index < 0 || index >= f->GetGlyphCount())
    {
        std::cerr << "No such glyph " << index << std::endl;
        return STATUS_ERROR;
    }
    
    std::cout << "Width: " << f->GetGlyphEntry(index).width << std::endl;
    std::cout << "Chars: ";
    for (int c: f->GetGlyphEntry(index).chars) std::cout << c << " ";
    std::cout << std::endl;
    
    std::cout << f->GlyphToText(index);
    return STATUS_OK;
}

static status_t cmd_rlefont_export(const std::vector<std::string> &args)
{
    if (args.size() != 2 && args.size() != 3)
        return STATUS_INVALID;
    
    std::string src = args.at(1);
    std::string dst = (args.size() == 2) ? strip_extension(src) + ".c" : args.at(2);
    std::unique_ptr<DataFile> f = load_dat(src);
    
    if (!f)
        return STATUS_ERROR;
    
    {
        std::ofstream source(dst);
        mcufont::rlefont::write_source(source, dst, *f);
        std::cout << "Wrote " << dst << std::endl;
    }
    
    return STATUS_OK;
}

static status_t cmd_rlefont_size(const std::vector<std::string> &args)
{
    if (args.size() != 2)
        return STATUS_INVALID;
    
    std::string src = args.at(1);
    std::unique_ptr<DataFile> f = load_dat(src);
    
    if (!f)
        return STATUS_ERROR;
    
    size_t size = mcufont::rlefont::get_encoded_size(*f);
    
    std::cout << "Glyph count:       " << f->GetGlyphCount() << std::endl;
    std::cout << "Glyph bbox:        " << f->GetFontInfo().max_width << "x"
        << f->GetFontInfo().max_height << " pixels" << std::endl;
    std::cout << "Uncompressed size: " << f->GetGlyphCount() *
        f->GetFontInfo().max_width * f->GetFontInfo().max_height / 2
        << " bytes" << std::endl;
    std::cout << "Compressed size:   " << size << " bytes" << std::endl;
    std::cout << "Bytes per glyph:   " << size / f->GetGlyphCount() << std::endl;
    return STATUS_OK;
}

static status_t cmd_rlefont_optimize(const std::vector<std::string> &args)
{
    if (args.size() != 2 && args.size() != 3)
        return STATUS_INVALID;
    
    std::string src = args.at(1);
    std::unique_ptr<DataFile> f = load_dat(src);
    
    if (!f)
        return STATUS_ERROR;
    
    size_t oldsize = mcufont::rlefont::get_encoded_size(*f);
    
    std::cout << "Original size is " << oldsize << " bytes" << std::endl;
    std::cout << "Press ctrl-C at any time to stop." << std::endl;
    std::cout << "Results are saved automatically after each iteration." << std::endl;
    
    int limit = 100;
    if (args.size() == 3)
    {
        limit = std::stoi(args.at(2));
    }
    
    if (limit > 0)
        std::cout << "Limit is " << limit << " iterations" << std::endl;
    
    int i = 0;
    time_t oldtime = time(NULL);
    while (!limit || i < limit)
    {
        mcufont::rlefont::optimize(*f);

        size_t newsize = mcufont::rlefont::get_encoded_size(*f);
        time_t newtime = time(NULL);
        
        int bytes_per_min = (oldsize - newsize) * 60 / (newtime - oldtime + 1);
        
        i++;
        std::cout << "iteration " << i << ", size " << newsize
                  << " bytes, speed " << bytes_per_min << " B/min"
                  << std::endl;
        
        {
            if (!save_dat(src, f.get()))
                return STATUS_ERROR;
        }
    }
    
    return STATUS_OK;
}

static status_t cmd_rlefont_show_encoded(const std::vector<std::string> &args)
{
    if (args.size() != 2)
        return STATUS_INVALID;
    
    std::string src = args.at(1);
    std::unique_ptr<DataFile> f = load_dat(src);
    
    if (!f)
        return STATUS_ERROR;
    
    std::unique_ptr<mcufont::rlefont::encoded_font_t> e =
        mcufont::rlefont::encode_font(*f, false);

    int i = 0;
    for (mcufont::rlefont::encoded_font_t::rlestring_t d : e->rle_dictionary)
    {
        std::cout << "Dict RLE " << 24 + i++ << ": ";
        for (uint8_t v : d)
            std::cout << std::setfill('0') << std::setw(2) << std::hex << (int)v << " ";
        std::cout << std::endl;
    }
    
    for (mcufont::rlefont::encoded_font_t::refstring_t d : e->ref_dictionary)
    {
        std::cout << "Dict Ref " << 24 + i++ << ": ";
        for (uint8_t v : d)
            std::cout << std::setfill('0') << std::setw(2) << std::hex << (int)v << " ";
        std::cout << std::endl;
    }
    
    i = 0;
    for (mcufont::rlefont::encoded_font_t::refstring_t g : e->glyphs)
    {
        std::cout << "Glyph " << i++ << ": ";
        for (uint8_t v : g)
            std::cout << std::setfill('0') << std::setw(2) << std::hex << (int)v << " ";
        std::cout << std::endl;
    }
    
    return STATUS_OK;
}

static status_t cmd_bwfont_export(const std::vector<std::string> &args)
{
    if (args.size() != 2 && args.size() != 3)
        return STATUS_INVALID;
    
    std::string src = args.at(1);
    std::string dst = (args.size() == 2) ? strip_extension(src) + ".c" : args.at(2);
    std::unique_ptr<DataFile> f = load_dat(src);
    
    if (!f)
        return STATUS_ERROR;
    
    if (!(f->GetFontInfo().flags & DataFile::FLAG_BW))
    {
        std::cout << "Warning: font is not black and white" << std::endl;
    }
    
    {
        std::ofstream source(dst);
        mcufont::bwfont::write_source(source, dst, *f);
        std::cout << "Wrote " << dst << std::endl;
    }
    
    return STATUS_OK;
}


static const char *usage_msg =
    "Usage: mcufont <command> [options] ...\n"
    "Commands for importing:\n"
    "   import_ttf <ttffile> <size> [bw]     Import a .ttf font into a data file.\n"
    "   import_bdf <bdffile>                 Import a .bdf font into a data file.\n"
    "\n"
    "Commands for inspecting and editing data files:\n"
    "   filter <datfile> <range> ...         Remove everything except specified characters.\n"
    "   show_glyph <datfile> <index>         Show the glyph at index.\n"
    "\n"
    "Commands specific to rlefont format:\n"
    "   rlefont_size <datfile>               Check the encoded size of the data file.\n"
    "   rlefont_optimize <datfile>           Perform an optimization pass on the data file.\n"
    "   rlefont_export <datfile> [outfile]   Export to .c source code.\n"
    "   rlefont_show_encoded <datfile>       Show the encoded data for debugging.\n"
    "\n"
    "Commands specific to bwfont format:\n"
    "   bwfont_export <datfile> [outfile     Export to .c source code.\n"
    "";

typedef status_t (*cmd_t)(const std::vector<std::string> &args);
static const std::map<std::string, cmd_t> command_list {
    {"import_ttf",              cmd_import_ttf},
    {"import_bdf",              cmd_import_bdf},
    {"filter",                  cmd_filter},
    {"show_glyph",              cmd_show_glyph},
    {"rlefont_size",            cmd_rlefont_size},
    {"rlefont_optimize",        cmd_rlefont_optimize},
    {"rlefont_export",          cmd_rlefont_export},
    {"rlefont_show_encoded",    cmd_rlefont_show_encoded},
    {"bwfont_export",           cmd_bwfont_export},
};

int main(int argc, char **argv)
{
    std::vector<std::string> args;
    for (int i = 1; i < argc; i++)
        args.push_back(argv[i]);
    
    status_t status = STATUS_INVALID;
    if (args.size() >= 1 && command_list.count(args.at(0)))
    {
        status = command_list.find(args.at(0))->second(args);
    }
    
    if (status == STATUS_INVALID)
    {
        std::cout << usage_msg << std::endl;
    }
    
    return status;
}
