/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <iostream>
#include <string>
#include <map>
#include <vector>

#include <getopt/getopt.h>

#include <utils/Path.h>

#include <filament/Engine.h>
#include <filament/LightManager.h>
#include <filament/Material.h>
#include <filament/MaterialInstance.h>
#include <filament/RenderableManager.h>
#include <filament/Scene.h>
#include <filament/Texture.h>
#include <filament/TextureSampler.h>
#include <filament/TransformManager.h>

#include <math/mat3.h>
#include <math/mat4.h>
#include <math/vec4.h>

#include <filamentapp/Config.h>
#include <filamentapp/FilamentApp.h>

#include <stb_image.h>

#include <utils/EntityManager.h>

#include <filamat/MaterialBuilder.h>
#include <filameshio/MeshReader.h>

using namespace filament::math;
using namespace filament;
using namespace filamesh;
using namespace filamat;
using namespace utils;

static std::vector<Path> g_filenames;

static MeshReader::MaterialRegistry g_materialInstances;
static std::vector<MeshReader::Mesh> g_meshes;
static const Material* g_material;
static Entity g_light;
static std::map<std::string, Texture*> g_maps;

static Config g_config;

static void printUsage(char* name) {
    std::string exec_name(Path(name).getName());
    std::string usage(
            "SAMPLE_CLOTH is an example of cloth rendering\n"
            "Usage:\n"
            "    SAMPLE_CLOTH [options] <filamesh input files>\n"
            "Options:\n"
            "   --help, -h\n"
            "       Prints this message\n\n"
            "   --ibl=<path to cmgen IBL>, -i <path>\n"
            "       Applies an IBL generated by cmgen's deploy option\n\n"
            "   --split-view, -v\n"
            "       Splits the window into 4 views\n\n"
            "   --scale=[number], -s [number]\n"
            "       Applies uniform scale\n\n"
    );
    const std::string from("SAMPLE_CLOTH");
    for (size_t pos = usage.find(from); pos != std::string::npos; pos = usage.find(from, pos)) {
        usage.replace(pos, from.length(), exec_name);
    }
    std::cout << usage;
}

static int handleCommandLineArgments(int argc, char* argv[], Config* config) {
    static constexpr const char* OPTSTR = "hi:vs:";
    static const struct option OPTIONS[] = {
            { "help",           no_argument,       0, 'h' },
            { "ibl",            required_argument, 0, 'i' },
            { "split-view",     no_argument,       0, 'v' },
            { "scale",          required_argument, 0, 's' },
            { 0, 0, 0, 0 }  // termination of the option list
    };
    int opt;
    int option_index = 0;
    while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &option_index)) >= 0) {
        std::string arg(optarg ? optarg : "");
        switch (opt) {
            default:
            case 'h':
                printUsage(argv[0]);
                exit(0);
            case 'i':
                config->iblDirectory = arg;
                break;
            case 's':
                try {
                    config->scale = std::stof(arg);
                } catch (std::invalid_argument& e) {
                    // keep scale of 1.0
                } catch (std::out_of_range& e) {
                    // keep scale of 1.0
                }
                break;
            case 'v':
                config->splitView = true;
                break;
        }
    }

    return optind;
}

static void cleanup(Engine* engine, View* view, Scene* scene) {
    for (auto map : g_maps) {
        engine->destroy(map.second);
    }
    std::vector<filament::MaterialInstance*> materialList(g_materialInstances.numRegistered());
    g_materialInstances.getRegisteredMaterials(materialList.data());
    for (auto material : materialList) {
        engine->destroy(material);
    }
    g_materialInstances.unregisterAll();
    engine->destroy(g_material);
    EntityManager& em = EntityManager::get();
    for (auto mesh : g_meshes) {
        engine->destroy(mesh.vertexBuffer);
        engine->destroy(mesh.indexBuffer);
        engine->destroy(mesh.renderable);
        em.destroy(mesh.renderable);
    }
    engine->destroy(g_light);
    em.destroy(g_light);
}

Texture* loadMap(Engine* engine, const char* name, bool sRGB = true) {
    Path path(name);
    if (path.exists()) {
        int w, h, n;
        unsigned char* data = stbi_load(path.getAbsolutePath().c_str(), &w, &h, &n, 3);
        if (data != nullptr) {
            Texture* map = Texture::Builder()
                    .width(uint32_t(w))
                    .height(uint32_t(h))
                    .levels(0xff)
                    .format(sRGB ? Texture::InternalFormat::SRGB8 : Texture::InternalFormat::RGB8)
                    .build(*engine);
            Texture::PixelBufferDescriptor buffer(data, size_t(w * h * 3),
                    Texture::Format::RGB, Texture::Type::UBYTE,
                    (Texture::PixelBufferDescriptor::Callback) &stbi_image_free);
            map->setImage(*engine, 0, std::move(buffer));
            map->generateMipmaps(*engine);
            g_maps[name] = map;
            return map;
        } else {
            std::cout << "The map " << path << " could not be loaded" << std::endl;
        }
    } else {
        std::cout << "The map " << path << " does not exist" << std::endl;
    }
    return nullptr;
}

