/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//
// Provide access to a read-only asset.
//

#define LOG_TAG "asset"
//#define NDEBUG 0

#include <utils/Asset.h>
#include <utils/Atomic.h>
#include <utils/FileMap.h>
#include <utils/ZipUtils.h>
#include <utils/ZipFileRO.h>
#include <utils/Log.h>
#include <utils/threads.h>

#include <string.h>
#include <memory.h>
#include <fcntl.h>
#include <errno.h>
#include <assert.h>

using namespace android;

#ifndef O_BINARY
# define O_BINARY 0
#endif

static Mutex gAssetLock;
static int32_t gCount = 0;
static Asset* gHead = NULL;
static Asset* gTail = NULL;

int32_t Asset::getGlobalCount()
{
    AutoMutex _l(gAssetLock);
    return gCount;
}

String8 Asset::getAssetAllocations()
{
    AutoMutex _l(gAssetLock);
    String8 res;
    Asset* cur = gHead;
    while (cur != NULL) {
        if (cur->isAllocated()) {
            res.append("    ");
            res.append(cur->getAssetSource());
            off_t size = (cur->getLength()+512)/1024;
            char buf[64];
            sprintf(buf, ": %dK\n", (int)size);
            res.append(buf);
        }
        cur = cur->mNext;
    }
    
    return res;
}

Asset::Asset(void)
    : mAccessMode(ACCESS_UNKNOWN)
{
    AutoMutex _l(gAssetLock);
    gCount++;
    mNext = mPrev = NULL;
    if (gTail == NULL) {
        gHead = gTail = this;
  	} else {
  	    mPrev = gTail;
  	    gTail->mNext = this;
  	    gTail = this;
  	}
    //LOGI("Creating Asset %p #%d\n", this, gCount);
}

Asset::~Asset(void)
{
    AutoMutex _l(gAssetLock);
	gCount--;
    if (gHead == this) {
        gHead = mNext;
    }
    if (gTail == this) {
        gTail = mPrev;
    }
    if (mNext != NULL) {
        mNext->mPrev = mPrev;
    }
    if (mPrev != NULL) {
        mPrev->mNext = mNext;
    }
    mNext = mPrev = NULL;
    //LOGI("Destroying Asset in %p #%d\n", this, gCount);
}

/*
 * Create a new Asset from a file on disk.  There is a fair chance that
 * the file doesn't actually exist.
 *
 * We can use "mode" to decide how we want to go about it.
 */
/*static*/ Asset* Asset::createFromFile(const char* fileName, AccessMode mode)
{
    _FileAsset* pAsset;
    status_t result;
    off_t length;
    int fd;

    fd = open(fileName, O_RDONLY | O_BINARY);
    if (fd < 0)
        return NULL;

    /*
     * Under Linux, the lseek fails if we actually opened a directory.  To
     * be correct we should test the file type explicitly, but since we
     * always open things read-only it doesn't really matter, so there's
     * no value in incurring the extra overhead of an fstat() call.
     */
    length = lseek(fd, 0, SEEK_END);
    if (length < 0) {
        ::close(fd);
        return NULL;
    }
    (void) lseek(fd, 0, SEEK_SET);

    pAsset = new _FileAsset;
    result = pAsset->openChunk(fileName, fd, 0, length);
    if (result != NO_ERROR) {
        delete pAsset;
        return NULL;
    }

    pAsset->mAccessMode = mode;
    return pAsset;
}


/*
 * Create a new Asset from a compressed file on disk.  There is a fair chance
 * that the file doesn't actually exist.
 *
 * We currently support gzip files.  We might want to handle .bz2 someday.
 */
/*static*/ Asset* Asset::createFromCompressedFile(const char* fileName,
    AccessMode mode)
{
    _CompressedAsset* pAsset;
    status_t result;
    off_t fileLen;
    bool scanResult;
    long offset;
    int method;
    long uncompressedLen, compressedLen;
    int fd;

    fd = open(fileName, O_RDONLY | O_BINARY);
    if (fd < 0)
        return NULL;

    fileLen = lseek(fd, 0, SEEK_END);
    if (fileLen < 0) {
        ::close(fd);
        return NULL;
    }
    (void) lseek(fd, 0, SEEK_SET);

    /* want buffered I/O for the file scan; must dup so fclose() is safe */
    FILE* fp = fdopen(dup(fd), "rb");
    if (fp == NULL) {
        ::close(fd);
        return NULL;
    }

    unsigned long crc32;
    scanResult = ZipUtils::examineGzip(fp, &method, &uncompressedLen,
                    &compressedLen, &crc32);
    offset = ftell(fp);
    fclose(fp);
    if (!scanResult) {
        LOGD("File '%s' is not in gzip format\n", fileName);
        ::close(fd);
        return NULL;
    }

    pAsset = new _CompressedAsset;
    result = pAsset->openChunk(fd, offset, method, uncompressedLen,
                compressedLen);
    if (result != NO_ERROR) {
        delete pAsset;
        return NULL;
    }

    pAsset->mAccessMode = mode;
    return pAsset;
}


