/**
 * @file FileLogManager.h
 * @author Bartosz Janiak
 * 
 * @brief Contains the definition of FileLogManager class.
 *
 * Copyright © 2009 NVIDIA Corporation. All rights reserved.
 *
 * NVIDIA Corporation and its licensors retain all intellectual property and proprietary
 * rights in and to this software, related documentation and any modifications thereto. Any
 * use, reproduction, disclosure or distribution of this software and related documentation
 * without express license agreement from NVIDIA Corporation is strictly prohibited.
 */

#pragma once

#include "TStd.h"
#include "TBoostFilesystem.h"
#include "RegistryKey.h"
#include "IGenericLogManager.h"
#include <boost/scoped_ptr.hpp>

#define LOGGING_KEYNAME_PATH_PREFIX _T("PathPrefix")
#define LOGGING_KEYNAME_APPEND_PROCESS_NAME_TO_PATH_PREFIX _T("AppendProcessNameToPathPrefix")
#define LOGGING_KEYNAME_GET_MAX_FILE_COUNT _T("MaxFileCount")
#define LOGGING_KEYNAME_GET_MAX_FILE_SIZE _T("MaxFileSize")
#define LOGGING_KEYNAME_GET_WRITE_THROUGH _T("WriteThrough")
#define LOGGING_FILELOGMANAGER_DEFAULT_PATH_PREFIX _T("%PUBLIC%\\Documents\\NvidiaLogging\\Log.")
#define LOGGING_FILELOGMANAGER_LOG_SUFFIX _T(".log")
#define LOGGING_FILELOGMANAGER_INSTANCE_SUFFIX _T(".inst")
#define LOGGING_MAX_INSTANCES_COUNT 16
#define LOGGING_DEFAULT_PROCESS _T("DefaultProcess")
#define LOGGING_MUTEX_SUFFIX _T("{C40CFCD4-C757-4139-A4DA-7CB51A8DBF80}")
#define LOGGING_MUTEX_PREFIX _T("Global\\")
#define LOGGING_END_LINE _T("\r\n")

#if !defined(MOCK_WINAPI)
#define MOCK_WINAPI
#define _NOT_MOCKED
#endif

namespace Nvidia {
namespace Logging {

namespace Test {
    class FileLogManagerTest;
}

/**
 * @brief Class to contain creating and releasing a named mutex.
 */
class NamedWindowsMutex
{
private:
    ///@brief The Windows handle for the named mutex
    HANDLE m_mutex;

public:
    /**
     * @brief Creates a mutex with the given name
     * @param name the name to use for the mutex.
     */
    NamedWindowsMutex(const std::tstring &name)
        : m_mutex(NULL)
    {
        std::tstring mutexName = LOGGING_MUTEX_PREFIX;
        mutexName += name;
        mutexName += LOGGING_MUTEX_SUFFIX;
        m_mutex = ::CreateMutex(NULL, FALSE, mutexName.c_str());
        if (m_mutex == NULL)
        {
            throw std::runtime_error("Could not create named mutex");
        }
    }

    /**
     * @brief Closes the named mutex.
     */
    ~NamedWindowsMutex()
    {
        if (m_mutex != NULL)
        {
            ::CloseHandle(m_mutex);
        }
    }

    /**
     * @brief Locks the named mutex
     * @exception std::runtime_error when the lock cannot be retrieved.
     */
    void Lock()
    {
        DWORD lockReturn = WaitForSingleObject(m_mutex, INFINITE);

        if (lockReturn == WAIT_FAILED)
        {
            throw std::runtime_error("Error locking mutex");
        }
    }

    ///@brief Unlocks the mutex
    void Unlock()
    {
        ReleaseMutex(m_mutex);
    }

