/* This file is (c) 2008-2012 Konstantin Isakov <ikm@goldendict.org>
 * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */

#include "zipfile.hh"
#include <QtEndian>
#include <QByteArray>
#include <QFileInfo>

namespace ZipFile {

#pragma pack( push, 1 )

/// End-of-central-directory record, as is
struct EndOfCdirRecord
{
  quint32 signature;
  quint16 numDisk, numDiskCd, totalEntriesDisk, totalEntries;
  quint32 size, offset;
  quint16 commentLength;
}
#ifndef _MSC_VER
__attribute__( ( packed ) )
#endif
;

struct CentralFileHeaderRecord
{
  quint32 signature;
  quint16 verMadeBy, verNeeded, gpBits, compressionMethod, fileTime, fileDate;
  quint32 crc32, compressedSize, uncompressedSize;
  quint16 fileNameLength, extraFieldLength, fileCommentLength, diskNumberStart, intFileAttrs;
  quint32 externalFileAttrs, offsetOfLocalHeader;
}
#ifndef _MSC_VER
__attribute__( ( packed ) )
#endif
;

struct LocalFileHeaderRecord
{
  quint32 signature;
  quint16 verNeeded, gpBits, compressionMethod, fileTime, fileDate;
  quint32 crc32, compressedSize, uncompressedSize;
  quint16 fileNameLength, extraFieldLength;
}
#ifndef _MSC_VER
__attribute__( ( packed ) )
#endif
;

#pragma pack( pop )

static quint32 const endOfCdirRecordSignatureValue = qToLittleEndian( 0x06054b50 );
static const quint32 centralFileHeaderSignature    = qToLittleEndian( 0x02014b50 );
static const quint32 localFileHeaderSignature      = qToLittleEndian( 0x04034b50 );

static CompressionMethod getCompressionMethod( quint16 compressionMethod )
{
  switch ( qFromLittleEndian( compressionMethod ) ) {
    case 0:
      return Uncompressed;
    case 8:
      return Deflated;
    default:
      return Unsupported;
  }
}

bool positionAtCentralDir( SplitZipFile & zip )
{
  // Find the end-of-central-directory record

  int maxEofBufferSize = 65535 + sizeof( EndOfCdirRecord );

  if ( zip.size() > maxEofBufferSize ) {
    zip.seek( zip.size() - maxEofBufferSize );
  }
  else if ( (size_t)zip.size() < sizeof( EndOfCdirRecord ) ) {
    return false;
  }
  else {
    zip.seek( 0 );
  }

  QByteArray eocBuffer = zip.read( maxEofBufferSize );

  if ( eocBuffer.size() < (int)sizeof( EndOfCdirRecord ) ) {
    return false;
  }

  int lastIndex = eocBuffer.size() - sizeof( EndOfCdirRecord );

  QByteArray endOfCdirRecordSignature( (const char *)&endOfCdirRecordSignatureValue,
                                       sizeof( endOfCdirRecordSignatureValue ) );

  EndOfCdirRecord endOfCdirRecord;

  quint32 cdir_offset;

  for ( ;; --lastIndex ) {
    lastIndex = eocBuffer.lastIndexOf( endOfCdirRecordSignature, lastIndex );

    if ( lastIndex == -1 ) {
      return false;
    }

    /// We need to copy it due to possible alignment issues on ARM etc
    memcpy( &endOfCdirRecord, eocBuffer.data() + lastIndex, sizeof( endOfCdirRecord ) );

    /// Sanitize the record by checking the offset

    cdir_offset = zip.calcAbsoluteOffset( qFromLittleEndian( endOfCdirRecord.offset ),
                                          qFromLittleEndian( endOfCdirRecord.numDiskCd ) );

    if ( !zip.seek( cdir_offset ) ) {
      continue;
    }

    quint32 signature;

    if ( zip.read( (char *)&signature, sizeof( signature ) ) != sizeof( signature ) ) {
      continue;
    }

    if ( signature == centralFileHeaderSignature ) {
      break;
    }
  }

  // Found cdir -- position the file on the first header

  return zip.seek( cdir_offset );
}

bool readNextEntry( SplitZipFile & zip, CentralDirEntry & entry )
{
  CentralFileHeaderRecord record;

  auto centralDirOffset = zip.pos();

  if ( zip.read( (char *)&record, sizeof( record ) ) != sizeof( record ) ) {
    return false;
  }

  if ( record.signature != centralFileHeaderSignature ) {
    return false;
  }

  // Read file name

  int fileNameLength = qFromLittleEndian( record.fileNameLength );
  entry.fileName     = zip.read( fileNameLength );

  if ( entry.fileName.size() != fileNameLength ) {
    return false;
  }

  // Skip extra fields

  if ( !zip.seek( ( zip.pos() + qFromLittleEndian( record.extraFieldLength ) )
                  + qFromLittleEndian( record.fileCommentLength ) ) ) {
    return false;
  }

  entry.centralHeaderOffset = zip.calcAbsoluteOffset( centralDirOffset, qFromLittleEndian( record.diskNumberStart ) );
  entry.localHeaderOffset   = zip.calcAbsoluteOffset( qFromLittleEndian( record.offsetOfLocalHeader ),
                                                    qFromLittleEndian( record.diskNumberStart ) );
  entry.compressedSize      = qFromLittleEndian( record.compressedSize );
  entry.uncompressedSize    = qFromLittleEndian( record.uncompressedSize );
  entry.compressionMethod   = getCompressionMethod( record.compressionMethod );
  entry.fileNameInUTF8      = ( qFromLittleEndian( record.gpBits ) & 0x800 ) != 0;

  return true;
}

bool skipLocalHeader( SplitZipFile & zip )
{
  LocalFileHeaderRecord record;

  if ( zip.read( (char *)&record, sizeof( record ) ) != sizeof( record ) ) {
    return false;
  }

  if ( record.signature != localFileHeaderSignature ) {
    return false;
  }

  // skip file name
  int fileNameLength = qFromLittleEndian( record.fileNameLength );
  // Skip extra field
  return zip.seek( zip.pos() + fileNameLength + qFromLittleEndian( record.extraFieldLength ) );
}

bool readLocalHeaderFromCentral( SplitZipFile & zip, LocalFileHeader & entry )
{
  CentralFileHeaderRecord record;

  if ( zip.read( (char *)&record, sizeof( record ) ) != sizeof( record ) ) {
    return false;
  }

  if ( record.signature != centralFileHeaderSignature ) {
    return false;
  }

  // Read file name
  int fileNameLength = qFromLittleEndian( record.fileNameLength );
  entry.fileName     = zip.read( fileNameLength );

  if ( entry.fileName.size() != fileNameLength ) {
    return false;
  }

  entry.compressedSize    = qFromLittleEndian( record.compressedSize );
  entry.uncompressedSize  = qFromLittleEndian( record.uncompressedSize );
  entry.compressionMethod = getCompressionMethod( record.compressionMethod );
  entry.offset            = zip.calcAbsoluteOffset( qFromLittleEndian( record.offsetOfLocalHeader ),
                                         qFromLittleEndian( record.diskNumberStart ) );

  return true;
}

SplitZipFile::SplitZipFile( const QString & name )
{
  setFileName( name );
}

void SplitZipFile::setFileName( const QString & name )
{
  {
    QString lname = name.toLower();
    if ( lname.endsWith( ".zips" ) ) {
      appendFile( name );
      return;
    }

    if ( !lname.endsWith( ".zip" ) ) {
      return;
    }
  }

  if ( QFileInfo( name ).isFile() ) {
    for ( int i = 1; i < 100; i++ ) {
      QString name2 = name.left( name.size() - 2 ) + QString( "%1" ).arg( i, 2, 10, QChar( '0' ) );
      if ( QFileInfo( name2 ).isFile() ) {
        appendFile( name2 );
      }
      else {
        break;
      }
    }
    appendFile( name );
  }
  else {
    for ( int i = 1; i < 1000; i++ ) {
      QString name2 = name + QString( ".%1" ).arg( i, 3, 10, QChar( '0' ) );
      if ( QFileInfo( name2 ).isFile() ) {
        appendFile( name2 );
      }
      else {
        break;
      }
    }
  }
}

QDateTime SplitZipFile::lastModified() const
{
  unsigned long ts = 0;
  for ( QList< QFile * >::const_iterator i = files.begin(); i != files.end(); ++i ) {
    unsigned long t = QFileInfo( ( *i )->fileName() ).lastModified().toSecsSinceEpoch();
    if ( t > ts ) {
      ts = t;
    }
  }
  return QDateTime::fromSecsSinceEpoch( ts );
}

qint64 SplitZipFile::calcAbsoluteOffset( qint64 offset, quint16 partNo )
{
  if ( partNo >= offsets.size() ) {
    return 0;
  }

  return offsets.at( partNo ) + offset;
}

} // namespace ZipFile
