// -*- c-basic-offset: 2 -*-
/* This file is part of the KDE project
   Copyright (C) 1998, 1999 Torben Weis <weis@kde.org>
   Copyright (C) 2000-2002 David Faure <faure@kde.org>, Werner Trobin <trobin@kde.org>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
*/

#include "KoStore.h"
//#include "KoTarStore.h"
#include "KoZipStore.h"
//#include "KoDirectoryStore.h"
#ifdef QCA2
#include "KoEncryptedStore.h"
#endif

#include <QBuffer>
#include <QFileInfo>
#include <QFile>
#include <QDir>

#include <kurl.h>
#include <kdebug.h>
#include <kdeversion.h>
#include <klocale.h>
#include <kmessagebox.h>
#include <kio/netaccess.h>

#define DefaultFormat KoStore::Zip

const int KoStore::s_area = 30002;

KoStore::Backend KoStore::determineBackend( QIODevice* dev )
{
    unsigned char buf[5];
    if ( dev->read( (char *)buf, 4 ) < 4 )
      return DefaultFormat; // will create a "bad" store (bad()==true)
    //if ( buf[0] == 0037 && buf[1] == 0213 ) // gzip -> tar.gz
      //return Tar;
    if ( buf[0] == 'P' && buf[1] == 'K' && buf[2] == 3 && buf[3] == 4 )
      return Zip;
    return DefaultFormat; // fallback
}

KoStore* KoStore::createStore( const QString& fileName, Mode mode, const QByteArray & appIdentification, Backend backend )
{
  bool automatic = false;
  if ( backend == Auto ) {
    automatic = true;
    if ( mode == KoStore::Write )
      backend = DefaultFormat;
    else
    {
      QFileInfo inf( fileName );
      if ( inf.isDir() )
        backend = Directory;
      else
      {
        QFile file( fileName );
        if ( file.open( QIODevice::ReadOnly ) )
          backend = determineBackend( &file );
        else
          backend = DefaultFormat; // will create a "bad" store (bad()==true)
      }
    }
  }
  switch ( backend )
  {
//  case Tar:
  //  return new KoTarStore( fileName, mode, appIdentification );
  case Zip:
#ifdef QCA2
    if( automatic && mode == Read ) {
        // When automatically detecting, this might as well be an encrypted file. We'll need to check anyway, so we'll just use the encrypted store.
        return new KoEncryptedStore( fileName, Read, appIdentification );
    }
#endif
    return new KoZipStore( fileName, mode, appIdentification );
//  case Directory:
//    return new KoDirectoryStore( fileName /* should be a dir name.... */, mode );
#ifdef QCA2
  case Encrypted:
    return new KoEncryptedStore( fileName, mode, appIdentification );
#endif
  default:
    kWarning(s_area) << "Unsupported backend requested for KoStore : " << backend;
    return 0L;
  }
}

KoStore* KoStore::createStore( QIODevice *device, Mode mode, const QByteArray & appIdentification, Backend backend )
{
  bool automatic = false;
  if ( backend == Auto )
  {
    automatic = true;
    if ( mode == KoStore::Write )
      backend = DefaultFormat;
    else {
      if ( device->open( QIODevice::ReadOnly ) ) {
        backend = determineBackend( device );
        device->close();
      }
    }
  }
  switch ( backend )
  {
  //case Tar:
    //return new KoTarStore( device, mode, appIdentification );
  case Directory:
    kError(s_area) << "Can't create a Directory store for a memory buffer!" << endl;
    // fallback
  case Zip:
#ifdef QCA2
    if( automatic && mode == Read ) {
        // When automatically detecting, this might as well be an encrypted file. We'll need to check anyway, so we'll just use the encrypted store.
        return new KoEncryptedStore( device, Read, appIdentification );
    }
#endif
    return new KoZipStore( device, mode, appIdentification );
#ifdef QCA2
  case Encrypted:
    return new KoEncryptedStore( device, mode, appIdentification );
#endif
  default:
    kWarning(s_area) << "Unsupported backend requested for KoStore : " << backend;
    return 0L;
  }
}

