#ifndef BU_MYRIAD_H
#define BU_MYRIAD_H

#include "bu/stream.h"
#include "bu/exceptionbase.h"
#include "bu/mutex.h"
#include "bu/array.h"
#include "bu/hash.h"

#include "bu/bitstring.h"

namespace Bu
{
    class MyriadStream;

    subExceptionDeclBegin( MyriadException )
        enum
        {
            emptyStream,
            invalidFormat,
            badVersion,
            invalidWordSize,
            noSuchStream,
            streamExists,
            invalidStreamId,
            protectedStream,
            invalidParameter,
            invalidBackingStream,
            badMode,
            streamOpen,
        };
    subExceptionDeclEnd();

    /**
     * Myriad Stream Multiplexer. This is a system that allows you to store
     * many streams within a single backing stream. This is great for databases,
     * caching, etc. It's fairly lightweight, and allows all streams to grow
     * dynamically using a block-allocation scheme.  This is used extensively
     * by the caching system and MyriadFs as well as other systems within
     * libbu++.
     */
    class Myriad
    {
    public:
        typedef int32_t StreamId;
        typedef Bu::Array<StreamId> StreamIdArray;
        typedef Bu::List<StreamId> StreamIdList;
        enum Mode : int32_t {
            None        = 0x00,

            // Flags
            Read        = 0x01, ///< Open file for reading
            Write       = 0x02, ///< Open file for writing
            Create      = 0x04, ///< Create file if it doesn't exist
            Truncate    = 0x08, ///< Truncate file if it does exist
            Append      = 0x10, ///< Start writing at end of file
            //NonBlock    = 0x20, ///< Open file in non-blocking mode
            Exclusive   = 0x40, ///< Create file, if it exists then fail

            // Helpful mixes
            ReadWrite   = 0x03, ///< Open for reading and writing
            WriteNew    = 0x0E  ///< Create a file (or truncate) for writing.
                                /// Same as Write|Create|Truncate
        };

    public:
        /**
         * Open existing Myriad container, or initialize a new one if the
         * backing stream is empty. If other data is already in the provided
         * backing stream an error is thrown.
         *
         * Myriad format V0
         *   0 -   3:  Myriad_MAGIC_CODE (0ad3fa84)
         *   4 -   4:  Version Id (1)
         *   5 -   5:  Bits per integer (32)
         *   6 -   9:  Block size in bytes.
         *  10 -  13:  Number of streams.
         *  14 - ...:  Stream Data
         *
         * Stream Data:
         *   0 -   3:  Stream Id
         *   4 -   7:  Size of stream in bytes
         *   8 - ...:  List of blocks in stream (4 bytes per block
         */
        Myriad( Bu::Stream &rBacking, int32_t iBlockSize=-1,
                int32_t iPreallocateBlocks=-1 );
        virtual ~Myriad();

        /**
         * Creates a new stream open in the specified eMode and, optionally,
         * preallocates the specificed amount of space. The stream is zero
         * bytes even if space is preallocated. The open stream is returned,
         * ready for use. Use this if you don't care what the id is of the
         * newly created stream.
         */
        MyriadStream create( Mode eMode, int32_t iPreallocateBytes=-1 );

        /**
         * Open an existing stream or create a new stream with the specified
         * id (iStream) with the specified eMode. This respects the normal file
         * modes, see Bu::Myriad::Mode for details.
         */
        MyriadStream open( StreamId iStream, Mode eMode );

        /**
         * Allocate a new stream but do not open it, just ensure it exists and
         * return the id of the newly allocated stream.
         */
        StreamId allocate();

        /**
         * Erase the stream specified by iStream. This only can work when the
         * stream is not open at the moment.
         */
        void erase( StreamId iStream );
        void setSize( StreamId iStream, int32_t iNewSize );
        int32_t getSize( StreamId iStream ) const;
        bool exists( StreamId iStream ) const;
        Bu::String getLocation() const;
        int32_t getBlockSize() const;
        int32_t getTotalBlocks() const;
        int32_t getUsedBlocks() const;
        int32_t getFreeBlocks() const;
        int32_t getTotalStreams() const;
        int32_t getTotalUsedBytes() const;
        int32_t getTotalUnusedBytes( int32_t iAssumeBlockSize=-1 ) const;
        Bu::BitString buildBlockUseMap() const;
        StreamIdArray buildBlockMap() const;
        