#if 0
/*
 * Create a new Asset from part of an open file.
 */
/*static*/ Asset* Asset::createFromFileSegment(int fd, off_t offset,
    size_t length, AccessMode mode)
{
    _FileAsset* pAsset;
    status_t result;

    pAsset = new _FileAsset;
    result = pAsset->openChunk(NULL, fd, offset, length);
    if (result != NO_ERROR)
        return NULL;

    pAsset->mAccessMode = mode;
    return pAsset;
}

/*
 * Create a new Asset from compressed data in an open file.
 */
/*static*/ Asset* Asset::createFromCompressedData(int fd, off_t offset,
    int compressionMethod, size_t uncompressedLen, size_t compressedLen,
    AccessMode mode)
{
    _CompressedAsset* pAsset;
    status_t result;

    pAsset = new _CompressedAsset;
    result = pAsset->openChunk(fd, offset, compressionMethod,
                uncompressedLen, compressedLen);
    if (result != NO_ERROR)
        return NULL;

    pAsset->mAccessMode = mode;
    return pAsset;
}
#endif

/*
 * Create a new Asset from a memory mapping.
 */
/*static*/ Asset* Asset::createFromUncompressedMap(FileMap* dataMap,
    AccessMode mode)
{
    _FileAsset* pAsset;
    status_t result;

    pAsset = new _FileAsset;
    result = pAsset->openChunk(dataMap);
    if (result != NO_ERROR)
        return NULL;

    pAsset->mAccessMode = mode;
    return pAsset;
}

/*
 * Create a new Asset from compressed data in a memory mapping.
 */
/*static*/ Asset* Asset::createFromCompressedMap(FileMap* dataMap,
    int method, size_t uncompressedLen, AccessMode mode)
{
    _CompressedAsset* pAsset;
    status_t result;

    pAsset = new _CompressedAsset;
    result = pAsset->openChunk(dataMap, method, uncompressedLen);
    if (result != NO_ERROR)
        return NULL;

    pAsset->mAccessMode = mode;
    return pAsset;
}


/*
 * Do generic seek() housekeeping.  Pass in the offset/whence values from
 * the seek request, along with the current chunk offset and the chunk
 * length.
 *
 * Returns the new chunk offset, or -1 if the seek is illegal.
 */
off_t Asset::handleSeek(off_t offset, int whence, off_t curPosn, off_t maxPosn)
{
    off_t newOffset;

    switch (whence) {
    case SEEK_SET:
        newOffset = offset;
        break;
    case SEEK_CUR:
        newOffset = curPosn + offset;
        break;
    case SEEK_END:
        newOffset = maxPosn + offset;
        break;
    default:
        LOGW("unexpected whence %d\n", whence);
        // this was happening due to an off_t size mismatch
        assert(false);
        return (off_t) -1;
    }

    if (newOffset < 0 || newOffset > maxPosn) {
        LOGW("seek out of range: want %ld, end=%ld\n",
            (long) newOffset, (long) maxPosn);
        return (off_t) -1;
    }

    return newOffset;
}


/*
 * ===========================================================================
 *      _FileAsset
 * ===========================================================================
 */

/*
 * Constructor.
 */
_FileAsset::_FileAsset(void)
    : mStart(0), mLength(0), mOffset(0), mFp(NULL), mFileName(NULL), mMap(NULL), mBuf(NULL)
{
}

/*
 * Destructor.  Release resources.
 */
_FileAsset::~_FileAsset(void)
{
    close();
}

/*
 * Operate on a chunk of an uncompressed file.
 *
 * Zero-length chunks are allowed.
 */