static void setup(Engine* engine, View* view, Scene* scene) {
    Texture* normal = loadMap(engine, "normal.png", false);
    Texture* basecolor = loadMap(engine, "basecolor.png");
    Texture* roughness = loadMap(engine, "roughness.png", false);

    if (!basecolor || !normal || !roughness) {
        std::cout << "Need basecolor.png, normal.png and roughness.png" << std::endl;
        return;
    }

    MaterialBuilder::init();
    MaterialBuilder builder = MaterialBuilder()
            .name("DefaultMaterial")
            .require(VertexAttribute::UV0)
            .parameter(MaterialBuilder::SamplerType::SAMPLER_2D, "normalMap")
            .parameter(MaterialBuilder::SamplerType::SAMPLER_2D, "basecolorMap")
            .parameter(MaterialBuilder::SamplerType::SAMPLER_2D, "roughnessMap")
            .material(R"SHADER(
                void material(inout MaterialInputs material) {
                    vec2 uv = getUV0() * 2.0;
                    material.normal = texture(materialParams_normalMap, uv).xyz * 2.0 - 1.0;
                    prepareMaterial(material);

                    vec3 baseColor = texture(materialParams_basecolorMap, uv).rgb;
                    float luma = dot(baseColor, vec3(0.2126, 0.7152, 0.0722));

                    material.baseColor.rgb = baseColor;
                    material.roughness = texture(materialParams_roughnessMap, uv).r;
                    material.sheenColor = vec3(luma) * 0.5;
                }
            )SHADER")
            .shading(Shading::CLOTH);

    Package pkg = builder.build(engine->getJobSystem());

    g_material = Material::Builder().package(pkg.getData(), pkg.getSize())
            .build(*engine);
    const utils::CString defaultMaterialName("DefaultMaterial");
    g_materialInstances.registerMaterialInstance(defaultMaterialName, g_material->createInstance());

    TextureSampler sampler(TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR,
            TextureSampler::MagFilter::LINEAR, TextureSampler::WrapMode::REPEAT);
    sampler.setAnisotropy(8.0f);
    g_materialInstances.getMaterialInstance(defaultMaterialName)->setParameter("normalMap", normal, sampler);
    g_materialInstances.getMaterialInstance(defaultMaterialName)->setParameter("basecolorMap", basecolor, sampler);
    g_materialInstances.getMaterialInstance(defaultMaterialName)->setParameter("roughnessMap", roughness, sampler);

    auto& tcm = engine->getTransformManager();
    for (const auto& filename : g_filenames) {
        MeshReader::Mesh mesh  = MeshReader::loadMeshFromFile(engine, filename, g_materialInstances);
        if (mesh.renderable) {
            auto ei = tcm.getInstance(mesh.renderable);
            tcm.setTransform(ei, mat4f{ mat3f(g_config.scale), float3(0.0f, 0.0f, -4.0f) } *
                                 tcm.getWorldTransform(ei));
            scene->addEntity(mesh.renderable);
            g_meshes.push_back(mesh);
        }
    }

    g_light = EntityManager::get().create();
    LightManager::Builder(LightManager::Type::DIRECTIONAL)
            .color(Color::toLinear<ACCURATE>({0.98f, 0.92f, 0.89f}))
            .intensity(110000)
            .direction({0.6, -1, -0.8})
            .build(*engine, g_light);
    scene->addEntity(g_light);
}

int main(int argc, char* argv[]) {
    int option_index = handleCommandLineArgments(argc, argv, &g_config);
    int num_args = argc - option_index;
    if (num_args < 1) {
        printUsage(argv[0]);
        return 1;
    }

    for (int i = option_index; i < argc; i++) {
        utils::Path filename = argv[i];
        if (!filename.exists()) {
            std::cerr << "file " << argv[i] << " not found!" << std::endl;
            return 1;
        }
        g_filenames.push_back(filename);
    }

    g_config.title = "Cloth shading";
    FilamentApp& filamentApp = FilamentApp::get();
    filamentApp.run(g_config, setup, cleanup);

    return 0;
}
