#include "loader.h"
#include <fstream>
#include <streambuf>
#include <cstring>
#include <SparrowRenderer/image.h>
#include "../resourcemanager.h"
#include <SFML/Graphics/Image.hpp>
#include <SparrowRenderer/mesh.h>
#include <SparrowRenderer/pbrmaterial.h>
#include "SparrowRenderer/texture.h"
#include "utils.h"
#include "font.h"

#include <iostream>

std::string Loader::obj_directory = "";
std::string Loader::mtl_directory = "";
std::string Loader::tex_directory = "";

std::string* Loader::loadTextFile(const std::string &filename)
{
    std::ifstream t(filename);
    std::string *str = new std::string();

    t.seekg(0, std::ios::end);
    str->reserve(t.tellg());
    t.seekg(0, std::ios::beg);

    str->assign((std::istreambuf_iterator<char>(t)),
                std::istreambuf_iterator<char>());
    return str;
}

std::unordered_map<std::string, std::string>* Loader::loadConfigFile(const std::string &filename)
{
    std::unordered_map<std::string, std::string>* configPtr = new std::unordered_map<std::string, std::string>();
    std::unordered_map<std::string, std::string>& config = *configPtr;

    std::ifstream t(filename);

    if(!t.is_open())
        return configPtr;

    char line[256];
    std::string currentLine;

    while(!t.eof())
    {
        currentLine = "";
        t.getline(line, 256);
        currentLine += std::string(line);

        if(t.eof() || t.fail() || currentLine[0] == '#')
            continue;

        unsigned int pos = currentLine.find_first_of('=');
        if(pos == std::string::npos)
            continue;

        std::string key = currentLine.substr(0, pos);
        std::string val = currentLine.substr(pos+1);
        config[key] = val;
    }
    return configPtr;
}

Image* Loader::loadImage(const std::string &filename, int depth, bool reversed)
{
    sf::Image sfImg;
    bool ok = sfImg.loadFromFile(tex_directory+filename);
    if(!ok)
        return NULL;
    if(reversed)
        sfImg.flipVertically();
    Image* img = new Image();
    img->depth = depth;
    img->width = sfImg.getSize().x;
    img->height = sfImg.getSize().y;
    int size = img->width*img->height*(img->depth/8);
    img->allocate(size);
    const sf::Uint8 *pixels = sfImg.getPixelsPtr();
    if(depth == 32)
        memcpy(img->pixels.data(), pixels, size);
    else
    {
        int bytesPerPixel = depth/8;
        sf::Uint8 *ptr = (sf::Uint8*)img->pixels.data();
        for(int i=0; i<img->width*img->height; ++i)
            memcpy(ptr + i*bytesPerPixel, pixels + i*4, bytesPerPixel);
    }
    return img;
}

Font* Loader::loadFont(const std::string &description_file, const std::string &texture_file)
{
    std::string line;
    std::ifstream file(description_file);

    if(!file)
    {
        fprintf(stderr, "can't load '%s'.\n", description_file.c_str());
        std::cerr << "Error code: " << strerror(errno) << std::endl;
        return nullptr;
    }

    Font* font = new Font();
    std::getline(file, line);
    char font_n[256];
    std::sscanf(line.c_str(),"info face=\"%[^\"]",font_n);
    std::string font_name(font_n);
    font->setName(font_name);

    std::getline(file, line);
    float line_h, base;
    glm::vec2 scale;
    std::sscanf(line.c_str(),"common lineHeight=%f base=%f scaleW=%f scaleH=%f",
                &line_h,&base,&(scale.x),&(scale.y));
    font->setLineHeight(line_h);
    font->setBase(base);
    font->setScale(1.f/scale);

    std::getline(file, line);//ignore 3rd line for now => only use 1 page.
    std::getline(file, line);
    int nbchar;
    std::sscanf(line.c_str(),"chars count=%d",&nbchar);
    font->setNbChar(nbchar);
    while(!file.eof()){
        std::getline(file, line);
        Font::CharInfo char_info;
        int id;
        sscanf(line.c_str(),"char id=%d x=%f y=%f width=%f height=%f xoffset=%f yoffset=%f xadvance=%f",
               &id, &(char_info.pos.x),&(char_info.pos.y),&(char_info.dim.x), &(char_info.dim.y),
               &(char_info.offset.x), &(char_info.offset.y), &(char_info.xadvance));
        font->addCharInfo(id,char_info);
    }

    Image* fucking_image_of_doom = loadImage(texture_file, 32, false);
    if(fucking_image_of_doom == NULL)
        printf("can't load \"%s\".\n", texture_file.c_str());
    Texture* texture = new Texture(fucking_image_of_doom, false); // mipmaps are doing a very bad job at interpolating alpha component
    delete fucking_image_of_doom;
    font->setTexture(texture);
    return font;
}

