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

#include <inttypes.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <thread>

//#define LOG_NDEBUG 0
#define LOG_TAG "codec2"
#include <log/log.h>

#include <binder/IServiceManager.h>
#include <binder/ProcessState.h>
#include <media/DataSource.h>
#include <media/ICrypto.h>
#include <media/IMediaHTTPService.h>
#include <media/MediaExtractor.h>
#include <media/MediaSource.h>
#include <media/stagefright/foundation/ABuffer.h>
#include <media/stagefright/foundation/ALooper.h>
#include <media/stagefright/foundation/AMessage.h>
#include <media/stagefright/foundation/AUtils.h>
#include <media/stagefright/DataSourceFactory.h>
#include <media/stagefright/MediaDefs.h>
#include <media/stagefright/MediaErrors.h>
#include <media/stagefright/MediaExtractorFactory.h>
#include <media/stagefright/MetaData.h>
#include <media/stagefright/Utils.h>

#include <gui/GLConsumer.h>
#include <gui/IProducerListener.h>
#include <gui/Surface.h>
#include <gui/SurfaceComposerClient.h>

#include <C2AllocatorGralloc.h>
#include <C2Buffer.h>
#include <C2BufferPriv.h>
#include <C2Component.h>
#include <C2Config.h>
#include <C2Debug.h>
#include <C2PlatformSupport.h>
#include <C2Work.h>

using namespace android;
using namespace std::chrono_literals;

namespace {

class LinearBuffer : public C2Buffer {
public:
    explicit LinearBuffer(const std::shared_ptr<C2LinearBlock> &block)
        : C2Buffer({ block->share(block->offset(), block->size(), ::C2Fence()) }) {}
};

class Listener;

class SimplePlayer {
public:
    SimplePlayer();
    ~SimplePlayer();

    void onWorkDone(std::weak_ptr<C2Component> component,
                    std::list<std::unique_ptr<C2Work>> workItems);
    void onTripped(std::weak_ptr<C2Component> component,
                   std::vector<std::shared_ptr<C2SettingResult>> settingResult);
    void onError(std::weak_ptr<C2Component> component, uint32_t errorCode);

    void play(const sp<IMediaSource> &source);

private:
    typedef std::unique_lock<std::mutex> ULock;

    std::shared_ptr<Listener> mListener;
    std::shared_ptr<C2Component> mComponent;

    sp<IProducerListener> mProducerListener;

    std::atomic_int mLinearPoolId;

    std::shared_ptr<C2Allocator> mAllocIon;
    std::shared_ptr<C2BlockPool> mLinearPool;

    std::mutex mQueueLock;
    std::condition_variable mQueueCondition;
    std::list<std::unique_ptr<C2Work>> mWorkQueue;

    std::mutex mProcessedLock;
    std::condition_variable mProcessedCondition;
    std::list<std::unique_ptr<C2Work>> mProcessedWork;

    sp<Surface> mSurface;
    sp<SurfaceComposerClient> mComposerClient;
    sp<SurfaceControl> mControl;
};

class Listener : public C2Component::Listener {
public:
    explicit Listener(SimplePlayer *thiz) : mThis(thiz) {}
    virtual ~Listener() = default;

    virtual void onWorkDone_nb(std::weak_ptr<C2Component> component,
                            std::list<std::unique_ptr<C2Work>> workItems) override {
        mThis->onWorkDone(component, std::move(workItems));
    }

    virtual void onTripped_nb(std::weak_ptr<C2Component> component,
                           std::vector<std::shared_ptr<C2SettingResult>> settingResult) override {
        mThis->onTripped(component, settingResult);
    }