status_t _FileAsset::openChunk(const char* fileName, int fd, off_t offset, size_t length)
{
    assert(mFp == NULL);    // no reopen
    assert(mMap == NULL);
    assert(fd >= 0);
    assert(offset >= 0);

    /*
     * Seek to end to get file length.
     */
    off_t fileLength;
    fileLength = lseek(fd, 0, SEEK_END);
    if (fileLength == (off_t) -1) {
        // probably a bad file descriptor
        LOGD("failed lseek (errno=%d)\n", errno);
        return UNKNOWN_ERROR;
    }

    if ((off_t) (offset + length) > fileLength) {
        LOGD("start (%ld) + len (%ld) > end (%ld)\n",
            (long) offset, (long) length, (long) fileLength);
        return BAD_INDEX;
    }

    /* after fdopen, the fd will be closed on fclose() */
    mFp = fdopen(fd, "rb");
    if (mFp == NULL)
        return UNKNOWN_ERROR;

    mStart = offset;
    mLength = length;
    assert(mOffset == 0);

    /* seek the FILE* to the start of chunk */
    if (fseek(mFp, mStart, SEEK_SET) != 0) {
        assert(false);
    }

    mFileName = fileName != NULL ? strdup(fileName) : NULL;
    
    return NO_ERROR;
}

/*
 * Create the chunk from the map.
 */
status_t _FileAsset::openChunk(FileMap* dataMap)
{
    assert(mFp == NULL);    // no reopen
    assert(mMap == NULL);
    assert(dataMap != NULL);

    mMap = dataMap;
    mStart = -1;            // not used
    mLength = dataMap->getDataLength();
    assert(mOffset == 0);

    return NO_ERROR;
}

/*
 * Read a chunk of data.
 */
ssize_t _FileAsset::read(void* buf, size_t count)
{
    size_t maxLen;
    size_t actual;

    assert(mOffset >= 0 && mOffset <= mLength);

    if (getAccessMode() == ACCESS_BUFFER) {
        /*
         * On first access, read or map the entire file.  The caller has
         * requested buffer access, either because they're going to be
         * using the buffer or because what they're doing has appropriate
         * performance needs and access patterns.
         */
        if (mBuf == NULL)
            getBuffer(false);
    }

    /* adjust count if we're near EOF */
    maxLen = mLength - mOffset;
    if (count > maxLen)
        count = maxLen;

    if (!count)
        return 0;

    if (mMap != NULL) {
        /* copy from mapped area */
        //printf("map read\n");
        memcpy(buf, (char*)mMap->getDataPtr() + mOffset, count);
        actual = count;
    } else if (mBuf != NULL) {
        /* copy from buffer */
        //printf("buf read\n");
        memcpy(buf, (char*)mBuf + mOffset, count);
        actual = count;
    } else {
        /* read from the file */
        //printf("file read\n");
        if (ftell(mFp) != mStart + mOffset) {
            LOGE("Hosed: %ld != %ld+%ld\n",
                ftell(mFp), (long) mStart, (long) mOffset);
            assert(false);
        }

        /*
         * This returns 0 on error or eof.  We need to use ferror() or feof()
         * to tell the difference, but we don't currently have those on the
         * device.  However, we know how much data is *supposed* to be in the
         * file, so if we don't read the full amount we know something is
         * hosed.
         */
        actual = fread(buf, 1, count, mFp);
        if (actual == 0)        // something failed -- I/O error?
            return -1;

        assert(actual == count);
    }

    mOffset += actual;
    return actual;
}

/*
 * Seek to a new position.
 */
off_t _FileAsset::seek(off_t offset, int whence)
{
    off_t newPosn;
    long actualOffset;

    // compute new position within chunk
    newPosn = handleSeek(offset, whence, mOffset, mLength);
    if (newPosn == (off_t) -1)
        return newPosn;

    actualOffset = (long) (mStart + newPosn);

    if (mFp != NULL) {
        if (fseek(mFp, (long) actualOffset, SEEK_SET) != 0)
            return (off_t) -1;
    }

    mOffset = actualOffset - mStart;
    return mOffset;
}

/*
 * Close the asset.
 */
void _FileAsset::close(void)
{
    if (mMap != NULL) {
        mMap->release();
        mMap = NULL;
    }
    if (mBuf != NULL) {
        delete[] mBuf;
        mBuf = NULL;
    }

    if (mFileName != NULL) {
        free(mFileName);
        mFileName = NULL;
    }
    
    if (mFp != NULL) {
        // can only be NULL when called from destructor
        // (otherwise we would never return this object)
        fclose(mFp);
        mFp = NULL;
    }
}

