// List.cpp

#include "StdAfx.h"

#include "Common/IntToString.h"
#include "Common/MyCom.h"
#include "Common/StdOutStream.h"
#include "Common/StringConvert.h"

#include "Windows/Error.h"
#include "Windows/FileDir.h"
#include "Windows/PropVariant.h"
#include "Windows/PropVariantConversions.h"

#include "../../Archive/IArchive.h"

#include "../Common/OpenArchive.h"
#include "../Common/PropIDUtils.h"

#include "ConsoleClose.h"
#include "List.h"
#include "OpenCallbackConsole.h"

using namespace NWindows;

struct CPropIdToName
{
  PROPID PropID;
  const wchar_t *Name;
};

static const CPropIdToName kPropIdToName[] =
{
  { kpidPath, L"Path" },
  { kpidName, L"Name" },
  { kpidIsDir, L"Folder" },
  { kpidSize, L"Size" },
  { kpidPackSize, L"Packed Size" },
  { kpidAttrib, L"Attributes" },
  { kpidCTime, L"Created" },
  { kpidATime, L"Accessed" },
  { kpidMTime, L"Modified" },
  { kpidSolid, L"Solid" },
  { kpidCommented, L"Commented" },
  { kpidEncrypted, L"Encrypted" },
  { kpidSplitBefore, L"Split Before" },
  { kpidSplitAfter, L"Split After" },
  { kpidDictionarySize, L"Dictionary Size" },
  { kpidCRC, L"CRC" },
  { kpidType, L"Type" },
  { kpidIsAnti, L"Anti" },
  { kpidMethod, L"Method" },
  { kpidHostOS, L"Host OS" },
  { kpidFileSystem, L"File System" },
  { kpidUser, L"User" },
  { kpidGroup, L"Group" },
  { kpidBlock, L"Block" },
  { kpidComment, L"Comment" },
  { kpidPosition, L"Position" },
  { kpidPrefix, L"Prefix" },
  { kpidNumSubDirs, L"Folders" },
  { kpidNumSubFiles, L"Files" },
  { kpidUnpackVer, L"Version" },
  { kpidVolume, L"Volume" },
  { kpidIsVolume, L"Multivolume" },
  { kpidOffset, L"Offset" },
  { kpidLinks, L"Links" },
  { kpidNumBlocks, L"Blocks" },
  { kpidNumVolumes, L"Volumes" },

  { kpidBit64, L"64-bit" },
  { kpidBigEndian, L"Big-endian" },
  { kpidCpu, L"CPU" },
  { kpidPhySize, L"Physical Size" },
  { kpidHeadersSize, L"Headers Size" },
  { kpidChecksum, L"Checksum" },
  { kpidCharacts, L"Characteristics" },
  { kpidVa, L"Virtual Address" },
  { kpidId, L"ID" },
  { kpidShortName, L"Short Name" },
  { kpidCreatorApp, L"Creator Application"},
  { kpidSectorSize, L"Sector Size" },
  { kpidPosixAttrib, L"Mode" },
  { kpidLink, L"Link" },
  { kpidError, L"Error" },

  { kpidTotalSize, L"Total Size" },
  { kpidFreeSpace, L"Free Space" },
  { kpidClusterSize, L"Cluster Size" },
  { kpidVolumeName, L"Label" }
};

static const char kEmptyAttribChar = '.';

static const char *kListing = "Listing archive: ";
static const wchar_t *kFilesMessage = L"files";
static const wchar_t *kDirsMessage = L"folders";

static void GetAttribString(DWORD wa, bool isDir, char *s)
{
  s[0] = ((wa & FILE_ATTRIBUTE_DIRECTORY) != 0 || isDir) ? 'D' : kEmptyAttribChar;
  s[1] = ((wa & FILE_ATTRIBUTE_READONLY) != 0) ? 'R': kEmptyAttribChar;
  s[2] = ((wa & FILE_ATTRIBUTE_HIDDEN) != 0) ? 'H': kEmptyAttribChar;
  s[3] = ((wa & FILE_ATTRIBUTE_SYSTEM) != 0) ? 'S': kEmptyAttribChar;
  s[4] = ((wa & FILE_ATTRIBUTE_ARCHIVE) != 0) ? 'A': kEmptyAttribChar;
  s[5] = '\0';
}

enum EAdjustment
{
  kLeft,
  kCenter,
  kRight
};

struct CFieldInfo
{
  PROPID PropID;
  UString Name;
  EAdjustment TitleAdjustment;
  EAdjustment TextAdjustment;
  int PrefixSpacesWidth;
  int Width;
};

