/**
 *  Copyright (C) 2004-2005 Alo Sarv <madcat_@users.sourceforge.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.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <hn/rangelist.h>
#include <hn/hasher.h>
#include <boost/filesystem/path.hpp>
#include <boost/weak_ptr.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/key_extractors.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <map>
#include <list>

class SharedFile;
class HashBase;
class PartData;

/**
 * Events emitted from PartData object
 */
enum PDEvent {
	PD_DATA_ADDED = 1,  //!< Posted whenever a new PartData is constructed
	PD_DATA_FLUSHED,    //!< Posted whenever a partdata flushes buffers
	PD_DESTROY,     //!< Posted whenever a partdata is about to be destroyed
	PD_VERIFYING,   //!< When file is complete, but in verification phase
	PD_MOVING,      //!< When file is verified, and moving is in progress
	PD_COMPLETE     //!< File has been completed and placed in Incoming dir
};

class PartData {
public:
	DECLARE_EVENT_TABLE(PartData*, int);
	class UsedRange;
	class LockedRange;
private:
	class Chunk : public Range64 {
	public:
		/**
		 * Construct new chunk.
		 *
		 * @param parent      Parent object
		 * @param begin       Begin offset (inclusive)
		 * @param end         End offset (inclusive)
		 * @param hash        Optional chunkhash for this data
		 */
		Chunk(
			PartData *parent, uint64_t begin, uint64_t end,
			const HashBase *hash = 0
		);

		/**
		 * Construct Chunk from pre-existing Range object
		 *
		 * @param parent      Parent object
		 * @param range       The range
		 * @param hash        Optional chunkhash for this data
		 */
		Chunk(PartData *parent, Range64 range, const HashBase *hash =0);

		//! \name Getters
		//! @{
		bool     isVerified() const { return m_verified; }
		bool     isPartial()  const { return m_partial;  }
		uint32_t getAvail()   const { return m_avail;    }
		uint32_t getUseCnt()  const { return m_useCnt;   }
		bool     isComplete() const;
		//! @}

		//! \name Setters
		//! @{
		void setVerified(bool v)   { m_verified = v; }
		void setPartial(bool p)    { m_partial = p;  }
		void setAvail(uint32_t a)  { m_avail = a;    }
		void setUseCnt(uint32_t u) { m_useCnt = u;   }
		//! @}

		//! Need to declare this here too for Boost.MultiIndex to work
		uint64_t length() const { return Range64::length(); }
	public: // Needed public for Boost::multi_index
		PartData       *m_parent;  //!< File this chunk belongs to
		const HashBase *m_hash;    //!< Optional
		bool            m_verified;//!< If it is verified.
		bool            m_partial; //!< If it is partially downloaded
		uint32_t        m_avail;   //!< Availability count
		uint32_t        m_useCnt;  //!< Use count
	private:
		Chunk(); //!< Forbidden
		friend class LockedRange;
		//! Allowed by LockedRange class
		void write(uint64_t begin, const std::string &data);
		void onHashEvent(HashWorkPtr w, HashEvent evt);
	};
	struct ID_Pos;
	struct ID_Verified;
	struct ID_Partial;
	struct ID_Avail;
	struct ID_UseCnt;
	struct ID_Length;
	typedef boost::multi_index_container<
		Chunk,
		boost::multi_index::indexed_by<
			boost::multi_index::ordered_non_unique<
				boost::multi_index::tag<ID_Pos>,
				boost::multi_index::identity<Chunk>
			>,
			boost::multi_index::ordered_non_unique<
				boost::multi_index::tag<ID_Verified>,
				boost::multi_index::member<
					Chunk, bool, &Chunk::m_verified
				>
			>,
			boost::multi_index::ordered_non_unique<
				boost::multi_index::tag<ID_Partial>,
				boost::multi_index::member<
					Chunk, bool, &Chunk::m_partial
				>
			>,
			boost::multi_index::ordered_non_unique<
				boost::multi_index::tag<ID_Avail>,
				boost::multi_index::member<
					Chunk, uint32_t, &Chunk::m_avail
				>
			>,
			boost::multi_index::ordered_non_unique<
				boost::multi_index::tag<ID_UseCnt>,
				boost::multi_index::member<
					Chunk, uint32_t, &Chunk::m_useCnt
				>
			>,
			boost::multi_index::ordered_non_unique<
				boost::multi_index::tag<ID_Length>,
				boost::multi_index::const_mem_fun<
					Chunk, uint64_t, &Chunk::length
				>
			>
		>
	> ChunkMap;
	typedef ChunkMap::index<ID_Pos     >::type CMPosIndex;
	typedef ChunkMap::index<ID_Verified>::type CMVerifiedIndex;
	typedef ChunkMap::index<ID_Partial >::type CMPartialIndex;
	typedef ChunkMap::index<ID_Avail   >::type CMAvailIndex;
	typedef ChunkMap::index<ID_UseCnt  >::type CMUseIndex;
	typedef ChunkMap::index<ID_Length  >::type CMLenIndex;
public:
	/**
	 * \brief Construct a NEW temporary file
	 *
	 * Using this constructor, a new temporary file is constructed, at
	 * specified location with specified size.
	 *
	 * @param size      Size of the resulting file.
	 * @param loc       Location on disk where to store the temp file
	 * @param dest      Destination where to write the complete file
	 *
	 * \note The disk space indicated by @param size is not allocated on
	 *       actual disk right away. Instead, the size is allocated
	 *       dynamically as the file grows. This can be changed from global
	 *       application preferences though.
	 */
	PartData(
		uint64_t size,
		const boost::filesystem::path &loc,
		const boost::filesystem::path &dest
	);

