/*
* 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.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.

//  Description : High-level manager class for code coverage system
//                including file handing of checkpoint lists

/* File format specification:
*   The "Code Coverage Context" file format is a text file defined as follows:
*   - The first line is a numeric string giving the number of checkpoints listed in this file
*   - All subsequent lines are a single alphanumeric string (no character restrictions so far)
*   - Each line is delimited by a DOS or Unix newline and is limited to 64 character excluding the delimiter
*
* Design notes:
*   We're only interested in checkpoints that we haven't hit yet
*   Hence we could wait until we have 90% of checkpoints and then load the context, keeping only new labels.
*   Could be worth file specifying total buffer size required.
*/
#include "CryLegacy_precompiled.h"

#if !defined(_RELEASE)

#include "CodeCoverageManager.h"


const static int LABEL_LENGTH = 64;
const static int BUFF_SIZE = LABEL_LENGTH + 3;

//---------------------------------------------------------------------------------------//

CCodeCoverageManager::CCodeCoverageManager()
    : m_bContextValid(false)
    , m_pLabelBlock(NULL)
    , m_nTotalCheckpoints(0)
{
    assert(GetTracker());
}

//---------------------------------------------------------------------------------------//

bool CCodeCoverageManager::ReadCodeCoverageContext(AZ::IO::HandleType fileHandle)
{
    assert(fileHandle != AZ::IO::InvalidHandle);

    // Temporary buffer
    char cBuffer[BUFF_SIZE];

    // Clear any packed string block
    SAFE_DELETE(m_pLabelBlock);

    char* pNextLabelInBlock = NULL;

    // Clear all state in manager

    bool bOk = true;

    // Allow a comment to start file?

    // Read number of checkpoints
    m_nTotalCheckpoints = 0;
    bOk = bOk && (GetLine(cBuffer, fileHandle) > 0);
    if (bOk)
    {
        // Grab as an integer and sanity check
        m_nTotalCheckpoints = atoi(cBuffer);
        bOk = (m_nTotalCheckpoints > 0 && m_nTotalCheckpoints < 1000000);
        assert(bOk);
    }

    if (bOk)
    {
        // Allocate packed string block (assuming worst-case of max length labels + \0)
        m_pLabelBlock = new char[m_nTotalCheckpoints * (BUFF_SIZE + 1)];
        pNextLabelInBlock = m_pLabelBlock;
    }


    for (int i = 0; i < m_nTotalCheckpoints && bOk; ++i)
    {
        // Get next checkpoint label
        int nCharsRead = GetLine(pNextLabelInBlock, fileHandle);
        bOk = nCharsRead > 0;

        if (bOk)
        {
            // Do a little check for practicality's sake
            if (strncmp(pNextLabelInBlock, "AI_", 3) != 0)
            {
                AIWarning("CodeCoverageManager: So far only the AI system uses code coverage - and this label doesn't start AI_. \"%s\"", pNextLabelInBlock);
            }

            // Add to set
            m_setCheckPoints.insert(pNextLabelInBlock);

            // Advance pointer
            pNextLabelInBlock += nCharsRead;
        }
    }

    m_bContextValid = bOk;

    if (bOk)
    {
        AILogComment("CodeCoverageManager: Read all %d code coverage checkpoints successfully", m_nTotalCheckpoints);
        // Does this finish initialisation?
        Update();
    }
    else
    {
        AIWarning("CodeCoverageManager: Failed! Found file but failed after reading %d code coverage checkpoints", m_nTotalCheckpoints);
    }

    return bOk;
}

//---------------------------------------------------------------------------------------//

void CCodeCoverageManager::GetRemainingCheckpointLabels(std::vector < const char* >& vLabels) const
{
    vLabels.clear();
    vLabels.reserve(m_nTotalCheckpoints - GetTotalRegistered());

    for (CheckPointSet::const_iterator it = m_setCheckPoints.begin(); it != m_setCheckPoints.end(); ++it)
    {
        CCodeCoverageCheckPoint* pCP = GetTracker()->GetCheckPoint(*it);
        if (!pCP)
        {
            vLabels.push_back(*it);
        }
    }
}

//---------------------------------------------------------------------------------------//

