#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <components/files/configurationmanager.hpp>
#include <components/fx/technique.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/settings/settings.hpp>
#include <components/testing/util.hpp>

namespace
{
    constexpr VFS::Path::NormalizedView techniquePropertiesPath("shaders/technique_properties.omwfx");

    TestingOpenMW::VFSTestFile techniqueProperties(R"(
    fragment main {}
    vertex main {}
    technique {
        passes = main;
        version = "0.1a";
        description = "description";
        author = "author";
        glsl_version = 330;
        glsl_profile = "compatability";
        glsl_extensions = GL_EXT_gpu_shader4, GL_ARB_uniform_buffer_object;
        flags = disable_sunglare;
        hdr = true;
    }
)");

    constexpr VFS::Path::NormalizedView rendertargetPropertiesPath("shaders/rendertarget_properties.omwfx");

    TestingOpenMW::VFSTestFile rendertargetProperties{ R"(
    render_target rendertarget {
        width_ratio = 0.5;
        height_ratio = 0.5;
        internal_format = r16f;
        source_type = float;
        source_format = red;
        mipmaps = true;
        wrap_s = clamp_to_edge;
        wrap_t = repeat;
        min_filter = linear;
        mag_filter = nearest;
    }
    fragment downsample2x(target=rendertarget) {

        omw_In vec2 omw_TexCoord;

        void main()
        {
            omw_FragColor.r = omw_GetLastShader(omw_TexCoord).r;
        }
    }
    fragment main { }
    technique { passes = downsample2x, main; }
)" };

    constexpr VFS::Path::NormalizedView uniformPropertiesPath("shaders/uniform_properties.omwfx");

    TestingOpenMW::VFSTestFile uniformProperties{ R"(
    uniform_vec4 uVec4 {
        default = vec4(0,0,0,0);
        min = vec4(0,1,0,0);
        max = vec4(0,0,1,0);
        step = 0.5;
        header = "header";
        static = true;
        description = "description";
    }
    fragment main { }
    technique { passes = main; }
)" };

    constexpr VFS::Path::NormalizedView missingSamplerSourcePath("shaders/missing_sampler_source.omwfx");

    TestingOpenMW::VFSTestFile missingSamplerSource{ R"(
    sampler_1d mysampler1d { }
    fragment main { }
    technique { passes = main; }
)" };

    constexpr VFS::Path::NormalizedView repeatedSharedBlockPath("shaders/repeated_shared_block.omwfx");

    TestingOpenMW::VFSTestFile repeatedSharedBlock{ R"(
    shared {
        float myfloat = 1.0;
    }
    shared {}
    fragment main { }
    technique { passes = main; }
)" };

    constexpr VFS::Path::NormalizedView invalidNumberInfPath("shaders/invalid_number_inf.omwfx");

    TestingOpenMW::VFSTestFile invalidNumberInf{ R"(
    uniform_vec4 uVec4 {
        step = inf;
    }
    fragment main { }
    technique { passes = main; }
)" };

    constexpr VFS::Path::NormalizedView invalidNumberNegativeInfPath("shaders/invalid_number_negative_inf.omwfx");

    TestingOpenMW::VFSTestFile invalidNumberNegativeInf{ R"(
    uniform_vec4 uVec4 {
        step = -inf;
    }
    fragment main { }
    technique { passes = main; }
)" };

    constexpr VFS::Path::NormalizedView invalidNumberUnsignedLongPath("shaders/invalid_number_ulong.omwfx");

    TestingOpenMW::VFSTestFile invalidNumberUnsignedLong{ R"(
    uniform_vec4 uVec4 {
        step = 18446744073709551615;
    }
    fragment main { }
    technique { passes = main; }
)" };

    constexpr VFS::Path::NormalizedView invalidNumberHexFloatPath("shaders/invalid_number_hex.omwfx");

    TestingOpenMW::VFSTestFile invalidNumberHexFloat{ R"(
    uniform_vec4 uVec4 {
        step = 0x1.fffffep+12;
    }
    fragment main { }
    technique { passes = main; }
)" };

    constexpr VFS::Path::NormalizedView invalidNumberDoublePath("shaders/invalid_number_double.omwfx");

    TestingOpenMW::VFSTestFile invalidNumberDouble{ R"(
    uniform_vec4 uVec4 {
        step = 1.79769e+50;
    }
    fragment main { }
    technique { passes = main; }
)" };

    using namespace testing;
    using namespace Fx;

    struct TechniqueTest : Test
    {
        std::unique_ptr<VFS::Manager> mVFS;
        Resource::ImageManager mImageManager;
        std::unique_ptr<Technique> mTechnique;

        TechniqueTest()
            : mVFS(TestingOpenMW::createTestVFS({
                { techniquePropertiesPath, &techniqueProperties },
                { rendertargetPropertiesPath, &rendertargetProperties },
                { uniformPropertiesPath, &uniformProperties },
                { missingSamplerSourcePath, &missingSamplerSource },
                { repeatedSharedBlockPath, &repeatedSharedBlock },
                { invalidNumberInfPath, &invalidNumberInf },
                { invalidNumberNegativeInfPath, &invalidNumberNegativeInf },
                { invalidNumberUnsignedLongPath, &invalidNumberUnsignedLong },
                { invalidNumberHexFloatPath, &invalidNumberHexFloat },
                { invalidNumberDoublePath, &invalidNumberDouble },
            }))
            , mImageManager(mVFS.get(), 0)
        {
        }

