#include "constrainedfiledatastream.hpp"
#include "lowlevelfile.hpp"

#include <stdexcept>
#include <cassert>

#include <stdint.h>

namespace {

class ConstrainedDataStream : public Ogre::DataStream {
public:

    static const size_t sBufferSize = 4096; // somewhat arbitrary though 64KB buffers didn't seem to improve performance any
    static const size_t sBufferThreshold = 1024; // reads larger than this bypass buffering as cost of memcpy outweighs cost of system call

    ConstrainedDataStream(const Ogre::String &fname, size_t start, size_t length)
        : Ogre::DataStream(fname)
    {
        mFile.open (fname.c_str ());
        mSize  = length != 0xFFFFFFFF ? length : mFile.size () - start;

        mPos    = 0;
        mOrigin = start;
        mExtent = start + mSize;

        mBufferOrigin = 0;
        mBufferExtent = 0;
    }


    size_t read(void* buf, size_t count)
    {
        try
        {
            assert (mPos <= mSize);

            uint8_t * out = reinterpret_cast <uint8_t *> (buf);

            size_t posBeg = mOrigin + mPos;
            size_t posEnd = posBeg + count;

            if (posEnd > mExtent)
                posEnd = mExtent;

            size_t posCur = posBeg;

            while (posCur != posEnd)
            {
                size_t readLeft = posEnd - posCur;

                if (posCur < mBufferOrigin || posCur >= mBufferExtent)
                {
                    if (readLeft >= sBufferThreshold || (posCur == mOrigin && posEnd == mExtent))
                    {
                        assert (mFile.tell () == mBufferExtent);

                        if (posCur != mBufferExtent)
                            mFile.seek (posCur);

                        posCur += mFile.read (out, readLeft);

                        mBufferOrigin = mBufferExtent = posCur;

                        mPos = posCur - mOrigin;

                        return posCur - posBeg;
                    }
                    else
                    {
                        size_t newBufferOrigin;

                        if ((posCur < mBufferOrigin) && (mBufferOrigin - posCur < sBufferSize))
                            newBufferOrigin = std::max (mOrigin, mBufferOrigin > sBufferSize ? mBufferOrigin - sBufferSize : 0);
                        else
                            newBufferOrigin = posCur;

                        fill (newBufferOrigin);
                    }
                }

                size_t xfer = std::min (readLeft, mBufferExtent - posCur);

                memcpy (out, mBuffer + (posCur - mBufferOrigin), xfer);

                posCur += xfer;
                out += xfer;
            }

            count = posEnd - posBeg;
            mPos += count;
            return count;
        }
        catch (std::exception& e)
        {
            std::stringstream error;
            error << "Failed to read '" << mName << "': " << e.what();
            throw std::runtime_error(error.str());
        }
    }

    void skip(long count)
    {
        assert (mPos <= mSize);

        if((count >= 0 && (size_t)count <= mSize-mPos) ||
           (count < 0 && (size_t)-count <= mPos))
            mPos += count;
    }

    void seek(size_t pos)
    {
        assert (mPos <= mSize);

        if (pos < mSize)
            mPos = pos;
    }

    virtual size_t tell() const
    {
        assert (mPos <= mSize);

        return mPos;
    }

    virtual bool eof() const
    {
        assert (mPos <= mSize);

        return mPos == mSize;
    }

    virtual void close()
    {
        mFile.close();
    }

private:

    void fill (size_t newOrigin)
    {
        assert (mFile.tell () == mBufferExtent);

        size_t newExtent = newOrigin + sBufferSize;

        if (newExtent > mExtent)
            newExtent = mExtent;

        size_t oldExtent = mBufferExtent;

        if (newOrigin != oldExtent)
            mFile.seek (newOrigin);

        mBufferOrigin = mBufferExtent = newOrigin;

        size_t amountRequested = newExtent - newOrigin;

        size_t amountRead = mFile.read (mBuffer, amountRequested);

        if (amountRead != amountRequested)
            throw std::runtime_error ("An unexpected condition occurred while reading from a file.");

        mBufferExtent = newExtent;
    }

    LowLevelFile mFile;

    size_t mOrigin;
    size_t mExtent;
    size_t mPos;

    uint8_t mBuffer [sBufferSize];
    size_t mBufferOrigin;
    size_t mBufferExtent;
};

} // end of unnamed namespace

Ogre::DataStreamPtr openConstrainedFileDataStream (char const * filename, size_t offset, size_t length)
{
    return Ogre::DataStreamPtr(new ConstrainedDataStream(filename, offset, length));
}