/*
* Copyright (c) 2003-2022 Rony Shapiro <ronys@pwsafe.org>.
* All rights reserved. Use of the code is allowed under the
* Artistic License 2.0 terms, as specified in the LICENSE file
* distributed with this code, or available from
* http://www.opensource.org/licenses/artistic-license-2.0.php
*/
/// \file Item.cpp
//-----------------------------------------------------------------------------

#include "Item.h"
#include "crypto/BlowFish.h"
#include "crypto/TwoFish.h"
#include "PWSrand.h"
#include "UTF8Conv.h"
#include "Util.h"
#include "os/env.h"

#include <vector>

CItem::CItem()
{
  PWSrand::GetInstance()->GetRandomData( m_key, sizeof(m_key) );
}

CItem::CItem(const CItem &that) :
  m_fields(that.m_fields),
  m_URFL(that.m_URFL)
{
  memcpy(m_key, that.m_key, sizeof(m_key));
}

CItem::~CItem()
{
  delete m_blowfish;
  // Following protects against possible use-after-delete
  // bug, since new BF will be created, rather than
  // using one with trashed values
  m_blowfish = nullptr;
}

CItem& CItem::operator=(const CItem &that)
{
  if (this != &that) { // Check for self-assignment
    m_fields = that.m_fields;
    m_URFL = that.m_URFL;

    memcpy(m_key, that.m_key, sizeof(m_key));
    delete m_blowfish;
    m_blowfish = nullptr;
  }
  return *this;
}

bool CItem::CompareFields(const CItemField &fthis,
                          const CItem &that, const CItemField &fthat) const
{
  if (fthis.GetLength() != fthat.GetLength() ||
      fthis.GetType() != fthat.GetType())
    return false;
  size_t flength = fthis.GetLength() + BlowFish::BLOCKSIZE;
  auto *dthis = new unsigned char[flength];
  auto *dthat = new unsigned char[flength];
  GetField(fthis, dthis, flength);
  flength = fthis.GetLength() + BlowFish::BLOCKSIZE; // GetField updates length, reset
  that.GetField(fthat, dthat, flength);
  bool retval = (memcmp(dthis, dthat, flength) == 0);
  trashMemory(dthis, flength); trashMemory(dthis, flength);
  delete[] dthis; delete[] dthat;
  return retval;
}

bool CItem::operator==(const CItem &that) const
{
  if (m_fields.size() == that.m_fields.size() &&
      m_URFL.size() == that.m_URFL.size()) {
    /**
     * It would be nice to be able to compare the m_fields
     * and m_URFL directly, but the fields would be
     * encrypted with different keys, making byte-wise
     * field comparisons infeasible.
     */
    FieldConstIter ithis, ithat;
    for (ithis = m_fields.begin(), ithat = that.m_fields.begin();
         ithis != m_fields.end();
         ithis++, ithat++) {
      if (ithis->first != ithat->first)
        return false;
      const CItemField &fthis = ithis->second;
      const CItemField &fthat = ithat->second;
      if (!CompareFields(fthis, that, fthat))
        return false;
    } // for m_fields
  } else
    return false;

  // If we made it so far, now compare the unknown record fields
  // (We already know their sizes are equal)
  if (!m_URFL.empty()) {
    auto ithis = m_URFL.begin();
    auto ithat = that.m_URFL.begin();
    for (;
         ithis != m_URFL.end();
         ithis++, ithat++) {
      if (!CompareFields(*ithis, that, *ithat))
        return false;
    } // for m_URFL
  }
  return true;
}

size_t CItem::GetSize() const
{
  size_t length(0);

  for (FieldConstIter fiter = m_fields.begin(); fiter != m_fields.end(); fiter++)
    length += fiter->second.GetLength();

  for (auto ufiter = m_URFL.begin();
       ufiter != m_URFL.end(); ufiter++)
    length += ufiter->GetLength();

  return length;
}

BlowFish *CItem::MakeBlowFish() const
{
  // Creating a BlowFish object's relatively expensive, so we use
  // the singleton design pattern for the life of the CItem object
  if(!m_blowfish) {
    m_blowfish = BlowFish::MakeBlowFish(m_key, sizeof(m_key));
  }
  return m_blowfish;
}

void CItem::SetUnknownField(unsigned char type,
                            size_t length,
                            const unsigned char *ufield)
{
  /**
     TODO - check that this unknown field from the XML Import file is now
     known and it should be added as that instead!
  **/

  CItemField unkrfe(type);
  unkrfe.Set(ufield, length, MakeBlowFish());
  m_URFL.push_back(unkrfe);
}