    friend class Test::FileLogManagerTest;
};
/**
 * @brief This locks a windows named mutex on construction and
 * releases it on destruction
 */
class ScopedMutexLock
{
private:
    ///@brief Windows mutex
    NamedWindowsMutex *m_mutex;

public:
    /**
     * @brief Locks a windows mutex with the name provided.
     * @param namedMutex The windows mutex to lock
     * @exception std::runtime_error thrown if the mutex cannot be created.
     */
    ScopedMutexLock(NamedWindowsMutex &namedMutex)
        : m_mutex(&namedMutex)
    {
        m_mutex->Lock();
    }

    /**
     * @brief Releases the mutex.
     */
    ~ScopedMutexLock()
    {
        m_mutex->Unlock();
    }
};

/**
 * @brief A simple file class for writing in append mode. We cannot
 * use file streams because it does not use FILE_SHARE_DELETE permissions.
 */
class WindowsFileAppender
{
public:
    ///@brief Flags passed to Open to control how file logging is done.
    enum OpenFlags
    {
        OpenFlags_WriteThrough = 0x0, ///<@brief waits until writes to the file are complete before returning
        OpenFlags_Buffered     = 0x1, ///<@brief buffers writes for performance
    };

private:
    ///@brief Windows handle for the file we append to
    HANDLE m_fileHandle;

public:
    /**
     * @brief Constructor initializes the appender to no open file.
     */
    WindowsFileAppender()
        : m_fileHandle(INVALID_HANDLE_VALUE)
    {
    }

    ///@brief Closes any open file on destruction.
    ~WindowsFileAppender()
    {
        if (m_fileHandle != INVALID_HANDLE_VALUE)
        {
            Close();
        }
    }

    /**
     * @brief Appends data to an open file. The file must be Open() prior
     * to calling this. This converts UTF-16 to UTF-8 when tstring is wstring.
     * @param data The string to append to the file.
     */
    void Append(const std::tstring &data)
    {
        DWORD bytesWritten = 0;
        const char *buf = NULL;
#if defined (UNICODE) || defined (_UNICODE)
        // convert from UTF-16 to UTF-8
        DWORD dataLength = static_cast<DWORD>(data.length());
        int dataSize = WideCharToMultiByte(CP_UTF8, 0, data.c_str(),
            dataLength, NULL, NULL, NULL, NULL);
        boost::scoped_array<char> dataContainer;
        if (dataSize > 0)
        {
            dataContainer.reset(new char[dataSize]);
            buf = dataContainer.get();
            int bytesCopied = WideCharToMultiByte(CP_UTF8, 0, data.c_str(), 
                dataLength, dataContainer.get(), dataSize, NULL, NULL);
            assert(bytesCopied == dataSize);
            DBG_UNREFERENCED_LOCAL_VARIABLE(bytesCopied);
        }
#else
        DWORD dataSize = static_cast<DWORD>(data.length());
        buf = data.c_str();
#endif
        BOOL written = WriteFile(
            m_fileHandle, // file handle
            buf, // data
            static_cast<DWORD>(dataSize), // data length
            &bytesWritten, // number of bytes written
            NULL // overlapped -- we aren't writing in overlapped mode.
            );
        if (written == FALSE ||  bytesWritten != static_cast<DWORD>(dataSize))
        {
            throw std::runtime_error("Error writing to log");
        }
    }

    /**
     * @brief Closes an open file if one is open.
     */
    void Close()
    {
        assert(m_fileHandle != INVALID_HANDLE_VALUE);
        CloseHandle(m_fileHandle);
        m_fileHandle = INVALID_HANDLE_VALUE;
    }

    /**
     * @brief Opens a file with the given name
     * @param fileName The file to try to open
     * @param flags Change how writes are done -- either write-through or buffered
     */
    bool Open(const std::tstring &fileName, OpenFlags flags)
    {
        DWORD fileAttributes = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN;
        if ((flags & OpenFlags_Buffered) != OpenFlags_Buffered)
        {
            fileAttributes |= FILE_FLAG_WRITE_THROUGH;
        }
        assert(m_fileHandle == INVALID_HANDLE_VALUE);
        m_fileHandle = MOCK_WINAPI::CreateFile(
            fileName.c_str(), // file name
            FILE_APPEND_DATA, // access
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // share mode
            NULL, // security attributes
            OPEN_ALWAYS, // create or open a file
            fileAttributes, // file attributes
            NULL // template file handle
            );

        return (m_fileHandle != INVALID_HANDLE_VALUE);
    }