    virtual void onError_nb(std::weak_ptr<C2Component> component,
                         uint32_t errorCode) override {
        mThis->onError(component, errorCode);
    }

private:
    SimplePlayer * const mThis;
};


SimplePlayer::SimplePlayer()
    : mListener(new Listener(this)),
      mProducerListener(new DummyProducerListener),
      mLinearPoolId(C2BlockPool::PLATFORM_START),
      mComposerClient(new SurfaceComposerClient) {
    CHECK_EQ(mComposerClient->initCheck(), (status_t)OK);

    std::shared_ptr<C2AllocatorStore> store = GetCodec2PlatformAllocatorStore();
    CHECK_EQ(store->fetchAllocator(C2AllocatorStore::DEFAULT_LINEAR, &mAllocIon), C2_OK);
    mLinearPool = std::make_shared<C2PooledBlockPool>(mAllocIon, mLinearPoolId++);

    mControl = mComposerClient->createSurface(
            String8("A Surface"),
            1280,
            800,
            HAL_PIXEL_FORMAT_YV12);
            //PIXEL_FORMAT_RGB_565);

    CHECK(mControl != NULL);
    CHECK(mControl->isValid());

    SurfaceComposerClient::Transaction{}
            .setLayer(mControl, INT_MAX)
            .show(mControl)
            .apply();

    mSurface = mControl->getSurface();
    CHECK(mSurface != NULL);
    mSurface->connect(NATIVE_WINDOW_API_CPU, mProducerListener);
}

SimplePlayer::~SimplePlayer() {
    mComposerClient->dispose();
}

void SimplePlayer::onWorkDone(
        std::weak_ptr<C2Component> component, std::list<std::unique_ptr<C2Work>> workItems) {
    ALOGV("SimplePlayer::onWorkDone");
    (void) component;
    ULock l(mProcessedLock);
    for (auto & item : workItems) {
        mProcessedWork.push_back(std::move(item));
    }
    mProcessedCondition.notify_all();
}

void SimplePlayer::onTripped(
        std::weak_ptr<C2Component> component,
        std::vector<std::shared_ptr<C2SettingResult>> settingResult) {
    (void) component;
    (void) settingResult;
    // TODO
}

void SimplePlayer::onError(std::weak_ptr<C2Component> component, uint32_t errorCode) {
    (void) component;
    (void) errorCode;
    // TODO
}

void SimplePlayer::play(const sp<IMediaSource> &source) {
    ALOGV("SimplePlayer::play");
    sp<AMessage> format;
    (void) convertMetaDataToMessage(source->getFormat(), &format);

    sp<ABuffer> csd0, csd1;
    format->findBuffer("csd-0", &csd0);
    format->findBuffer("csd-1", &csd1);

    status_t err = source->start();

    if (err != OK) {
        fprintf(stderr, "source returned error %d (0x%08x)\n", err, err);
        return;
    }

    std::shared_ptr<C2ComponentStore> store = GetCodec2PlatformComponentStore();
    std::shared_ptr<C2Component> component;
    (void)store->createComponent("c2.android.avc.decoder", &component);

    (void)component->setListener_vb(mListener, C2_DONT_BLOCK);
    std::unique_ptr<C2PortBlockPoolsTuning::output> pools =
        C2PortBlockPoolsTuning::output::AllocUnique({ (uint64_t)C2BlockPool::BASIC_GRAPHIC });
    std::vector<std::unique_ptr<C2SettingResult>> result;
    (void)component->intf()->config_vb({pools.get()}, C2_DONT_BLOCK, &result);
    component->start();

    for (int i = 0; i < 8; ++i) {
        mWorkQueue.emplace_back(new C2Work);
    }

    std::atomic_bool running(true);
    std::thread surfaceThread([this, &running]() {
        const sp<IGraphicBufferProducer> &igbp = mSurface->getIGraphicBufferProducer();
        while (running) {
            std::unique_ptr<C2Work> work;
            {
                ULock l(mProcessedLock);
                if (mProcessedWork.empty()) {
                    mProcessedCondition.wait_for(l, 100ms);
                    if (mProcessedWork.empty()) {
                        continue;
                    }
                }
                work.swap(mProcessedWork.front());
                mProcessedWork.pop_front();
            }
            int slot;
            sp<Fence> fence;
            ALOGV("Render: Frame #%lld", work->worklets.front()->output.ordinal.frameIndex.peekll());
            const std::shared_ptr<C2Buffer> &output = work->worklets.front()->output.buffers[0];
            if (output) {
                const C2ConstGraphicBlock block = output->data().graphicBlocks().front();
                native_handle_t *grallocHandle = UnwrapNativeCodec2GrallocHandle(block.handle());
                sp<GraphicBuffer> buffer(new GraphicBuffer(
                        grallocHandle,
                        GraphicBuffer::CLONE_HANDLE,
                        block.width(),
                        block.height(),
                        HAL_PIXEL_FORMAT_YV12,
                        1,
                        (uint64_t)GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN,
                        block.width()));
                native_handle_delete(grallocHandle);

                status_t err = igbp->attachBuffer(&slot, buffer);

                IGraphicBufferProducer::QueueBufferInput qbi(
                        (work->worklets.front()->output.ordinal.timestamp * 1000ll).peekll(),
                        false,
                        HAL_DATASPACE_UNKNOWN,
                        Rect(block.width(), block.height()),
                        NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW,
                        0,
                        Fence::NO_FENCE,
                        0);
                IGraphicBufferProducer::QueueBufferOutput qbo;
                err = igbp->queueBuffer(slot, qbi, &qbo);
            }

            work->input.buffers.clear();
            work->worklets.clear();

            ULock l(mQueueLock);
            mWorkQueue.push_back(std::move(work));
            mQueueCondition.notify_all();
        }
        ALOGV("render loop finished");
    });

    long numFrames = 0;
    mLinearPool.reset(new C2PooledBlockPool(mAllocIon, mLinearPoolId++));

    for (;;) {
        size_t size = 0u;
        void *data = nullptr;
        int64_t timestamp = 0u;
        MediaBufferBase *buffer = nullptr;
        sp<ABuffer> csd;
        if (csd0 != nullptr) {
            csd = csd0;
            csd0 = nullptr;
        } else if (csd1 != nullptr) {
            csd = csd1;
            csd1 = nullptr;
        } else {
            status_t err = source->read(&buffer);
            if (err != OK) {
                CHECK(buffer == NULL);

                if (err == INFO_FORMAT_CHANGED) {
                    continue;
                }

                break;
            }
            MetaDataBase &meta = buffer->meta_data();
            CHECK(meta.findInt64(kKeyTime, &timestamp));

            size = buffer->size();
            data = buffer->data();
        }

        if (csd != nullptr) {
            size = csd->size();
            data = csd->data();
        }

        // Prepare C2Work

        std::unique_ptr<C2Work> work;
        while (!work) {
            ULock l(mQueueLock);
            if (!mWorkQueue.empty()) {
                work.swap(mWorkQueue.front());
                mWorkQueue.pop_front();
            } else {
                mQueueCondition.wait_for(l, 100ms);
            }
        }
        work->input.flags = (C2FrameData::flags_t)0;
        work->input.ordinal.timestamp = timestamp;
        work->input.ordinal.frameIndex = numFrames;

        std::shared_ptr<C2LinearBlock> block;
        mLinearPool->fetchLinearBlock(
                size,
                { C2MemoryUsage::CPU_READ, C2MemoryUsage::CPU_WRITE },
                &block);
        C2WriteView view = block->map().get();
        if (view.error() != C2_OK) {
            fprintf(stderr, "C2LinearBlock::map() failed : %d", view.error());
            break;
        }
        memcpy(view.base(), data, size);

        work->input.buffers.clear();
        work->input.buffers.emplace_back(new LinearBuffer(block));
        work->worklets.clear();
        work->worklets.emplace_back(new C2Worklet);

        std::list<std::unique_ptr<C2Work>> items;
        items.push_back(std::move(work));

        ALOGV("Frame #%ld size = %zu", numFrames, size);
        // DO THE DECODING
        component->queue_nb(&items);

        if (buffer) {
            buffer->release();
            buffer = NULL;
        }

        ++numFrames;
    }
    ALOGV("main loop finished");
    source->stop();
    running.store(false);
    surfaceThread.join();

    component->release();
    printf("\n");
}

}  // namespace

