#include "pool.hpp"
#include "common/object_descr.hpp"
#include "entity.hpp"
#include "package.hpp"
#include "padstack.hpp"
#include "part.hpp"
#include "symbol.hpp"
#include "unit.hpp"
#include "pool_manager.hpp"
#include <glibmm/fileutils.h>
#include <glibmm/miscutils.h>
#include <giomm.h>

namespace horizon {

Pool::Pool(const std::string &bp, bool read_only)
    : db(bp + "/pool.db", read_only ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE), base_path(bp)
{
}

const UUID Pool::tmp_pool_uuid = "5e8d1bb6-7e61-4c59-9f01-1e1307069df0";

Pool::~Pool()
{
}

void Pool::clear()
{
    units.clear();
    symbols.clear();
    entities.clear();
    padstacks.clear();
    packages.clear();
    parts.clear();
    frames.clear();
    decals.clear();
    pool_uuid_cache.clear();
}

std::string Pool::get_filename(ObjectType type, const UUID &uu, UUID *pool_uuid_out)
{
    std::string query;
    switch (type) {
    case ObjectType::UNIT:
        query = "SELECT filename, pool_uuid FROM units WHERE uuid = ?";
        break;

    case ObjectType::ENTITY:
        query = "SELECT filename, pool_uuid FROM entities WHERE uuid = ?";
        break;

    case ObjectType::SYMBOL:
        query = "SELECT filename, pool_uuid FROM symbols WHERE uuid = ?";
        break;

    case ObjectType::PACKAGE:
        query = "SELECT filename, pool_uuid FROM packages WHERE uuid = ?";
        break;

    case ObjectType::PADSTACK:
        query = "SELECT filename, pool_uuid FROM padstacks WHERE uuid = ?";
        break;

    case ObjectType::PART:
        query = "SELECT filename, pool_uuid FROM parts WHERE uuid = ?";
        break;

    case ObjectType::FRAME:
        query = "SELECT filename, pool_uuid FROM frames WHERE uuid = ?";
        break;

    case ObjectType::DECAL:
        query = "SELECT filename, pool_uuid FROM decals WHERE uuid = ?";
        break;

    default:
        return "";
    }
    SQLite::Query q(db, query);
    q.bind(1, uu);
    if (!q.step()) {
        auto tf = get_tmp_filename(type, uu);

        if (tf.size() && Glib::file_test(tf, Glib::FILE_TEST_IS_REGULAR)) {
            if (pool_uuid_out)
                *pool_uuid_out = Pool::tmp_pool_uuid;
            return tf;
        }
        else {
            throw std::runtime_error(object_descriptions.at(type).name + " " + (std::string)uu + " not found");
        }
    }
    auto filename = q.get<std::string>(0);
    std::string bp = base_path;

    UUID other_pool_uuid(q.get<std::string>(1));
    if (pool_uuid_out)
        *pool_uuid_out = other_pool_uuid;
    pool_uuid_cache.emplace(std::piecewise_construct, std::forward_as_tuple(type, uu),
                            std::forward_as_tuple(other_pool_uuid));
    const auto other_pool_info = PoolManager::get().get_by_uuid(other_pool_uuid);
    UUID this_pool_uuid;
    const auto &pools = PoolManager::get().get_pools();
    if (pools.count(base_path))
        this_pool_uuid = pools.at(base_path).uuid;

    if (other_pool_info && this_pool_uuid
        && this_pool_uuid != other_pool_info->uuid) // don't override if item is in local pool
        bp = other_pool_info->base_path;

    switch (type) {
    case ObjectType::UNIT:
    case ObjectType::ENTITY:
    case ObjectType::SYMBOL:
    case ObjectType::PACKAGE:
    case ObjectType::PADSTACK:
    case ObjectType::PART:
    case ObjectType::FRAME:
    case ObjectType::DECAL:
        return Glib::build_filename(bp, filename);

    default:
        return "";
    }
}

const std::string &Pool::get_base_path() const
{
    return base_path;
}

int Pool::get_required_schema_version()
{ // keep in sync with schema definition
    return 17;
}

std::string Pool::get_tmp_filename(ObjectType type, const UUID &uu) const
{
    auto suffix = static_cast<std::string>(uu) + ".json";
    auto base = Glib::build_filename(Glib::get_tmp_dir(), "horizon-tmp");
    if (!Glib::file_test(base, Glib::FILE_TEST_IS_DIR)) {
        Gio::File::create_for_path(base)->make_directory();
    }
    return Glib::build_filename(base, get_flat_filename(type, uu));
}

std::string Pool::get_flat_filename(ObjectType type, const UUID &uu) const
{
    auto suffix = static_cast<std::string>(uu) + ".json";
    switch (type) {
    case ObjectType::UNIT:
        return "unit_" + suffix;

    case ObjectType::ENTITY:
        return "entity_" + suffix;

    case ObjectType::SYMBOL:
        return "sym_" + suffix;

    case ObjectType::PACKAGE:
        return "pkg_" + suffix;

    case ObjectType::PADSTACK:
        return "ps_" + suffix;

    case ObjectType::PART:
        return "part_" + suffix;

    case ObjectType::FRAME:
        return "frame_" + suffix;

    case ObjectType::DECAL:
        return "decal_" + suffix;

    default:
        return "";
    }
}

void Pool::get_pool_uuid(ObjectType type, const UUID &uu, UUID *pool_uuid_out)
{
    if (pool_uuid_out)
        *pool_uuid_out = pool_uuid_cache.at(std::make_pair(type, uu));
}

const Unit *Pool::get_unit(const UUID &uu, UUID *pool_uuid_out)
{
    if (units.count(uu) == 0) {
        std::string path = get_filename(ObjectType::UNIT, uu, pool_uuid_out);
        Unit u = Unit::new_from_file(path);
        units.insert(std::make_pair(uu, u));
    }
    else {
        get_pool_uuid(ObjectType::UNIT, uu, pool_uuid_out);
    }
    return &units.at(uu);
}

const Entity *Pool::get_entity(const UUID &uu, UUID *pool_uuid_out)
{
    if (entities.count(uu) == 0) {
        std::string path = get_filename(ObjectType::ENTITY, uu, pool_uuid_out);
        Entity e = Entity::new_from_file(path, *this);
        entities.insert(std::make_pair(uu, e));
    }
    else {
        get_pool_uuid(ObjectType::ENTITY, uu, pool_uuid_out);
    }
    return &entities.at(uu);
}

const Symbol *Pool::get_symbol(const UUID &uu, UUID *pool_uuid_out)
{
    if (symbols.count(uu) == 0) {
        std::string path = get_filename(ObjectType::SYMBOL, uu, pool_uuid_out);
        Symbol s = Symbol::new_from_file(path, *this);
        symbols.insert(std::make_pair(uu, s));
    }
    else {
        get_pool_uuid(ObjectType::SYMBOL, uu, pool_uuid_out);
    }
    return &symbols.at(uu);
}

const Package *Pool::get_package(const UUID &uu, UUID *pool_uuid_out)
{
    if (packages.count(uu) == 0) {
        std::string path = get_filename(ObjectType::PACKAGE, uu, pool_uuid_out);
        Package p = Package::new_from_file(path, *this);
        packages.emplace(uu, p);
    }
    else {
        get_pool_uuid(ObjectType::PACKAGE, uu, pool_uuid_out);
    }
    return &packages.at(uu);
}

const Padstack *Pool::get_well_known_padstack(const std::string &name, UUID *pool_uuid_out)
{
    SQLite::Query q(db, "SELECT uuid FROM padstacks WHERE well_known_name = ?");
    q.bind(1, name);
    if (q.step()) {
        UUID uu = q.get<std::string>(0);
        return get_padstack(uu, pool_uuid_out);
    }
    else {
        return nullptr;
    }
}

const Padstack *Pool::get_padstack(const UUID &uu, UUID *pool_uuid_out)
{
    if (padstacks.count(uu) == 0) {
        std::string path = get_filename(ObjectType::PADSTACK, uu, pool_uuid_out);
        Padstack p = Padstack::new_from_file(path);
        padstacks.insert(std::make_pair(uu, p));
    }
    else {
        get_pool_uuid(ObjectType::PADSTACK, uu, pool_uuid_out);
    }
    return &padstacks.at(uu);
}

const Part *Pool::get_part(const UUID &uu, UUID *pool_uuid_out)
{
    if (parts.count(uu) == 0) {
        std::string path = get_filename(ObjectType::PART, uu, pool_uuid_out);
        Part p = Part::new_from_file(path, *this);
        parts.insert(std::make_pair(uu, p));
    }
    else {
        get_pool_uuid(ObjectType::PART, uu, pool_uuid_out);
    }
    return &parts.at(uu);
}

const Frame *Pool::get_frame(const UUID &uu, UUID *pool_uuid_out)
{
    if (frames.count(uu) == 0) {
        std::string path = get_filename(ObjectType::FRAME, uu, pool_uuid_out);
        Frame f = Frame::new_from_file(path);
        frames.insert(std::make_pair(uu, f));
    }
    else {
        get_pool_uuid(ObjectType::FRAME, uu, pool_uuid_out);
    }
    return &frames.at(uu);
}

const Decal *Pool::get_decal(const UUID &uu, UUID *pool_uuid_out)
{
    if (decals.count(uu) == 0) {
        std::string path = get_filename(ObjectType::DECAL, uu, pool_uuid_out);
        Decal d = Decal::new_from_file(path);
        decals.insert(std::make_pair(uu, d));
    }
    else {
        get_pool_uuid(ObjectType::DECAL, uu, pool_uuid_out);
    }
    return &decals.at(uu);
}

std::set<UUID> Pool::get_alternate_packages(const UUID &uu)
{
    std::set<UUID> r;
    SQLite::Query q(db, "SELECT uuid FROM packages WHERE alternate_for = ?");
    q.bind(1, uu);
    while (q.step()) {
        r.insert(q.get<std::string>(0));
    }
    return r;
}

std::string Pool::get_model_filename(const UUID &pkg_uuid, const UUID &model_uuid)
{
    UUID pool_uuid;
    auto pkg = get_package(pkg_uuid, &pool_uuid);
    auto model = pkg->get_model(model_uuid);
    auto pool = PoolManager::get().get_by_uuid(pool_uuid);
    if (model && pool) {
        return Glib::build_filename(pool->base_path, model->filename);
    }
    else {
        return "";
    }
}

static std::string get_dir_for_type(ObjectType type)
{
    using O = ObjectType;
    switch (type) {
    case O::UNIT:
        return "units";

    case O::SYMBOL:
        return "symbols";

    case O::ENTITY:
        return "entities";

    case O::PADSTACK:
        return "padstacks";

    case O::PACKAGE:
        return "packages";

    case O::PART:
        return "parts";

    case O::FRAME:
        return "frames";

    case O::DECAL:
        return "decals";

    default:
        return "";
    }
}

static bool check_type(ObjectType type)
{
    return get_dir_for_type(type).size();
}

bool Pool::check_filename(ObjectType type, const std::string &filename, std::string *error_msg) const
{
    if (!check_type(type)) {
        if (error_msg)
            *error_msg = "unsupported object type";
        return false;
    }
    if (type == ObjectType::PADSTACK) {
        const std::string bp_ps = Glib::build_filename(base_path, get_dir_for_type(type));
        if (Gio::File::create_for_path(filename)->has_prefix(Gio::File::create_for_path(bp_ps)))
            return true;
        const std::string bp_pkg = Glib::build_filename(base_path, get_dir_for_type(type));
        if (Gio::File::create_for_path(filename)->has_prefix(Gio::File::create_for_path(bp_pkg))) {
            return true;
        }
        if (error_msg)
            *error_msg = "incorrect directory";
        return false;
    }
    else {
        const std::string bp = Glib::build_filename(base_path, get_dir_for_type(type));
        auto r = Gio::File::create_for_path(filename)->has_prefix(Gio::File::create_for_path(bp));
        if (!r) {
            if (error_msg)
                *error_msg = "incorrect directory";
        }
        return r;
    }
}


} // namespace horizon