KoStore* KoStore::createStore( QWidget* window, const KUrl& url, Mode mode, const QByteArray & appIdentification, Backend backend )
{
  const bool automatic = ( backend == Auto );
  if ( url.isLocalFile() )
    return createStore(url.toLocalFile(), mode,  appIdentification, backend );

  QString tmpFile;
  if ( mode == KoStore::Write )
  {
    if ( automatic )
      backend = DefaultFormat;
  }
  else
  {
    const bool downloaded =
        KIO::NetAccess::download( url, tmpFile, window );

    if (!downloaded)
    {
      kError(s_area) << "Could not download file!" << endl;
      backend = DefaultFormat; // will create a "bad" store (bad()==true)
    }
    else if ( automatic )
    {
      QFile file( tmpFile );
      if ( file.open( QIODevice::ReadOnly ) )
      {
        backend = determineBackend( &file );
        file.close();
      }
    }
  }
  switch ( backend )
  {
  //case Tar:
    //return new KoTarStore( window, url, tmpFile, mode, appIdentification );
  case Zip:
#ifdef QCA2
    if( automatic && mode == Read ) {
        // When automatically detecting, this might as well be an encrypted file. We'll need to check anyway, so we'll just use the encrypted store.
        return new KoEncryptedStore( window, url, tmpFile, Read, appIdentification );
    }
#endif
    return new KoZipStore( window, url, tmpFile, mode, appIdentification );
#ifdef QCA2
  case Encrypted:
    return new KoEncryptedStore( window, url, tmpFile, mode, appIdentification );
#endif
  default:
    kWarning(s_area) << "Unsupported backend requested for KoStore (KUrl) : " << backend;
    KMessageBox::sorry( window,
        i18n("The directory mode is not supported for remote locations."),
        i18n("KOffice Storage"));
    return 0L;
  }
}

namespace {
  const char* const ROOTPART = "root";
  const char* const MAINNAME = "maindoc.xml";
}

KoStore::KoStore()
{
}

bool KoStore::init( Mode _mode )
{
  m_bIsOpen = false;
  m_mode = _mode;
  m_stream = 0;
  m_bFinalized = false;

  // Assume new style names.
  m_namingVersion = NAMING_VERSION_2_2;
  return true;
}

KoStore::~KoStore()
{
  delete m_stream;
}

bool KoStore::open( const QString & _name )
{
  // This also converts from relative to absolute, i.e. merges the currentPath()
  m_sName = toExternalNaming( _name );

  if ( m_bIsOpen )
  {
    kWarning(s_area) << "KoStore: File is already opened";
    //return KIO::ERR_INTERNAL;
    return false;
  }

  if ( m_sName.length() > 512 )
  {
      kError(s_area) << "KoStore: Filename " << m_sName << " is too long" << endl;
      //return KIO::ERR_MALFORMED_URL;
      return false;
  }

  if ( m_mode == Write )
  {
    kDebug(s_area) <<"KoStore: opening for writing '" << m_sName <<"'";
    if ( m_strFiles.contains( m_sName ) )
    {
      kWarning(s_area) << "KoStore: Duplicate filename " << m_sName;
      //return KIO::ERR_FILE_ALREADY_EXIST;
      return false;
    }

    m_strFiles.append( m_sName );

    m_iSize = 0;
    if ( !openWrite( m_sName ) )
      return false;
  }
  else if ( m_mode == Read )
  {
    kDebug(s_area) <<"Opening for reading '" << m_sName <<"'";
    if ( !openRead( m_sName ) )
      return false;
  }
  else
    //return KIO::ERR_UNSUPPORTED_ACTION;
    return false;

  m_bIsOpen = true;
  return true;
}

bool KoStore::isOpen() const
{
  return m_bIsOpen;
}

bool KoStore::close()
{
  kDebug(s_area) <<"KoStore: Closing";

  if ( !m_bIsOpen )
  {
    kWarning(s_area) << "KoStore: You must open before closing";
    //return KIO::ERR_INTERNAL;
    return false;
  }

  bool ret = m_mode == Write ? closeWrite() : closeRead();

  delete m_stream;
  m_stream = 0L;
  m_bIsOpen = false;
  return ret;
}

QIODevice* KoStore::device() const
{
  if ( !m_bIsOpen )
    kWarning(s_area) << "KoStore: You must open before asking for a device";
  if ( m_mode != Read )
    kWarning(s_area) << "KoStore: Can not get device from store that is opened for writing";
  return m_stream;
}

QByteArray KoStore::read( qint64 max )
{
  QByteArray data;

  if ( !m_bIsOpen )
  {
    kWarning(s_area) << "KoStore: You must open before reading";
    return data;
  }
  if ( m_mode != Read )
  {
    kError(s_area) << "KoStore: Can not read from store that is opened for writing" << endl;
    return data;
  }

  return m_stream->read( max );
}

qint64 KoStore::write( const QByteArray& data )
{
  return write( data.data(), data.size() ); // see below
}