	/**
	 * \brief Load a previously constructed temporary file.
	 *
	 * This method can be used to resume a previously started download, by
	 * reading the necessery data from @param loc.
	 *
	 * @param loc    Path to PartData reference file, which contains the
	 *               data required to resume the download.
	 */
	PartData(const boost::filesystem::path &loc);

	/**
	 * \brief Add an availability chunk mask
	 *
	 * Allows modules to register chunk availability maps, so PartData
	 * can decide the lowest-available chunk to be returned from get*
	 * methods
	 *
	 * @param chunkSize     Size of one chunk
	 * @param chunks        Boolean vector, where each true indicates
	 *                      the source having the part, and false the source
	 *                      not having the part.
	 */
	void addSourceMask(uint32_t chunkSize, const std::vector<bool> &chunks);

	/**
	 * \brief Optimized version of addSourceMask(), adds a full source.
	 *
	 * Similar to addSourceMask(), this adds a source which has the entire
	 * file.
	 *
	 * @param chunkSize    Size of one chunk
	 */
	void addFullSource(uint32_t chunkSize);

	/**
	 * \brief Locates a range that PartData considers most important.
	 *
	 * The current implementation considers partially completed ranges top
	 * priority, followed by rarest chunks, and then lowest used chunks.
	 *
	 * @param size      Optional, size of the range to be aquired.
	 * @return          Pointer to a range marked as 'used'.
	 *
	 * \note The size of the given range my be smaller than was requested.
	 * \note Current implementation ignores the size parameter.
	 * \throws PartData::RangeError if no usable range could be found.
	 */
	boost::shared_ptr<UsedRange> getRange(uint32_t size = 0);

	/**
	 * \brief Locates a range which is also contained in passed chunkmap.
	 *
	 * This method restricts getRange() call to only chunks which are
	 * indicated by a true value in the passed chunkmap (e.g. partial
	 * sources).
	 *
	 * @param size      Size of a chunk in the chunkmap
	 * @param chunks    The chunks the source has
	 * @return          Pointer to a range marked as 'used'.
	 *
	 * \throws PartData::RangeError if no usable range could be found.
	 */
	boost::shared_ptr<UsedRange> getRange(
		uint32_t size, const std::vector<bool> &chunks
	);

	/**
	 * \brief Simply writes data starting at specified offset.
	 *
	 * Checks will be performed to ensure the validity of the location
	 * and that it's not already complete or locked.
	 */
	void write(uint64_t beginOffset, const std::string &data);

	/**
	 * \name Check for completeness.
	 * Methods to check whether the entire file, or a part of it, is
	 * complete.
	 */
	//!@{
	bool isComplete() const;
	bool isComplete(const Range64 &subRange) const;
	bool isComplete(uint64_t begin, uint64_t end) const;
	//!@}
	/**
	 * \name Generic accessors
	 */
	//!@{
	uint32_t  getChunkCount(uint32_t chunkSize) const;
	MetaData* getMetaData() const { return m_md; }
	void      setMetaData(MetaData *md);
	//!@}

	/**
	 * \brief Range marked as "in use".
	 *
	 * UsedRange concept is similar to many thread libraries lock object
	 * concepts - you retrieve one via get() methods in PartData, and when
	 * it is destroyed, it takes care that all used/locked ranges do get
	 * freed properly. This object may only be used when wrapped in
	 * boost::shared_ptr.
	 *
	 * \note There may be multiple UsedRange's refering to same Chunk.
	 *       This is indicated by m_useCnt member of Chunk class.
	 */
	class UsedRange :
		public Range64,
		public boost::enable_shared_from_this<UsedRange>
	{
	public:
		/**
		 * \brief Aquire a lock within this UsedRange
		 *
		 * @param size     Size of the requested lock
		 * @return         Locked range object
		 *
		 * \note The returned locked range may be smaller than requested
		 * \throws PartData::LockError if locking fails for any reason
		 */
		boost::shared_ptr<LockedRange> getLock(uint32_t size);

		/**
		 * Check this UsedRange for completeness
		 */
		bool isComplete() const;

		//! Destructor public for boost::checked_deleter
		~UsedRange();
	private:
		friend class PartData;

		/**
		 * \brief Constructor
		 *
		 * Allowed only by PartData. UsedRange keeps a pointer back to
		 * its parent object, and also sets up event handers as
		 * neccesery to ensure the pointer remains valid.
		 *
		 * \note The template may be left undefined here since it's
		 *       only called from inside partdata.cpp
		 */
		template<typename IterType>
		UsedRange(PartData *parent, IterType it);

		//! copying is not allowed
		UsedRange(const UsedRange&);
		UsedRange& operator=(const UsedRange&);

		//! Parent PartData
		PartData *m_parent;

		//! Chunk this UsedRange refers to
		CMAvailIndex::iterator m_chunk;
	};

