/*
 * Copyright (C) 2014 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.
 */

// #define LOG_NDEBUG 0
#define LOG_TAG "WebmElement"

#include "EbmlUtil.h"
#include "WebmElement.h"
#include "WebmConstants.h"

#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/foundation/ColorUtils.h>
#include <media/stagefright/MetaData.h>
#include <utils/Log.h>

#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>

using namespace android;
using namespace webm;

namespace {

int64_t voidSize(int64_t totalSize) {
    if (totalSize < 2) {
        return -1;
    }
    if (totalSize < 9) {
        return totalSize - 2;
    }
    return totalSize - 9;
}

uint64_t childrenSum(const List<sp<WebmElement> >& children) {
    uint64_t total = 0;
    for (List<sp<WebmElement> >::const_iterator it = children.begin();
            it != children.end(); ++it) {
        total += (*it)->totalSize();
    }
    return total;
}

void populateCommonTrackEntries(
        int num,
        uint64_t uid,
        bool lacing,
        const char *lang,
        const char *codec,
        TrackTypes type,
        List<sp<WebmElement> > &ls) {
    ls.push_back(new WebmUnsigned(kMkvTrackNumber, num));
    ls.push_back(new WebmUnsigned(kMkvTrackUid, uid));
    ls.push_back(new WebmUnsigned(kMkvFlagLacing, lacing));
    ls.push_back(new WebmString(kMkvLanguage, lang));
    ls.push_back(new WebmString(kMkvCodecId, codec));
    ls.push_back(new WebmUnsigned(kMkvTrackType, type));
}
}

