// TarHandler.cpp

#include "StdAfx.h"

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

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

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

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

#include "TarHandler.h"
#include "TarIn.h"

using namespace NWindows;

namespace NArchive {
namespace NTar {

static const char *kUnexpectedEnd = "Unexpected end of archive";

static const STATPROPSTG kProps[] =
{
  { NULL, kpidPath, VT_BSTR},
  { NULL, kpidIsDir, VT_BOOL},
  { NULL, kpidSize, VT_UI8},
  { NULL, kpidPackSize, VT_UI8},
  { NULL, kpidMTime, VT_FILETIME},
  { NULL, kpidPosixAttrib, VT_UI4},
  { NULL, kpidUser, VT_BSTR},
  { NULL, kpidGroup, VT_BSTR},
  { NULL, kpidLink, VT_BSTR}
};

static const STATPROPSTG kArcProps[] =
{
  { NULL, kpidPhySize, VT_UI8},
  { NULL, kpidHeadersSize, VT_UI8}
};

IMP_IInArchive_Props
IMP_IInArchive_ArcProps

STDMETHODIMP CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value)
{
  NCOM::CPropVariant prop;
  switch(propID)
  {
    case kpidPhySize: if (_phySizeDefined) prop = _phySize; break;
    case kpidHeadersSize: if (_phySizeDefined) prop = _headersSize; break;
    case kpidError: if (!_errorMessage.IsEmpty()) prop = _errorMessage; break;
  }
  prop.Detach(value);
  return S_OK;
}

HRESULT CHandler::ReadItem2(ISequentialInStream *stream, bool &filled, CItemEx &item)
{
  item.HeaderPos = _phySize;
  RINOK(ReadItem(stream, filled, item, _errorMessage));
  if (filled && item.IsSparse())
    _isSparse = true;
  _phySize += item.HeaderSize;
  _headersSize += item.HeaderSize;
  return S_OK;
}

HRESULT CHandler::Open2(IInStream *stream, IArchiveOpenCallback *callback)
{
  UInt64 endPos = 0;
  {
    RINOK(stream->Seek(0, STREAM_SEEK_END, &endPos));
    RINOK(stream->Seek(0, STREAM_SEEK_SET, NULL));
  }
  
  _phySizeDefined = true;
  for (;;)
  {
    CItemEx item;
    bool filled;
    RINOK(ReadItem2(stream, filled, item));
    if (!filled)
      break;
    _items.Add(item);
    
    RINOK(stream->Seek(item.GetPackSizeAligned(), STREAM_SEEK_CUR, &_phySize));
    if (_phySize > endPos)
    {
      _errorMessage = kUnexpectedEnd;
      break;
    }
    /*
    if (_phySize == endPos)
    {
      _errorMessage = "There are no trailing zero-filled records";
      break;
    }
    */
    if (callback != NULL)
    {
      if (_items.Size() == 1)
      {
        RINOK(callback->SetTotal(NULL, &endPos));
      }
      if (_items.Size() % 100 == 0)
      {
        UInt64 numFiles = _items.Size();
        RINOK(callback->SetCompleted(&numFiles, &_phySize));
      }
    }
  }

  if (_items.Size() == 0)
  {
    CMyComPtr<IArchiveOpenVolumeCallback> openVolumeCallback;
    if (!callback)
      return S_FALSE;
    callback->QueryInterface(IID_IArchiveOpenVolumeCallback, (void **)&openVolumeCallback);
    if (!openVolumeCallback)
      return S_FALSE;
    NCOM::CPropVariant prop;
    if (openVolumeCallback->GetProperty(kpidName, &prop) != S_OK)
      return S_FALSE;
    if (prop.vt != VT_BSTR)
      return S_FALSE;
    UString baseName = prop.bstrVal;
    baseName = baseName.Right(4);
    if (baseName.CompareNoCase(L".tar") != 0)
      return S_FALSE;
  }
  return S_OK;
}

STDMETHODIMP CHandler::Open(IInStream *stream, const UInt64 *, IArchiveOpenCallback *openArchiveCallback)
{
  COM_TRY_BEGIN
  {
    Close();
    RINOK(Open2(stream, openArchiveCallback));
    _stream = stream;
  }
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::OpenSeq(ISequentialInStream *stream)
{
  Close();
  _seqStream = stream;
  return S_OK;
}

STDMETHODIMP CHandler::Close()
{
  _errorMessage.Empty();
  _phySizeDefined = false;
  _phySize = 0;
  _headersSize = 0;
  _curIndex = 0;
  _latestIsRead = false;
  _isSparse = false;
  _items.Clear();
  _seqStream.Release();
  _stream.Release();
  return S_OK;
}

STDMETHODIMP CHandler::GetNumberOfItems(UInt32 *numItems)
{
  *numItems = (_stream ? _items.Size() : (UInt32)(Int32)-1);
  return S_OK;
}

CHandler::CHandler()
{
  copyCoderSpec = new NCompress::CCopyCoder();
  copyCoder = copyCoderSpec;
}

HRESULT CHandler::SkipTo(UInt32 index)
{
  while (_curIndex < index || !_latestIsRead)
  {
    if (_latestIsRead)
    {
      UInt64 packSize = _latestItem.GetPackSizeAligned();
      RINOK(copyCoderSpec->Code(_seqStream, NULL, &packSize, &packSize, NULL));
      _phySize += copyCoderSpec->TotalSize;
      if (copyCoderSpec->TotalSize != packSize)
      {
        _errorMessage = kUnexpectedEnd;
        return S_FALSE;
      }
      _latestIsRead = false;
      _curIndex++;
    }
    else
    {
      bool filled;
      RINOK(ReadItem2(_seqStream, filled, _latestItem));
      if (!filled)
      {
        _phySizeDefined = true;
        return E_INVALIDARG;
      }
      _latestIsRead = true;
    }
  }
  return S_OK;
}

static UString TarStringToUnicode(const AString &s)
{
  return MultiByteToUnicodeString(s, CP_OEMCP);
}

STDMETHODIMP CHandler::GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NWindows::NCOM::CPropVariant prop;

  const CItemEx *item;
  if (_stream)
    item = &_items[index];
  else
  {
    if (index < _curIndex)
      return E_INVALIDARG;
    else
    {
      RINOK(SkipTo(index));
      item = &_latestItem;
    }
  }

  switch(propID)
  {
    case kpidPath: prop = NItemName::GetOSName2(TarStringToUnicode(item->Name)); break;
    case kpidIsDir: prop = item->IsDir(); break;
    case kpidSize: prop = item->GetUnpackSize(); break;
    case kpidPackSize: prop = item->GetPackSizeAligned(); break;
    case kpidMTime:
      if (item->MTime != 0)
      {
        FILETIME ft;
        if (NTime::UnixTime64ToFileTime(item->MTime, ft))
          prop = ft;
      }
      break;
    case kpidPosixAttrib: prop = item->Mode; break;
    case kpidUser: prop = TarStringToUnicode(item->User); break;
    case kpidGroup: prop = TarStringToUnicode(item->Group); break;
    case kpidLink: prop = TarStringToUnicode(item->LinkName); break;
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

HRESULT CHandler::Extract(const UInt32 *indices, UInt32 numItems,
    Int32 testMode, IArchiveExtractCallback *extractCallback)
{
  COM_TRY_BEGIN
  ISequentialInStream *stream = _seqStream;
  bool seqMode = (_stream == NULL);
  if (!seqMode)
    stream = _stream;

  bool allFilesMode = (numItems == (UInt32)-1);
  if (allFilesMode)
    numItems = _items.Size();
  if (_stream && numItems == 0)
    return S_OK;
  UInt64 totalSize = 0;
  UInt32 i;
  for (i = 0; i < numItems; i++)
    totalSize += _items[allFilesMode ? i : indices[i]].GetUnpackSize();
  extractCallback->SetTotal(totalSize);

  UInt64 totalPackSize;
  totalSize = totalPackSize = 0;
  
  CLocalProgress *lps = new CLocalProgress;
  CMyComPtr<ICompressProgressInfo> progress = lps;
  lps->Init(extractCallback, false);

  CLimitedSequentialInStream *streamSpec = new CLimitedSequentialInStream;
  CMyComPtr<ISequentialInStream> inStream(streamSpec);
  streamSpec->SetStream(stream);

  CLimitedSequentialOutStream *outStreamSpec = new CLimitedSequentialOutStream;
  CMyComPtr<ISequentialOutStream> outStream(outStreamSpec);

  for (i = 0; i < numItems || seqMode; i++)
  {
    lps->InSize = totalPackSize;
    lps->OutSize = totalSize;
    RINOK(lps->SetCur());
    CMyComPtr<ISequentialOutStream> realOutStream;
    Int32 askMode = testMode ?
        NExtract::NAskMode::kTest :
        NExtract::NAskMode::kExtract;
    Int32 index = allFilesMode ? i : indices[i];
    const CItemEx *item;
    if (seqMode)
    {
      HRESULT res = SkipTo(index);
      if (res == E_INVALIDARG)
        break;
      RINOK(res);
      item = &_latestItem;
    }
    else
      item = &_items[index];

    RINOK(extractCallback->GetStream(index, &realOutStream, askMode));
    UInt64 unpackSize = item->GetUnpackSize();
    totalSize += unpackSize;
    totalPackSize += item->GetPackSizeAligned();
    if (item->IsDir())
    {
      RINOK(extractCallback->PrepareOperation(askMode));
      RINOK(extractCallback->SetOperationResult(NExtract::NOperationResult::kOK));
      continue;
    }
    bool skipMode = false;
    if (!testMode && !realOutStream)
    {
      if (!seqMode)
        continue;
      skipMode = true;
      askMode = NExtract::NAskMode::kSkip;
    }
    RINOK(extractCallback->PrepareOperation(askMode));

    outStreamSpec->SetStream(realOutStream);
    realOutStream.Release();
    outStreamSpec->Init(skipMode ? 0 : unpackSize, true);

    Int32 opRes = NExtract::NOperationResult::kOK;
    if (item->IsSparse())
      opRes = NExtract::NOperationResult::kUnSupportedMethod;
    else
    {
      if (item->IsLink())
      {
        RINOK(WriteStream(outStreamSpec, (const char *)item->LinkName, item->LinkName.Length()));
      }
      else
      {
        if (!seqMode)
        {
          RINOK(_stream->Seek(item->GetDataPosition(), STREAM_SEEK_SET, NULL));
        }
        streamSpec->Init(item->GetPackSizeAligned());
        RINOK(copyCoder->Code(inStream, outStream, NULL, NULL, progress));
      }
      if (outStreamSpec->GetRem() != 0)
        opRes = NExtract::NOperationResult::kDataError;
    }
    if (seqMode)
    {
      _latestIsRead = false;
      _curIndex++;
    }
    outStreamSpec->ReleaseStream();
    RINOK(extractCallback->SetOperationResult(opRes));
  }
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::GetStream(UInt32 index, ISequentialInStream **stream)
{
  COM_TRY_BEGIN
  const CItemEx &item = _items[index];
  if (item.IsSparse())
    return E_NOTIMPL;
  if (item.IsLink())
  {
    CBufInStream *streamSpec = new CBufInStream;
    CMyComPtr<IInStream> streamTemp = streamSpec;
    streamSpec->Init((const Byte *)(const char *)item.LinkName, item.LinkName.Length(), (IInArchive *)this);
    *stream = streamTemp.Detach();
    return S_OK;
  }
  return CreateLimitedInStream(_stream, item.GetDataPosition(), item.PackSize, stream);
  COM_TRY_END
}

}}