        void compile(const std::string& name)
        {
            mTechnique = std::make_unique<Technique>(
                *mVFS.get(), mImageManager, Technique::makeFileName(name), name, 1, 1, true, true);
            mTechnique->compile();
        }

        void expectFailure(const std::string& name, std::string_view errorString)
        {
            internal::CaptureStdout();

            compile(name);

            std::string output = internal::GetCapturedStdout();
            Log(Debug::Error) << output;
            EXPECT_THAT(output, HasSubstr(errorString));
        }
    };

    TEST_F(TechniqueTest, technique_properties)
    {
        std::unordered_set<std::string> targetExtensions = { "GL_EXT_gpu_shader4", "GL_ARB_uniform_buffer_object" };

        compile("technique_properties");

        EXPECT_EQ(mTechnique->getVersion(), "0.1a");
        EXPECT_EQ(mTechnique->getDescription(), "description");
        EXPECT_EQ(mTechnique->getAuthor(), "author");
        EXPECT_EQ(mTechnique->getGLSLVersion(), 330);
        EXPECT_EQ(mTechnique->getGLSLProfile(), "compatability");
        EXPECT_EQ(mTechnique->getGLSLExtensions(), targetExtensions);
        EXPECT_EQ(mTechnique->getFlags(), Technique::Flag_Disable_SunGlare);
        EXPECT_EQ(mTechnique->getHDR(), true);
        EXPECT_EQ(mTechnique->getPasses().size(), 1);
        EXPECT_EQ(mTechnique->getPasses().front()->getName(), "main");
    }

    TEST_F(TechniqueTest, rendertarget_properties)
    {
        compile("rendertarget_properties");

        EXPECT_EQ(mTechnique->getRenderTargetsMap().size(), 1);

        const std::string_view name = mTechnique->getRenderTargetsMap().begin()->first;
        auto& rt = mTechnique->getRenderTargetsMap().begin()->second;
        auto& texture = rt.mTarget;

        EXPECT_EQ(name, "rendertarget");
        EXPECT_EQ(rt.mMipMap, true);
        EXPECT_EQ(rt.mSize.mWidthRatio, 0.5f);
        EXPECT_EQ(rt.mSize.mHeightRatio, 0.5f);
        EXPECT_EQ(texture->getWrap(osg::Texture::WRAP_S), osg::Texture::CLAMP_TO_EDGE);
        EXPECT_EQ(texture->getWrap(osg::Texture::WRAP_T), osg::Texture::REPEAT);
        EXPECT_EQ(texture->getFilter(osg::Texture::MIN_FILTER), osg::Texture::LINEAR);
        EXPECT_EQ(texture->getFilter(osg::Texture::MAG_FILTER), osg::Texture::NEAREST);
        EXPECT_EQ(texture->getSourceType(), static_cast<GLenum>(GL_FLOAT));
        EXPECT_EQ(texture->getSourceFormat(), static_cast<GLenum>(GL_RED));
        EXPECT_EQ(texture->getInternalFormat(), static_cast<GLint>(GL_R16F));

        EXPECT_EQ(mTechnique->getPasses().size(), 2);
        EXPECT_EQ(mTechnique->getPasses()[0]->getTarget(), "rendertarget");
    }

    TEST_F(TechniqueTest, uniform_properties)
    {
        compile("uniform_properties");

        EXPECT_EQ(mTechnique->getUniformMap().size(), 1);

        const auto& uniform = mTechnique->getUniformMap().front();

        EXPECT_TRUE(uniform->mStatic);
        EXPECT_DOUBLE_EQ(uniform->mStep, 0.5);
        EXPECT_EQ(uniform->getDefault<osg::Vec4f>(), osg::Vec4f(0, 0, 0, 0));
        EXPECT_EQ(uniform->getMin<osg::Vec4f>(), osg::Vec4f(0, 1, 0, 0));
        EXPECT_EQ(uniform->getMax<osg::Vec4f>(), osg::Vec4f(0, 0, 1, 0));
        EXPECT_EQ(uniform->mHeader, "header");
        EXPECT_EQ(uniform->mDescription, "description");
        EXPECT_EQ(uniform->mName, "uVec4");
    }

    TEST_F(TechniqueTest, fail_with_missing_source_for_sampler)
    {
        expectFailure("missing_sampler_source", "sampler_1d 'mysampler1d' requires a filename");
    }

    TEST_F(TechniqueTest, fail_with_repeated_shared_block)
    {
        expectFailure("repeated_shared_block", "repeated 'shared' block");
    }

    TEST_F(TechniqueTest, fail_with_invalid_float)
    {
        expectFailure("invalid_number_inf", "expected float value");
        expectFailure("invalid_number_negative_inf", "expected float value");
        expectFailure("invalid_number_hex", "expected float value");
    }

    TEST_F(TechniqueTest, fail_with_out_of_range)
    {
        expectFailure("invalid_number_ulong", "number out of range");
        expectFailure("invalid_number_double", "number out of range");
    }
}
