/* This file is part of the KDE project

   Copyright (C) 2008 Lukas Appelhans <l.appelhans@gmx.de>
   Copyright (C) 2009 Matthias Fuchs <mat69@gmx.net>

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

#include "kget_export.h"

#include "transferdatasource.h"
#include "job.h"

#include <kio/job.h>

#include <QDomElement>

class BitSet;
class TransferDataSource;
class QTimer;
class Signature;
class Verifier;

namespace KIO
{
    class FileJob;
}

/**
 This class manages multiple DataSources and saves the received data to the file
 */
class KGET_EXPORT DataSourceFactory : public QObject
{
    Q_OBJECT

    public:
        /**
         * In general use this constructor, if the size is 0, the datasourcefactory will try to
         * find the filesize
         * @note when you want to load a datasourcefactory you do not have to specify the url and segSize
         */
        explicit DataSourceFactory(QObject *parent, const QUrl &dest = QUrl(), KIO::filesize_t size = 0, KIO::fileoffset_t segSize = 512000);

        ~DataSourceFactory() override;

        /**
         * The capabilities the DataSourceFactory supports
         */
        Transfer::Capabilities capabilities() const {return m_capabilities;}

        /**
         * Deletes the created (downloadInitialized() is true) file if the download was not finished
         * Does not delete anything if the download never got started
         * @see downloadInitialized()
         */
        void deinit();

        /**
         * @return true if the DataSourceFactory has enough information to start a download
         */
        bool isValid() const;

        void start();
        void stop();
        KIO::filesize_t size() const {return m_size;}
        KIO::filesize_t downloadedSize() const {return m_downloadedSize;}
        ulong currentSpeed() const {return m_speed;}
        ulong percent() const {return m_percent;}

        QUrl dest() const {return m_dest;}

        /**
         * The maximum number of mirrors that will be used for downloading, default is 3
         */
        int maxMirrorsUsed() const {return m_maxMirrorsUsed;}

        /**
         * Change the maximum number off mirrors that will be used for downloading,
         * if the download started already some mirrors might be added or removed automatically
         */
        void setMaxMirrorsUsed(int maxMirrorsUsed) {m_maxMirrorsUsed = maxMirrorsUsed;}

        /**
         * Add a mirror that can be used for downloading
         * @param url the url to the file
         * @param used defines whether the mirror should initially be used for downloading or not,
         * if true m_maxMirrorsUsed might be increased if needed
         * @param numParallelConnections the number of simultaneous connections allowed to that mirror,
         * minimum is 1
         * @note when you add an already existing mirror only the numParallelConnections are adapted
         * to the new value, so to change the number of parallel connections of a mirror you are already
         * using simply call addMirror again
         */
        void addMirror(const QUrl &url, bool used, int numParallelConnections = 1);

        /**
         * Add a mirror that can be used for downloading, if it will be used depends if maxMirrorsUsed
         * has been reached yet
         * @param url the url to the file
         * @param numParallelConnections the number of simultaneous connections allowed to that mirror,
         * minimum is 1
         * @note when you add an already existing mirror only the numParallelConnections are adapted
         * to the new value, so to change the number of parallel connections of a mirror you are already
         * using simply call addMirror again
        */
        void addMirror(const QUrl &url, int numParallelConnections = 1);

        /**
         * Does not use the specified mirror for downloading the file
         * @note if the mirror has been used for downloading it will be moved to m_unusedMirrors,
         * otherwise nohting will happen
         * @param url the mirror that should not be used anymore
         */
        void removeMirror(const QUrl &url);

        /**
         * Sets the mirrors that should be used/not used for downloading
         * @param mirrors url of the mirror, if it should be used and its number of parallel connections
         * (minimum is 1)
         * @note if you want the download to work at least one entry should be set to true
         */
        void setMirrors(const QHash<QUrl, QPair<bool, int> > &mirrors);

        /**
         * Return all mirrors, where bool defines if the mirror is used,
         * while in defines the number of parallel connections for that mirror
         */
        QHash<QUrl, QPair<bool, int> > mirrors() const;

        /**
         * Returns whether the datasourcefactory should download the file or not,
         * true by default
         * @note can be used for multiple datasourcefactory downloads
         */
        bool doDownload() const {return m_doDownload;}

        /**
         * Set if the datasourcefactory should download the file or not,
         * if set to false the download will be stopped if needed
         * @note can be used for multiple datasourcefactory downloads
         */
        void setDoDownload(bool doDownload);

        bool setNewDestination(const QUrl &newDest);

        Job::Status status() const {return m_status;}

        /**
         * @return true if the download was already initialized, i.e. a file has been
         * created and maybe even written to
         * @see deinit()
         */
        bool downloadInitialized() const {return m_downloadInitialized;}

