// ZipHandlerOut.cpp

#include "StdAfx.h"

#include "../../../Common/ComTry.h"
#include "../../../Common/StringConvert.h"
#include "../../../Common/StringToInt.h"

#include "../../../Windows/PropVariant.h"
#include "../../../Windows/TimeUtils.h"

#include "../../IPassword.h"

#include "../../Common/OutBuffer.h"

#include "../../Crypto/WzAes.h"

#include "../Common/ItemNameUtils.h"
#include "../Common/ParseProperties.h"

#include "ZipHandler.h"
#include "ZipUpdate.h"

using namespace NWindows;
using namespace NCOM;
using namespace NTime;

namespace NArchive {
namespace NZip {

STDMETHODIMP CHandler::GetFileTimeType(UInt32 *timeType)
{
  *timeType = NFileTimeType::kDOS;
  return S_OK;
}

static bool IsSimpleAsciiString(const wchar_t *s)
{
  for (;;)
  {
    wchar_t c = *s++;
    if (c == 0)
      return true;
    if (c < 0x20 || c > 0x7F)
      return false;
  }
}

#define COM_TRY_BEGIN2 try {
#define COM_TRY_END2 } \
catch(const CSystemException &e) { return e.ErrorCode; } \
catch(...) { return E_OUTOFMEMORY; }

static HRESULT GetTime(IArchiveUpdateCallback *callback, int index, PROPID propID, FILETIME &filetime)
{
  filetime.dwHighDateTime = filetime.dwLowDateTime = 0;
  NCOM::CPropVariant prop;
  RINOK(callback->GetProperty(index, propID, &prop));
  if (prop.vt == VT_FILETIME)
    filetime = prop.filetime;
  else if (prop.vt != VT_EMPTY)
    return E_INVALIDARG;
  return S_OK;
}

STDMETHODIMP CHandler::UpdateItems(ISequentialOutStream *outStream, UInt32 numItems,
    IArchiveUpdateCallback *callback)
{
  COM_TRY_BEGIN2

  if (m_Archive.IsOpen())
  {
    if (!m_Archive.CanUpdate())
      return E_NOTIMPL;
  }

  CObjectVector<CUpdateItem> updateItems;
  bool thereAreAesUpdates = false;
  UInt64 largestSize = 0;
  bool largestSizeDefined = false;

  for (UInt32 i = 0; i < numItems; i++)
  {
    CUpdateItem ui;
    Int32 newData;
    Int32 newProps;
    UInt32 indexInArchive;
    if (!callback)
      return E_FAIL;
    RINOK(callback->GetUpdateItemInfo(i, &newData, &newProps, &indexInArchive));
    ui.NewProps = IntToBool(newProps);
    ui.NewData = IntToBool(newData);
    ui.IndexInArc = indexInArchive;
    ui.IndexInClient = i;
    bool existInArchive = (indexInArchive != (UInt32)(Int32)-1);
    if (existInArchive && newData)
      if (m_Items[indexInArchive].IsAesEncrypted())
        thereAreAesUpdates = true;

    if (IntToBool(newProps))
    {
      UString name;
      {
        NCOM::CPropVariant prop;
        RINOK(callback->GetProperty(i, kpidAttrib, &prop));
        if (prop.vt == VT_EMPTY)
          ui.Attrib = 0;
        else if (prop.vt != VT_UI4)
          return E_INVALIDARG;
        else
          ui.Attrib = prop.ulVal;
      }

      {
        NCOM::CPropVariant prop;
        RINOK(callback->GetProperty(i, kpidPath, &prop));
        if (prop.vt == VT_EMPTY)
          name.Empty();
        else if (prop.vt != VT_BSTR)
          return E_INVALIDARG;
        else
          name = prop.bstrVal;
      }
      {
        NCOM::CPropVariant prop;
        RINOK(callback->GetProperty(i, kpidIsDir, &prop));
        if (prop.vt == VT_EMPTY)
          ui.IsDir = false;
        else if (prop.vt != VT_BOOL)
          return E_INVALIDARG;
        else
          ui.IsDir = (prop.boolVal != VARIANT_FALSE);
      }

      {
        CPropVariant prop;
        RINOK(callback->GetProperty(i, kpidTimeType, &prop));
        if (prop.vt == VT_UI4)
          ui.NtfsTimeIsDefined = (prop.ulVal == NFileTimeType::kWindows);
        else
          ui.NtfsTimeIsDefined = m_WriteNtfsTimeExtra;
      }
      RINOK(GetTime(callback, i, kpidMTime, ui.Ntfs_MTime));
      RINOK(GetTime(callback, i, kpidATime, ui.Ntfs_ATime));
      RINOK(GetTime(callback, i, kpidCTime, ui.Ntfs_CTime));

      {
        FILETIME localFileTime = { 0, 0 };
        if (ui.Ntfs_MTime.dwHighDateTime != 0 ||
            ui.Ntfs_MTime.dwLowDateTime != 0)
          if (!FileTimeToLocalFileTime(&ui.Ntfs_MTime, &localFileTime))
            return E_INVALIDARG;
        FileTimeToDosTime(localFileTime, ui.Time);
      }

      name = NItemName::MakeLegalName(name);
      bool needSlash = ui.IsDir;
      const wchar_t kSlash = L'/';
      if (!name.IsEmpty())
      {
        if (name.Back() == kSlash)
        {
          if (!ui.IsDir)
            return E_INVALIDARG;
          needSlash = false;
        }
      }
      if (needSlash)
        name += kSlash;

      UINT codePage = _forceCodePage ? _specifiedCodePage : CP_OEMCP;

      bool tryUtf8 = true;
      if ((m_ForceLocal || !m_ForceUtf8) && codePage != CP_UTF8)
      {
        ui.Name = UnicodeStringToMultiByte(name, CP_OEMCP);
        tryUtf8 = (!m_ForceLocal);
      }

      if (tryUtf8)
      {
        ui.IsUtf8 = !name.IsAscii();
        ConvertUnicodeToUTF8(name, ui.Name);
      }

      if (ui.Name.Len() >= (1 << 16))
        return E_INVALIDARG;

      ui.IndexInClient = i;
      /*
      if (existInArchive)
      {
        const CItemEx &itemInfo = m_Items[indexInArchive];
        // ui.Commented = itemInfo.IsCommented();
        ui.Commented = false;
        if (ui.Commented)
        {
          ui.CommentRange.Position = itemInfo.GetCommentPosition();
          ui.CommentRange.Size  = itemInfo.CommentSize;
        }
      }
      else
        ui.Commented = false;
      */
    }
    if (IntToBool(newData))
    {
      UInt64 size = 0;
      if (!ui.IsDir)
      {
        NCOM::CPropVariant prop;
        RINOK(callback->GetProperty(i, kpidSize, &prop));
        if (prop.vt != VT_UI8)
          return E_INVALIDARG;
        size = prop.uhVal.QuadPart;
        if (largestSize < size)
          largestSize = size;
        largestSizeDefined = true;
      }
      ui.Size = size;

      // ui.Size -= ui.Size / 2;
    }
    updateItems.Add(ui);
  }

  CMyComPtr<ICryptoGetTextPassword2> getTextPassword;
  {
    CMyComPtr<IArchiveUpdateCallback> udateCallBack2(callback);
    udateCallBack2.QueryInterface(IID_ICryptoGetTextPassword2, &getTextPassword);
  }
  CCompressionMethodMode options;
  (CBaseProps &)options = _props;
  options._dataSizeReduce = largestSize;
  options._dataSizeReduceDefined = largestSizeDefined;

  options.PasswordIsDefined = false;
  options.Password.Empty();
  if (getTextPassword)
  {
    CMyComBSTR password;
    Int32 passwordIsDefined;
    RINOK(getTextPassword->CryptoGetTextPassword2(&passwordIsDefined, &password));
    options.PasswordIsDefined = IntToBool(passwordIsDefined);
    if (options.PasswordIsDefined)
    {
      if (!m_ForceAesMode)
        options.IsAesMode = thereAreAesUpdates;

      if (!IsSimpleAsciiString(password))
        return E_INVALIDARG;
      if (password)
        options.Password = UnicodeStringToMultiByte((LPCOLESTR)password, CP_OEMCP);
      if (options.IsAesMode)
      {
        if (options.Password.Len() > NCrypto::NWzAes::kPasswordSizeMax)
          return E_INVALIDARG;
      }
    }
  }

  Byte mainMethod;
  if (m_MainMethod < 0)
    mainMethod = (Byte)(((_props.Level == 0) ?
        NFileHeader::NCompressionMethod::kStored :
        NFileHeader::NCompressionMethod::kDeflated));
  else
    mainMethod = (Byte)m_MainMethod;
  options.MethodSequence.Add(mainMethod);
  if (mainMethod != NFileHeader::NCompressionMethod::kStored)
    options.MethodSequence.Add(NFileHeader::NCompressionMethod::kStored);

  return Update(
      EXTERNAL_CODECS_VARS
      m_Items, updateItems, outStream,
      m_Archive.IsOpen() ? &m_Archive : NULL, _removeSfxBlock,
      &options, callback);

  COM_TRY_END2
}

struct CMethodIndexToName
{
  unsigned Method;
  const char *Name;
};

static const CMethodIndexToName k_SupportedMethods[] =
{
  { NFileHeader::NCompressionMethod::kStored, "copy" },
  { NFileHeader::NCompressionMethod::kDeflated, "deflate" },
  { NFileHeader::NCompressionMethod::kDeflated64, "deflate64" },
  { NFileHeader::NCompressionMethod::kBZip2, "bzip2" },
  { NFileHeader::NCompressionMethod::kLZMA, "lzma" },
  { NFileHeader::NCompressionMethod::kPPMd, "ppmd" }
};

STDMETHODIMP CHandler::SetProperties(const wchar_t * const *names, const PROPVARIANT *values, UInt32 numProps)
{
  InitMethodProps();
  const UInt32 numProcessors = _props.NumThreads;

  for (UInt32 i = 0; i < numProps; i++)
  {
    UString name = names[i];
    name.MakeLower_Ascii();
    if (name.IsEmpty())
      return E_INVALIDARG;

    const PROPVARIANT &prop = values[i];

    if (name[0] == L'x')
    {
      UInt32 level = 9;
      RINOK(ParsePropToUInt32(name.Ptr(1), prop, level));
      _props.Level = level;
      _props.MethodInfo.AddProp_Level(level);
    }
    else if (name == L"m")
    {
      if (prop.vt == VT_BSTR)
      {
        UString m = prop.bstrVal, m2;
        m.MakeLower_Ascii();
        int colonPos = m.Find(L':');
        if (colonPos >= 0)
        {
          m2 = m.Ptr(colonPos + 1);
          m.DeleteFrom(colonPos);
        }
        unsigned k;
        for (k = 0; k < ARRAY_SIZE(k_SupportedMethods); k++)
        {
          const CMethodIndexToName &pair = k_SupportedMethods[k];
          if (m.IsEqualTo(pair.Name))
          {
            if (!m2.IsEmpty())
            {
              RINOK(_props.MethodInfo.ParseParamsFromString(m2));
            }
            m_MainMethod = pair.Method;
            break;
          }
        }
        if (k == ARRAY_SIZE(k_SupportedMethods))
          return E_INVALIDARG;
      }
      else if (prop.vt == VT_UI4)
      {
        unsigned k;
        for (k = 0; k < ARRAY_SIZE(k_SupportedMethods); k++)
        {
          unsigned method = k_SupportedMethods[k].Method;
          if (prop.ulVal == method)
          {
            m_MainMethod = method;
            break;
          }
        }
        if (k == ARRAY_SIZE(k_SupportedMethods))
          return E_INVALIDARG;
      }
      else
        return E_INVALIDARG;
    }
    else if (name.IsPrefixedBy(L"em"))
    {
      if (prop.vt != VT_BSTR)
        return E_INVALIDARG;
      {
        UString m = prop.bstrVal;
        m.MakeLower_Ascii();
        if (m.IsPrefixedBy(L"aes"))
        {
          m.DeleteFrontal(3);
          if (m == L"128")
            _props.AesKeyMode = 1;
          else if (m == L"192")
            _props.AesKeyMode = 2;
          else if (m == L"256" || m.IsEmpty())
            _props.AesKeyMode = 3;
          else
            return E_INVALIDARG;
          _props.IsAesMode = true;
          m_ForceAesMode = true;
        }
        else if (m == L"zipcrypto")
        {
          _props.IsAesMode = false;
          m_ForceAesMode = true;
        }
        else
          return E_INVALIDARG;
      }
    }
    else if (name.IsPrefixedBy(L"mt"))
    {
      RINOK(ParseMtProp(name.Ptr(2), prop, numProcessors, _props.NumThreads));
      _props.NumThreadsWasChanged = true;
    }
    else if (name.IsEqualTo("tc"))
    {
      RINOK(PROPVARIANT_to_bool(prop, m_WriteNtfsTimeExtra));
    }
    else if (name.IsEqualTo("cl"))
    {
      RINOK(PROPVARIANT_to_bool(prop, m_ForceLocal));
      if (m_ForceLocal)
        m_ForceUtf8 = false;
    }
    else if (name.IsEqualTo("cu"))
    {
      RINOK(PROPVARIANT_to_bool(prop, m_ForceUtf8));
      if (m_ForceUtf8)
        m_ForceLocal = false;
    }
    else if (name.IsEqualTo("cp"))
    {
      UInt32 cp = CP_OEMCP;
      RINOK(ParsePropToUInt32(L"", prop, cp));
      _forceCodePage = true;
      _specifiedCodePage = cp;
    }
    else if (name.IsEqualTo("rsfx"))
    {
      RINOK(PROPVARIANT_to_bool(prop, _removeSfxBlock));
    }
    else
    {
      RINOK(_props.MethodInfo.ParseParamsFromPROPVARIANT(name, prop));
    }
  }
  return S_OK;
}

}}