        /**
         * Lists all stream ids that you are allowed to open. Technically there
         * is always a zero stream, but it is used by Myriad for stream/block
         * accounting. It works like a normal stream but you should not open
         * it.
         */
        StreamIdList getStreamList() const;

        /**
         * Flush all caches to the backing stream, write all structural and
         * header changes.
         */
        void sync();

    private:
        bool loadMyriad();
        void createMyriad( int32_t iBlockSize, int32_t iPreallocateBlocks );
        void writeHeader();
        int32_t __calcHeaderSize();
        int32_t allocateBlock();
        int32_t __allocateBlock();
        void releaseBlock( int32_t iBlockId, bool bBlank=true );
        void __releaseBlock( int32_t iBlockId, bool bBlank=true );
        void blankBlock( int32_t iBlockId );

        void openStream( StreamId id );
        void closeStream( StreamId id );
        /**
         * Block restricted read, it will not read past the end of the block
         * that iStart places it in.
         */
        int32_t blockRead( int32_t iBlock, int32_t iStart,
                void *pTarget, int32_t iSize );

        /**
         * Block restricted write, it will not write past the end of the block
         * that iStart places it in. If this returns a non-zero number it's an
         * indication that you need to allocate a new block.
         */
        int32_t blockWrite( int32_t iBlock, int32_t iStart,
                const void *pTarget, int32_t iSize );

    public:
        /**
         * Bridge/communication/tracking class for individual Myriad streams.
         * Not for general use, this is used by Myriad and MyriadStream to
         * control access.
         */
        class Stream
        {
        friend Bu::Myriad;
        private:
            Stream( Myriad &rParent, StreamId iStream, int32_t iSize );
            virtual ~Stream();

        public:
            int32_t getSize() const;
            int32_t getBlockSize() const;
            StreamId getStreamId() const;
            int32_t getOpenCount() const;

            void setSize( int32_t iNewSize );
            int32_t read( int32_t iStart, void *pTarget, int32_t iSize );
            int32_t write( int32_t iStart, const void *pTarget, int32_t iSize );
            Bu::String getLocation() const;
            Bu::Array<int32_t> getBlockList() const;

            /**
             * Doesn't actually open, just increments the open counter.
             * If the open counter is non-zero then at least one stream has
             * a lock on this stream.
             */
            void open();

            /**
             * Doesn't actually close, just decrements the open counter.
             *@returns true if there are still handles open, false if no
             * streams have a lock.
             */
            bool close();

        private:
            mutable Bu::Mutex mAccess;
            Myriad &rParent;
            StreamId iStream;
            int32_t iSize;
            Bu::Array<int32_t> aBlocks;
            int32_t iOpenCount;
        };

    private:
        typedef Bu::Hash<StreamId, Stream *> StreamHash;
        typedef Bu::List<int32_t> IndexList;
        mutable Bu::Mutex mAccess;
        mutable Bu::Mutex mBacking;
        Bu::Stream &rBacking;
        int32_t iBlockSize;
        int32_t iBlockCount;
        bool bIsNewStream;
        bool bStructureChanged;
        mutable Bu::Mutex mhStream;
        StreamHash hStream;
        IndexList lFreeBlocks;
        StreamId iLastUsedIndex;
    };
    constexpr Myriad::Mode operator&( Myriad::Mode a, Myriad::Mode b )
    {
        return static_cast<Myriad::Mode>(
            static_cast<std::underlying_type<Myriad::Mode>::type>(a) &
            static_cast<std::underlying_type<Myriad::Mode>::type>(b)
            );
    }
    constexpr Myriad::Mode operator|( Myriad::Mode a, Myriad::Mode b )
    {
        return static_cast<Myriad::Mode>(
            static_cast<std::underlying_type<Myriad::Mode>::type>(a) |
            static_cast<std::underlying_type<Myriad::Mode>::type>(b)
            );
    }
};

#endif