static void usage(const char *me) {
    fprintf(stderr, "usage: %s [options] [input_filename]\n", me);
    fprintf(stderr, "       -h(elp)\n");
}

int main(int argc, char **argv) {
    android::ProcessState::self()->startThreadPool();

    int res;
    while ((res = getopt(argc, argv, "h")) >= 0) {
        switch (res) {
            case 'h':
            default:
            {
                usage(argv[0]);
                exit(1);
                break;
            }
        }
    }

    argc -= optind;
    argv += optind;

    if (argc < 1) {
        fprintf(stderr, "No input file specified\n");
        return 1;
    }

    status_t err = OK;
    SimplePlayer player;

    for (int k = 0; k < argc && err == OK; ++k) {
        const char *filename = argv[k];

        sp<DataSource> dataSource =
            DataSourceFactory::CreateFromURI(NULL /* httpService */, filename);

        if (strncasecmp(filename, "sine:", 5) && dataSource == NULL) {
            fprintf(stderr, "Unable to create data source.\n");
            return 1;
        }

        Vector<sp<IMediaSource> > mediaSources;
        sp<IMediaSource> mediaSource;

        sp<IMediaExtractor> extractor = MediaExtractorFactory::Create(dataSource);

        if (extractor == NULL) {
            fprintf(stderr, "could not create extractor.\n");
            return -1;
        }

        sp<MetaData> meta = extractor->getMetaData();

        if (meta != NULL) {
            const char *mime;
            if (!meta->findCString(kKeyMIMEType, &mime)) {
                fprintf(stderr, "extractor did not provide MIME type.\n");
                return -1;
            }
        }

        size_t numTracks = extractor->countTracks();

        size_t i;
        for (i = 0; i < numTracks; ++i) {
            meta = extractor->getTrackMetaData(
                    i, MediaExtractor::kIncludeExtensiveMetaData);

            if (meta == NULL) {
                break;
            }
            const char *mime;
            meta->findCString(kKeyMIMEType, &mime);

            // TODO: allowing AVC only for the time being
            if (!strncasecmp(mime, "video/avc", 9)) {
                break;
            }

            meta = NULL;
        }

        if (meta == NULL) {
            fprintf(stderr, "No AVC track found.\n");
            return -1;
        }

        mediaSource = extractor->getTrack(i);
        if (mediaSource == nullptr) {
            fprintf(stderr, "skip NULL track %zu, total tracks %zu.\n", i, numTracks);
            return -1;
        }

        player.play(mediaSource);
    }

    return 0;
}