std::vector<Mesh*> Loader::loadMesh(const std::string &filename){
    std::vector<Mesh*> meshes;

    std::vector<glm::vec3> pos;
    std::vector<glm::vec3> norm;
    std::vector<glm::vec2> tex;

    std::string line;

    Material* defaultMat = RESOURCE_GET(Material, "default");

    if(defaultMat == NULL)
    {
        defaultMat = new PBRMaterial();
        RESOURCE_ADD(defaultMat, Material, "default");
    }
    Material* currentMat = defaultMat;
    std::ifstream file(obj_directory + filename);

    if(!file.is_open())
    {
        fprintf(stderr, "can't load %s.\n", filename.c_str());
        return meshes;
    }

    Mesh* currentMesh = new Mesh();
    meshes.push_back(currentMesh);
    currentMesh->setMaterial(currentMat);

    std::getline(file, line);
    while(!file.eof())
    {
        if(line.length() == 0)  // line vide
        {
            std::getline(file, line);
            continue;
        }

        switch(line[0])
        {
        case 'v':
            //vertex attribute
            switch(line[1])
            {
            case ' ': // vertex position
            {
                glm::vec3 p;
                std::sscanf(line.c_str(),"v %f %f %f",&(p.x),&(p.y),&(p.z));
                pos.push_back(p);
                break;
            }
            case 't': // texCoord
            {
                glm::vec2 t;
                std::sscanf(line.c_str(),"vt %f %f",&(t.x),&(t.y));
                tex.push_back(t);
                break;
            }
            case 'n':    // normal
            {
                glm::vec3 n;
                std::sscanf(line.c_str(),"vn %f %f %f",&(n.x),&(n.y),&(n.z));
                norm.push_back(n);
                break;
            }
            }
            break;
        case 'f': // face
        {
            int tab[9];
            std::sscanf(line.c_str(),"f %d/%d/%d %d/%d/%d %d/%d/%d",tab,tab+1,tab+2,tab+3,tab+4,tab+5,tab+6,tab+7,tab+8);
            //TODO: check sscanf success

            int nb_vertices = currentMesh->positions3D.size();

            currentMesh->addTriangle(nb_vertices, nb_vertices+1, nb_vertices+2);
            for(int i=0; i<3; ++i)
            {
                int offset = i*3;
                if(norm.size() == 0)
                {
                    if(tex.size() == 0)
                        currentMesh->addVertex(pos[tab[offset]-1]);
                    else
                        currentMesh->addVertex(pos[tab[offset]-1], tex[tab[offset+1]-1]);
                }
                else
                {
                    if(tex.size() == 0)
                        currentMesh->addVertex(pos[tab[offset]-1], norm[tab[offset+2]-1]);
                    else
                        currentMesh->addVertex(pos[tab[offset]-1], norm[tab[offset+2]-1], tex[tab[offset+1]-1]);
                }
            }
        }
            break;
        case 'g':
            currentMesh = new Mesh();
            meshes.push_back(currentMesh);
            currentMesh->setMaterial(currentMat);
            break;
        case 'm': // mtllib
        {
            char mat_filename[256];
            std::sscanf(line.c_str(),"mtllib %s",mat_filename);
            loadMTL(std::string(mat_filename));
            break;
        }
        case 'u':
        {
            // usemtl
            char mat_name[256];
            std::sscanf(line.c_str(),"usemtl %s",mat_name);
            std::string material_name(mat_name);
            currentMat = RESOURCE_GET(Material, material_name);
            if(currentMat == NULL)
            {
                fprintf(stderr, "cannot find any material named : %s.\n", material_name.c_str());
                currentMat = new PBRMaterial();
                RESOURCE_ADD(currentMat, Material, material_name);
            }
            currentMesh->setMaterial(currentMat);
            currentMesh->setName(material_name);
        }
            break;
        default:
        case '#':
            // comment
            break;
        }
        std::getline(file,line);
    }

    for(std::size_t i=0; i<meshes.size(); ++i)
    {
        if(meshes[i]->indices.size() == 0)
        {
            meshes[i] = meshes.back();
            meshes.pop_back();
            --i;
        }
        else
        {
            Mesh* m = meshes[i];
            if(m->normals.empty())
                m->computeNormals();
            if(m->getFlags() & (1 << Mesh::MATERIAL_PBR_NORMAL_MAP))
                m->computeTangents();
            m->mergeVertices();
        }
    }
    return meshes;
}