struct CFieldInfoInit
{
  PROPID PropID;
  const wchar_t *Name;
  EAdjustment TitleAdjustment;
  EAdjustment TextAdjustment;
  int PrefixSpacesWidth;
  int Width;
};

static CFieldInfoInit kStandardFieldTable[] =
{
  { kpidMTime, L"   Date      Time", kLeft, kLeft, 0, 19 },
  { kpidAttrib, L"Attr", kRight, kCenter, 1, 5 },
  { kpidSize, L"Size", kRight, kRight, 1, 12 },
  { kpidPackSize, L"Compressed", kRight, kRight, 1, 12 },
  { kpidPath, L"Name", kLeft, kLeft, 2, 24 }
};

static void PrintSpaces(int numSpaces)
{
  for (int i = 0; i < numSpaces; i++)
    g_StdOut << ' ';
}

static void PrintString(EAdjustment adjustment, int width, const UString &textString)
{
  const int numSpaces = width - textString.Length();
  int numLeftSpaces = 0;
  switch (adjustment)
  {
    case kLeft:
      numLeftSpaces = 0;
      break;
    case kCenter:
      numLeftSpaces = numSpaces / 2;
      break;
    case kRight:
      numLeftSpaces = numSpaces;
      break;
  }
  PrintSpaces(numLeftSpaces);
  g_StdOut << textString;
  PrintSpaces(numSpaces - numLeftSpaces);
}

class CFieldPrinter
{
  CObjectVector<CFieldInfo> _fields;
public:
  void Clear() { _fields.Clear(); }
  void Init(const CFieldInfoInit *standardFieldTable, int numItems);
  HRESULT Init(IInArchive *archive);
  void PrintTitle();
  void PrintTitleLines();
  HRESULT PrintItemInfo(const CArc &arc, UInt32 index, bool techMode);
  HRESULT PrintSummaryInfo(UInt64 numFiles, UInt64 numDirs,
      const UInt64 *size, const UInt64 *compressedSize);
};

void CFieldPrinter::Init(const CFieldInfoInit *standardFieldTable, int numItems)
{
  Clear();
  for (int i = 0; i < numItems; i++)
  {
    CFieldInfo fieldInfo;
    const CFieldInfoInit &fieldInfoInit = standardFieldTable[i];
    fieldInfo.PropID = fieldInfoInit.PropID;
    fieldInfo.Name = fieldInfoInit.Name;
    fieldInfo.TitleAdjustment = fieldInfoInit.TitleAdjustment;
    fieldInfo.TextAdjustment = fieldInfoInit.TextAdjustment;
    fieldInfo.PrefixSpacesWidth = fieldInfoInit.PrefixSpacesWidth;
    fieldInfo.Width = fieldInfoInit.Width;
    _fields.Add(fieldInfo);
  }
}

static UString GetPropName(PROPID propID, BSTR name)
{
  for (int i = 0; i < sizeof(kPropIdToName) / sizeof(kPropIdToName[0]); i++)
  {
    const CPropIdToName &propIdToName = kPropIdToName[i];
    if (propIdToName.PropID == propID)
      return propIdToName.Name;
  }
  if (name)
    return name;
  wchar_t s[16];
  ConvertUInt32ToString(propID, s);
  return s;
}

HRESULT CFieldPrinter::Init(IInArchive *archive)
{
  Clear();
  UInt32 numProps;
  RINOK(archive->GetNumberOfProperties(&numProps));
  for (UInt32 i = 0; i < numProps; i++)
  {
    CMyComBSTR name;
    PROPID propID;
    VARTYPE vt;
    RINOK(archive->GetPropertyInfo(i, &name, &propID, &vt));
    CFieldInfo fieldInfo;
    fieldInfo.PropID = propID;
    fieldInfo.Name = GetPropName(propID, name);
    _fields.Add(fieldInfo);
  }
  return S_OK;
}

void CFieldPrinter::PrintTitle()
{
  for (int i = 0; i < _fields.Size(); i++)
  {
    const CFieldInfo &fieldInfo = _fields[i];
    PrintSpaces(fieldInfo.PrefixSpacesWidth);
    PrintString(fieldInfo.TitleAdjustment,
      ((fieldInfo.PropID == kpidPath) ? 0: fieldInfo.Width), fieldInfo.Name);
  }
}

void CFieldPrinter::PrintTitleLines()
{
  for (int i = 0; i < _fields.Size(); i++)
  {
    const CFieldInfo &fieldInfo = _fields[i];
    PrintSpaces(fieldInfo.PrefixSpacesWidth);
    for (int i = 0; i < fieldInfo.Width; i++)
      g_StdOut << '-';
  }
}


