/****************************************************************************
 Copyright (c) 2021-2023 Xiamen Yaji Software Co., Ltd.

 http://www.cocos.com

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights to
 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 of the Software, and to permit persons to whom the Software is furnished to do so,
 subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
****************************************************************************/

#pragma once

#include "base/Ptr.h"
#include "base/std/container/string.h"
#include "base/std/container/unordered_map.h"
#include "base/std/optional.h"
#include "base/std/variant.h"
#include "core/assets/EffectAsset.h"
#include "core/event/Event.h"

namespace cc {

// class RenderableComponent;

namespace scene {
class Pass;
}

/**
 * @en The basic infos for material initialization.
 * @zh 用来初始化材质的基本信息。
 */
struct IMaterialInfo {
    /**
     * @en The EffectAsset to use. Must provide if `effectName` is not specified.
     * @zh
     * 这个材质将使用的 EffectAsset，直接提供资源引用，和 `effectName` 至少要指定一个。
     */
    EffectAsset *effectAsset{nullptr};
    /**
     * @en
     * The name of the EffectAsset to use. Must provide if `effectAsset` is not specified.
     * @zh
     * 这个材质将使用的 EffectAsset，通过 effect 名指定，和 `effectAsset` 至少要指定一个。
     */
    ccstd::optional<ccstd::string> effectName;
    /**
     * @en
     * The index of the technique to use.
     * @zh
     * 这个材质将使用第几个 technique，默认为 0。
     */
    ccstd::optional<uint32_t> technique;

    using DefinesType = ccstd::variant<ccstd::monostate, MacroRecord, ccstd::vector<MacroRecord>>;
    /**
     * @en
     * The shader macro definitions. Default to 0 or the specified value in [[EffectAsset]].
     * @zh
     * 这个材质定义的预处理宏，默认全为 0，或 [[EffectAsset]] 中的指定值。
     */
    ccstd::optional<DefinesType> defines;

    using PassOverridesType = ccstd::variant<ccstd::monostate, PassOverrides, ccstd::vector<PassOverrides>>;
    /**
     * @en
     * The override values on top of the pipeline states specified in [[EffectAsset]].
     * @zh
     * 这个材质的自定义管线状态，将覆盖 effect 中的属性。<br>
     * 注意在可能的情况下请尽量少的自定义管线状态，以减小对渲染效率的影响。
     */
    ccstd::optional<PassOverridesType> states;
    bool isEmpty() const {
        return effectAsset == nullptr && !effectName.has_value() && !technique.has_value() && !defines.has_value() && !states.has_value();
    }
};

class Material : public Asset {
    IMPL_EVENT_TARGET(Material)
    DECLARE_TARGET_EVENT_BEGIN(Material)
    TARGET_EVENT_ARG0(PassesUpdated)
    DECLARE_TARGET_EVENT_END()
public:
    using Super = Asset;
    /**
     * @en Get hash for a material
     * @zh 获取一个材质的哈希值
     * @param material
     */
    static ccstd::hash_t getHashForMaterial(Material *material);

    Material();
    ~Material() override;

    /**
     * @en Initialize this material with the given information.
     * @zh 根据所给信息初始化这个材质，初始化正常结束后材质即可立即用于渲染。
     * @param info Material description info.
     */
    void initialize(const IMaterialInfo &info);
    void reset(const IMaterialInfo &info);

    void initDefault(const ccstd::optional<ccstd::string> &uuid) override;
    bool validate() const override;

    /**
     * @en
     * Destroy the material definitively.<br>
     * Cannot re-initialize after destroy.<br>
     * Modifications on active materials can be acheived by<br>
     * creating a new Material, invoke the `copy` function<br>
     * with the desired overrides, and assigning it to the target components.
     * @zh
     * 彻底销毁材质，注意销毁后无法重新初始化。<br>
     * 如需修改现有材质，请创建一个新材质，<br>
     * 调用 copy 函数传入需要的 overrides 并赋给目标组件。
     */
    bool destroy() override;

    /**
     * @en Recompile the shader with the specified macro overrides. Allowed only on material instances.
     * @zh 使用指定预处理宏重新编译当前 pass（数组）中的 shader。只允许对材质实例执行。
     * @param overrides The shader macro override values.
     * @param passIdx The pass to apply to. Will apply to all passes if not specified.
     */
    virtual void recompileShaders(const MacroRecord &overrides) {
        Material::recompileShaders(overrides, CC_INVALID_INDEX);
    }
    virtual void recompileShaders(const MacroRecord &overrides, index_t passIdx);

    /**
     * @en Override the passes with the specified pipeline states. Allowed only on material instances.
     * @zh 使用指定管线状态重载当前的 pass（数组）。只允许对材质实例执行。
     * @param overrides The pipeline state override values.
     * @param passIdx The pass to apply to. Will apply to all passes if not specified.
     */
    virtual void overridePipelineStates(const PassOverrides &overrides) {
        Material::overridePipelineStates(overrides, CC_INVALID_INDEX);
    }
    virtual void overridePipelineStates(const PassOverrides &overrides, index_t passIdx);