int CCodeCoverageManager::GetLine(char* pBuff, AZ::IO::HandleType fileHandle)
{
    assert(pBuff && fileHandle != AZ::IO::InvalidHandle);

    // Wraps fgets to remove newlines and use correct buffer/string lengths

    // Must use CryPak FGets on CryPak files
    if (!gEnv->pCryPak->FGets(pBuff, BUFF_SIZE, fileHandle))
    {
        return 0;
    }

    // Convert newlines to \0 and discover length
    int i = 0;
    int nLimit = LABEL_LENGTH + 1;
    for (; i < nLimit; i++)
    {
        char c = pBuff[i];
        if (c == '\n' || c == '\r' || c == '\0')
        {
            break;
        }
    }

    // If i == LABEL_LENGTH, the string was of maximum length
    // If i == LABEL_LENGTH + 1, the string was too long
    if (i == nLimit)
    {
        // Malformed - fail
        pBuff[--i] = '\0';
        AIWarningID("<CCodeCoverageManager>", "Checkpoint label %s... in code coverage context file is too long", pBuff);
        return 0;
    }

    // Overwrite the last character (usually line terminator) with string delimiter
    pBuff[i] = '\0';

    if (i == 0)
    {
        // Malformed - fail
        AIWarningID("<CCodeCoverageManager>", "Empty line in code coverage context file");
        return 0;
    }

    return i + 1;
}

//---------------------------------------------------------------------------------------//

void CCodeCoverageManager::Update(void)
{
    const CheckPointVector& lst = GetTracker()->GetRecentCheckpoints();
    for (CheckPointVector::const_iterator it = lst.begin(); it != lst.end(); ++it)
    {
        if (m_setCheckPoints.find((*it)->GetLabel()) != m_setCheckPoints.end())
        {
            // add to map
            GetTracker()->AddCheckpoint(*it);
        }
        else
        {
            m_setUnexpectedCheckPoints.insert((*it)->GetLabel());
        }
    }

    GetTracker()->ResetRecentCheckpoints();
}

void CCodeCoverageManager::DumpCheckpoints(bool bToFile) const
{
    if (bToFile)
    {
        // Should really be in the level folder, when process is mature
        string filePath = "@user@/DumpedPoints.txt";
        AZ::IO::HandleType streamFileHandle = gEnv->pCryPak->FOpen(filePath.c_str(), "w");

        // Print the unexpected checkpoints we've hit
        gEnv->pCryPak->FPrintf(streamFileHandle, "Unexpected Checkpoints Hit: %" PRISIZE_T "\n", m_setUnexpectedCheckPoints.size());
        for (CheckPointSet::const_iterator it = m_setUnexpectedCheckPoints.begin(); it != m_setUnexpectedCheckPoints.end(); ++it)
        {
            gEnv->pCryPak->FPrintf(streamFileHandle, "%s\n", *it);
        }

        // If we've hit more than 80% of the expected checkpoints, then print the remaining ones
        if (m_setCheckPoints.size() * .8f < (float)GetTracker()->GetTotalRegistered())
        {
            gEnv->pCryPak->FPrintf(streamFileHandle, "Remaining Checkpoints: %" PRISIZE_T "\n", m_setUnexpectedCheckPoints.size());

            std::vector < const char* > vec;
            GetRemainingCheckpointLabels(vec);

            for (std::vector < const char* >::iterator it = vec.begin(); it != vec.end(); ++it)
            {
                gEnv->pCryPak->FPrintf(streamFileHandle, "%s\n", *it);
            }
        }
        gEnv->pCryPak->FClose(streamFileHandle);
        AILogAlways("Dumped checkpoints to file \"%s\"", filePath.c_str());
    }
    else
    {
        // Print the unexpected checkpoints we've hit
        gEnv->pLog->Log("CC:Unexpected Checkpoints Hit:");
        for (CheckPointSet::const_iterator it = m_setUnexpectedCheckPoints.begin(); it != m_setUnexpectedCheckPoints.end(); ++it)
        {
            gEnv->pLog->Log("  CC:%s", *it);
        }

        // If we've hit more than 30% of the expected checkpoints, then print the remaining ones
        // If not, probably this dump was done too early
        if (m_setCheckPoints.size() * .3f < (float)GetTracker()->GetTotalRegistered())
        {
            gEnv->pLog->Log("CC:Remaining Checkpoints:");

            for (CheckPointSet::const_iterator it = m_setCheckPoints.begin(); it != m_setCheckPoints.end(); ++it)
            {
                CCodeCoverageCheckPoint* pCP = GetTracker()->GetCheckPoint(*it);
                if (!pCP)
                {
                    gEnv->pLog->Log("  CC:%s", *it);
                }
            }

            gEnv->pLog->Log("CC:Checkpoints Hit:");

            for (CheckPointSet::const_iterator it = m_setCheckPoints.begin(); it != m_setCheckPoints.end(); ++it)
            {
                CCodeCoverageCheckPoint* pCP = GetTracker()->GetCheckPoint(*it);
                if (pCP)
                {
                    gEnv->pLog->Log("  CC:%s", *it);
                }
            }
        }
        AILogAlways("Dumped checkpoints to log");
    }
}

#endif //_RELEASE