/*
* 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.

#include <StdAfx.h>
#include "XMLBinaryWriter.h"
#include "CryEndian.h"
#include <string.h>  // memcpy()

//////////////////////////////////////////////////////////////////////////
namespace XMLBinary
{
    void SwapEndianness_Node(Node& t)
    {
        SwapEndian(t.nTagStringOffset, true);
        SwapEndian(t.nContentStringOffset, true);
        SwapEndian(t.nAttributeCount, true);
        SwapEndian(t.nChildCount, true);
        SwapEndian(t.nParentIndex, true);
        SwapEndian(t.nFirstAttributeIndex, true);
        SwapEndian(t.nFirstChildIndex, true);
    }

    void SwapEndianness_Attribute(Attribute& t)
    {
        SwapEndian(t.nKeyStringOffset, true);
        SwapEndian(t.nValueStringOffset, true);
    }

    void SwapEndianness_Header(BinaryFileHeader& t)
    {
        SwapEndian(t.nXMLSize, true);
        SwapEndian(t.nNodeTablePosition, true);
        SwapEndian(t.nNodeCount, true);
        SwapEndian(t.nAttributeTablePosition, true);
        SwapEndian(t.nAttributeCount, true);
        SwapEndian(t.nChildTablePosition, true);
        SwapEndian(t.nChildCount, true);
        SwapEndian(t.nStringDataPosition, true);
        SwapEndian(t.nStringDataSize, true);
    }
}

//////////////////////////////////////////////////////////////////////////
XMLBinary::CXMLBinaryWriter::CXMLBinaryWriter()
{
    m_nStringDataSize = 0;
}

static void align(size_t& nPosition, const size_t nAlignment)
{
    const size_t nPadSize = ((nPosition + (nAlignment - 1)) & ~(nAlignment - 1)) - nPosition;
    nPosition += nPadSize;
}

static void alignWrite(XMLBinary::IDataWriter* const pFile, size_t& nPosition, const size_t nAlignment)
{
    size_t nPadSize = ((nPosition + (nAlignment - 1)) & ~(nAlignment - 1)) - nPosition;

    if (nPadSize > 0)
    {
        nPosition += nPadSize;

        static const char zeroes[32] = { 0 };

        while (nPadSize > 0)
        {
            const size_t n = (nPadSize <= sizeof(zeroes)) ? nPadSize : sizeof(zeroes);
            nPadSize -= n;
            pFile->Write(zeroes, n);
        }
    }
}

static void write(XMLBinary::IDataWriter* const pFile, size_t& nPosition, const void* const pData, const size_t nDataSize)
{
    pFile->Write(pData, nDataSize);
    nPosition += nDataSize;
}

//////////////////////////////////////////////////////////////////////////
bool XMLBinary::CXMLBinaryWriter::WriteNode(IDataWriter* pFile, XmlNodeRef node, bool bNeedSwapEndian, XMLBinary::IFilter* pFilter, string& error)
{
    error = "";

    // Scan the node tree, building a flat node list, attribute list and string table.
    m_nStringDataSize = 0;

    if (!CompileTables(node, pFilter, error))
    {
        return false;
    }

    static const uint nMaxNodeCount = (NodeIndex) ~0;
    if (m_nodes.size() > nMaxNodeCount)
    {
        error.Format("XMLBinary: Too many nodes: %d (max is %i)", m_nodes.size(), nMaxNodeCount);
        return false;
    }

    // Initialize the file header.
    size_t nTheoreticalPosition = 0;
    static const size_t nAlignment = sizeof(uint32);

    BinaryFileHeader header;
    static const char signature[] = "CryXmlB";
    COMPILE_TIME_ASSERT(sizeof(signature) == sizeof(header.szSignature));
    memcpy(header.szSignature, signature, sizeof(header.szSignature));
    nTheoreticalPosition += sizeof(header);
    align(nTheoreticalPosition, nAlignment);

    header.nNodeTablePosition = nTheoreticalPosition;
    header.nNodeCount = int(m_nodes.size());
    nTheoreticalPosition += header.nNodeCount * sizeof(Node);
    align(nTheoreticalPosition, nAlignment);

    header.nChildTablePosition = nTheoreticalPosition;
    header.nChildCount = int(m_childs.size());
    nTheoreticalPosition += header.nChildCount * sizeof(NodeIndex);
    align(nTheoreticalPosition, nAlignment);

    header.nAttributeTablePosition = nTheoreticalPosition;
    header.nAttributeCount = int(m_attributes.size());
    nTheoreticalPosition += header.nAttributeCount * sizeof(Attribute);
    align(nTheoreticalPosition, nAlignment);

    header.nStringDataPosition = nTheoreticalPosition;
    header.nStringDataSize = m_nStringDataSize;
    nTheoreticalPosition += header.nStringDataSize;

    header.nXMLSize = nTheoreticalPosition;

    // Swap endianness of the data structures
    if (bNeedSwapEndian)
    {
        SwapEndianness_Header(header);
        for (size_t i = 0, iCount = m_nodes.size(); i < iCount; ++i)
        {
            SwapEndianness_Node(m_nodes[i]);
        }
        for (size_t i = 0, iCount = m_attributes.size(); i < iCount; ++i)
        {
            SwapEndianness_Attribute(m_attributes[i]);
        }
        for (size_t i = 0, iCount = m_childs.size(); i < iCount; ++i)
        {
            SwapEndian(m_childs[i], true);
        }
    }

    // Write file
    {
        nTheoreticalPosition = 0;

        // Write out the file header.
        write(pFile, nTheoreticalPosition, &header, sizeof(header));
        alignWrite(pFile, nTheoreticalPosition, nAlignment);

        // Write out the node table.
        if (!m_nodes.empty())
        {
            write(pFile, nTheoreticalPosition, &m_nodes[0], sizeof(m_nodes[0]) * m_nodes.size());
            alignWrite(pFile, nTheoreticalPosition, nAlignment);
        }

        // Write out the children table.
        if (!m_childs.empty())
        {
            write(pFile, nTheoreticalPosition, &m_childs[0], sizeof(m_childs[0]) * m_childs.size());
            alignWrite(pFile, nTheoreticalPosition, nAlignment);
        }

        // Write out the attribute table.
        if (!m_attributes.empty())
        {
            write(pFile, nTheoreticalPosition, &m_attributes[0], sizeof(m_attributes[0]) * m_attributes.size());
            alignWrite(pFile, nTheoreticalPosition, nAlignment);
        }

        // Write out the data of all the m_strings.
        for (size_t nString = 0; nString < m_strings.size(); ++nString)
        {
            pFile->Write(m_strings[nString].c_str(), m_strings[nString].size() + 1);
        }
    }

    return true;
}

bool XMLBinary::CXMLBinaryWriter::CompileTables(XmlNodeRef node, XMLBinary::IFilter* pFilter, string& error)
{
    bool ok = CompileTablesForNode(node, -1, pFilter, error);
    ok = ok && CompileChildTable(node, pFilter, error);
    return ok;
}

//////////////////////////////////////////////////////////////////////////
bool XMLBinary::CXMLBinaryWriter::CompileTablesForNode(XmlNodeRef node, int nParentIndex, XMLBinary::IFilter* pFilter, string& error)
{
    // Add the tag to the string table.
    int nTagStringOffset = AddString(node->getTag());

    // Add the content string to the string table.
    int nContentStringOffset = AddString(node->getContent());

    // Add all the attributes to the attributes table.
    const char* szKey;
    const char* szValue;
    const int nFirstAttributeIndex = int(m_attributes.size());
    for (int i = 0, attrCount = node->getNumAttributes(); i < attrCount; ++i)
    {
        if (node->getAttributeByIndex(i, &szKey, &szValue) &&
            (!pFilter || pFilter->IsAccepted(IFilter::eType_AttributeName, szKey)))
        {
            // Add the key and the value to the string table.
            Attribute attribute;
            attribute.nKeyStringOffset = AddString(szKey);
            attribute.nValueStringOffset = AddString(szValue);

            // Add the attribute to the attribute table.
            m_attributes.push_back(attribute);
        }
    }
    const int nAttributeCount = int(m_attributes.size()) - nFirstAttributeIndex;

    static const int nMaxAttributeCount = (uint16) ~0;
    if (nAttributeCount > nMaxAttributeCount)
    {
        error.Format("XMLBinary: Too many attributes in a node: %d (max is %i)", nAttributeCount, nMaxAttributeCount);
        return false;
    }

    // Add ourselves to the node list.
    const int nIndex = int(m_nodes.size());
    {
        Node nd;
        memset(&nd, 0, sizeof(nd));
        nd.nTagStringOffset = nTagStringOffset;
        nd.nContentStringOffset = nContentStringOffset;
        nd.nParentIndex = nParentIndex;
        nd.nFirstAttributeIndex = nFirstAttributeIndex;
        nd.nAttributeCount = nAttributeCount;

        m_nodes.push_back(nd);
    }

    m_nodesMap.insert(NodesMap::value_type(node, nIndex));

    // Recurse to the child nodes.
    int nChildCount = 0;
    static const int nMaxChildCount = (uint16) ~0;
    for (int nChild = 0, numChilds = node->getChildCount(); nChild < numChilds; ++nChild)
    {
        XmlNodeRef childNode = node->getChild(nChild);
        if (!pFilter || pFilter->IsAccepted(IFilter::eType_ElementName, childNode->getTag()))
        {
            if (++nChildCount > nMaxChildCount)
            {
                error.Format("XMLBinary: Too many children in node '%s': %d (max is %i)", childNode->getTag(), nChildCount, nMaxChildCount);
                return false;
            }
            if (!CompileTablesForNode(childNode, nIndex, pFilter, error))
            {
                return false;
            }
        }
    }

    m_nodes[nIndex].nChildCount = nChildCount;

    return true;
}

//////////////////////////////////////////////////////////////////////////
bool XMLBinary::CXMLBinaryWriter::CompileChildTable(XmlNodeRef node, XMLBinary::IFilter* pFilter, string& error)
{
    const int nIndex = m_nodesMap.find(node)->second; // Assume node always exist in map.
    const int nFirstChildIndex = (int)m_childs.size();

    Node& nd = m_nodes[nIndex];
    nd.nFirstChildIndex = nFirstChildIndex;

    int nChildCount = 0;
    for (int nChild = 0, numChilds = node->getChildCount(); nChild < numChilds; ++nChild)
    {
        XmlNodeRef childNode = node->getChild(nChild);
        if (!pFilter || pFilter->IsAccepted(IFilter::eType_ElementName, childNode->getTag()))
        {
            ++nChildCount;
            const int nChildIndex = m_nodesMap.find(childNode)->second; // Assume node always exist in map.
            m_childs.push_back(nChildIndex);
        }
    }
    if (nChildCount != nd.nChildCount)
    {
        error.Format("XMLBinary: Internal error in CompileChildTable()");
        return false;
    }

    // Recurse to the child nodes.
    for (int nChild = 0, numChilds = node->getChildCount(); nChild < numChilds; ++nChild)
    {
        XmlNodeRef childNode = node->getChild(nChild);
        if (!pFilter || pFilter->IsAccepted(IFilter::eType_ElementName, childNode->getTag()))
        {
            if (!CompileChildTable(childNode, pFilter, error))
            {
                return false;
            }
        }
    }

    return true;
}

//////////////////////////////////////////////////////////////////////////
int XMLBinary::CXMLBinaryWriter::AddString(const XmlString& sString)
{
    // If we have such string already, then we will re-use its data.
    StringMap::const_iterator itStringEntry = m_stringMap.find(sString);
    if (itStringEntry == m_stringMap.end())
    {
        // We don't have such string yet, so we should add it to the tables.
        m_strings.push_back(sString);
        itStringEntry = m_stringMap.insert(StringMap::value_type(sString, m_nStringDataSize)).first;
        m_nStringDataSize += sString.length() + 1;
    }

    // Return offset of the string in the string data buffer.
    return (*itStringEntry).second;
}