    /**
     * @en Callback function after material is loaded in [[Loader]]. Initialize the resources automatically.
     * @zh 通过 [[Loader]] 加载完成时的回调，将自动初始化材质资源。
     */
    void onLoaded() override;

    /**
     * @en Reset all the uniforms to the default value specified in [[EffectAsset]].
     * @zh 重置材质的所有 uniform 参数数据为 [[EffectAsset]] 中的默认初始值。
     * @param clearPasses Will the rendering data be cleared too?
     */
    void resetUniforms(bool clearPasses = true);

    /**
     * @en
     * Convenient property setter provided for quick material setup.<br>
     * [[Pass.setUniform]] should be used instead if you need to do per-frame uniform update.
     * @zh
     * 设置材质 uniform 参数的统一入口。<br>
     * 注意如果需要每帧更新 uniform，建议使用 [[Pass.setUniform]] 以获得更好的性能。
     * @param name The target uniform name.
     * @param val The target value.
     * @param passIdx The pass to apply to. Will apply to all passes if not specified.
     */
    void setProperty(const ccstd::string &name, const MaterialPropertyVariant &val, index_t passIdx = CC_INVALID_INDEX);

    void setPropertyNull(const ccstd::string &name, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyFloat32(const ccstd::string &name, float val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyInt32(const ccstd::string &name, int32_t val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyVec2(const ccstd::string &name, const Vec2 &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyVec3(const ccstd::string &name, const Vec3 &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyVec4(const ccstd::string &name, const Vec4 &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyColor(const ccstd::string &name, const Color &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyMat3(const ccstd::string &name, const Mat3 &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyMat4(const ccstd::string &name, const Mat4 &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyQuaternion(const ccstd::string &name, const Quaternion &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyTextureBase(const ccstd::string &name, TextureBase *val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyGFXTexture(const ccstd::string &name, gfx::Texture *val, index_t passIdx = CC_INVALID_INDEX);

    void setPropertyFloat32Array(const ccstd::string &name, const ccstd::vector<float> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyInt32Array(const ccstd::string &name, const ccstd::vector<int32_t> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyVec2Array(const ccstd::string &name, const ccstd::vector<Vec2> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyVec3Array(const ccstd::string &name, const ccstd::vector<Vec3> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyVec4Array(const ccstd::string &name, const ccstd::vector<Vec4> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyColorArray(const ccstd::string &name, const ccstd::vector<cc::Color> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyMat3Array(const ccstd::string &name, const ccstd::vector<Mat3> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyMat4Array(const ccstd::string &name, const ccstd::vector<Mat4> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyQuaternionArray(const ccstd::string &name, const ccstd::vector<Quaternion> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyTextureBaseArray(const ccstd::string &name, const ccstd::vector<TextureBase *> &val, index_t passIdx = CC_INVALID_INDEX);
    void setPropertyGFXTextureArray(const ccstd::string &name, const ccstd::vector<gfx::Texture *> &val, index_t passIdx = CC_INVALID_INDEX);

    /**
     * @en
     * Get the specified uniform value for this material.<br>
     * Note that only uniforms set through [[Material.setProperty]] can be acquired here.<br>
     * For the complete rendering data, use [[Pass.getUniform]] instead.
     * @zh
     * 获取当前材质的指定 uniform 参数的值。<br>
     * 注意只有通过 [[Material.setProperty]] 函数设置的参数才能从此函数取出，<br>
     * 如需取出完整的渲染数据，请使用 [[Pass.getUniform]]。
     * @param name The property or uniform name.
     * @param passIdx The target pass index. If not specified, return the first found value in all passes.
     */
    const MaterialPropertyVariant *getProperty(const ccstd::string &name, index_t passIdx = CC_INVALID_INDEX) const;

    /**
     * @en Copy the target material, with optional overrides.
     * @zh 复制目标材质到当前实例，允许提供重载信息。。
     * @param mat The material to be copied.
     * @param overrides The overriding states on top of the original material.
     */
    void copy(const Material *mat, const IMaterialInfo &overrides);
    inline void copy(const Material *mat) { copy(mat, IMaterialInfo()); }

    void fillInfo(const IMaterialInfo &info);

    // For deserialization, we need to make the following properties public
    /* @type(EffectAsset) */
    IntrusivePtr<EffectAsset> _effectAsset;

    /* @serializable */
    uint32_t _techIdx{0};

    /* @serializable */
    ccstd::vector<MacroRecord> _defines;

    /* @serializable */
    ccstd::vector<PassOverrides> _states;

    /* @serializable */
    ccstd::vector<ccstd::unordered_map<ccstd::string, MaterialPropertyVariant>> _props;
    //

protected:
    std::shared_ptr<ccstd::vector<IntrusivePtr<scene::Pass>>> _passes;

    ccstd::hash_t _hash{0U};

public:
    /**
     * @en Set current [[EffectAsset]].
     * @zh 设置使用的 [[EffectAsset]] 资源。
     */
    inline void setEffectAsset(EffectAsset *val) {
        _effectAsset = val;
    }

    /**
     * @en The current [[EffectAsset]].
     * @zh 当前使用的 [[EffectAsset]] 资源。
     */
    inline EffectAsset *getEffectAsset() const {
        return _effectAsset.get();
    }

    /**
     * @en Name of the current [[EffectAsset]].
     * @zh 当前使用的 [[EffectAsset]] 资源名。
     */
    inline ccstd::string getEffectName() const {
        return _effectAsset ? _effectAsset->getName() : "";
    }

    /**
     * @en The current technique index.
     * @zh 当前的 technique 索引。
     */
    inline uint32_t getTechniqueIndex() const {
        return _techIdx;
    }

    /**
     * @en The passes defined in this material.
     * @zh 当前正在使用的 pass 数组。
     */
    std::shared_ptr<ccstd::vector<IntrusivePtr<scene::Pass>>> &getPasses() {
        return _passes;
    }

    /**
     * @en The hash value of this material.
     * @zh 材质的 hash。
     */
    inline ccstd::hash_t getHash() const {
        return _hash;
    }

    /**
     * @en The parent material
     * @zh 父材质
     */
    virtual Material *getParent() const {
        return nullptr;
    }

    /**
     * @en The owner render component
     * @zh 该材质所归属的渲染组件
     */
    //    virtual RenderableComponent *getOwner() const {
    //        return nullptr;
    //    }

protected:
    void update(bool keepProps = true);
    bool uploadProperty(scene::Pass *pass, const ccstd::string &name, const MaterialPropertyVariant &val);
    void bindTexture(scene::Pass *pass, uint32_t handle, const MaterialProperty &val, uint32_t index = 0);

    template <typename T1>
    void prepareInfo(const T1 &patch, ccstd::vector<MacroRecord> &curr) {
        size_t len = _effectAsset != nullptr ? _effectAsset->_techniques[_techIdx].passes.size() : 1;
        auto *macroRecordElem = ccstd::get_if<MacroRecord>(&patch);
        if (macroRecordElem != nullptr) {
            const auto &macroRecord = *macroRecordElem;
            if (curr.size() < len) curr.resize(len);
            for (size_t i = 0; i < len; ++i) {
                for (const auto &field : macroRecord) {
                    curr[i][field.first] = field.second;
                }
            }
        } else {
            auto *macroRecordArray = ccstd::get_if<ccstd::vector<MacroRecord>>(&patch);
            if (macroRecordArray != nullptr) {
                const auto &patchArray = *macroRecordArray;
                size_t len = patchArray.size();
                if (curr.size() < len) curr.resize(len);
                for (size_t i = 0; i < len; ++i) {
                    for (const auto &patchPair : patchArray[i]) {
                        curr[i][patchPair.first] = patchPair.second;
                    }
                }
            }
        }
    }
    template <typename T1>
    void prepareInfo(const T1 &patch, ccstd::vector<PassOverrides> &curr) {
        size_t len = _effectAsset != nullptr ? _effectAsset->_techniques[_techIdx].passes.size() : 1;
        auto *passOverrideElem = ccstd::get_if<PassOverrides>(&patch);
        if (passOverrideElem != nullptr) {
            const auto &passOverride = *passOverrideElem;
            if (curr.size() < len) curr.resize(len);
            for (size_t i = 0; i < len; ++i) {
                curr[i].overrides(IPassInfoFull(passOverride));
            }
        } else {
            auto *passOverrideArray = ccstd::get_if<ccstd::vector<PassOverrides>>(&patch);
            if (passOverrideArray != nullptr) {
                const auto &patchArray = *passOverrideArray;
                size_t len = patchArray.size();
                if (curr.size() < len) curr.resize(len);
                for (size_t i = 0; i < len; ++i) {
                    curr[i].overrides(IPassInfoFull(patchArray[i]));
                }
            }
        }
    }
    template <typename T1, typename T2>
    void prepareInfo(const T1 &patch, ccstd::vector<T2> &cur) {
        auto *pOneElement = ccstd::get_if<T2>(&patch);
        if (pOneElement != nullptr) {
            size_t len = _effectAsset != nullptr ? _effectAsset->_techniques[_techIdx].passes.size() : 1;

            ccstd::vector<T2> patchArray;
            patchArray.reserve(len);
            for (size_t i = 0; i < len; ++i) {
                patchArray.emplace_back(*pOneElement);
            }

            if (cur.size() < len) cur.resize(patchArray.size());

            for (size_t i = 0; i < len; ++i) {
                cur[i] = patchArray[i];
            }
        } else {
            auto *pPatchArray = ccstd::get_if<ccstd::vector<T2>>(&patch);
            if (pPatchArray != nullptr) {
                const auto &patchArray = *pPatchArray;
                size_t len = patchArray.size();
                if (cur.size() < len) cur.resize(len);

                for (size_t i = 0; i < len; ++i) {
                    cur[i] = patchArray[i];
                }
            }
        }
    }

    virtual void doDestroy();

    virtual ccstd::vector<IntrusivePtr<scene::Pass>> createPasses();

private:
    friend class MaterialDeserializer;

    CC_DISALLOW_COPY_MOVE_ASSIGN(Material);
};

} // namespace cc
