/**
 * Copyright (C) 2019, 2022 Xilinx, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License"). You may
 * not use this file except in compliance with the License. A copy of the
 * License is located at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

#include "FDTProperty.h"

#include "DTCStringsBlock.h"
#include "XclBinUtilities.h"
#include <boost/format.hpp>
#include <boost/tokenizer.hpp>
#include <boost/tuple/tuple.hpp>
#include <limits.h>
#include <stdint.h>

namespace XUtil = XclBinUtilities;

#ifdef _WIN32
  #pragma comment(lib, "wsock32.lib")
  #pragma warning( disable : 4267 )
  #include <winsock2.h>
#else
  #include <arpa/inet.h>
#endif


typedef struct {
  FDTProperty::DataFormat eDataFormat;
  unsigned int wordSize;
  bool isArray;
  std::string extension;
  std::string prettyName;
} DataFormatElement;

static const std::vector<DataFormatElement> _DataFormatTable = {
  { FDTProperty::DataFormat::au8,     1,  true,  "_au8",  "array uint8_t" },
  { FDTProperty::DataFormat::au16,    2,  true,  "_au16", "array uint16_t" },
  { FDTProperty::DataFormat::au32,    4,  true,  "_au32", "array uint32_t" },
  { FDTProperty::DataFormat::au64,    8,  true,  "_au64", "array uint64_t" },
  { FDTProperty::DataFormat::asz,     1,  true,  "_asz",  "string" },
  { FDTProperty::DataFormat::u16,     2,  false, "_u16",  "uint16_t" },
  { FDTProperty::DataFormat::u32,     4,  false, "_u32",  "uint32_t" },
  { FDTProperty::DataFormat::u64,     8,  false, "_u64",  "uint64_t" },
  { FDTProperty::DataFormat::u128,    16, false, "_u128", "uint128_t" },
  { FDTProperty::DataFormat::sz,      1,  false,  "_sz",  "string" },
  { FDTProperty::DataFormat::unknown, 1,  true,  "", "Unknown (default array uint8_t)" }
};

FDTProperty::DataFormat
FDTProperty::getDataFormat(const std::string& _variableName) const
{
  for (unsigned int index = 0; index < _DataFormatTable.size(); ++index) {
    const std::string& extension = _DataFormatTable[index].extension;
    // To handle the case where the table doesn't have an extension
    if (extension.length() == 0) {
      continue;
    }

    // Do we have a match
    if (hasEnding(_variableName, extension) == true) {
      return _DataFormatTable[index].eDataFormat;
    }
  }

  return DataFormat::unknown;
}

const std::string&
FDTProperty::getDataFormatPrettyName(DataFormat _eDataFormat) const
{
  for (unsigned int index = 0; index < _DataFormatTable.size(); ++index) {
    if (_DataFormatTable[index].eDataFormat == _eDataFormat) {
      return _DataFormatTable[index].prettyName;
    }
  }

  // Should never get here, but just in case we do
  return getDataFormatPrettyName(DataFormat::unknown);
}

unsigned int
FDTProperty::getWordLength(DataFormat _eDataFormat) const
{
  for (unsigned int index = 0; index < _DataFormatTable.size(); ++index) {
    if (_DataFormatTable[index].eDataFormat == _eDataFormat) {
      return _DataFormatTable[index].wordSize;
    }
  }

  auto errMsg = boost::format("ERROR: Unknown data format: %d") % (unsigned int)_eDataFormat;
  throw std::runtime_error(errMsg.str());
  return 0;
}

bool
FDTProperty::isDataFormatArray(DataFormat _eDataFormat) const
{
  for (unsigned int index = 0; index < _DataFormatTable.size(); ++index) {
    if (_DataFormatTable[index].eDataFormat == _eDataFormat) {
      return _DataFormatTable[index].isArray;
    }
  }
  return false;
}


FDTProperty::FDTProperty()
    : m_dataLength(0)
    , m_pDataBuffer(nullptr)
    , m_eDataFormat(DataFormat::unknown)
{
  // Empty
}


FDTProperty::~FDTProperty()
{
  // Delete m_pDataBuffer
  if (m_pDataBuffer != NULL) {
    delete m_pDataBuffer;
    m_pDataBuffer = NULL;
  }
  m_dataLength = 0;
}


void
FDTProperty::runningBufferCheck(const unsigned int _bytesExamined, const unsigned int _size)
{
  if (_bytesExamined > _size) {
    throw std::runtime_error("ERROR: Bytes examined exceeded size of buffer.");
  }
}


struct FDTLenOffset {
  uint32_t len;
  uint32_t nameoff;
};

FDTProperty::FDTProperty(const char* _pBuffer,
                         const unsigned int _size,
                         const DTCStringsBlock& _dtcStringsBlock,
                         unsigned int& _bytesExamined,
                         const PropertyNameFormat& _propertyNameFormat)
    : FDTProperty()
{
  XUtil::TRACE("Extracting FDT Property.");

  // Initialize return variables
  _bytesExamined = 0;

  // Validate the buffer
  if (_pBuffer == NULL) {
    throw std::runtime_error("ERROR: The given property buffer pointer is NULL.");
  }

  if (_size == 0) {
    throw std::runtime_error("ERROR: The given property size is empty.");
  }

  // Check the header size
  if (_size < sizeof(FDTLenOffset)) {
    auto errMsg = boost::format("ERROR: The given property buffer's header size (%d bytes) is smaller then its header (%d bytes).") %  _size % sizeof(FDTLenOffset);
    throw std::runtime_error(errMsg.str());
  }

  // -- Get the offset, length, and name values --
  unsigned int index = 0;

  const FDTLenOffset* pHdr = (const FDTLenOffset*)&_pBuffer[index];
  index += sizeof(FDTLenOffset);
  runningBufferCheck(index, _size);

  m_name = _dtcStringsBlock.getString(ntohl(pHdr->nameoff));
  m_dataLength = ntohl(pHdr->len);

  // -- Determine the type of data
  DataFormat eDataType = getDataFormat(m_name);
  if (_propertyNameFormat.find(m_name) != _propertyNameFormat.end()) {
    eDataType = _propertyNameFormat.find(m_name)->second;
  }

  const std::string& prettyTypeName = getDataFormatPrettyName(eDataType);

  XUtil::TRACE(boost::format("Property Name: '%s', length: %d, type: %s") % m_name % m_dataLength % prettyTypeName);

  // Get the data (if any)
  if (m_dataLength != 0) {
    m_pDataBuffer = new char[m_dataLength];
    memcpy(m_pDataBuffer, &_pBuffer[index], m_dataLength);
    XUtil::TRACE_BUF("Property Data", m_pDataBuffer, m_dataLength);
  }

  // Update index
  index += m_dataLength;

  // Align to uint32 byte boundary
  if ((index % 4) != 0) {
    index += 4 - (index % 4);
  }

  _bytesExamined = index;
}

bool
FDTProperty::hasEnding(std::string const& _sFullString, std::string const& _sEndSubString)
{
  // See if there is room
  if (_sFullString.length() < _sEndSubString.length()) {
    return false;
  }

  // Compare the ending of the string to see if they don't match
  if (_sFullString.compare(_sFullString.length() - _sEndSubString.length(), _sEndSubString.length(), _sEndSubString) != 0) {
    return false;
  }

  // If we get this far, then they match
  return true;
}


void
FDTProperty::au8MarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: Array of 8 bits");

  const uint8_t* uint8Array = (const uint8_t*)m_pDataBuffer;

  boost::property_tree::ptree ptProperty;
  for (unsigned int index = 0; index < m_dataLength; ++index) {
    boost::property_tree::ptree ptChildArrayElement;
    ptChildArrayElement.put("", (boost::format("0x%x") % uint8Array[index]).str());
    ptProperty.push_back({ "", ptChildArrayElement });
  }
  _ptTree.add_child(m_name.c_str(), ptProperty);
}

void
FDTProperty::au16MarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: Array of 16 bits");

  // Check and make sure that all is good
  static const unsigned int byteBoundary = 2;
  if ((m_dataLength % byteBoundary) != 0) {
    auto errMsg = boost::format("ERROR: Data length (%d) does not end on a 2-byte boundary.") % m_dataLength;
    throw std::runtime_error(errMsg.str());
  }

  unsigned int numElements = m_dataLength / byteBoundary;
  const uint16_t* uint16Array = (const uint16_t*)m_pDataBuffer;

  boost::property_tree::ptree ptProperty;
  for (unsigned int index = 0; index < numElements; ++index) {
    boost::property_tree::ptree ptChildArrayElement;
    ptChildArrayElement.put("", (boost::format("0x%x") % ntohs(uint16Array[index])).str());
    ptProperty.push_back({ "", ptChildArrayElement });
  }
  _ptTree.add_child(m_name.c_str(), ptProperty);
}

void
FDTProperty::au32MarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: Array of 32 bits");

  // Check and make sure that all is good
  static unsigned int byteBoundary = 4;
  if ((m_dataLength % byteBoundary) != 0) {
    auto errMsg = boost::format("ERROR: Data length (%d) does not end on a 4-byte boundary.") % m_dataLength;
    throw std::runtime_error(errMsg.str());
  }

  unsigned int numElements = m_dataLength / byteBoundary;
  const uint32_t* uint32Array = (const uint32_t*)m_pDataBuffer;

  boost::property_tree::ptree ptProperty;
  for (unsigned int index = 0; index < numElements; ++index) {
    boost::property_tree::ptree ptChildArrayElement;
    ptChildArrayElement.put("", (boost::format("0x%x") % ntohl(uint32Array[index])).str());
    ptProperty.push_back({ "", ptChildArrayElement });
  }
  _ptTree.add_child(m_name.c_str(), ptProperty);
}

void
FDTProperty::u16MarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: 16 bits");

  // Check and make sure that all is good
  static const unsigned int byteBoundary = 2;
  if ((m_dataLength % byteBoundary) != 0) {
    auto errMsg = boost::format("ERROR: Data length (%d) does not end on a 2-byte boundary.") % m_dataLength;
    throw std::runtime_error(errMsg.str());
  }

  const uint16_t uint16Value = ntohs(*((const uint16_t*)m_pDataBuffer));
  _ptTree.put(m_name.c_str(), (boost::format("0x%x") % uint16Value).str());
}

void
FDTProperty::u32MarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: 32 bits");

  // Check and make sure that all is good
  if (m_dataLength != sizeof(uint32_t)) {
    auto errMsg = boost::format("ERROR: Data length for a 32-bit word is invalid: Expected: %d, Actual: %d") % sizeof(uint32_t) % m_dataLength;
    throw std::runtime_error(errMsg.str());
  }

  const uint32_t uint32Value = ntohl(*((const uint32_t*)m_pDataBuffer));
  _ptTree.put(m_name.c_str(), (boost::format("0x%x") % uint32Value).str());
}

void
FDTProperty::u128MarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: 128 bits");

  // Check and make sure that all is good
  static const unsigned int expectedSize = 16;
  if (m_dataLength != expectedSize) {
    auto errMsg = boost::format("ERROR: Data length for a 128-bit word is invalid: Expected: %d, Actual: %d") % expectedSize % m_dataLength;
    throw std::runtime_error(errMsg.str());
  }

  std::string s128Hex;

  XUtil::binaryBufferToHexString((const unsigned char*)m_pDataBuffer, m_dataLength, s128Hex);
  _ptTree.put(m_name.c_str(), (boost::format("0x%s") % s128Hex).str());
}

void
FDTProperty::u64MarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: 64 bits");

  // Check and make sure that all is good
  if (m_dataLength != sizeof(uint64_t)) {
    auto errMsg = boost::format("ERROR: Data length for a 64-bit word is invalid: Expected: %d, Actual: %d") % sizeof(uint64_t) % m_dataLength;
    throw std::runtime_error(errMsg.str());
  }

  std::string s64Hex;
  XUtil::binaryBufferToHexString((const unsigned char*)m_pDataBuffer, sizeof(uint64_t), s64Hex);
  _ptTree.put(m_name.c_str(), (boost::format("0x%s") % s64Hex).str());
}

void
FDTProperty::au64MarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: Array 64 bits");

  // Check and make sure that all is good
  static const unsigned int byteBoundary = 8;
  if ((m_dataLength % byteBoundary) != 0) {
    auto errMsg = boost::format("ERROR: Data length (%d) does not end on a 8-byte boundary.") % m_dataLength;
    throw std::runtime_error(errMsg.str());
  }

  boost::property_tree::ptree ptProperty;
  for (unsigned int index = 0; index < m_dataLength; index += byteBoundary) {
    boost::property_tree::ptree ptChildArrayElement;
    std::string s64Hex;
    XUtil::binaryBufferToHexString((const unsigned char*)&m_pDataBuffer[index], byteBoundary, s64Hex);
    ptChildArrayElement.put("", (boost::format("0x%s") % s64Hex).str());
    ptProperty.push_back({ "", ptChildArrayElement });
  }

  _ptTree.add_child(m_name.c_str(), ptProperty);
}


void
FDTProperty::aszMarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: Array String");

  // Check and make sure that all is good
  if (m_dataLength == 0) {
    throw std::runtime_error("ERROR: Malformed string.  Missing terminator.");
  }

  if (m_pDataBuffer[m_dataLength - 1] != '\0') {
    throw std::runtime_error("ERROR: Missing string terminator.");
  }

  std::vector<std::string> arrayOfStrings;
  unsigned int index = 0;
  unsigned int lastIndex = 0;
  for (index = 0; index < m_dataLength; ++index) {
    if (m_pDataBuffer[index] == '\0') {
      std::string sString = &m_pDataBuffer[lastIndex];
      lastIndex = index + 1;
      arrayOfStrings.push_back(sString);
    }
  }

  boost::property_tree::ptree ptProperty;
  for (auto sString : arrayOfStrings) {
    boost::property_tree::ptree ptChildArrayElement;
    ptChildArrayElement.put("", sString.c_str());
    ptProperty.push_back({ "", ptChildArrayElement });
    XUtil::TRACE(boost::format("String: %s") % sString);
  }

  _ptTree.add_child(m_name.c_str(), ptProperty);
}

void
FDTProperty::szMarshalToJSON(boost::property_tree::ptree& _ptTree) const
{
  XUtil::TRACE("   Type: String");

  // Check and make sure that all is good
  if (m_dataLength == 0) {
    throw std::runtime_error("ERROR: Malformed string.  Missing terminator.");
  }

  if (m_pDataBuffer[m_dataLength - 1] != '\0') {
    throw std::runtime_error("ERROR: Missing string terminator.");
  }

  _ptTree.put(m_name.c_str(), m_pDataBuffer);
}



unsigned int
FDTProperty::writeDataWord(DataFormat _eDataFormat,
                           char* _buffer,
                           const std::string& _sData)
{
  unsigned int bytesWritten = 0;
  XUtil::TRACE(boost::format("Storing property: '%s' with value: '%s'") % m_name % _sData);

  switch (_eDataFormat) {
    case DataFormat::sz:
    case DataFormat::asz:
      // Copy the string + the '\0' byte
      memcpy(_buffer, _sData.c_str(), (_sData.size() + 1));
      bytesWritten = _sData.size() + 1;
      break;

    case DataFormat::au8: {
        uint64_t dataWord = std::strtoul(_sData.c_str(), NULL, 0);
        if (dataWord > UINT8_MAX) {
          auto errMsg = boost::format("ERROR: Property '%s' data value '%s' exceeds the maximum byte storage space'.") % m_name % _sData;
          throw std::runtime_error(errMsg.str());
        }

        uint8_t* pWord = (uint8_t*)_buffer;
        *pWord = (uint8_t)dataWord;
        bytesWritten = sizeof(uint8_t);
      }
      break;

    case DataFormat::au16:
    case DataFormat::u16: {
        uint64_t dataWord = std::strtoul(_sData.c_str(), NULL, 0);
        if (dataWord > UINT16_MAX) {
          auto errMsg = boost::format("ERROR: Property '%s' data value '%s' exceeds the maximum uint16_t storage space.") %  m_name % _sData;
          throw std::runtime_error(errMsg.str());
        }

        uint16_t* pWord = (uint16_t*)_buffer;
        *pWord = htons((uint16_t)dataWord);

        bytesWritten = sizeof(uint16_t);
      }
      break;

    case DataFormat::au32:
    case DataFormat::u32: {
        uint64_t dataWord = std::strtoul(_sData.c_str(), NULL, 0);
        if (dataWord > UINT32_MAX) {
          auto errMsg = boost::format("ERROR: Property '%s' data value '%s' exceeds the maximum uint32_t storage space.") % m_name % _sData;
          throw std::runtime_error(errMsg.str());
        }

        uint32_t* pWord = (uint32_t*)_buffer;
        *pWord = htonl((uint32_t)dataWord);

        bytesWritten = sizeof(uint32_t);
      }
      break;

    case DataFormat::au64:
    case DataFormat::u64: {
        errno = 0;
        uint64_t dataWord = std::strtoul(_sData.c_str(), NULL, 0);
        if (errno == ERANGE) {
          auto errMsg = boost::format("ERROR: Property '%s' data value '%s' exceeds the maximum uint64_t storage space.") % m_name % _sData;
          throw std::runtime_error(errMsg.str());
        }

        uint64_t* pWord = (uint64_t*)_buffer;
#ifdef _WIN32
        *pWord = _byteswap_uint64((uint64_t)dataWord);
#else
        *pWord = __builtin_bswap64((uint64_t)dataWord);
#endif

        bytesWritten = sizeof(uint64_t);
      }
      break;

    case DataFormat::u128: {
        // Only support hex values
        if ((_sData.compare(0, 2, "0x") != 0) &&
            (_sData.compare(0, 2, "0X") != 0)) {
          auto errMsg = boost::format("ERROR: Property '%s' data value '%s' must be a hex value (e.g., start with '0x').") % m_name % _sData;
          throw std::runtime_error(errMsg.str());
        }

        // Must be of even length
        if ((_sData.size() % 2) != 0) {
          auto errMsg = boost::format("ERROR: Property '%s' data value '%s' doesn't support nibble length values, must be full byte values.") % m_name % _sData;
          throw std::runtime_error(errMsg.str());
        }

        // Must not be too long
        if (_sData.size() > 34) {
          auto errMsg = boost::format("ERROR: Property '%s' data value '%s' exceeds the maximum uint128_t storage space.") % m_name % _sData;
          throw std::runtime_error(errMsg.str());
        }

        std::string sHex(_sData.c_str() + 2);  // Strip off the 2 two characters

        static const int sizeWord = 16;
        uint8_t dataWord[sizeWord] = { };

        XUtil::hexStringToBinaryBuffer(sHex, &dataWord[0], sizeWord);

        memcpy(_buffer, &dataWord[0], sizeWord);

        bytesWritten = sizeWord;
      }
      break;

    case DataFormat::unknown:
    default: {
        auto errMsg = boost::format("ERROR: Unknown data type for property '%s'") % m_name;
        throw std::runtime_error(errMsg.str());
      }
      break;
  }
  return bytesWritten;
}

void
FDTProperty::marshalDataFromJSON(boost::property_tree::ptree::const_iterator& _iter,
                                 const PropertyNameFormat& _propertyNameFormat)
{
  // Get the name
  m_name = _iter->first;

  m_eDataFormat = getDataFormat(m_name);

  if (_propertyNameFormat.find(m_name) != _propertyNameFormat.end()) {
    m_eDataFormat = _propertyNameFormat.find(m_name)->second;
  }
  unsigned int wordSizeBytes = getWordLength(m_eDataFormat);
  const boost::property_tree::ptree& ptData = _iter->second;
  unsigned int arraySize = (unsigned int)ptData.size();

  // Make sure that we are not dealing with an array of data for non arrays
  if ((arraySize > 1) && !isDataFormatArray(m_eDataFormat)) {
    auto errMsg = boost::format("ERROR: Array of data found for the variable: '%s'") % m_name;
    throw std::runtime_error(errMsg.str());
  }

  // Address the non-array values first
  if (isDataFormatArray(m_eDataFormat) == false) {
    std::string sData = ptData.data();

    if (m_eDataFormat == DataFormat::sz) {
      m_dataLength = (unsigned int)sData.size() + 1; // Add room for the '\0' character
    } else {
      m_dataLength = wordSizeBytes;
    }

    m_pDataBuffer = new char[m_dataLength]();
    writeDataWord(m_eDataFormat, m_pDataBuffer, sData);
    return;
  }

  // Just arrays are remaining
  if (m_eDataFormat == DataFormat::asz) {
    m_dataLength = 0;
    for (auto localIter : ptData) {
      std::string sData = localIter.second.data();
      m_dataLength += sData.size() + 1;  // Add room for the string and the '\0' character
    }
  } else {
    m_dataLength = wordSizeBytes * arraySize;
  }

  m_pDataBuffer = new char[m_dataLength]();

  int byteIndex = 0;
  for (auto localIter : ptData) {
    std::string sData = localIter.second.data();
    byteIndex += writeDataWord(m_eDataFormat, m_pDataBuffer + byteIndex, sData);
  }
  return;
}


FDTProperty::FDTProperty(boost::property_tree::ptree::const_iterator& _iter,
                         const PropertyNameFormat& _propertyNameFormat)
    : FDTProperty()
{
  marshalDataFromJSON(_iter, _propertyNameFormat);
}






void
FDTProperty::marshalToJSON(boost::property_tree::ptree& _ptTree,
                           const PropertyNameFormat& _propertyNameFormat) const
{

  DataFormat eDataFormat = getDataFormat(m_name);

  if (_propertyNameFormat.find(m_name) != _propertyNameFormat.end()) {
    eDataFormat = _propertyNameFormat.find(m_name)->second;
  }

  std::string sTypeName = getDataFormatPrettyName(eDataFormat);
  XUtil::TRACE(boost::format("-- Serializing Property: '%s', Type: %s") % m_name % sTypeName);

  boost::property_tree::ptree ptProperty;

  switch (eDataFormat) {
    case DataFormat::au8:
      au8MarshalToJSON(_ptTree);
      break;
    case DataFormat::au16:
      au16MarshalToJSON(_ptTree);
      break;
    case DataFormat::au32:
      au32MarshalToJSON(_ptTree);
      break;
    case DataFormat::au64:
      au64MarshalToJSON(_ptTree);
      break;

    case DataFormat::u16:
      u16MarshalToJSON(_ptTree);
      break;
    case DataFormat::u32:
      u32MarshalToJSON(_ptTree);
      break;
    case DataFormat::u64:
      u64MarshalToJSON(_ptTree);
      break;
    case DataFormat::u128:
      u128MarshalToJSON(_ptTree);
      break;

    case DataFormat::asz:
      aszMarshalToJSON(_ptTree);
      break;
    case DataFormat::sz:
      szMarshalToJSON(_ptTree);
      break;

    default:
      au8MarshalToJSON(_ptTree);
      break;
  }
}


#define FDT_PROP        0x00000003

void
FDTProperty::marshalToDTC(DTCStringsBlock& _dtcStringsBlock, std::ostream& _buf) const
{
  // Add property keyword
  XUtil::write_htonl(_buf, FDT_PROP);

  // Add length and offset values
  XUtil::write_htonl(_buf, m_dataLength);
  XUtil::write_htonl(_buf, _dtcStringsBlock.addString(m_name));

  // Add data (if any)
  if (m_dataLength != 0) {
    _buf.write(m_pDataBuffer, m_dataLength);
  }

  // Pad if necessary
  XUtil::alignBytes(_buf, sizeof(uint32_t));
}
