/*
 * Copyright (C) 2019 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 <filaflat/ChunkContainer.h>

#include <filament/MaterialEnums.h>

#include <backend/DriverEnums.h>

#include <matdbg/JsonWriter.h>
#include <matdbg/ShaderInfo.h>

#include "CommonWriter.h"

#include <private/filament/Variant.h>

#include <iomanip>
#include <sstream>

using namespace filament;
using namespace backend;
using namespace filaflat;
using namespace filamat;
using namespace std;
using namespace utils;

namespace filament {
namespace matdbg {

static std::string arraySizeToString(uint64_t size) {
    if (size > 1) {
        std::string s = "[";
        s += size;
        s += "]";
        return s;
    }
    return "";
}

template<typename T, typename V>
static void printChunk(ostream& json, const ChunkContainer& container, ChunkType type,
        const char* title) {
    T value;
    if (read(container, type, reinterpret_cast<V*>(&value))) {
        json << "\"" << title << "\": \"" << toString(value) << "\",\n";
    }
}

static void printFloatChunk(ostream& json, const ChunkContainer& container, ChunkType type,
        const char* title) {
    float value;
    if (read(container, type, &value)) {
        json << "\"" << title << "\": " << setprecision(2) << value << ",\n";
    }
}

static void printUint32Chunk(ostream& json, const ChunkContainer& container,
        ChunkType type, const char* title) {
    uint32_t value;
    if (read(container, type, &value)) {
        json << "\"" << title << "\": " << value << ",\n";
    }
}

static void printStringChunk(ostream& json, const ChunkContainer& container,
        ChunkType type, const char* title) {
    CString value;
    if (read(container, type, &value)) {
        json << "\"" << title << "\": \"" << value.c_str_safe() << "\",\n";
    }
}

static bool printMaterial(ostream& json, const ChunkContainer& container) {
    printStringChunk(json, container, MaterialName, "name");
    printUint32Chunk(json, container, MaterialVersion, "version");
    json << "\"shading\": {\n";
    printChunk<Shading, uint8_t>(json, container, MaterialShading, "model");
    printChunk<MaterialDomain, uint8_t>(json, container, ChunkType::MaterialDomain, "material_domain");
    printChunk<VertexDomain, uint8_t>(json, container, MaterialVertexDomain, "vertex_domain");
    printChunk<Interpolation, uint8_t>(json, container, MaterialInterpolation, "interpolation");
    printChunk<bool, bool>(json, container, MaterialShadowMultiplier, "shadow_multiply");
    printChunk<bool, bool>(json, container, MaterialSpecularAntiAliasing, "specular_antialiasing");
    printFloatChunk(json, container, MaterialSpecularAntiAliasingVariance, "variance");
    printFloatChunk(json, container, MaterialSpecularAntiAliasingThreshold, "threshold");
    printChunk<bool, bool>(json, container, MaterialClearCoatIorChange, "clear_coat_IOR_change");
    json << "\"_\": 0 },\n";
    json << "\"raster\": {\n";
    printChunk<BlendingMode, uint8_t>(json, container, MaterialBlendingMode, "blending");
    printFloatChunk(json, container, MaterialMaskThreshold, "mask_threshold");
    printChunk<bool, bool>(json, container, MaterialColorWrite, "color_write");
    printChunk<bool, bool>(json, container, MaterialDepthWrite, "depth_write");
    printChunk<bool, bool>(json, container, MaterialDepthTest, "depth_test");
    printChunk<bool, bool>(json, container, MaterialDoubleSided, "double_sided");
    printChunk<CullingMode, uint8_t>(json, container, MaterialCullingMode, "culling");
    printChunk<TransparencyMode, uint8_t>(json, container, MaterialTransparencyMode, "transparency");
    json << "\"_\": 0 },\n";
    return true;
}

static bool printParametersInfo(ostream& json, const ChunkContainer& container) {
    // TODO
    return true;
}

static void printShaderInfo(ostream& json, const vector<ShaderInfo>& info, const ChunkContainer& container) {
    MaterialDomain domain = MaterialDomain::SURFACE;
    read(container, ChunkType::MaterialDomain, reinterpret_cast<uint8_t*>(&domain));
    for (uint64_t i = 0; i < info.size(); ++i) {
        const auto& item = info[i];
        string variantString = formatVariantString(item.variant, domain);
        string ps = (item.pipelineStage == backend::ShaderType::VERTEX) ? "vertex  " : "fragment";
        json
            << "    {"
            << "\"index\": \"" << std::setw(2) << i << "\", "
            << "\"shaderModel\": \"" << toString(item.shaderModel) << "\", "
            << "\"pipelineStage\": \"" << ps << "\", "
            << "\"variantString\": \"" << variantString << "\", "
            << "\"variant\": \"" << std::hex << int(item.variant) << std::dec << "\" }"
            << ((i == info.size() - 1) ? "\n" : ",\n");
    }
}

static bool printGlslInfo(ostream& json, const ChunkContainer& container) {
    std::vector<ShaderInfo> info;
    info.resize(getShaderCount(container, ChunkType::MaterialGlsl));
    if (!getGlShaderInfo(container, info.data())) {
        return false;
    }
    json << "\"opengl\": [\n";
    printShaderInfo(json, info, container);
    json << "],\n";
    return true;
}

static bool printVkInfo(ostream& json, const ChunkContainer& container) {
    std::vector<ShaderInfo> info;
    info.resize(getShaderCount(container, ChunkType::MaterialSpirv));
    if (!getVkShaderInfo(container, info.data())) {
        return false;
    }
    json << "\"vulkan\": [\n";
    printShaderInfo(json, info, container);
    json << "],\n";
    return true;
}

static bool printMetalInfo(ostream& json, const ChunkContainer& container) {
    std::vector<ShaderInfo> info;
    info.resize(getShaderCount(container, ChunkType::MaterialMetal));
    if (!getMetalShaderInfo(container, info.data())) {
        return false;
    }
    json << "\"metal\": [\n";
    printShaderInfo(json, info, container);
    json << "],\n";
    return true;
}

bool JsonWriter::writeMaterialInfo(const filaflat::ChunkContainer& container) {
    ostringstream json;
    if (!printMaterial(json, container)) {
        return false;
    }
    if (!printParametersInfo(json, container)) {
        return false;
    }
    if (!printGlslInfo(json, container)) {
        return false;
    }
    if (!printVkInfo(json, container)) {
        return false;
    }
    if (!printMetalInfo(json, container)) {
        return false;
    }

    json << "\"required_attributes\": [\n";
    uint32_t requiredAttributes;
    if (read(container, MaterialRequiredAttributes, &requiredAttributes)) {
        string comma;
        AttributeBitset bitset;
        bitset.setValue(requiredAttributes);
        if (bitset.count() > 0) {
            for (size_t i = 0; i < bitset.size(); i++) {
                if (bitset.test(i)) {
                    json << comma << "\"" << toString(static_cast<VertexAttribute>(i)) << "\"";
                    comma = ",";
                }
            }
        }
    }
    json << "]\n";

    mJsonString = CString(json.str().c_str());
    return true;
}

const char* JsonWriter::getJsonString() const {
    return mJsonString.c_str();
}

size_t JsonWriter::getJsonSize() const {
    return mJsonString.size();
}

bool JsonWriter::writeActiveInfo(const filaflat::ChunkContainer& package,
        Backend backend, uint64_t activeVariants) {
    vector<ShaderInfo> shaders;
    ostringstream json;
    json << "[\"";
    switch (backend) {
        case Backend::OPENGL:
            shaders.resize(getShaderCount(package, ChunkType::MaterialGlsl));
            getGlShaderInfo(package, shaders.data());
            json << "opengl";
            break;
        case Backend::VULKAN:
            shaders.resize(getShaderCount(package, ChunkType::MaterialSpirv));
            getVkShaderInfo(package, shaders.data());
            json << "vulkan";
            break;
        case Backend::METAL:
            shaders.resize(getShaderCount(package, ChunkType::MaterialMetal));
            getMetalShaderInfo(package, shaders.data());
            json << "metal";
            break;
        default:
            return false;
    }
    json << "\"";
    for (uint8_t variant = 0; variant < VARIANT_COUNT; variant++) {
        if (activeVariants & (1 << variant)) {
            int shaderIndex = 0;
            for (const auto& info : shaders) {
                if (info.variant == variant) {
                    json << ", " << shaderIndex;
                }
                shaderIndex++;
            }
        }
    }
    json << "]";
    mJsonString = CString(json.str().c_str());
    return true;
}

} // namespace matdbg
} // namespace filament
