// Copyright 2018 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

#pragma once

#include <array>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include <boost/functional/hash.hpp>
#include "common/common_types.h"
#include "common/hash.h"

namespace OpenGL::GLShader {

constexpr size_t MAX_PROGRAM_CODE_LENGTH{0x1000};
using ProgramCode = std::vector<u64>;

class ConstBufferEntry {
    using Maxwell = Tegra::Engines::Maxwell3D::Regs;

public:
    void MarkAsUsed(u64 index, u64 offset, Maxwell::ShaderStage stage) {
        is_used = true;
        this->index = static_cast<unsigned>(index);
        this->stage = stage;
        max_offset = std::max(max_offset, static_cast<unsigned>(offset));
    }

    void MarkAsUsedIndirect(u64 index, Maxwell::ShaderStage stage) {
        is_used = true;
        is_indirect = true;
        this->index = static_cast<unsigned>(index);
        this->stage = stage;
    }

    bool IsUsed() const {
        return is_used;
    }

    bool IsIndirect() const {
        return is_indirect;
    }

    unsigned GetIndex() const {
        return index;
    }

    unsigned GetSize() const {
        return max_offset + 1;
    }

    std::string GetName() const {
        return BufferBaseNames[static_cast<size_t>(stage)] + std::to_string(index);
    }

private:
    static constexpr std::array<const char*, Maxwell::MaxShaderStage> BufferBaseNames = {
        "buffer_vs_c", "buffer_tessc_c", "buffer_tesse_c", "buffer_gs_c", "buffer_fs_c",
    };

    bool is_used{};
    bool is_indirect{};
    unsigned index{};
    unsigned max_offset{};
    Maxwell::ShaderStage stage;
};

class SamplerEntry {
    using Maxwell = Tegra::Engines::Maxwell3D::Regs;

public:
    SamplerEntry(Maxwell::ShaderStage stage, size_t offset, size_t index)
        : offset(offset), stage(stage), sampler_index(index) {}

    size_t GetOffset() const {
        return offset;
    }

    size_t GetIndex() const {
        return sampler_index;
    }

    Maxwell::ShaderStage GetStage() const {
        return stage;
    }

    std::string GetName() const {
        return std::string(TextureSamplerNames[static_cast<size_t>(stage)]) + '[' +
               std::to_string(sampler_index) + ']';
    }

    static std::string GetArrayName(Maxwell::ShaderStage stage) {
        return TextureSamplerNames[static_cast<size_t>(stage)];
    }

private:
    static constexpr std::array<const char*, Maxwell::MaxShaderStage> TextureSamplerNames = {
        "tex_vs", "tex_tessc", "tex_tesse", "tex_gs", "tex_fs",
    };
    /// Offset in TSC memory from which to read the sampler object, as specified by the sampling
    /// instruction.
    size_t offset;
    Maxwell::ShaderStage stage; ///< Shader stage where this sampler was used.
    size_t sampler_index;       ///< Value used to index into the generated GLSL sampler array.
};

struct ShaderEntries {
    std::vector<ConstBufferEntry> const_buffer_entries;
    std::vector<SamplerEntry> texture_samplers;
};

using ProgramResult = std::pair<std::string, ShaderEntries>;

struct ShaderSetup {
    explicit ShaderSetup(ProgramCode program_code) {
        program.code = std::move(program_code);
    }

    struct {
        ProgramCode code;
        ProgramCode code_b; // Used for dual vertex shaders
    } program;

    bool program_code_hash_dirty = true;

    u64 GetProgramCodeHash() {
        if (program_code_hash_dirty) {
            program_code_hash = GetNewHash();
            program_code_hash_dirty = false;
        }
        return program_code_hash;
    }

    /// Used in scenarios where we have a dual vertex shaders
    void SetProgramB(ProgramCode program_b) {
        program.code_b = std::move(program_b);
        has_program_b = true;
    }

    bool IsDualProgram() const {
        return has_program_b;
    }

private:
    u64 GetNewHash() const {
        size_t hash = 0;

        const u64 hash_a = Common::ComputeHash64(program.code.data(), program.code.size());
        boost::hash_combine(hash, hash_a);

        if (has_program_b) {
            // Compute hash over dual shader programs
            const u64 hash_b = Common::ComputeHash64(program.code_b.data(), program.code_b.size());
            boost::hash_combine(hash, hash_b);
        }

        return hash;
    }

    u64 program_code_hash{};
    bool has_program_b{};
};

struct MaxwellShaderConfigCommon {
    void Init(ShaderSetup& setup) {
        program_hash = setup.GetProgramCodeHash();
    }

    u64 program_hash;
};

struct MaxwellVSConfig : Common::HashableStruct<MaxwellShaderConfigCommon> {
    explicit MaxwellVSConfig(ShaderSetup& setup) {
        state.Init(setup);
    }
};

struct MaxwellFSConfig : Common::HashableStruct<MaxwellShaderConfigCommon> {
    explicit MaxwellFSConfig(ShaderSetup& setup) {
        state.Init(setup);
    }
};

/**
 * Generates the GLSL vertex shader program source code for the given VS program
 * @returns String of the shader source code
 */
ProgramResult GenerateVertexShader(const ShaderSetup& setup, const MaxwellVSConfig& config);

/**
 * Generates the GLSL fragment shader program source code for the given FS program
 * @returns String of the shader source code
 */
ProgramResult GenerateFragmentShader(const ShaderSetup& setup, const MaxwellFSConfig& config);

} // namespace OpenGL::GLShader

namespace std {

template <>
struct hash<OpenGL::GLShader::MaxwellVSConfig> {
    size_t operator()(const OpenGL::GLShader::MaxwellVSConfig& k) const {
        return k.Hash();
    }
};

template <>
struct hash<OpenGL::GLShader::MaxwellFSConfig> {
    size_t operator()(const OpenGL::GLShader::MaxwellFSConfig& k) const {
        return k.Hash();
    }
};

} // namespace std