qint64 KoStore::read( char *_buffer, qint64 _len )
{
  if ( !m_bIsOpen )
  {
    kError(s_area) << "KoStore: You must open before reading" << endl;
    return -1;
  }
  if ( m_mode != Read )
  {
    kError(s_area) << "KoStore: Can not read from store that is opened for writing" << endl;
    return -1;
  }

  return m_stream->read( _buffer, _len );
}

qint64 KoStore::write( const char* _data, qint64 _len )
{
  if ( _len == 0L ) return 0;

  if ( !m_bIsOpen )
  {
    kError(s_area) << "KoStore: You must open before writing" << endl;
    return 0L;
  }
  if ( m_mode != Write  )
  {
    kError(s_area) << "KoStore: Can not write to store that is opened for reading" << endl;
    return 0L;
  }

  int nwritten = m_stream->write( _data, _len );
  Q_ASSERT( nwritten == (int)_len );
  m_iSize += nwritten;

  return nwritten;
}

qint64 KoStore::size() const
{
  if ( !m_bIsOpen )
  {
    kWarning(s_area) << "KoStore: You must open before asking for a size";
    return static_cast<qint64>(-1);
  }
  if ( m_mode != Read )
  {
    kWarning(s_area) << "KoStore: Can not get size from store that is opened for writing";
    return static_cast<qint64>(-1);
  }
  return m_iSize;
}

bool KoStore::enterDirectory( const QString& directory )
{
  //kDebug(s_area) <<"KoStore::enterDirectory" << directory;
  int pos;
  bool success = true;
  QString tmp( directory );

  while ( ( pos = tmp.indexOf( '/' ) ) != -1 &&
          ( success = enterDirectoryInternal( tmp.left( pos ) ) ) )
          tmp = tmp.mid( pos + 1 );

  if ( success && !tmp.isEmpty() )
    return enterDirectoryInternal( tmp );
  return success;
}

bool KoStore::leaveDirectory()
{
  if ( m_currentPath.isEmpty() )
    return false;

  m_currentPath.pop_back();

  return enterAbsoluteDirectory( expandEncodedDirectory( currentPath() ) );
}

QString KoStore::currentDirectory() const
{
  return expandEncodedDirectory( currentPath() );
}

QString KoStore::currentPath() const
{
  QString path;
  QStringList::ConstIterator it = m_currentPath.begin();
  QStringList::ConstIterator end = m_currentPath.end();
  for ( ; it != end; ++it ) {
    path += *it;
    path += '/';
  }
  return path;
}

void KoStore::pushDirectory()
{
  m_directoryStack.push( currentPath() );
}

void KoStore::popDirectory()
{
  m_currentPath.clear();
  enterAbsoluteDirectory( QString() );
  enterDirectory( m_directoryStack.pop() );
}

bool KoStore::addLocalFile( const QString &fileName, const QString &destName )
{
  QFileInfo fi( fileName );
  uint size = fi.size();
  QFile file( fileName );
  if ( !file.open( QIODevice::ReadOnly ))
  {
    return false;
  }

  if ( !open ( destName ) )
  {
    return false;
  }

  QByteArray data;
  data.resize( 8 * 1024 );

  uint total = 0;
  for ( int block = 0; ( block = file.read( data.data(), data.size() ) ) > 0; total += block )
  {
    data.resize(block);
    if ( write( data ) != block )
      return false;
    data.resize(8*1024);
  }
  Q_ASSERT( total == size );

  close();
  file.close();

  return true;
}

bool KoStore::addDataToFile( QByteArray &buffer, const QString &destName )
{
  QBuffer file( &buffer );
  if ( !file.open( QIODevice::ReadOnly ))
  {
    return false;
  }

  if ( !open ( destName ) )
  {
    return false;
  }

  QByteArray data;
  data.resize( 8 * 1024 );

  uint total = 0;
  for ( int block = 0; ( block = file.read( data.data(), data.size() ) ) > 0; total += block )
  {
    data.resize(block);
    if ( write( data ) != block )
      return false;
    data.resize(8*1024);
  }

  close();
  file.close();

  return true;
}

bool KoStore::extractFile ( const QString &srcName, const QString &fileName )
{
  QFile file( fileName );
  return extractFile( srcName, file );
}


bool KoStore::extractFile( const QString &srcName, QByteArray &data )
{
  QBuffer buffer( &data );
  return extractFile( srcName, buffer );
}