void CItem::GetUnknownField(unsigned char &type, size_t &length,
                            unsigned char * &pdata,
                            const CItemField &item) const
{
  ASSERT(pdata == nullptr && length == 0);

  type = item.GetType();
  size_t flength = item.GetLength();
  length = flength;
  flength += BlowFish::BLOCKSIZE; // ensure that we've enough for at least one block
  pdata = new unsigned char[flength];
  GetField(item, pdata, flength);
}

void CItem::Clear()
{
  m_fields.clear();
  m_URFL.clear();
}

void CItem::SetField(int ft, const unsigned char *value, size_t length)
{
  if (length != 0) {
    m_fields[ft].Set(value, length,
                     MakeBlowFish(),
                     static_cast<unsigned char>(ft));
  } else
    m_fields.erase(ft);
}

void CItem::SetField(int ft, const StringX &value)
{
  if (!value.empty()) {
    m_fields[ft].Set(value,
                     MakeBlowFish(),
                     static_cast<unsigned char>(ft));
  } else
    m_fields.erase(ft);
}

static bool pull_string(StringX &str,
                        const unsigned char *data, size_t len)
{
  /**
   * cp_acp is used to force reading data as non-utf8 encoded
   * This is for databases that were incorrectly written, e.g., 3.05.02
   * PWS_CP_ACP is either set externally or via the --CP_ACP argv
   *
   * We use a static variable purely for efficiency, as this won't change
   * over the course of the program.
   */

  static int cp_acp = -1;
  if (cp_acp == -1) {
    cp_acp = pws_os::getenv("PWS_CP_ACP", false).empty() ? 0 : 1;
  }
  CUTF8Conv utf8conv(cp_acp != 0);
  std::vector<unsigned char> v(data, (data + len));
  v.push_back(0); // null terminate for FromUTF8.
  bool utf8status = utf8conv.FromUTF8(&v[0], len, str);
  if (!utf8status) {
    pws_os::Trace(_T("Item.cpp: pull_string(): FromUTF8 failed!\n"));
  }
  trashMemory(&v[0], len);
  return utf8status;
}

bool CItem::SetTextField(int ft, const unsigned char *value,
                         size_t length)
{
  StringX str;
  if (pull_string(str, value, length)) {
    SetField(ft, str);
    return true;
  } else
    return false;
}

void CItem::SetTime(const int whichtime, time_t t)
{
  unsigned char buf[sizeof(time_t)];
  putInt(buf, t);
  SetField(whichtime, buf, sizeof(time_t));
}

bool CItem::SetTimeField(int ft, const unsigned char *value,
                         size_t length)
{
  time_t t;
  if (PWSUtil::pull_time(t, value, length)) {
    SetTime(ft, t);
    return true;
  } else
    return false;
}

void CItem::GetField(const CItemField &field,
                     unsigned char *value, size_t &length) const
{
  field.Get(value, length, MakeBlowFish());
}

StringX CItem::GetField(const int ft) const
{
  auto fiter = m_fields.find(ft);
  return fiter == m_fields.end() ? _T("") : GetField(fiter->second);
}

StringX CItem::GetField(const CItemField &field) const
{
  StringX retval;
  field.Get(retval, MakeBlowFish());
  return retval;
}

void CItem::GetTime(int whichtime, time_t &t) const
{
  auto fiter = m_fields.find(whichtime);
  if (fiter != m_fields.end()) {
    unsigned char in[TwoFish::BLOCKSIZE] = {0}; // required by GetField
    size_t tlen = sizeof(in); // ditto

    CItem::GetField(fiter->second, in, tlen);
    if (tlen != 0) {
    // time field's store in native time_t size, regardless of
    // the representation on file
      ASSERT(tlen == sizeof(t));
      if (!PWSUtil::pull_time(t, in, tlen))
        ASSERT(0);
    } else {
      t = 0;
    }
  } else // fiter == m_fields.end()
    t = 0;
}


void CItem::push_length(std::vector<char> &v, uint32 s) const
{
  v.insert(v.end(),
    reinterpret_cast<char *>(&s), reinterpret_cast<char *>(&s) + sizeof(uint32));
}

// Overload rather than specialize template function
// See http://www.gotw.ca/publications/mill17.htm
void CItem::push(std::vector<char> &v, char type, const StringX &str) const
{
  if (!str.empty()) {
    CUTF8Conv utf8conv;
    bool status;
    const unsigned char *utf8;
    size_t utf8Len;
    status = utf8conv.ToUTF8(str, utf8, utf8Len);
    if (status) {
      v.push_back(type);
      push_length(v, static_cast<uint32>(utf8Len));
      v.insert(v.end(), reinterpret_cast<const char *>(utf8),
               reinterpret_cast<const char *>(utf8) + utf8Len);
    } else
      pws_os::Trace(_T("ItemData.cpp: push(%ls): ToUTF8 failed!\n"), str.c_str());
  }
}
