// NSisHandler.cpp

#include "StdAfx.h"

#include "../../../../C/CpuArch.h"

#include "../../../Common/ComTry.h"
#include "../../../Common/IntToString.h"

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

#include "../../Common/ProgressUtils.h"
#include "../../Common/StreamUtils.h"

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

#include "NsisHandler.h"

#define Get32(p) GetUi32(p)

using namespace NWindows;

namespace NArchive {
namespace NNsis {

static const char *kBcjMethod = "BCJ";
static const char *kUnknownMethod = "Unknown";

static const char * const kMethods[] =
{
    "Copy"
  , "Deflate"
  , "BZip2"
  , "LZMA"
};

static const Byte kProps[] =
{
  kpidPath,
  kpidSize,
  kpidPackSize,
  kpidMTime,
  kpidAttrib,
  kpidMethod,
  kpidSolid,
  kpidOffset
};

static const Byte kArcProps[] =
{
  kpidMethod,
  kpidSolid,
  kpidHeadersSize,
  kpidEmbeddedStubSize,
  kpidSubType
  // kpidCodePage
};

IMP_IInArchive_Props
IMP_IInArchive_ArcProps


static AString UInt32ToString(UInt32 val)
{
  char s[16];
  ConvertUInt32ToString(val, s);
  return s;
}

static AString GetStringForSizeValue(UInt32 val)
{
  for (int i = 31; i >= 0; i--)
    if (((UInt32)1 << i) == val)
      return UInt32ToString(i);
  char c = 'b';
  if      ((val & ((1 << 20) - 1)) == 0) { val >>= 20; c = 'm'; }
  else if ((val & ((1 << 10) - 1)) == 0) { val >>= 10; c = 'k'; }
  return UInt32ToString(val) + c;
}

static AString GetMethod(bool useFilter, NMethodType::EEnum method, UInt32 dict)
{
  AString s;
  if (useFilter)
  {
    s += kBcjMethod;
    s.Add_Space();
  }
  s += ((unsigned)method < ARRAY_SIZE(kMethods)) ? kMethods[(unsigned)method] : kUnknownMethod;
  if (method == NMethodType::kLZMA)
  {
    s += ':';
    s += GetStringForSizeValue(dict);
  }
  return s;
}

/*
AString CHandler::GetMethod(NMethodType::EEnum method, bool useItemFilter, UInt32 dictionary) const
{
  AString s;
  if (_archive.IsSolid && _archive.UseFilter || !_archive.IsSolid && useItemFilter)
  {
    s += kBcjMethod;
    s.Add_Space();
  }
  s += (method < ARRAY_SIZE(kMethods)) ? kMethods[method] : kUnknownMethod;
  if (method == NMethodType::kLZMA)
  {
    s += ':';
    s += GetStringForSizeValue(_archive.IsSolid ? _archive.DictionarySize: dictionary);
  }
  return s;
}
*/

STDMETHODIMP CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NCOM::CPropVariant prop;
  switch (propID)
  {
    // case kpidCodePage: if (_archive.IsUnicode) prop = "UTF-16"; break;
    case kpidSubType:
    {
      AString s = _archive.GetFormatDescription();
      if (!_archive.IsInstaller)
      {
        s.Add_Space_if_NotEmpty();
        s += "(Uninstall)";
      }
      if (!s.IsEmpty())
        prop = s;
      break;
    }

    case kpidMethod: prop = _methodString; break;
    case kpidSolid: prop = _archive.IsSolid; break;
    case kpidOffset: prop = _archive.StartOffset; break;
    case kpidPhySize: prop = (UInt64)((UInt64)_archive.ExeStub.Size() + _archive.FirstHeader.ArcSize); break;
    case kpidEmbeddedStubSize: prop = (UInt64)_archive.ExeStub.Size(); break;
    case kpidHeadersSize: prop = _archive.FirstHeader.HeaderSize; break;
    
    case kpidErrorFlags:
    {
      UInt32 v = 0;
      if (!_archive.IsArc) v |= kpv_ErrorFlags_IsNotArc;
      if (_archive.IsTruncated()) v |= kpv_ErrorFlags_UnexpectedEnd;
      prop = v;
      break;
    }
    
    case kpidName:
    {
      AString s;
      
      #ifdef NSIS_SCRIPT
        if (!_archive.Name.IsEmpty())
          s = _archive.Name;
        if (!_archive.IsInstaller)
        {
          if (!s.IsEmpty())
            s += '.';
          s += "Uninstall";
        }
      #endif

      if (s.IsEmpty())
        s = _archive.IsInstaller ? "Install" : "Uninstall";
      s += (_archive.ExeStub.Size() == 0) ? ".nsis" : ".exe";

      prop = _archive.ConvertToUnicode(s);
      break;
    }
    
    #ifdef NSIS_SCRIPT
    case kpidShortComment:
    {
      if (!_archive.BrandingText.IsEmpty())
        prop = _archive.ConvertToUnicode(_archive.BrandingText);
      break;
    }
    #endif
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}


STDMETHODIMP CHandler::Open(IInStream *stream, const UInt64 *maxCheckStartPosition, IArchiveOpenCallback * /* openArchiveCallback */)
{
  COM_TRY_BEGIN
  Close();
  {
    if (_archive.Open(stream, maxCheckStartPosition) != S_OK)
      return S_FALSE;
    {
      UInt32 dict = _archive.DictionarySize;
      if (!_archive.IsSolid)
      {
        FOR_VECTOR (i, _archive.Items)
        {
          const CItem &item = _archive.Items[i];
          if (item.DictionarySize > dict)
            dict = item.DictionarySize;
        }
      }
      _methodString = GetMethod(_archive.UseFilter, _archive.Method, dict);
    }
  }
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::Close()
{
  _archive.Clear();
  _archive.Release();
  return S_OK;
}

STDMETHODIMP CHandler::GetNumberOfItems(UInt32 *numItems)
{
  *numItems = _archive.Items.Size()
  #ifdef NSIS_SCRIPT
    + 1 + _archive.LicenseFiles.Size();
  #endif
  ;
  return S_OK;
}

bool CHandler::GetUncompressedSize(unsigned index, UInt32 &size) const
{
  size = 0;
  const CItem &item = _archive.Items[index];
  if (item.Size_Defined)
    size = item.Size;
  else if (_archive.IsSolid && item.EstimatedSize_Defined)
    size = item.EstimatedSize;
  else
    return false;
  return true;
}

bool CHandler::GetCompressedSize(unsigned index, UInt32 &size) const
{
  size = 0;
  const CItem &item = _archive.Items[index];
  if (item.CompressedSize_Defined)
    size = item.CompressedSize;
  else
  {
    if (_archive.IsSolid)
    {
      if (index == 0)
        size = _archive.FirstHeader.GetDataSize();
      else
        return false;
    }
    else
    {
      if (!item.IsCompressed)
        size = item.Size;
      else
        return false;
    }
  }
  return true;
}


STDMETHODIMP CHandler::GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NCOM::CPropVariant prop;
  #ifdef NSIS_SCRIPT
  if (index >= (UInt32)_archive.Items.Size())
  {
    if (index == (UInt32)_archive.Items.Size())
    {
      switch (propID)
      {
        case kpidPath: prop = "[NSIS].nsi"; break;
        case kpidSize:
        case kpidPackSize: prop = (UInt64)_archive.Script.Len(); break;
        case kpidSolid: prop = false; break;
      }
    }
    else
    {
      const CLicenseFile &lic = _archive.LicenseFiles[index - (_archive.Items.Size() + 1)];
      switch (propID)
      {
        case kpidPath: prop = lic.Name; break;
        case kpidSize:
        case kpidPackSize: prop = (UInt64)lic.Size; break;
        case kpidSolid: prop = false; break;
      }
    }
  }
  else
  #endif
  {
    const CItem &item = _archive.Items[index];
    switch (propID)
    {
      case kpidOffset: prop = item.Pos; break;
      case kpidPath:
      {
        UString s = NItemName::WinNameToOSName(_archive.GetReducedName(index));
        if (!s.IsEmpty())
          prop = (const wchar_t *)s;
        break;
      }
      case kpidSize:
      {
        UInt32 size;
        if (GetUncompressedSize(index, size))
          prop = (UInt64)size;
        break;
      }
      case kpidPackSize:
      {
        UInt32 size;
        if (GetCompressedSize(index, size))
          prop = (UInt64)size;
        break;
      }
      case kpidMTime:
      {
        if (item.MTime.dwHighDateTime > 0x01000000 &&
            item.MTime.dwHighDateTime < 0xFF000000)
          prop = item.MTime;
        break;
      }
      case kpidAttrib:
      {
        if (item.Attrib_Defined)
          prop = item.Attrib;
        break;
      }
      
      case kpidMethod:
        if (_archive.IsSolid)
          prop = _methodString;
        else
          prop = GetMethod(_archive.UseFilter, item.IsCompressed ? _archive.Method :
              NMethodType::kCopy, item.DictionarySize);
        break;
      
      case kpidSolid:  prop = _archive.IsSolid; break;
    }
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}


static bool UninstallerPatch(const Byte *p, size_t size, CByteBuffer &dest)
{
  for (;;)
  {
    if (size < 4)
      return false;
    UInt32 len = Get32(p);
    if (len == 0)
      return size == 4;
    if (size < 8)
      return false;
    UInt32 offs = Get32(p + 4);
    p += 8;
    size -= 8;
    if (size < len || offs > dest.Size() || len > dest.Size() - offs)
      return false;
    memcpy(dest + offs, p, len);
    p += len;
    size -= len;
  }
}


STDMETHODIMP CHandler::Extract(const UInt32 *indices, UInt32 numItems,
    Int32 testMode, IArchiveExtractCallback *extractCallback)
{
  COM_TRY_BEGIN
  bool allFilesMode = (numItems == (UInt32)(Int32)-1);
  if (allFilesMode)
    GetNumberOfItems(&numItems);
  if (numItems == 0)
    return S_OK;
  
  UInt64 totalSize = 0;
  UInt64 solidPosMax = 0;

  UInt32 i;
  for (i = 0; i < numItems; i++)
  {
    UInt32 index = (allFilesMode ? i : indices[i]);
    
    #ifdef NSIS_SCRIPT
    if (index >= _archive.Items.Size())
    {
      if (index == _archive.Items.Size())
        totalSize += _archive.Script.Len();
      else
        totalSize += _archive.LicenseFiles[index - (_archive.Items.Size() + 1)].Size;
    }
    else
    #endif
    {
      UInt32 size;
      if (_archive.IsSolid)
      {
        GetUncompressedSize(index, size);
        UInt64 pos = (UInt64)_archive.GetPosOfSolidItem(index) + size;
        if (solidPosMax < pos)
          solidPosMax = pos;
      }
      else
      {
        GetCompressedSize(index, size);
        totalSize += size;
      }
    }
  }

  extractCallback->SetTotal(totalSize + solidPosMax);

  CLocalProgress *lps = new CLocalProgress;
  CMyComPtr<ICompressProgressInfo> progress = lps;
  lps->Init(extractCallback, !_archive.IsSolid);

  if (_archive.IsSolid)
  {
    RINOK(_archive.SeekTo_DataStreamOffset());
    RINOK(_archive.InitDecoder());
    _archive.Decoder.StreamPos = 0;
  }

  /* We use tempBuf for solid archives, if there is duplicate item.
     We don't know uncompressed size for non-solid archives, so we can't
     allocate exact buffer.
     We use tempBuf also for first part (EXE stub) of unistall.exe
     and tempBuf2 is used for second part (NSIS script). */

  CByteBuffer tempBuf;
  CByteBuffer tempBuf2;
  
  /* tempPos is pos in uncompressed stream of previous item for solid archive, that
     was written to tempBuf  */
  UInt64 tempPos = (UInt64)(Int64)-1;
  
  /* prevPos is pos in uncompressed stream of previous item for solid archive.
     It's used for test mode (where we don't need to test same file second time */
  UInt64 prevPos =  (UInt64)(Int64)-1;
  
  // if there is error in solid archive, we show error for all subsequent files
  bool solidDataError = false;

  UInt64 curTotalPacked = 0, curTotalUnpacked = 0;
  UInt32 curPacked = 0;
  UInt64 curUnpacked = 0;

  for (i = 0; i < numItems; i++,
      curTotalPacked += curPacked,
      curTotalUnpacked += curUnpacked)
  {
    lps->InSize = curTotalPacked;
    lps->OutSize = curTotalUnpacked;
    if (_archive.IsSolid)
      lps->OutSize += _archive.Decoder.StreamPos;

    curPacked = 0;
    curUnpacked = 0;
    RINOK(lps->SetCur());

    // RINOK(extractCallback->SetCompleted(&currentTotalSize));
    CMyComPtr<ISequentialOutStream> realOutStream;
    Int32 askMode = testMode ?
        NExtract::NAskMode::kTest :
        NExtract::NAskMode::kExtract;
    UInt32 index = allFilesMode ? i : indices[i];

    RINOK(extractCallback->GetStream(index, &realOutStream, askMode));

    bool dataError = false;

    #ifdef NSIS_SCRIPT
    if (index >= (UInt32)_archive.Items.Size())
    {
      const void *data;
      size_t size;
      if (index == (UInt32)_archive.Items.Size())
      {
        data = (const Byte *)_archive.Script;
        size = _archive.Script.Len();
      }
      else
      {
        CLicenseFile &lic = _archive.LicenseFiles[index - (_archive.Items.Size() + 1)];
        if (lic.Text.Size() != 0)
          data = lic.Text;
        else
          data = _archive._data + lic.Offset;
        size = lic.Size;
      }
      curUnpacked = size;
      if (!testMode && !realOutStream)
        continue;
      RINOK(extractCallback->PrepareOperation(askMode));
      if (realOutStream)
        RINOK(WriteStream(realOutStream, data, size));
    }
    else
    #endif
    {
      const CItem &item = _archive.Items[index];
      
      if (!_archive.IsSolid)
        GetCompressedSize(index, curPacked);
      
      if (!testMode && !realOutStream)
        continue;
      
      RINOK(extractCallback->PrepareOperation(askMode));
      
      dataError = solidDataError;

      bool needDecompress = !solidDataError;
      if (needDecompress)
      {
        if (testMode && _archive.IsSolid && _archive.GetPosOfSolidItem(index) == prevPos)
          needDecompress = false;
      }

      if (needDecompress)
      {
        bool writeToTemp = false;
        bool readFromTemp = false;

        if (!_archive.IsSolid)
        {
          RINOK(_archive.SeekToNonSolidItem(index));
        }
        else
        {
          UInt64 pos = _archive.GetPosOfSolidItem(index);
          if (pos < _archive.Decoder.StreamPos)
          {
            if (pos != tempPos)
              solidDataError = dataError = true;
            readFromTemp = true;
          }
          else
          {
            HRESULT res = _archive.Decoder.SetToPos(pos, progress);
            if (res != S_OK)
            {
              if (res != S_FALSE)
                return res;
              solidDataError = dataError = true;
            }
            else if (!testMode && i + 1 < numItems)
            {
              UInt32 next = allFilesMode ? i + 1 : indices[i + 1];
              if (next < _archive.Items.Size())
              {
                UInt64 nextPos = _archive.GetPosOfSolidItem(next);
                if (nextPos == pos)
                {
                  writeToTemp = true;
                  tempPos = pos;
                }
              }
            }
          }
          prevPos = pos;
        }

        if (!dataError)
        {
          // UInt32 unpackSize = 0;
          // bool unpackSize_Defined = false;
          bool writeToTemp1 = writeToTemp;
          if (item.IsUninstaller)
          {
            // unpackSize = item.PatchSize;
            // unpackSize_Defined = true;
            if (!readFromTemp)
              writeToTemp = true;
            writeToTemp1 = writeToTemp;
            if (_archive.ExeStub.Size() == 0)
            {
              if (writeToTemp1 && !readFromTemp)
                tempBuf.Free();
              writeToTemp1 = false;
            }
          }

          if (readFromTemp)
          {
            if (realOutStream && !item.IsUninstaller)
              RINOK(WriteStream(realOutStream, tempBuf, tempBuf.Size()));
          }
          else
          {
            UInt32 curUnpacked32 = 0;
            HRESULT res = _archive.Decoder.Decode(
                writeToTemp1 ? &tempBuf : NULL,
                item.IsUninstaller, item.PatchSize,
                item.IsUninstaller ? NULL : (ISequentialOutStream *)realOutStream,
                progress,
                curPacked, curUnpacked32);
            curUnpacked = curUnpacked32;
            if (_archive.IsSolid)
              curUnpacked = 0;
            if (res != S_OK)
            {
              if (res != S_FALSE)
                return res;
              dataError = true;
              if (_archive.IsSolid)
                solidDataError = true;
            }
          }
        }
        
        if (!dataError && item.IsUninstaller)
        {
          if (_archive.ExeStub.Size() != 0)
          {
            CByteBuffer destBuf = _archive.ExeStub;
            dataError = !UninstallerPatch(tempBuf, tempBuf.Size(), destBuf);
           
            if (realOutStream)
              RINOK(WriteStream(realOutStream, destBuf, destBuf.Size()));
          }
          
          if (readFromTemp)
          {
            if (realOutStream)
              RINOK(WriteStream(realOutStream, tempBuf2, tempBuf2.Size()));
          }
          else
          {
            UInt32 curPacked2 = 0;
            UInt32 curUnpacked2 = 0;
            HRESULT res = _archive.Decoder.Decode(
                writeToTemp ? &tempBuf2 : NULL,
                false, 0,
                realOutStream,
                progress,
                curPacked2, curUnpacked2);
            curPacked += curPacked2;
            if (!_archive.IsSolid)
              curUnpacked += curUnpacked2;
            if (res != S_OK)
            {
              if (res != S_FALSE)
                return res;
              dataError = true;
              if (_archive.IsSolid)
                solidDataError = true;
            }
          }
        }
      }
    }
    realOutStream.Release();
    RINOK(extractCallback->SetOperationResult(dataError ?
        NExtract::NOperationResult::kDataError :
        NExtract::NOperationResult::kOK));
  }
  return S_OK;
  COM_TRY_END
}

}}