bool KoStore::extractFile( const QString &srcName, QIODevice &buffer )
{
  if ( !open ( srcName ) )
     return false;

  if( !buffer.open ( QIODevice::WriteOnly ) )
  {
    close();
    return false;
  }
  // ### This could use KArchive::copy or something, no?

  QByteArray data;
  data.resize( 8 * 1024 );
  uint total = 0;
  for( int block = 0; ( block = read( data.data(), data.size() ) ) > 0; total += block )
  {
    buffer.write( data.data(), block );
  }

  if( size() != static_cast<qint64>(-1) )
    Q_ASSERT( total == size() );

  buffer.close();
  close();

  return true;
}


QStringList KoStore::addLocalDirectory( const QString &dirPath, const QString &destName )
{
  QString dot = ".";
  QString dotdot = "..";
  QStringList content;

  QDir dir(dirPath);
  if ( !dir.exists() )
    return QStringList();

  QStringList files = dir.entryList();
  for ( QStringList::Iterator it = files.begin(); it != files.end(); ++it )
  {
     if ( *it != dot && *it != dotdot )
     {
        QString currentFile = dirPath + '/' + *it;
        QString dest = destName.isEmpty() ? *it : (destName + '/' + *it);

        QFileInfo fi ( currentFile );
        if ( fi.isFile() )
        {
          addLocalFile ( currentFile, dest );
          content.append(dest);
        }
        else if ( fi.isDir() )
        {
          content += addLocalDirectory ( currentFile, dest );
        }
     }
  }

  return content;
}


bool KoStore::seek( qint64 pos )
{
  return m_stream->seek( pos );
}

qint64 KoStore::pos() const
{
  return m_stream->pos();
}

bool KoStore::atEnd() const
{
  return m_stream->atEnd();
}

// See the specification for details of what this function does.
QString KoStore::toExternalNaming( const QString & _internalNaming ) const
{
  if ( _internalNaming == ROOTPART )
    return expandEncodedDirectory( currentPath() ) + MAINNAME;

  QString intern;
  if ( _internalNaming.startsWith( "tar:/" ) ) // absolute reference
    intern = _internalNaming.mid( 5 ); // remove protocol
  else
    intern = currentPath() + _internalNaming;

  return expandEncodedPath( intern );
}

QString KoStore::expandEncodedPath( const QString& _intern ) const
{
  QString intern = _intern;

  if ( m_namingVersion == NAMING_VERSION_RAW )
    return intern;

  QString result;
  int pos;

  if ( ( pos = intern.lastIndexOf( '/', -1 ) ) != -1 ) {
    result = expandEncodedDirectory( intern.left( pos ) ) + '/';
    intern = intern.mid( pos + 1 );
  }

  // Now process the filename. If the first character is numeric, we have
  // a main document.
  if ( QChar(intern.at(0)).isDigit() )
  {
    // If this is the first part name, check if we have a store with
    // old-style names.
    if ( ( m_namingVersion == NAMING_VERSION_2_2 ) &&
         ( m_mode == Read ) &&
         ( fileExists( result + "part" + intern + ".xml" ) ) )
      m_namingVersion = NAMING_VERSION_2_1;

    if ( m_namingVersion == NAMING_VERSION_2_1 )
      result = result + "part" + intern + ".xml";
    else
      result = result + "part" + intern + '/' + MAINNAME;
  }
  else
    result += intern;
  return result;
}

QString KoStore::expandEncodedDirectory( const QString& _intern ) const
{
  QString intern = _intern;

  if ( m_namingVersion == NAMING_VERSION_RAW )
    return intern;

  QString result;
  int pos;
  while ( ( pos = intern.indexOf( '/' ) ) != -1 ) {
    if ( QChar(intern.at(0)).isDigit() )
      result += "part";
    result += intern.left( pos + 1 ); // copy numbers (or "pictures") + "/"
    intern = intern.mid( pos + 1 ); // remove the dir we just processed
  }

  if ( !intern.isEmpty() && QChar(intern.at(0)).isDigit() )
    result += "part";
  result += intern;
  return result;
}

bool KoStore::enterDirectoryInternal( const QString& directory )
{
    if ( enterRelativeDirectory( expandEncodedDirectory( directory ) ) )
    {
      m_currentPath.append( directory );
      return true;
    }
    return false;
}

void KoStore::disallowNameExpansion( void )
{
    m_namingVersion = NAMING_VERSION_RAW;
}

bool KoStore::hasFile( const QString& fileName ) const
{
  return fileExists( toExternalNaming( currentPath() + fileName ) );
}

bool KoStore::finalize()
{
  Q_ASSERT( !m_bFinalized ); // call this only once!
  m_bFinalized = true;
  return doFinalize();
}

bool KoStore::isEncrypted()
{
    return false;
}

bool KoStore::setPassword( const QString& /*password*/ )
{
    return false;
}

QString KoStore::password( )
{
    return QString();
}