    /**
     * @brief Returns the size of the currently-opened file.
     */
    long long GetSize()
    {
        assert(m_fileHandle != INVALID_HANDLE_VALUE);
        LARGE_INTEGER fileSize = {0};

        if (::GetFileSizeEx(m_fileHandle, &fileSize) == FALSE)
        {
            throw std::runtime_error("Can't get log file size");
        }
        return fileSize.QuadPart;
    }
};

/// @brief Log manager which outputs string messages to files.
class FileLogManager : public IGenericLogManager<std::tstring>
{
    friend class Test::FileLogManagerTest;

private:
    ///@brief If not initializing from the registry, this is the
    /// default maximum number of log files kept
    static const unsigned int DefaultMaxFileCount = 10;

    ///@brief If not initializing from the registry, this is the
    /// default maximum size of the log file before rolling over
    static const unsigned int DefaultMaxFileSize = 1000000;

    /// @brief File handle which writes to the active log file.
    WindowsFileAppender m_file;

    /**
     * @brief Path prefix of the names of files managed by this LogManager.
     *
     * This is equivalent to the name of the active log file.
     */
    std::tstring m_pathPrefix;

    /** 
     * @brief Maximum number of files managed by this FileLogManager. 
     *
     * If exceeded, the oldest file is deleted.
     */
    unsigned int m_maxFileCount;

    /** 
     * @brief Maximum size of the file managed by this FileLogManager.
     *
     * If exceeded, the log files are rotated.
     */
    unsigned int m_maxFileSize;

    ///@brief write-through or buffered.
    WindowsFileAppender::OpenFlags m_openFlags;

    ///@brief A mutex to lock change access to the file.
    boost::scoped_ptr<NamedWindowsMutex> m_mutex;

public:
    /**
     * @brief Default constructor. Uses the default process name if the
     * process name cannot be retrieved.
     */
    FileLogManager();

    /**
     * @brief Constructor. Configures the FileLogManager using configuration stored in the registry.
     * Uses the default process name if it cannot be retrieved.
     *
     * @throws RegistryException When the configuration stored in the registry is invalid.
     */
    FileLogManager(const RegistryKey& key);

    /// @brief Writes the message to m_fstream and rotates the log files, if necessary.
    virtual void ManageMessage(const std::tstring& message);

    /// @brief Enables/Disables write-through for log file
    virtual void EnableWriteThrough(bool enable = true);

private:
    /// @brief Checks whether the size of the active log file exceeds m_maxFileSize. If true, it rotates the log files.
    void RotateIfNecessary();

    /**
     * @brief Initializes the m_file and m_mutex members.
     *
     * This involves creating all directories leading to m_pathPrefix, and modifying m_pathPrefix if (m_pathPrefix + ".log")
     * is currently being written to by another process. LOGGING_MAX_INSTANCES_COUNT variations of m_pathPrefix are
     * tried before the function will give up.
     */
    void InitFile();

