/*
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * The Original Code is MPEG4IP.
 *
 * The Initial Developer of the Original Code is Cisco Systems Inc.
 * Portions created by Cisco Systems Inc. are
 * Copyright (C) Cisco Systems Inc. 2001.  All Rights Reserved.
 *
 * Contributor(s):
 *      Dave Mackie     dmackie@cisco.com
 */

#include "src/impl.h"

namespace mp4v2 {
namespace impl {

///////////////////////////////////////////////////////////////////////////////

// MP4File low level IO support

uint64_t MP4File::GetPosition( File* file )
{
    if( m_memoryBuffer )
        return m_memoryBufferPosition;

    if( !file )
        file = m_file;

    ASSERT( file );
    return file->position;
}

void MP4File::SetPosition( uint64_t pos, File* file )
{
    if( m_memoryBuffer ) {
        if( pos >= m_memoryBufferSize )
            throw new EXCEPTION("position out of range");
        m_memoryBufferPosition = pos;
        return;
    }

    if( !file )
        file = m_file;

    ASSERT( file );
    if( file->seek( pos ))
        throw new PLATFORM_EXCEPTION("seek failed", sys::getLastError());
}

uint64_t MP4File::GetSize( File* file )
{
    if( m_memoryBuffer )
        return m_memoryBufferSize;

    if( !file )
        file = m_file;

    ASSERT( file );
    return file->size;
}

void MP4File::ReadBytes( uint8_t* buf, uint32_t bufsiz, File* file )
{
    if( bufsiz == 0 )
        return;

    ASSERT( buf );
    if ( m_numReadBits > 0 ) {
        WARNING( m_numReadBits > 0 );
    }

    if( m_memoryBuffer ) {
        if( m_memoryBufferPosition + bufsiz > m_memoryBufferSize )
            throw new EXCEPTION("not enough bytes, reached end-of-memory");
        memcpy( buf, &m_memoryBuffer[m_memoryBufferPosition], bufsiz );
        m_memoryBufferPosition += bufsiz;
        return;
    }

    if( !file )
        file = m_file;

    ASSERT( file );
    File::Size nin;
    if( file->read( buf, bufsiz, nin ))
        throw new PLATFORM_EXCEPTION("read failed", sys::getLastError());
    if( nin != bufsiz )
        throw new EXCEPTION("not enough bytes, reached end-of-file");
}

void MP4File::PeekBytes( uint8_t* buf, uint32_t bufsiz, File* file )
{
    const uint64_t pos = GetPosition( file );
    ReadBytes( buf, bufsiz, file );
    SetPosition( pos, file );
}

void MP4File::EnableMemoryBuffer( uint8_t* pBytes, uint64_t numBytes )
{
    ASSERT( !m_memoryBuffer );

    if (pBytes) {
        m_memoryBuffer = pBytes;
        m_memoryBufferSize = numBytes;
    } else {
        if (numBytes) {
            m_memoryBufferSize = numBytes;
        } else {
            m_memoryBufferSize = 4096;
        }
        m_memoryBuffer = (uint8_t*)MP4Malloc(m_memoryBufferSize);
    }
    m_memoryBufferPosition = 0;
}

void MP4File::DisableMemoryBuffer( uint8_t** ppBytes, uint64_t* pNumBytes )
{
    ASSERT(m_memoryBuffer != NULL);

    if (ppBytes) {
        *ppBytes = m_memoryBuffer;
    }
    if (pNumBytes) {
        *pNumBytes = m_memoryBufferPosition;
    }

    m_memoryBuffer = NULL;
    m_memoryBufferSize = 0;
    m_memoryBufferPosition = 0;
}

void MP4File::WriteBytes( uint8_t* buf, uint32_t bufsiz, File* file )
{
    ASSERT( m_numWriteBits == 0 || m_numWriteBits >= 8 );

    if( !buf || bufsiz == 0 )
        return;

    if( m_memoryBuffer ) {
        if( m_memoryBufferPosition + bufsiz > m_memoryBufferSize ) {
            m_memoryBufferSize = 2 * (m_memoryBufferSize + bufsiz);
            m_memoryBuffer = (uint8_t*)MP4Realloc( m_memoryBuffer, m_memoryBufferSize );
        }
        memcpy( &m_memoryBuffer[m_memoryBufferPosition], buf, bufsiz );
        m_memoryBufferPosition += bufsiz;
        return;
    }

    if( !file )
        file = m_file;

    ASSERT( file );
    File::Size nout;
    if( file->write( buf, bufsiz, nout ))
        throw new PLATFORM_EXCEPTION("write failed", sys::getLastError());
    if( nout != bufsiz )
        throw new EXCEPTION("not all bytes written");
}

uint8_t MP4File::ReadUInt8()
{
    uint8_t data;
    ReadBytes(&data, 1);
    return data;
}

void MP4File::WriteUInt8(uint8_t value)
{
    WriteBytes(&value, 1);
}

uint16_t MP4File::ReadUInt16()
{
    uint8_t data[2];
    ReadBytes(&data[0], 2);
    return ((data[0] << 8) | data[1]);
}

void MP4File::WriteUInt16(uint16_t value)
{
    uint8_t data[2];
    data[0] = (value >> 8) & 0xFF;
    data[1] = value & 0xFF;
    WriteBytes(data, 2);
}

uint32_t MP4File::ReadUInt24()
{
    uint8_t data[3];
    ReadBytes(&data[0], 3);
    return ((data[0] << 16) | (data[1] << 8) | data[2]);
}

void MP4File::WriteUInt24(uint32_t value)
{
    uint8_t data[3];
    data[0] = (value >> 16) & 0xFF;
    data[1] = (value >> 8) & 0xFF;
    data[2] = value & 0xFF;
    WriteBytes(data, 3);
}

uint32_t MP4File::ReadUInt32()
{
    uint8_t data[4];
    ReadBytes(&data[0], 4);
    return ((data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]);
}

void MP4File::WriteUInt32(uint32_t value)
{
    uint8_t data[4];
    data[0] = (value >> 24) & 0xFF;
    data[1] = (value >> 16) & 0xFF;
    data[2] = (value >> 8) & 0xFF;
    data[3] = value & 0xFF;
    WriteBytes(data, 4);
}

uint64_t MP4File::ReadUInt64()
{
    uint8_t data[8];
    uint64_t result = 0;
    uint64_t temp;

    ReadBytes(&data[0], 8);

    for (int i = 0; i < 8; i++) {
        temp = data[i];
        result |= temp << ((7 - i) * 8);
    }
    return result;
}

void MP4File::WriteUInt64(uint64_t value)
{
    uint8_t data[8];

    for (int i = 7; i >= 0; i--) {
        data[i] = value & 0xFF;
        value >>= 8;
    }
    WriteBytes(data, 8);
}

float MP4File::ReadFixed16()
{
    uint8_t iPart = ReadUInt8();
    uint8_t fPart = ReadUInt8();

    return iPart + (((float)fPart) / 0x100);
}

void MP4File::WriteFixed16(float value)
{
    if (value >= 0x100) {
        ostringstream msg;
        msg << value << " out of range";
        throw new PLATFORM_EXCEPTION(msg.str().c_str(), ERANGE);
    }

    uint8_t iPart = (uint8_t)value;
    uint8_t fPart = (uint8_t)((value - iPart) * 0x100);

    WriteUInt8(iPart);
    WriteUInt8(fPart);
}

float MP4File::ReadFixed32()
{
    uint16_t iPart = ReadUInt16();
    uint16_t fPart = ReadUInt16();

    return iPart + (((float)fPart) / 0x10000);
}

void MP4File::WriteFixed32(float value)
{
    if (value >= 0x10000) {
        ostringstream msg;
        msg << value << " out of range";
        throw new PLATFORM_EXCEPTION(msg.str().c_str(), ERANGE);
    }

    uint16_t iPart = (uint16_t)value;
    uint16_t fPart = (uint16_t)((value - iPart) * 0x10000);

    WriteUInt16(iPart);
    WriteUInt16(fPart);
}

float MP4File::ReadFloat()
{
    union {
        float f;
        uint32_t i;
    } u;

    u.i = ReadUInt32();
    return u.f;
}

void MP4File::WriteFloat(float value)
{
    union {
        float f;
        uint32_t i;
    } u;

    u.f = value;
    WriteUInt32(u.i);
}

char* MP4File::ReadString()
{
    uint32_t length = 0;
    uint32_t alloced = 64;
    char* data = (char*)MP4Malloc(alloced);

    do {
        if (length == alloced) {
            data = (char*)MP4Realloc(data, alloced * 2);
            if (data == NULL) return NULL;
            alloced *= 2;
        }
        ReadBytes((uint8_t*)&data[length], 1);
        length++;
    } while (data[length - 1] != 0);

    data = (char*)MP4Realloc(data, length);
    return data;
}

void MP4File::WriteString(char* string)
{
    if (string == NULL) {
        uint8_t zero = 0;
        WriteBytes(&zero, 1);
    } else {
        WriteBytes((uint8_t*)string, (uint32_t)strlen(string) + 1);
    }
}

char* MP4File::ReadCountedString(uint8_t charSize, bool allowExpandedCount, uint8_t fixedLength)
{
    uint32_t charLength;
    if (allowExpandedCount) {
        uint8_t b;
        uint32_t ix = 0;
        charLength = 0;
        do {
            b = ReadUInt8();
            charLength += b;
            ix++;
            if (ix > 25)
                throw new PLATFORM_EXCEPTION("Counted string too long 25 * 255", ERANGE);
        } while (b == 255);
    } else {
        charLength = ReadUInt8();
    }
    
    if (fixedLength && (charLength >= fixedLength)) {
        /*
         * The counted length of this string is greater than the
         * maximum fixed length, so truncate the string to the
         * maximum fixed length amount (take 1 byte away from the
         * fixedlength since we've already sacrificed one byte for
         * reading the counted length, and there has been a bug where
         * a non counted string has been used in the place of a
         * counted string).
         */  
        WARNING(charLength >= fixedLength);
        charLength = fixedLength - 1U;
    }

    uint32_t byteLength = charLength * charSize;
    char* data = (char*)MP4Malloc(byteLength + 1);
    if (byteLength > 0) {
        ReadBytes((uint8_t*)data, byteLength);
    }
    data[byteLength] = '\0';

    // read padding
    if (fixedLength) {
        const uint8_t padsize = fixedLength - byteLength -1U;
        if( padsize ) {
            uint8_t* padbuf = (uint8_t*)malloc( padsize );
            ReadBytes( padbuf, padsize );
            free( padbuf );
        }
    }

    return data;
}

void MP4File::WriteCountedString(char* string,
                                 uint8_t charSize, bool allowExpandedCount,
                                 uint32_t fixedLength)
{
    uint32_t byteLength;
    uint8_t zero[1];

    if (string) {
        byteLength = (uint32_t)strlen(string);
        if (fixedLength && (byteLength >= fixedLength)) {
            byteLength = fixedLength-1;
        }
    }
    else {
        byteLength = 0;
    }
    uint32_t charLength = byteLength / charSize;

    if (allowExpandedCount) {
        while (charLength >= 0xFF) {
            WriteUInt8(0xFF);
            charLength -= 0xFF;
        }
        // Write the count
        WriteUInt8(charLength);
    } else {
        if (charLength > 255) {
            ostringstream msg;
            msg << "Length is " << charLength;
            throw new PLATFORM_EXCEPTION(msg.str().c_str(), ERANGE);
        }
        // Write the count
        WriteUInt8(charLength);
    }

    if (byteLength > 0) {
        // Write the string (or the portion that we want to write)
        WriteBytes((uint8_t*)string, byteLength);
    }

    // Write any padding if this is a fixed length counted string
    if (fixedLength) {
        zero[0] = 0;
        while (byteLength < fixedLength-1U) {
            WriteBytes(zero, 1);
            byteLength++;
        }
    }
}

uint64_t MP4File::ReadBits(uint8_t numBits)
{
    ASSERT(numBits > 0);
    ASSERT(numBits <= 64);

    uint64_t bits = 0;

    for (uint8_t i = numBits; i > 0; i--) {
        if (m_numReadBits == 0) {
            ReadBytes(&m_bufReadBits, 1);
            m_numReadBits = 8;
        }
        bits = (bits << 1) | ((m_bufReadBits >> (--m_numReadBits)) & 1);
    }

    return bits;
}

void MP4File::FlushReadBits()
{
    // eat any remaining bits in the read buffer
    m_numReadBits = 0;
}

void MP4File::WriteBits(uint64_t bits, uint8_t numBits)
{
    ASSERT(numBits <= 64);

    for (uint8_t i = numBits; i > 0; i--) {
        m_bufWriteBits |=
            (((bits >> (i - 1)) & 1) << (8 - ++m_numWriteBits));

        if (m_numWriteBits == 8) {
            FlushWriteBits();
        }
    }
}

void MP4File::PadWriteBits(uint8_t pad)
{
    if (m_numWriteBits) {
        WriteBits(pad ? 0xFF : 0x00, 8 - m_numWriteBits);
    }
}

void MP4File::FlushWriteBits()
{
    if (m_numWriteBits > 0) {
        WriteBytes(&m_bufWriteBits, 1);
        m_numWriteBits = 0;
        m_bufWriteBits = 0;
    }
}

uint32_t MP4File::ReadMpegLength()
{
    uint32_t length = 0;
    uint8_t numBytes = 0;
    uint8_t b;

    do {
        b = ReadUInt8();
        length = (length << 7) | (b & 0x7F);
        numBytes++;
    } while ((b & 0x80) && numBytes < 4);

    return length;
}

void MP4File::WriteMpegLength(uint32_t value, bool compact)
{
    if (value > 0x0FFFFFFF) {
        ostringstream msg;
        msg << "out of range: " << value;
        throw new PLATFORM_EXCEPTION(msg.str().c_str(), ERANGE); 
    }

    int8_t numBytes;

    if (compact) {
        if (value <= 0x7F) {
            numBytes = 1;
        } else if (value <= 0x3FFF) {
            numBytes = 2;
        } else if (value <= 0x1FFFFF) {
            numBytes = 3;
        } else {
            numBytes = 4;
        }
    } else {
        numBytes = 4;
    }

    int8_t i = numBytes;
    do {
        i--;
        uint8_t b = (value >> (i * 7)) & 0x7F;
        if (i > 0) {
            b |= 0x80;
        }
        WriteUInt8(b);
    } while (i > 0);
}

///////////////////////////////////////////////////////////////////////////////

}
} // namespace mp4v2::impl