	/**
	 * \brief LockedRange object is an exclusivly locked Range in PartData.
	 *
	 * The lock is aquired upon call to UsedRange::getLock() method, which
	 * constructs the lock object and returns to client code. The indicated
	 * range in PartData is then exclusivly locked, with this object being
	 * the only one allowed to access the locked region of the file. Upon
	 * the destruction of this object, the lock is automatically freed.
	 */
	class LockedRange : public Range64 {
	public:
		//! Destructor public for boost::checked_deleter
		~LockedRange();

		/**
		 * \brief Write data to within this locked region.
		 *
		 * @param beginOffset     Begin offset where to write data
		 * @param data            Data to be written
		 *
		 * \throws LockError If attempting to write outside this lock
		 */
		void write(uint64_t beginOffset, const std::string &data);
	private:
		friend class PartData;

		/**
		 * \brief Construct new Lock
		 *
		 * @param parent      PartData object this lock belongs to
		 * @param r           Range to be locked
		 */
		LockedRange(PartData *parent, Range64 r);

		/**
		 * \brief Construct new lock and associate with chunk
		 *
		 * @param parent      PartData object this lock belongs to
		 * @param r           Range to be locked
		 * @param it          Iterator to chunk the lock belongs to
		 */
		template<typename IterType>
		LockedRange(PartData *parent, Range64 r, IterType it);

		//! Copying locks is forbidden
		LockedRange(const LockedRange&);
		//! Copying locks is forbidden
		LockedRange& operator=(const LockedRange&);

		//!< Parent file
		PartData *m_parent;

		//! The chunk containing this LockedRange. May be invalid.
		CMAvailIndex::iterator m_chunk;
	};

	//! Exception class
	struct LockError : public std::runtime_error {
		LockError(const std::string&);
	};
	//! Exception class
	struct RangeError : public std::runtime_error {
		RangeError(const std::string&);
	};
private:
	friend class SharedFile;
	friend int test_main(int, char[]);

	//! Copying part files is not allowed
	PartData(const PartData&);
	PartData& operator=(const PartData&);

	//! Only allowed by SharedFile
	~PartData();


	/**
	 * \short Aquire lock on a subrange of UsedRange.
	 *
	 * @param r      UsedRange to aquire lock within
	 * @param size   Upper limit on the size of the LockedRange requested.
	 * @return       Locked range
	 *
	 * \throws PartData::LockError if no lock could be aquired.
	 */
	boost::shared_ptr<LockedRange> getLock(
		boost::shared_ptr<UsedRange> r, uint32_t size
	);

	/**
	 * \name Implementation functions
	 */
	//!@{
	void checkAddChunkMap(uint32_t chunkSize);
	void addHashSet(const HashSetBase *hs);
	template<typename Predicate>
	boost::shared_ptr<UsedRange> doGetRange(uint64_t size, Predicate &pred);
	void doWrite(uint64_t begin, const std::string &data);
	void flushBuffer();
	void onHashEvent(HashWorkPtr p, HashEvent evt);
	boost::logic::tribool verifyHashSet(const HashSetBase *hs);
	void doComplete();
	//!@}

	/**
	 * \name Main rangelists.
	 *
	 * These lists are exclusive, no Range may exist simultaneously in
	 * multiple of these lists. Also, none of these lists may contain
	 * overlapping ranges.
	 */
	//! @{
	RangeList64 m_complete;  //!< Complete ranges
	RangeList64 m_locked;    //!< Locked ranges
	RangeList64 m_corrupt;   //!< Corrupt ranges
	//! @}

	/**
	 * \name Implementation data members
	 */
	//!@{
	ChunkMap m_chunks;
	uint64_t m_size;
	boost::filesystem::path m_location;
	boost::filesystem::path m_destination;
	std::map<uint64_t, std::string> m_buffer;
	typedef std::map<uint64_t, std::string>::iterator BIter;
	uint32_t m_toFlush;
	MetaData *m_md;
	uint16_t m_pendingHashes;
	//! Pointer to full rehash job (if any) in progress, used for canceling
	//! full rehash in case a chunkhash fails while this is in progress.
	HashWorkPtr m_fullJob;
	//!}
public:
	//! For testing purposes only
	void printCompleted();
};

typedef boost::shared_ptr<PartData::UsedRange  > UsedRangePtr;
typedef boost::shared_ptr<PartData::LockedRange> LockedRangePtr;