//load MTL
bool Loader::loadMTL(const std::string &filename)
{
    std::string line;
    std::ifstream file(mtl_directory + filename);

    if(!file.is_open())
    {
        fprintf(stderr, "can't load %s.\n", filename.c_str());
        return false;
    }

    PBRMaterial* mat = NULL;
    bool hasNormalMap = false;

    std::getline(file,line);

    while(!file.eof())
    {
        if(line.length() == 0)
        {
            std::getline(file,line);
            continue;
        }
        //QStringList tokens = line.split(' ');
        std::vector<std::string> tokens = utils::split(line,' ');

        if(tokens[0].substr(0,1) == "#")
        {
                // this is a comment
        }
        else if((tokens[0] == "newmtl") && tokens.size() == 2)
        {
            mat = new PBRMaterial();
            RESOURCE_ADD(mat, Material, tokens[1]);
        }
        else if((tokens[0].compare("emission") == 0) && tokens.size() == 4)
        {
            mat->emission.r = std::stof(tokens[1]);
            mat->emission.g = std::stof(tokens[2]);
            mat->emission.b = std::stof(tokens[3]);
        }
        else if(tokens[0].compare("albedo") == 0 && tokens.size() == 4)
        {
            mat->albedo.r = std::stof(tokens[1]);
            mat->albedo.g = std::stof(tokens[2]);
            mat->albedo.b = std::stof(tokens[3]);
        }
        else if(tokens[0].compare("roughness") == 0 && tokens.size() == 2)
        {
            mat->roughness = std::stof(tokens[1]);
        }
        else if(tokens[0].compare("metallic") == 0 && tokens.size() == 2)
        {
            mat->metallic = std::stof(tokens[1]);
        }
        else if((tokens[0].substr(0,4) == "map_") && tokens.size() == 2)
        {
            if(tokens[0].compare("map_emission") == 0){
                mat->textures[PBRMaterial::EMISSION_SLOT] = RESOURCE_GET(Texture,tokens[1]);
                if (mat->textures[PBRMaterial::EMISSION_SLOT] == NULL){
                    mat->textures[PBRMaterial::EMISSION_SLOT] = new Texture(loadImage(tokens[1], 24));
                    RESOURCE_ADD(mat->textures[PBRMaterial::EMISSION_SLOT], Texture, tokens[1]);
                }
            } else if(tokens[0].compare("map_albedo") == 0) {
                mat->textures[PBRMaterial::ALBEDO_SLOT] = RESOURCE_GET(Texture,tokens[1]);
                if (mat->textures[PBRMaterial::ALBEDO_SLOT] == NULL){
                    mat->textures[PBRMaterial::ALBEDO_SLOT] = new Texture(loadImage(tokens[1]));
                    RESOURCE_ADD(mat->textures[PBRMaterial::ALBEDO_SLOT],Texture,tokens[1]);
                }
            } else if(tokens[0].compare("map_roughness") == 0) {
                mat->textures[PBRMaterial::ROUGHNESS_SLOT] = RESOURCE_GET(Texture,tokens[1]);
                if (mat->textures[PBRMaterial::ROUGHNESS_SLOT] == NULL){
                    mat->textures[PBRMaterial::ROUGHNESS_SLOT] = new Texture(loadImage(tokens[1], 8));
                    RESOURCE_ADD(mat->textures[PBRMaterial::ROUGHNESS_SLOT],Texture,tokens[1]);
                }
            } else if(tokens[0].compare("map_metallic") == 0) {
                mat->textures[PBRMaterial::METALLIC_SLOT] = RESOURCE_GET(Texture,tokens[1]);
                if (mat->textures[PBRMaterial::METALLIC_SLOT] == NULL){
                    mat->textures[PBRMaterial::METALLIC_SLOT] = new Texture(loadImage(tokens[1], 8));
                    RESOURCE_ADD(mat->textures[PBRMaterial::METALLIC_SLOT],Texture,tokens[1]);
                }
            } else if(tokens[0].compare("map_normal") == 0) {
                mat->textures[PBRMaterial::NORMALS_SLOT] = RESOURCE_GET(Texture,tokens[1]);
                if (mat->textures[PBRMaterial::NORMALS_SLOT] == NULL){
                    mat->textures[PBRMaterial::NORMALS_SLOT] = new Texture(loadImage(tokens[1], 24));
                    RESOURCE_ADD(mat->textures[PBRMaterial::NORMALS_SLOT],Texture,tokens[1]);
                }
                hasNormalMap = true;
            } else if(tokens[0].compare("map_alpha") == 0) {
                mat->textures[PBRMaterial::ALPHA_SLOT] = RESOURCE_GET(Texture,tokens[1]);
                if (mat->textures[PBRMaterial::ALPHA_SLOT] == NULL){
                    mat->textures[PBRMaterial::ALPHA_SLOT] = new Texture(loadImage(tokens[1], 8));
                    RESOURCE_ADD(mat->textures[PBRMaterial::ALPHA_SLOT],Texture,tokens[1]);
                }
            } else
                fprintf(stderr, "unsupported material property : \"%s\"\n", tokens[0].c_str());
        }
        else
            fprintf(stderr, "unsupported material property : \"%s\"\n", tokens[0].c_str());

        std::getline(file,line);
    }
    return hasNormalMap;
}

void Loader::setObjDirectory(std::string dir_){
    obj_directory = dir_;
}
void Loader::setMtlDirectory(std::string dir_){
    mtl_directory = dir_;
}
void Loader::setTexDirectory(std::string dir_){
    tex_directory = dir_;
}