    /**
     * @brief Creates the named mutex for the log file so that we don't have
     * simultaneous writes to it.
     * @return true if the mutex was created successfully.
     */
    bool InitMutex();
};

inline FileLogManager::FileLogManager()
{
    m_pathPrefix = LOGGING_FILELOGMANAGER_DEFAULT_PATH_PREFIX;
    Utils::ExpandEnvironmentVariables(m_pathPrefix);
    std::tstring processName = Utils::GetProcessName();
    m_pathPrefix += processName;

    m_maxFileCount = DefaultMaxFileCount;
    m_maxFileSize = DefaultMaxFileSize;
    m_openFlags = WindowsFileAppender::OpenFlags_WriteThrough;

    InitFile();
}

inline FileLogManager::FileLogManager(const RegistryKey& key)
{
    m_pathPrefix = key.GetStringValue(LOGGING_KEYNAME_PATH_PREFIX);
    Utils::ExpandEnvironmentVariables(m_pathPrefix);

    if ( key.GetDWORDValue(LOGGING_KEYNAME_APPEND_PROCESS_NAME_TO_PATH_PREFIX) )
    {
        m_pathPrefix += _T(".");
        std::tstring processName = Utils::GetProcessName();
        m_pathPrefix += processName;
    }
    m_maxFileCount = key.GetDWORDValue(LOGGING_KEYNAME_GET_MAX_FILE_COUNT);
    m_maxFileSize = key.GetDWORDValue(LOGGING_KEYNAME_GET_MAX_FILE_SIZE);
    DWORD writeThrough = FALSE;
    key.TryGetValue(LOGGING_KEYNAME_GET_WRITE_THROUGH, writeThrough);
    m_openFlags = (writeThrough != FALSE)
        ? WindowsFileAppender::OpenFlags_WriteThrough
        : WindowsFileAppender::OpenFlags_Buffered;

    InitFile();
}

inline void FileLogManager::InitFile()
{
    m_pathPrefix += LOGGING_FILELOGMANAGER_LOG_SUFFIX;
    boost::filesystem::tpath file(m_pathPrefix);
    boost::filesystem::create_directories(file.parent_path());

    bool opened = m_file.Open(m_pathPrefix, m_openFlags);

    if (!opened)
    {
        throw std::runtime_error("Failed to create or open the output file for FileLogManager");
    }
    m_mutex.reset(new NamedWindowsMutex(file.filename().native()));
}

inline void FileLogManager::ManageMessage(const std::tstring& message)
{
    ScopedMutexLock lock(*m_mutex);
    RotateIfNecessary();
    m_file.Append(message);
    m_file.Append(LOGGING_END_LINE);
}

inline void FileLogManager::RotateIfNecessary()
{
    if (m_file.GetSize() > m_maxFileSize)
    {
        m_file.Close();
        if (!m_file.Open(m_pathPrefix, m_openFlags))
        {
            throw std::runtime_error("Can't reopen log file");
        }
        if (m_file.GetSize() > m_maxFileSize)
        {
            m_file.Close();
            boost::filesystem::tpath file(m_pathPrefix);

            boost::filesystem::tpath lastFile(m_pathPrefix + boost::lexical_cast<std::tstring>(m_maxFileCount));
            boost::filesystem::remove(lastFile);

            for ( unsigned int i = m_maxFileCount - 1 ; i >= 1 ; i-- )
            {
                boost::filesystem::tpath thisFile(m_pathPrefix + boost::lexical_cast<std::tstring>(i));
                if ( boost::filesystem::exists(thisFile) )
                {
                    boost::filesystem::tpath nextFile(m_pathPrefix + boost::lexical_cast<std::tstring>(i + 1));
                    boost::filesystem::rename(thisFile, nextFile);
                }
            }

            boost::filesystem::tpath firstFile(m_pathPrefix + boost::lexical_cast<std::tstring>(1));
            boost::filesystem::rename(file, firstFile);
            if (!m_file.Open(m_pathPrefix, m_openFlags))
            {
                throw std::runtime_error("Cannot open log file");
            }
        }
    }
}

inline void FileLogManager::EnableWriteThrough(bool enable)
{
    ScopedMutexLock lock(*m_mutex);
    WindowsFileAppender::OpenFlags openFlags = enable
        ? WindowsFileAppender::OpenFlags_WriteThrough
        : WindowsFileAppender::OpenFlags_Buffered;
    if (m_openFlags != openFlags)
    {
        m_openFlags = openFlags;
        m_file.Close();
        if (!m_file.Open(m_pathPrefix, m_openFlags))
        {
            throw std::runtime_error("Can't reopen log file");
        }
    }
}

}
}

#ifdef _NOT_MOCKED
#undef MOCK_WINAPI
#undef _NOT_MOCKED
#endif