/*
 * Return a read-only pointer to a buffer.
 *
 * We can either read the whole thing in or map the relevant piece of
 * the source file.  Ideally a map would be established at a higher
 * level and we'd be using a different object, but we didn't, so we
 * deal with it here.
 */
const void* _FileAsset::getBuffer(bool wordAligned)
{
    /* subsequent requests just use what we did previously */
    if (mBuf != NULL)
        return mBuf;
    if (mMap != NULL) {
        if (!wordAligned) {
            return  mMap->getDataPtr();
        }
        return ensureAlignment(mMap);
    }

    assert(mFp != NULL);

    if (mLength < kReadVsMapThreshold) {
        unsigned char* buf;
        long allocLen;

        /* zero-length files are allowed; not sure about zero-len allocs */
        /* (works fine with gcc + x86linux) */
        allocLen = mLength;
        if (mLength == 0)
            allocLen = 1;

        buf = new unsigned char[allocLen];
        if (buf == NULL) {
            LOGE("alloc of %ld bytes failed\n", (long) allocLen);
            return NULL;
        }

        LOGV("Asset %p allocating buffer size %d (smaller than threshold)", this, (int)allocLen);
        if (mLength > 0) {
            long oldPosn = ftell(mFp);
            fseek(mFp, mStart, SEEK_SET);
            if (fread(buf, 1, mLength, mFp) != (size_t) mLength) {
                LOGE("failed reading %ld bytes\n", (long) mLength);
                delete[] buf;
                return NULL;
            }
            fseek(mFp, oldPosn, SEEK_SET);
        }

        LOGV(" getBuffer: loaded into buffer\n");

        mBuf = buf;
        return mBuf;
    } else {
        FileMap* map;

        map = new FileMap;
        if (!map->create(NULL, fileno(mFp), mStart, mLength, true)) {
            map->release();
            return NULL;
        }

        LOGV(" getBuffer: mapped\n");

        mMap = map;
        if (!wordAligned) {
            return  mMap->getDataPtr();
        }
        return ensureAlignment(mMap);
    }
}

int _FileAsset::openFileDescriptor(off_t* outStart, off_t* outLength) const
{
    if (mMap != NULL) {
        const char* fname = mMap->getFileName();
        if (fname == NULL) {
            fname = mFileName;
        }
        if (fname == NULL) {
            return -1;
        }
        *outStart = mMap->getDataOffset();
        *outLength = mMap->getDataLength();
        return open(fname, O_RDONLY | O_BINARY);
    }
    if (mFileName == NULL) {
        return -1;
    }
    *outStart = mStart;
    *outLength = mLength;
    return open(mFileName, O_RDONLY | O_BINARY);
}

const void* _FileAsset::ensureAlignment(FileMap* map)
{
    void* data = map->getDataPtr();
    if ((((size_t)data)&0x3) == 0) {
        // We can return this directly if it is aligned on a word
        // boundary.
        LOGV("Returning aligned FileAsset %p (%s).", this,
                getAssetSource());
        return data;
    }
    // If not aligned on a word boundary, then we need to copy it into
    // our own buffer.
    LOGV("Copying FileAsset %p (%s) to buffer size %d to make it aligned.", this,
            getAssetSource(), (int)mLength);
    unsigned char* buf = new unsigned char[mLength];
    if (buf == NULL) {
        LOGE("alloc of %ld bytes failed\n", (long) mLength);
        return NULL;
    }
    memcpy(buf, data, mLength);
    mBuf = buf;
    return buf;
}

/*
 * ===========================================================================
 *      _CompressedAsset
 * ===========================================================================
 */

/*
 * Constructor.
 */
_CompressedAsset::_CompressedAsset(void)
    : mStart(0), mCompressedLen(0), mUncompressedLen(0), mOffset(0),
      mMap(NULL), mFd(-1), mBuf(NULL)
{
}

/*
 * Destructor.  Release resources.
 */
_CompressedAsset::~_CompressedAsset(void)
{
    close();
}

/*
 * Open a chunk of compressed data inside a file.
 *
 * This currently just sets up some values and returns.  On the first
 * read, we expand the entire file into a buffer and return data from it.
 */