namespace android {

WebmElement::WebmElement(uint64_t id, uint64_t size)
    : mId(id), mSize(size) {
}

WebmElement::~WebmElement() {
}

int WebmElement::serializePayloadSize(uint8_t *buf) {
    return serializeCodedUnsigned(encodeUnsigned(mSize), buf);
}

uint64_t WebmElement::serializeInto(uint8_t *buf) {
    uint8_t *cur = buf;
    int head = serializeCodedUnsigned(mId, cur);
    cur += head;
    int neck = serializePayloadSize(cur);
    cur += neck;
    serializePayload(cur);
    cur += mSize;
    return cur - buf;
}

uint64_t WebmElement::totalSize() {
    uint8_t buf[8];
    //............... + sizeOf(encodeUnsigned(size))
    return sizeOf(mId) + serializePayloadSize(buf) + mSize;
}

uint8_t *WebmElement::serialize(uint64_t& size) {
    size = totalSize();
    uint8_t *buf = new uint8_t[size];
    serializeInto(buf);
    return buf;
}

int WebmElement::write(int fd, uint64_t& size) {
    uint8_t buf[8];
    size = totalSize();
    off64_t off = ::lseek64(fd, (size - 1), SEEK_CUR) - (size - 1);
    ::write(fd, buf, 1); // extend file

    off64_t curOff = off + size;
    off64_t alignedOff = off & ~(::sysconf(_SC_PAGE_SIZE) - 1);
    off64_t mapSize = curOff - alignedOff;
    off64_t pageOff = off - alignedOff;
    void *dst = ::mmap64(NULL, mapSize, PROT_WRITE, MAP_SHARED, fd, alignedOff);
    if (dst == MAP_FAILED) {
        ALOGE("mmap64 failed; errno = %d", errno);
        ALOGE("fd %d; flags: %o", fd, ::fcntl(fd, F_GETFL, 0));
        return errno;
    } else {
        serializeInto((uint8_t*) dst + pageOff);
        ::msync(dst, mapSize, MS_SYNC);
        return ::munmap(dst, mapSize);
    }
}

//=================================================================================================

WebmUnsigned::WebmUnsigned(uint64_t id, uint64_t value)
    : WebmElement(id, sizeOf(value)), mValue(value) {
}

void WebmUnsigned::serializePayload(uint8_t *buf) {
    serializeCodedUnsigned(mValue, buf);
}

//=================================================================================================

WebmFloat::WebmFloat(uint64_t id, double value)
    : WebmElement(id, sizeof(double)), mValue(value) {
}

WebmFloat::WebmFloat(uint64_t id, float value)
    : WebmElement(id, sizeof(float)), mValue(value) {
}

void WebmFloat::serializePayload(uint8_t *buf) {
    uint64_t data;
    if (mSize == sizeof(float)) {
        float f = mValue;
        data = *reinterpret_cast<const uint32_t*>(&f);
    } else {
        data = *reinterpret_cast<const uint64_t*>(&mValue);
    }
    for (int i = mSize - 1; i >= 0; --i) {
        buf[i] = data & 0xff;
        data >>= 8;
    }
}

//=================================================================================================

WebmBinary::WebmBinary(uint64_t id, const sp<ABuffer> &ref)
    : WebmElement(id, ref->size()), mRef(ref) {
}

void WebmBinary::serializePayload(uint8_t *buf) {
    memcpy(buf, mRef->data(), mRef->size());
}

//=================================================================================================

WebmString::WebmString(uint64_t id, const char *str)
    : WebmElement(id, strlen(str)), mStr(str) {
}

void WebmString::serializePayload(uint8_t *buf) {
    memcpy(buf, mStr, strlen(mStr));
}

//=================================================================================================

WebmSimpleBlock::WebmSimpleBlock(
        int trackNum,
        int16_t relTimecode,
        bool key,
        const sp<ABuffer>& orig)
    // ............................ trackNum*1 + timecode*2 + flags*1
    //                                ^^^
    // Only the least significant byte of trackNum is encoded
    : WebmElement(kMkvSimpleBlock, orig->size() + 4),
      mTrackNum(trackNum),
      mRelTimecode(relTimecode),
      mKey(key),
      mRef(orig) {
}

void WebmSimpleBlock::serializePayload(uint8_t *buf) {
    serializeCodedUnsigned(encodeUnsigned(mTrackNum), buf);
    buf[1] = (mRelTimecode & 0xff00) >> 8;
    buf[2] = mRelTimecode & 0xff;
    buf[3] = mKey ? 0x80 : 0;
    memcpy(buf + 4, mRef->data(), mSize - 4);
}

//=================================================================================================

EbmlVoid::EbmlVoid(uint64_t totalSize)
    : WebmElement(kMkvVoid, voidSize(totalSize)),
      mSizeWidth(totalSize - sizeOf(kMkvVoid) - voidSize(totalSize)) {
    CHECK_GE(voidSize(totalSize), 0);
}

int EbmlVoid::serializePayloadSize(uint8_t *buf) {
    return serializeCodedUnsigned(encodeUnsigned(mSize, mSizeWidth), buf);
}

void EbmlVoid::serializePayload(uint8_t *buf) {
    ::memset(buf, 0, mSize);
    return;
}

//=================================================================================================

WebmMaster::WebmMaster(uint64_t id, const List<sp<WebmElement> >& children)
    : WebmElement(id, childrenSum(children)), mChildren(children) {
}

WebmMaster::WebmMaster(uint64_t id)
    : WebmElement(id, 0) {
}

int WebmMaster::serializePayloadSize(uint8_t *buf) {
    if (mSize == 0){
        return serializeCodedUnsigned(kMkvUnknownLength, buf);
    }
    return WebmElement::serializePayloadSize(buf);
}

void WebmMaster::serializePayload(uint8_t *buf) {
    uint64_t off = 0;
    for (List<sp<WebmElement> >::const_iterator it = mChildren.begin(); it != mChildren.end();
            ++it) {
        sp<WebmElement> child = (*it);
        child->serializeInto(buf + off);
        off += child->totalSize();
    }
}

//=================================================================================================

sp<WebmElement> WebmElement::CuePointEntry(uint64_t time, int track, uint64_t off) {
    List<sp<WebmElement> > cuePointEntryFields;
    cuePointEntryFields.push_back(new WebmUnsigned(kMkvCueTrack, track));
    cuePointEntryFields.push_back(new WebmUnsigned(kMkvCueClusterPosition, off));
    WebmElement *cueTrackPositions = new WebmMaster(kMkvCueTrackPositions, cuePointEntryFields);

    cuePointEntryFields.clear();
    cuePointEntryFields.push_back(new WebmUnsigned(kMkvCueTime, time));
    cuePointEntryFields.push_back(cueTrackPositions);
    return new WebmMaster(kMkvCuePoint, cuePointEntryFields);
}

sp<WebmElement> WebmElement::SeekEntry(uint64_t id, uint64_t off) {
    List<sp<WebmElement> > seekEntryFields;
    seekEntryFields.push_back(new WebmUnsigned(kMkvSeekId, id));
    seekEntryFields.push_back(new WebmUnsigned(kMkvSeekPosition, off));
    return new WebmMaster(kMkvSeek, seekEntryFields);
}

sp<WebmElement> WebmElement::EbmlHeader(
        int ver,
        int readVer,
        int maxIdLen,
        int maxSizeLen,
        int docVer,
        int docReadVer) {
    List<sp<WebmElement> > headerFields;
    headerFields.push_back(new WebmUnsigned(kMkvEbmlVersion, ver));
    headerFields.push_back(new WebmUnsigned(kMkvEbmlReadVersion, readVer));
    headerFields.push_back(new WebmUnsigned(kMkvEbmlMaxIdlength, maxIdLen));
    headerFields.push_back(new WebmUnsigned(kMkvEbmlMaxSizeLength, maxSizeLen));
    headerFields.push_back(new WebmString(kMkvDocType, "webm"));
    headerFields.push_back(new WebmUnsigned(kMkvDocTypeVersion, docVer));
    headerFields.push_back(new WebmUnsigned(kMkvDocTypeReadVersion, docReadVer));
    return new WebmMaster(kMkvEbml, headerFields);
}

sp<WebmElement> WebmElement::SegmentInfo(uint64_t scale, double dur) {
    List<sp<WebmElement> > segmentInfo;
    // place duration first; easier to patch
    segmentInfo.push_back(new WebmFloat(kMkvSegmentDuration, dur));
    segmentInfo.push_back(new WebmUnsigned(kMkvTimecodeScale, scale));
    segmentInfo.push_back(new WebmString(kMkvMuxingApp, "android"));
    segmentInfo.push_back(new WebmString(kMkvWritingApp, "android"));
    return new WebmMaster(kMkvInfo, segmentInfo);
}

sp<WebmElement> WebmElement::AudioTrackEntry(
        int chans,
        double rate,
        const sp<ABuffer> &buf,
        int bps,
        uint64_t uid,
        bool lacing,
        const char *lang) {
    if (uid == 0) {
        uid = kAudioTrackNum;
    }

    List<sp<WebmElement> > trackEntryFields;
    populateCommonTrackEntries(
            kAudioTrackNum,
            uid,
            lacing,
            lang,
            "A_VORBIS",
            kAudioType,
            trackEntryFields);

    List<sp<WebmElement> > audioInfo;
    audioInfo.push_back(new WebmUnsigned(kMkvChannels, chans));
    audioInfo.push_back(new WebmFloat(kMkvSamplingFrequency, rate));
    if (bps) {
        WebmElement *bitDepth = new WebmUnsigned(kMkvBitDepth, bps);
        audioInfo.push_back(bitDepth);
    }

    trackEntryFields.push_back(new WebmMaster(kMkvAudio, audioInfo));
    trackEntryFields.push_back(new WebmBinary(kMkvCodecPrivate, buf));
    return new WebmMaster(kMkvTrackEntry, trackEntryFields);
}

sp<WebmElement> WebmElement::VideoTrackEntry(
        const char *codec,
        uint64_t width,
        uint64_t height,
        const sp<MetaData> &meta,
        uint64_t uid,
        bool lacing,
        const char *lang) {
    if (uid == 0) {
        uid = kVideoTrackNum;
    }

    List<sp<WebmElement> > trackEntryFields;
    populateCommonTrackEntries(
            kVideoTrackNum,
            uid,
            lacing,
            lang,
            codec,
            kVideoType,
            trackEntryFields);

    // CSD
    uint32_t type;
    const void *data;
    size_t size;
    if (meta->findData(kKeyVp9CodecPrivate, &type, &data, &size)) {
        sp<ABuffer> buf = new ABuffer((void *)data, size); // note: buf does not own data
        trackEntryFields.push_back(new WebmBinary(kMkvCodecPrivate, buf));
    }

    List<sp<WebmElement> > videoInfo;
    videoInfo.push_back(new WebmUnsigned(kMkvPixelWidth, width));
    videoInfo.push_back(new WebmUnsigned(kMkvPixelHeight, height));

    // Color aspects
    {
        List<sp<WebmElement> > colorInfo;

        ColorAspects aspects;
        aspects.mPrimaries = ColorAspects::PrimariesUnspecified;
        aspects.mTransfer = ColorAspects::TransferUnspecified;
        aspects.mMatrixCoeffs = ColorAspects::MatrixUnspecified;
        aspects.mRange = ColorAspects::RangeUnspecified;
        bool havePrimaries = meta->findInt32(kKeyColorPrimaries, (int32_t*)&aspects.mPrimaries);
        bool haveTransfer = meta->findInt32(kKeyTransferFunction, (int32_t*)&aspects.mTransfer);
        bool haveCoeffs = meta->findInt32(kKeyColorMatrix, (int32_t*)&aspects.mMatrixCoeffs);
        bool haveRange = meta->findInt32(kKeyColorRange, (int32_t*)&aspects.mRange);

        int32_t primaries, transfer, coeffs;
        bool fullRange;
        ColorUtils::convertCodecColorAspectsToIsoAspects(
                aspects, &primaries, &transfer, &coeffs, &fullRange);
        if (havePrimaries) {
            colorInfo.push_back(new WebmUnsigned(kMkvPrimaries, primaries));
        }
        if (haveTransfer) {
            colorInfo.push_back(new WebmUnsigned(kMkvTransferCharacteristics, transfer));
        }
        if (haveCoeffs) {
            colorInfo.push_back(new WebmUnsigned(kMkvMatrixCoefficients, coeffs));
        }
        if (haveRange) {
            colorInfo.push_back(new WebmUnsigned(kMkvRange, fullRange ? 2 : 1));
        }

        // Also add HDR static info, some of which goes to MasteringMetadata element

        const HDRStaticInfo *info;
        uint32_t type;
        const void *data;
        size_t size;
        if (meta->findData(kKeyHdrStaticInfo, &type, &data, &size)
                && type == 'hdrS' && size == sizeof(*info)) {
            info = (const HDRStaticInfo*)data;
            if (info->mID == HDRStaticInfo::kType1) {
                List<sp<WebmElement> > masteringInfo;

                // convert HDRStaticInfo values to matroska equivalent values for each non-0 group
                if (info->sType1.mMaxFrameAverageLightLevel) {
                    colorInfo.push_back(new WebmUnsigned(
                            kMkvMaxFALL, info->sType1.mMaxFrameAverageLightLevel));
                }
                if (info->sType1.mMaxContentLightLevel) {
                    colorInfo.push_back(new WebmUnsigned(
                            kMkvMaxCLL, info->sType1.mMaxContentLightLevel));
                }
                if (info->sType1.mMinDisplayLuminance) {
                    // HDRStaticInfo Type1 stores min luminance scaled 10000:1
                    masteringInfo.push_back(new WebmFloat(
                            kMkvLuminanceMin, info->sType1.mMinDisplayLuminance * 0.0001));
                }
                if (info->sType1.mMaxDisplayLuminance) {
                    masteringInfo.push_back(new WebmFloat(
                            kMkvLuminanceMax, (float)info->sType1.mMaxDisplayLuminance));
                }
                // HDRStaticInfo Type1 stores primaries scaled 50000:1
                if (info->sType1.mW.x || info->sType1.mW.y) {
                    masteringInfo.push_back(new WebmFloat(
                            kMkvWhitePointChromaticityX, info->sType1.mW.x * 0.00002));
                    masteringInfo.push_back(new WebmFloat(
                            kMkvWhitePointChromaticityY, info->sType1.mW.y * 0.00002));
                }
                if (info->sType1.mR.x || info->sType1.mR.y || info->sType1.mG.x
                        || info->sType1.mG.y || info->sType1.mB.x || info->sType1.mB.y) {
                    masteringInfo.push_back(new WebmFloat(
                            kMkvPrimaryRChromaticityX, info->sType1.mR.x * 0.00002));
                    masteringInfo.push_back(new WebmFloat(
                            kMkvPrimaryRChromaticityY, info->sType1.mR.y * 0.00002));
                    masteringInfo.push_back(new WebmFloat(
                            kMkvPrimaryGChromaticityX, info->sType1.mG.x * 0.00002));
                    masteringInfo.push_back(new WebmFloat(
                            kMkvPrimaryGChromaticityY, info->sType1.mG.y * 0.00002));
                    masteringInfo.push_back(new WebmFloat(
                            kMkvPrimaryBChromaticityX, info->sType1.mB.x * 0.00002));
                    masteringInfo.push_back(new WebmFloat(
                            kMkvPrimaryBChromaticityY, info->sType1.mB.y * 0.00002));
                }
                if (masteringInfo.size()) {
                    colorInfo.push_back(new WebmMaster(kMkvMasteringMetadata, masteringInfo));
                }
            }
        }
        if (colorInfo.size()) {
            videoInfo.push_back(new WebmMaster(kMkvColour, colorInfo));
        }
    }

    trackEntryFields.push_back(new WebmMaster(kMkvVideo, videoInfo));
    return new WebmMaster(kMkvTrackEntry, trackEntryFields);
}
} /* namespace android */