static BOOL IsFileTimeZero(CONST FILETIME *lpFileTime)
{
  return (lpFileTime->dwLowDateTime == 0) && (lpFileTime->dwHighDateTime == 0);
}

static const char *kEmptyTimeString = "                   ";
static void PrintTime(const NCOM::CPropVariant &prop)
{
  if (prop.vt != VT_FILETIME)
    throw "incorrect item";
  if (IsFileTimeZero(&prop.filetime))
    g_StdOut << kEmptyTimeString;
  else
  {
    FILETIME localFileTime;
    if (!FileTimeToLocalFileTime(&prop.filetime, &localFileTime))
      throw "FileTimeToLocalFileTime error";
    char s[32];
    if (ConvertFileTimeToString(localFileTime, s, true, true))
      g_StdOut << s;
    else
      g_StdOut << kEmptyTimeString;
  }
}

HRESULT CFieldPrinter::PrintItemInfo(const CArc &arc, UInt32 index, bool techMode)
{
  /*
  if (techMode)
  {
    g_StdOut << "Index = ";
    g_StdOut << (UInt64)index;
    g_StdOut << endl;
  }
  */
  for (int i = 0; i < _fields.Size(); i++)
  {
    const CFieldInfo &fieldInfo = _fields[i];
    if (!techMode)
      PrintSpaces(fieldInfo.PrefixSpacesWidth);

    NCOM::CPropVariant prop;
    if (fieldInfo.PropID == kpidPath)
    {
      UString s;
      RINOK(arc.GetItemPath(index, s));
      prop = s;
    }
    else
    {
      RINOK(arc.Archive->GetProperty(index, fieldInfo.PropID, &prop));
    }
    if (techMode)
    {
      g_StdOut << fieldInfo.Name << " = ";
    }
    int width = (fieldInfo.PropID == kpidPath) ? 0: fieldInfo.Width;
    if (fieldInfo.PropID == kpidAttrib && (prop.vt == VT_EMPTY || prop.vt == VT_UI4))
    {
      UInt32 attrib = (prop.vt == VT_EMPTY) ? 0 : prop.ulVal;
      bool isFolder;
      RINOK(IsArchiveItemFolder(arc.Archive, index, isFolder));
      char s[8];
      GetAttribString(attrib, isFolder, s);
      g_StdOut << s;
    }
    else if (prop.vt == VT_EMPTY)
    {
      if (!techMode)
        PrintSpaces(width);
    }
    else if (fieldInfo.PropID == kpidMTime)
    {
      PrintTime(prop);
    }
    else if (prop.vt == VT_BSTR)
    {
      if (techMode)
        g_StdOut << prop.bstrVal;
      else
        PrintString(fieldInfo.TextAdjustment, width, prop.bstrVal);
    }
    else
    {
      UString s = ConvertPropertyToString(prop, fieldInfo.PropID);
      s.Replace(wchar_t(0xA), L' ');
      s.Replace(wchar_t(0xD), L' ');

      if (techMode)
        g_StdOut << s;
      else
        PrintString(fieldInfo.TextAdjustment, width, s);
    }
    if (techMode)
      g_StdOut << endl;
  }
  return S_OK;
}

static void PrintNumberString(EAdjustment adjustment, int width, const UInt64 *value)
{
  wchar_t textString[32] = { 0 };
  if (value != NULL)
    ConvertUInt64ToString(*value, textString);
  PrintString(adjustment, width, textString);
}


HRESULT CFieldPrinter::PrintSummaryInfo(UInt64 numFiles, UInt64 numDirs,
    const UInt64 *size, const UInt64 *compressedSize)
{
  for (int i = 0; i < _fields.Size(); i++)
  {
    const CFieldInfo &fieldInfo = _fields[i];
    PrintSpaces(fieldInfo.PrefixSpacesWidth);
    NCOM::CPropVariant prop;
    if (fieldInfo.PropID == kpidSize)
      PrintNumberString(fieldInfo.TextAdjustment, fieldInfo.Width, size);
    else if (fieldInfo.PropID == kpidPackSize)
      PrintNumberString(fieldInfo.TextAdjustment, fieldInfo.Width, compressedSize);
    else if (fieldInfo.PropID == kpidPath)
    {
      wchar_t textString[32];
      ConvertUInt64ToString(numFiles, textString);
      UString temp = textString;
      temp += L" ";
      temp += kFilesMessage;
      temp += L", ";
      ConvertUInt64ToString(numDirs, textString);
      temp += textString;
      temp += L" ";
      temp += kDirsMessage;
      PrintString(fieldInfo.TextAdjustment, 0, temp);
    }
    else
      PrintString(fieldInfo.TextAdjustment, fieldInfo.Width, L"");
  }
  return S_OK;
}