status_t _CompressedAsset::openChunk(int fd, off_t offset,
    int compressionMethod, size_t uncompressedLen, size_t compressedLen)
{
    assert(mFd < 0);        // no re-open
    assert(mMap == NULL);
    assert(fd >= 0);
    assert(offset >= 0);
    assert(compressedLen > 0);

    if (compressionMethod != ZipFileRO::kCompressDeflated) {
        assert(false);
        return UNKNOWN_ERROR;
    }

    mStart = offset;
    mCompressedLen = compressedLen;
    mUncompressedLen = uncompressedLen;
    assert(mOffset == 0);
    mFd = fd;
    assert(mBuf == NULL);

    return NO_ERROR;
}

/*
 * Open a chunk of compressed data in a mapped region.
 *
 * Nothing is expanded until the first read call.
 */
status_t _CompressedAsset::openChunk(FileMap* dataMap, int compressionMethod,
    size_t uncompressedLen)
{
    assert(mFd < 0);        // no re-open
    assert(mMap == NULL);
    assert(dataMap != NULL);

    if (compressionMethod != ZipFileRO::kCompressDeflated) {
        assert(false);
        return UNKNOWN_ERROR;
    }

    mMap = dataMap;
    mStart = -1;        // not used
    mCompressedLen = dataMap->getDataLength();
    mUncompressedLen = uncompressedLen;
    assert(mOffset == 0);

    return NO_ERROR;
}

/*
 * Read data from a chunk of compressed data.
 *
 * [For now, that's just copying data out of a buffer.]
 */
ssize_t _CompressedAsset::read(void* buf, size_t count)
{
    size_t maxLen;
    size_t actual;

    assert(mOffset >= 0 && mOffset <= mUncompressedLen);

    // TODO: if mAccessMode == ACCESS_STREAMING, use zlib more cleverly

    if (mBuf == NULL) {
        if (getBuffer(false) == NULL)
            return -1;
    }
    assert(mBuf != NULL);

    /* adjust count if we're near EOF */
    maxLen = mUncompressedLen - mOffset;
    if (count > maxLen)
        count = maxLen;

    if (!count)
        return 0;

    /* copy from buffer */
    //printf("comp buf read\n");
    memcpy(buf, (char*)mBuf + mOffset, count);
    actual = count;

    mOffset += actual;
    return actual;
}

/*
 * Handle a seek request.
 *
 * If we're working in a streaming mode, this is going to be fairly
 * expensive, because it requires plowing through a bunch of compressed
 * data.
 */
off_t _CompressedAsset::seek(off_t offset, int whence)
{
    off_t newPosn;

    // compute new position within chunk
    newPosn = handleSeek(offset, whence, mOffset, mUncompressedLen);
    if (newPosn == (off_t) -1)
        return newPosn;

    mOffset = newPosn;
    return mOffset;
}

/*
 * Close the asset.
 */
void _CompressedAsset::close(void)
{
    if (mMap != NULL) {
        mMap->release();
        mMap = NULL;
    }
    if (mBuf != NULL) {
        delete[] mBuf;
        mBuf = NULL;
    }

    if (mFd > 0) {
        ::close(mFd);
        mFd = -1;
    }
}

/*
 * Get a pointer to a read-only buffer of data.
 *
 * The first time this is called, we expand the compressed data into a
 * buffer.
 */
const void* _CompressedAsset::getBuffer(bool wordAligned)
{
    unsigned char* buf = NULL;

    if (mBuf != NULL)
        return mBuf;

    if (mUncompressedLen > UNCOMPRESS_DATA_MAX) {
        LOGD("Data exceeds UNCOMPRESS_DATA_MAX (%ld vs %d)\n",
            (long) mUncompressedLen, UNCOMPRESS_DATA_MAX);
        goto bail;
    }

    /*
     * Allocate a buffer and read the file into it.
     */
    buf = new unsigned char[mUncompressedLen];
    if (buf == NULL) {
        LOGW("alloc %ld bytes failed\n", (long) mUncompressedLen);
        goto bail;
    }

    if (mMap != NULL) {
        if (!ZipFileRO::inflateBuffer(buf, mMap->getDataPtr(),
                mUncompressedLen, mCompressedLen))
            goto bail;
    } else {
        assert(mFd >= 0);

        /*
         * Seek to the start of the compressed data.
         */
        if (lseek(mFd, mStart, SEEK_SET) != mStart)
            goto bail;

        /*
         * Expand the data into it.
         */
        if (!ZipUtils::inflateToBuffer(mFd, buf, mUncompressedLen,
                mCompressedLen))
            goto bail;
    }

    /* success! */
    mBuf = buf;
    buf = NULL;

bail:
    delete[] buf;
    return mBuf;
}