/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/

#include <AzCore/Debug/Timer.h>
#include <AzCore/Debug/StackTracer.h>
#include <AzCore/Debug/TraceMessagesDrillerBus.h>
#include <AzCore/Debug/Profiler.h>
#include <AzCore/std/parallel/thread.h>
#include <AzCore/Math/Crc.h>
#include <AzCore/std/time.h>
#include <AzCore/std/chrono/clocks.h>
#include <AzCore/std/string/conversions.h>
#include <AzCore/UnitTest/TestTypes.h>

using namespace AZ;
using namespace AZ::Debug;

namespace UnitTest
{
    TEST(StackTracer, Test)
    {
        // Test can be executed only on supported platforms
        StackFrame  frames[10];
        unsigned int numFrames = StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames));
        if (numFrames)
        {
            u32 numValidFrames = 0;
            for (u32 i = 0; i < AZ_ARRAY_SIZE(frames); ++i)
            {
                if (frames[i].IsValid())
                {
                    ++numValidFrames;
                }
            }
            AZ_TEST_ASSERT(numValidFrames == numFrames);
#if AZ_TRAIT_OS_STACK_FRAMES_VALID
            // We have:
            // StackTracer::run
            // DebugSuite::run
            // main_tests.cpp : DoTests
            // main_tests.cpp : main()
            // + system calls (in release some func are inlined but still >= 5)
            AZ_TEST_ASSERT(numValidFrames >= 5);
#else
            AZ_TEST_ASSERT(numValidFrames == 0);
#endif

#if AZ_TRAIT_OS_STACK_FRAMES_TRACE  // Symbols are available only on windows.
            {
                //// We should have loaded at least AZCore_tests.exe module
                int isFoundModule = 0;
                for (u32 i = 0; i < SymbolStorage::GetNumLoadedModules(); ++i)
                {
                    char nameBuffer[AZ_ARRAY_SIZE(SymbolStorage::ModuleInfo::m_modName)];
                    azstrncpy(nameBuffer, SymbolStorage::GetModuleInfo(i)->m_fileName, AZ_ARRAY_SIZE(nameBuffer));
                    AZStd::to_lower(nameBuffer, nameBuffer + AZ_ARRAY_SIZE(nameBuffer));

                    if (strstr(nameBuffer, "azcoretests.dll"))
                    {
                        isFoundModule = 1;
                        break;
                    }
                }

                AZ_TEST_ASSERT(isFoundModule);
                SymbolStorage::StackLine stackLines[AZ_ARRAY_SIZE(frames)];
                AZ_TracePrintf("StackTracer", "\n====================================\n");
                AZ_TracePrintf("StackTracer", "Callstack:\n");
                SymbolStorage::DecodeFrames(frames, numFrames, stackLines);
                for (u32 i = 0; i < numFrames; ++i)
                {
                    AZ_TEST_ASSERT(strlen(stackLines[i]) > 0); // We should some result for each stack frame
                    AZ_TracePrintf("StackTracer", "%s\n", stackLines[i]);
                }
                AZ_TracePrintf("StackTracer", "====================================\n");
            }
#elif AZ_TRAIT_OS_STACK_FRAMES_PRINT
            {
                SymbolStorage::StackLine stackLines[AZ_ARRAY_SIZE(frames)];
                printf("\n====================================\n");
                printf("Callstack:\n");
                SymbolStorage::DecodeFrames(frames, numFrames, stackLines);
                for (u32 i = 0; i < numFrames; ++i)
                {
                    AZ_TEST_ASSERT(strlen(stackLines[i]) > 0); // We should some result for each stack frame
                    printf("%s\n", stackLines[i]);
                }
                printf("====================================\n");
            }
#endif
        }
    }

    class TraceTest
        : public AZ::Debug::TraceMessageDrillerBus::Handler
        , public ::testing::Test
    {
        int m_numTracePrintfs;
    public:
        TraceTest()
            : m_numTracePrintfs(0) {}

        //////////////////////////////////////////////////////////////////////////
        // TraceMessagesDrillerBus
        void OnPrintf(const char* windowName, const char* message) override
        {
            (void)windowName;
            (void)message;
            ++m_numTracePrintfs;
        }
        //////////////////////////////////////////////////////////////////////////

        void run()
        {
            bool falseExp = false;
            int  m_data = 0;
            (void)falseExp;
            (void)m_data;

            BusConnect();

            printf("\n");

            // Assert
            AZ_Assert(true, "Constand expression - always pass!");

            // Error

            // Regular warning
            AZ_Warning("Streamer", falseExp, "Test Message");

            // Printf
            int oldNumTracePrintfs = m_numTracePrintfs;
            AZ_Printf("AI | A*", "Test Message");
            // only one traceprintf should have occurred here.
            AZ_TEST_ASSERT(m_numTracePrintfs == oldNumTracePrintfs + 1);

            // test verify
            bool result = false;
            AZ_Verify(result = true, "Expression should execute even in release!");
            AZ_TEST_ASSERT(result == true);

            result = false;
            AZ_VerifyError("Test", result = true, "Expression should execute even in release!");
            AZ_TEST_ASSERT(result == true);

            result = false;
            AZ_VerifyWarning("Test", result = true, "Expression should execute even in release!");
            AZ_TEST_ASSERT(result == true);
            (void)result;

            BusDisconnect();
        }
    };

    TEST_F(TraceTest, Test)
    {
        run();
    }

    class ProfilerTest
        : public AllocatorsFixture
    {
    public:
        int m_numRegistersReceived;

        bool ReadRegisterCallback(const ProfilerRegister& reg, const AZStd::thread_id& id)
        {
            (void)reg;
            (void)id;
            switch (reg.m_type)
            {
            case ProfilerRegister::PRT_TIME:
            {
                AZ_TEST_ASSERT(reg.m_timeData.m_time > 0);
                AZ_TEST_ASSERT(reg.m_timeData.m_calls > 0);
            } break;
            case ProfilerRegister::PRT_VALUE:
            {
                AZ_TEST_ASSERT(reg.m_userValues.m_value1 == 1 || reg.m_userValues.m_value1 == 2);
                AZ_TEST_ASSERT(reg.m_userValues.m_value2 == 0 || reg.m_userValues.m_value2 == 2 || reg.m_userValues.m_value2 == 4);
                AZ_TEST_ASSERT(reg.m_userValues.m_value3 == 0 || reg.m_userValues.m_value3 == 3 || reg.m_userValues.m_value3 == 6);
                AZ_TEST_ASSERT(reg.m_userValues.m_value4 == 0 || reg.m_userValues.m_value4 == 4 || reg.m_userValues.m_value4 == 8);
                AZ_TEST_ASSERT(reg.m_userValues.m_value5 == 0 || reg.m_userValues.m_value5 == 5 || reg.m_userValues.m_value5 == 10);
            } break;
            }

            //AZ::u64 threadId = (AZ::u64)id.m_id;
            //AZ_TracePrintf("Profiler","[%llu] '%s' '%s'(%d) %d Ms (Child calls: %d time: %d Ms) Parent: '%s'!\n",threadId,
            //  reg.m_name,reg.m_function,reg.m_line,reg.m_time.count(),reg.m_childrenCalls,reg.m_childrenTime.count(),reg.m_lastParent ? reg.m_lastParent->m_name : "No");
            ++m_numRegistersReceived;
            return true;
        }

        int ChildFunction(int input)
        {
            AZ_PROFILE_TIMER("UnitTest");

            auto start = AZStd::chrono::system_clock::now();

            int result = 5;
            for (int i = 0; i < 30000; ++i)
            {
                result += i % (input + 3);
            }

            auto end = AZStd::chrono::system_clock::now();
            AZ_TEST_ASSERT(end >= start);
            while (end <= start)
            {
                end = AZStd::chrono::system_clock::now();
            }
            return result;
        }

        int ChildFunction1(int input)
        {
            AZ_PROFILE_TIMER("UnitTest", "Child1");

            auto start = AZStd::chrono::system_clock::now();

            int result = 5;
            for (int i = 0; i < 30000; ++i)
            {
                result += i % (input + 1);
            }


            auto end = AZStd::chrono::system_clock::now();
            AZ_TEST_ASSERT(end >= start);
            while (end <= start)
            {
                end = AZStd::chrono::system_clock::now();
            }

            return result;
        }

        int Profile1(int numIterations)
        {
            AZ_PROFILE_TIMER("UnitTest", "Custom name");
            int result = 0;
            for (int i = 0; i < numIterations; ++i)
            {
                result += ChildFunction(i);
            }

            result += ChildFunction1(numIterations / 3);
            return result;
        }

        void UserValuesSet()
        {
            AZ_PROFILE_VALUE_SET("UnitTest", "UserValues1", 1);
            AZ_PROFILE_VALUE_SET("UnitTest", "UserValues2", 1, 2);
            AZ_PROFILE_VALUE_SET("UnitTest", "UserValues3", 1, 2, 3);
            AZ::s64 v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5;
            AZ_PROFILE_VALUE_SET("UnitTest", "UserValues4", v1, v2, v3, v4);
            AZ_PROFILE_VALUE_SET("UnitTest", "UserValues5", v1, v2, v3, v4, v5);

            // test named register
            AZ_PROFILE_VALUE_SET_NAMED("UnitTest", "UserValues5", userValues5, v1, v2, v3, v4, v5);
#if defined(AZ_PROFILER_MACRO_DISABLE)
            (void)v1;
            (void)v2;
            (void)v3;
            (void)v4;
            (void)v5;
#else
            AZ_TEST_ASSERT(userValues5 != nullptr);
#endif // !defined(AZ_PROFILER_MACRO_DISABLE)
        }

        void UserValuesAdd(int numAdditions)
        {
            for (int i = 0; i < numAdditions; ++i)
            {
                AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues1", 1);
                AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues2", 1, 2);
                AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues3", 1, 2, 3);
                AZ::s64 v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5;
                AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues4", v1, v2, v3, v4);
                AZ_PROFILE_VALUE_ADD("UnitTest", "UserValues5", v1, v2, v3, v4, v5);

                // test named register
                AZ_PROFILE_VALUE_ADD_NAMED("UnitTest", "UserValues5", userValues5, v1, v2, v3, v4, v5);
#if defined(AZ_PROFILER_MACRO_DISABLE)
                (void)v1;
                (void)v2;
                (void)v3;
                (void)v4;
                (void)v5;
#else
                AZ_TEST_ASSERT(userValues5 != nullptr);
#endif // !defined(AZ_PROFILER_MACRO_DISABLE)
            }
        }

        void run()
        {
            AZ_TEST_ASSERT(!Profiler::IsReady());
            Profiler::Create();
            AZ_TEST_ASSERT(Profiler::IsReady());
            Profiler::Destroy();
            AZ_TEST_ASSERT(!Profiler::IsReady());

#if !defined(AZ_PROFILER_MACRO_DISABLE)
            Profiler::Create();

            //Profile1();

            //Profiler::Instance().ReadRegisterValues(AZStd::bind(&ProfilerTest::ReadRegisterCallback,this,AZStd::placeholders::_1,AZStd::placeholders::_2));

            //Profiler::Instance().ResetRegisters();

            AZStd::thread_id removeThreadId;
            AZStd::chrono::microseconds elapsed[2];
            int numIterations = 10000;
            for (int i = 0; i < 2; ++i)
            {
                // for the second run we should not record any data
                if (i == 1)
                {
                    Profiler::Instance().DeactivateSystem("UnitTest");
                }

                AZStd::chrono::system_clock::time_point start = AZStd::chrono::system_clock::now();
                AZStd::thread t1(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
                AZStd::thread t2(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
                AZStd::thread t3(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
                AZStd::thread t4(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
                AZStd::thread t5(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
                AZStd::thread t6(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
                AZStd::thread t7(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));
                AZStd::thread t8(AZStd::bind(&ProfilerTest::Profile1, this, numIterations));

                removeThreadId = t4.get_id();

                t1.join();
                t2.join();
                t3.join();
                t4.join();
                t5.join();
                t6.join();
                t7.join();
                t8.join();
                elapsed[i] = AZStd::chrono::system_clock::now() - start;
                //AZ_Printf("Profiler","Elapsed time %d\n",elapsed[i].count());

                if (i == 0)
                {
                    // just as test remove all associated data and registers.
                    Profiler::Instance().RemoveThreadData(removeThreadId);
                }

                m_numRegistersReceived = 0;
                Profiler::Instance().ReadRegisterValues(AZStd::bind(&ProfilerTest::ReadRegisterCallback, this, AZStd::placeholders::_1, AZStd::placeholders::_2));
                if (i == 0)
                {
                    AZ_TEST_ASSERT(m_numRegistersReceived == 7 * 3); // 3 registers for each thread (8 threads - 1 we removed the data for 't4')
                }
                else
                {
                    AZ_TEST_ASSERT(m_numRegistersReceived == 0);
                }
            }
            Profiler::Destroy();

            // Test user value registers
            Profiler::Create();

            for (int i = 0; i < 2; ++i)
            {
                // for the second run we should not record any data
                if (i == 1)
                {
                    Profiler::Instance().DeactivateSystem("UnitTest");
                }

                AZStd::thread t1(AZStd::bind(&ProfilerTest::UserValuesSet, this));
                AZStd::thread t2(AZStd::bind(&ProfilerTest::UserValuesSet, this));
                AZStd::thread t3(AZStd::bind(&ProfilerTest::UserValuesSet, this));
                AZStd::thread t4(AZStd::bind(&ProfilerTest::UserValuesSet, this));
                AZStd::thread t5(AZStd::bind(&ProfilerTest::UserValuesAdd, this, 2));
                AZStd::thread t6(AZStd::bind(&ProfilerTest::UserValuesAdd, this, 2));
                AZStd::thread t7(AZStd::bind(&ProfilerTest::UserValuesAdd, this, 2));
                AZStd::thread t8(AZStd::bind(&ProfilerTest::UserValuesAdd, this, 2));

                removeThreadId = t4.get_id();

                t1.join();
                t2.join();
                t3.join();
                t4.join();
                t5.join();
                t6.join();
                t7.join();
                t8.join();

                if (i == 0)
                {
                    // just as test remove all associated data and registers.
                    Profiler::Instance().RemoveThreadData(removeThreadId);
                }

                m_numRegistersReceived = 0;
                Profiler::Instance().ReadRegisterValues(AZStd::bind(&ProfilerTest::ReadRegisterCallback, this, AZStd::placeholders::_1, AZStd::placeholders::_2));
                if (i == 0)
                {
                    AZ_TEST_ASSERT(m_numRegistersReceived == 7 * 6); // 6 registers for each thread (8 threads - 1 we removed the data for 't4' )
                }
                else
                {
                    AZ_TEST_ASSERT(m_numRegistersReceived == 0);
                }
            }
            Profiler::Destroy();
#endif
        }
    };

    TEST_F(ProfilerTest, Test)
    {
        run();
    }

    TEST(Time, Test)
    {
        AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond();
        AZ_TEST_ASSERT(ticksPerSecond != 0);

        AZStd::sys_time_t timeNowSeconds = AZStd::GetTimeNowSecond();
        AZ_TEST_ASSERT(timeNowSeconds != 0);

        AZStd::sys_time_t timeNowTicks = AZStd::GetTimeNowTicks();
        AZ_TEST_ASSERT(timeNowTicks != 0);

        AZStd::sys_time_t timeNowMs = AZStd::GetTimeNowMicroSecond();
        AZ_TEST_ASSERT(timeNowMs != 0);

        AZ::u64 timeNowUTCms = AZStd::GetTimeUTCMilliSecond();
        AZ_TEST_ASSERT(timeNowUTCms != 0);
    };
}