        /**
         * Tries to repair a broken download, via completely redownloading it
         * or only the borken parts
         * @note call this if verification returned NotVerified
         */
        void repair();

        Verifier *verifier();
        Signature *signature();

    Q_SIGNALS:
        void capabilitiesChanged();
        void dataSourceFactoryChange(Transfer::ChangesFlags change);
        void log(const QString &message, Transfer::LogLevel logLevel);

    public slots:
        void save(const QDomElement &element);
        void load(const QDomElement *e);

    private slots:
        void slotUpdateCapabilities();

        void slotRemovedFile();

        /**
         * Tries to find the size of the file, automatically called
         * by start if no file size has been specified
         */
        void findFileSize();

        void slotFoundFileSize(TransferDataSource *source, KIO::filesize_t fileSize, const QPair<int,int> &segmentRange);

        void assignSegments(TransferDataSource *source);
        /**
         * Called when segments are broken
         */
        void brokenSegments(TransferDataSource *source, const QPair<int, int> &segmentRange);
        void finishedSegment(TransferDataSource *source, int segmentNumber, bool connectionFinished = true);

        /**
         * A TransferDataSource is broken
         */
        void broken(TransferDataSource *source, TransferDataSource::Error error);
        /**
         * Emitted when a Datasource itself decides to not download a specific segmentRange,
         * e.g. when there are too many connections for this TransferDataSource
         */
        void slotFreeSegments(TransferDataSource *source, QPair<int, int> segmentRange);
        void slotWriteData(KIO::fileoffset_t offset, const QByteArray &data, bool &worked);
        void slotOffset(KIO::Job *job, KIO::filesize_t offset);
        void slotDataWritten(KIO::Job *job, KIO::filesize_t offset);
        void slotPercent(KJob *job, ulong percent);
        void slotOpen(KIO::Job *job);
        void speedChanged();
        /**
         * Kills the putjob and starts the moving of files
         */
        void startMove();
        void slotPutJobDestroyed(QObject *job);
        void newDestResult(KJob *job);

        void slotRepair(const QList<KIO::fileoffset_t> &offsets, KIO::filesize_t length);

        void slotFinishedDownload(TransferDataSource *source, KIO::filesize_t size);
        
        void slotUrlChanged(const QUrl &, const QUrl &);

    private:
        /**
         * Add a mirror that can be used for downloading
         * @param used always true if usedDefined is false
         * @param usedDefined true if the user defined used, otherwise false,
         * needed to know if m_maxMirrorsUsed should be changed or not
         */
        void addMirror(const QUrl &url, bool used, int numParallelConnections, bool usedDefined);

        /**
         * Checks if an assign is needed, i.e. there are no (running) TransferDataSources,
         * yet some segments are still not finished
         */
        bool assignNeeded() const;

        bool checkLocalFile();

        void init();
        void killPutJob();
        void changeStatus(Job::Status status);

    private:
        Transfer::Capabilities m_capabilities;
        QUrl m_dest;
        QUrl m_newDest;
        KIO::filesize_t m_size;
        KIO::filesize_t m_downloadedSize;
        QList<KIO::filesize_t> m_prevDownloadedSizes;
        KIO::fileoffset_t m_segSize;
        ulong m_speed;
        ulong m_percent;

        /**
         * the cache of data that could not be written yet
         */
        QHash<KIO::fileoffset_t, QByteArray> m_cache;

        KIO::filesize_t m_tempOffset;
        QByteArray m_tempData;

        BitSet *m_startedChunks;
        BitSet *m_finishedChunks;
        KIO::FileJob* m_putJob;
        bool m_doDownload;
        bool m_open;
        /**
         * the write access is currently blocked, the data gets cached in m_cache
         */
        bool m_blocked;
        /**
         * If start() was called but did not work this is true, once the conditions changed
         * start() could be recalled
         */
        bool m_startTried;
        bool m_findFilesizeTried;

        bool m_assignTried;
        bool m_movingFile;

        bool m_finished;

        /**
         * True if download gets started the first time, if it gets never started there
         * is no reason to remove any -- maybe preexisting -- file
         */
        bool m_downloadInitialized;

        /**
         * Whether the file-size has been initially defined (it is to be trusted) or not
         */
        bool m_sizeInitiallyDefined;

        /**
         * Downloadsize has only been found out once the download was finished
         */
        bool m_sizeFoundOnFinish;

        int m_maxMirrorsUsed;
        QHash<QUrl, TransferDataSource*> m_sources;
        QList<QUrl> m_unusedUrls;
        QList<int> m_unusedConnections;
        QTimer *m_speedTimer;
        Job::Status m_status;
        Job::Status m_statusBeforeMove;

        Verifier *m_verifier;
        Signature *m_signature;
};

#endif