bool GetUInt64Value(IInArchive *archive, UInt32 index, PROPID propID, UInt64 &value)
{
  NCOM::CPropVariant prop;
  if (archive->GetProperty(index, propID, &prop) != S_OK)
    throw "GetPropertyValue error";
  if (prop.vt == VT_EMPTY)
    return false;
  value = ConvertPropVariantToUInt64(prop);
  return true;
}

static void PrintPropPair(const wchar_t *name, const wchar_t *value)
{
  g_StdOut << name << " = " << value << endl;
}

HRESULT ListArchives(CCodecs *codecs, const CIntVector &formatIndices,
    bool stdInMode,
    UStringVector &arcPaths, UStringVector &arcPathsFull,
    const NWildcard::CCensorNode &wildcardCensor,
    bool enableHeaders, bool techMode,
    #ifndef _NO_CRYPTO
    bool &passwordEnabled, UString &password,
    #endif
    UInt64 &numErrors)
{
  numErrors = 0;
  CFieldPrinter fieldPrinter;
  if (!techMode)
    fieldPrinter.Init(kStandardFieldTable, sizeof(kStandardFieldTable) / sizeof(kStandardFieldTable[0]));

  UInt64 numFiles2 = 0, numDirs2 = 0, totalPackSize2 = 0, totalUnPackSize2 = 0;
  UInt64 *totalPackSizePointer2 = 0, *totalUnPackSizePointer2 = 0;
  int numArcs = /* stdInMode ? 1 : */ arcPaths.Size();
  for (int i = 0; i < numArcs; i++)
  {
    const UString &archiveName = arcPaths[i];
    UInt64 arcPackSize = 0;
    if (!stdInMode)
    {
      NFile::NFind::CFileInfoW fi;
      if (!fi.Find(archiveName) || fi.IsDir())
      {
        g_StdOut << endl << "Error: " << archiveName << " is not file" << endl;
        numErrors++;
        continue;
      }
      arcPackSize = fi.Size;
    }

    CArchiveLink archiveLink;

    COpenCallbackConsole openCallback;
    openCallback.OutStream = &g_StdOut;

    #ifndef _NO_CRYPTO

    openCallback.PasswordIsDefined = passwordEnabled;
    openCallback.Password = password;

    #endif

    HRESULT result = archiveLink.Open2(codecs, formatIndices, stdInMode, NULL, archiveName, &openCallback);
    if (result != S_OK)
    {
      if (result == E_ABORT)
        return result;
      g_StdOut << endl << "Error: " << archiveName << ": ";
      if (result == S_FALSE)
      {
        #ifndef _NO_CRYPTO
        if (openCallback.Open_WasPasswordAsked())
          g_StdOut << "Can not open encrypted archive. Wrong password?";
        else
        #endif
          g_StdOut << "Can not open file as archive";
      }
      else if (result == E_OUTOFMEMORY)
        g_StdOut << "Can't allocate required memory";
      else
        g_StdOut << NError::MyFormatMessage(result);
      g_StdOut << endl;
      numErrors++;
      continue;
    }

    if (!stdInMode)
    for (int v = 0; v < archiveLink.VolumePaths.Size(); v++)
    {
      int index = arcPathsFull.FindInSorted(archiveLink.VolumePaths[v]);
      if (index >= 0 && index > i)
      {
        arcPaths.Delete(index);
        arcPathsFull.Delete(index);
        numArcs = arcPaths.Size();
      }
    }

    if (enableHeaders)
    {
      g_StdOut << endl << kListing << archiveName << endl << endl;

      for (int i = 0; i < archiveLink.Arcs.Size(); i++)
      {
        const CArc &arc = archiveLink.Arcs[i];
        
        g_StdOut << "--\n";
        PrintPropPair(L"Path", arc.Path);
        PrintPropPair(L"Type", codecs->Formats[arc.FormatIndex].Name);
        if (!arc.ErrorMessage.IsEmpty())
          PrintPropPair(L"Error", arc.ErrorMessage);
        UInt32 numProps;
        IInArchive *archive = arc.Archive;
        if (archive->GetNumberOfArchiveProperties(&numProps) == S_OK)
        {
          for (UInt32 j = 0; j < numProps; j++)
          {
            CMyComBSTR name;
            PROPID propID;
            VARTYPE vt;
            RINOK(archive->GetArchivePropertyInfo(j, &name, &propID, &vt));
            NCOM::CPropVariant prop;
            RINOK(archive->GetArchiveProperty(propID, &prop));
            UString s = ConvertPropertyToString(prop, propID);
            if (!s.IsEmpty())
              PrintPropPair(GetPropName(propID, name), s);
          }
        }
        if (i != archiveLink.Arcs.Size() - 1)
        {
          UInt32 numProps;
          g_StdOut << "----\n";
          if (archive->GetNumberOfProperties(&numProps) == S_OK)
          {
            UInt32 mainIndex = archiveLink.Arcs[i + 1].SubfileIndex;
            for (UInt32 j = 0; j < numProps; j++)
            {
              CMyComBSTR name;
              PROPID propID;
              VARTYPE vt;
              RINOK(archive->GetPropertyInfo(j, &name, &propID, &vt));
              NCOM::CPropVariant prop;
              RINOK(archive->GetProperty(mainIndex, propID, &prop));
              UString s = ConvertPropertyToString(prop, propID);
              if (!s.IsEmpty())
                PrintPropPair(GetPropName(propID, name), s);
            }
          }
        }
        
      }
      g_StdOut << endl;
      if (techMode)
        g_StdOut << "----------\n";
    }

    if (enableHeaders && !techMode)
    {
      fieldPrinter.PrintTitle();
      g_StdOut << endl;
      fieldPrinter.PrintTitleLines();
      g_StdOut << endl;
    }

    const CArc &arc = archiveLink.Arcs.Back();
    IInArchive *archive = arc.Archive;
    if (techMode)
    {
      RINOK(fieldPrinter.Init(archive));
    }
    UInt64 numFiles = 0, numDirs = 0, totalPackSize = 0, totalUnPackSize = 0;
    UInt64 *totalPackSizePointer = 0, *totalUnPackSizePointer = 0;
    UInt32 numItems;
    RINOK(archive->GetNumberOfItems(&numItems));
    for (UInt32 i = 0; i < numItems; i++)
    {
      if (NConsoleClose::TestBreakSignal())
        return E_ABORT;

      UString filePath;
      HRESULT res = arc.GetItemPath(i, filePath);
      if (stdInMode && res == E_INVALIDARG)
        break;
      RINOK(res);

      bool isFolder;
      RINOK(IsArchiveItemFolder(archive, i, isFolder));
      if (!wildcardCensor.CheckPath(filePath, !isFolder))
        continue;
      
      fieldPrinter.PrintItemInfo(arc, i, techMode);
      
      UInt64 packSize, unpackSize;
      if (!GetUInt64Value(archive, i, kpidSize, unpackSize))
        unpackSize = 0;
      else
        totalUnPackSizePointer = &totalUnPackSize;
      if (!GetUInt64Value(archive, i, kpidPackSize, packSize))
        packSize = 0;
      else
        totalPackSizePointer = &totalPackSize;
      
      g_StdOut << endl;

      if (isFolder)
        numDirs++;
      else
        numFiles++;
      totalPackSize += packSize;
      totalUnPackSize += unpackSize;
    }

    if (!stdInMode && totalPackSizePointer == 0)
    {
      if (archiveLink.VolumePaths.Size() != 0)
        arcPackSize += archiveLink.VolumesSize;
      totalPackSize = (numFiles == 0) ? 0 : arcPackSize;
      totalPackSizePointer = &totalPackSize;
    }
    if (totalUnPackSizePointer == 0 && numFiles == 0)
    {
      totalUnPackSize = 0;
      totalUnPackSizePointer = &totalUnPackSize;
    }
    if (enableHeaders && !techMode)
    {
      fieldPrinter.PrintTitleLines();
      g_StdOut << endl;
      fieldPrinter.PrintSummaryInfo(numFiles, numDirs, totalUnPackSizePointer, totalPackSizePointer);
      g_StdOut << endl;
    }
    if (totalPackSizePointer != 0)
    {
      totalPackSizePointer2 = &totalPackSize2;
      totalPackSize2 += totalPackSize;
    }
    if (totalUnPackSizePointer != 0)
    {
      totalUnPackSizePointer2 = &totalUnPackSize2;
      totalUnPackSize2 += totalUnPackSize;
    }
    numFiles2 += numFiles;
    numDirs2 += numDirs;
  }
  if (enableHeaders && !techMode && numArcs > 1)
  {
    g_StdOut << endl;
    fieldPrinter.PrintTitleLines();
    g_StdOut << endl;
    fieldPrinter.PrintSummaryInfo(numFiles2, numDirs2, totalUnPackSizePointer2, totalPackSizePointer2);
    g_StdOut << endl;
    g_StdOut << "Archives: " << numArcs << endl;
  }
  return S_OK;
}
