/*
 * Copyright (C) 2020 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 <viewer/AutomationEngine.h>

#include <filament/Camera.h>
#include <filament/Engine.h>
#include <filament/Renderer.h>
#include <filament/Viewport.h>

#include <backend/PixelBufferDescriptor.h>

#include <utils/Log.h>
#include <utils/Path.h>

#include <iomanip>
#include <fstream>
#include <sstream>

using namespace utils;

namespace filament {
namespace viewer {

static std::string gStatus;

struct ScreenshotState {
    View* view;
    std::string filename;
    bool autoclose;
    AutomationEngine* engine;
};

static void exportScreenshot(View* view, Renderer* renderer, std::string filename,
        bool autoclose, AutomationEngine* automationEngine) {
    const Viewport& vp = view->getViewport();
    const size_t byteCount = vp.width * vp.height * 3;

    // Create a buffer descriptor that writes the PPM after the data becomes ready on the CPU.
    backend::PixelBufferDescriptor buffer(
        new uint8_t[byteCount], byteCount,
        backend::PixelBufferDescriptor::PixelDataFormat::RGB,
        backend::PixelBufferDescriptor::PixelDataType::UBYTE,
        [](void* buffer, size_t size, void* user) {
            ScreenshotState* state = static_cast<ScreenshotState*>(user);
            if (state->engine->isTerminated()) {
                delete[] static_cast<uint8_t*>(buffer);
                delete state;
                return;
            }
            const Viewport& vp = state->view->getViewport();
            Path out(state->filename);
            std::ofstream ppmStream(out);
            ppmStream << "P6 " << vp.width << " " << vp.height << " " << 255 << std::endl;
            ppmStream.write(static_cast<char*>(buffer), vp.width * vp.height * 3);
            delete[] static_cast<uint8_t*>(buffer);
            if (state->autoclose) {
                state->engine->requestClose();
            }
            delete state;
        },
        new ScreenshotState { view, filename, autoclose, automationEngine }
    );

    // Invoke readPixels asynchronously.
    renderer->readPixels((uint32_t) vp.left, (uint32_t) vp.bottom, vp.width, vp.height,
            std::move(buffer));
}

AutomationEngine* AutomationEngine::createFromJSON(const char* jsonSpec, size_t size) {
    AutomationSpec* spec = AutomationSpec::generate(jsonSpec, size);
    if (!spec) {
        return nullptr;
    }
    Settings* settings = new Settings();
    AutomationEngine* result = new AutomationEngine(spec, settings);
    result->mOwnsSettings = true;
    return result;
}

AutomationEngine* AutomationEngine::createDefault() {
    AutomationSpec* spec = AutomationSpec::generateDefaultTestCases();
    if (!spec) {
        return nullptr;
    }
    Settings* settings = new Settings();
    AutomationEngine* result = new AutomationEngine(spec, settings);
    result->mOwnsSettings = true;
    return result;
}

AutomationEngine::~AutomationEngine() {
    if (mColorGrading) {
        mColorGradingEngine->destroy(mColorGrading);
    }
    if (mOwnsSettings) {
        delete mSpec;
        delete mSettings;
    }
}

void AutomationEngine::startRunning() {
    mRequestStart = true;
}

void AutomationEngine::startBatchMode() {
    mRequestStart = true;
    mBatchModeEnabled = true;
}

void AutomationEngine::terminate() {
    stopRunning();
    mTerminated = true;
}

void AutomationEngine::exportSettings(const Settings& settings, const char* filename) {
    JsonSerializer serializer;
    std::string contents = serializer.writeJson(settings);
    std::ofstream out(filename);
    if (!out) {
        gStatus = "Failed to export settings file.";
    }
    out << contents << std::endl;
    gStatus = "Exported to '" + std::string(filename) + "' in the current folder.";
}

void AutomationEngine::applySettings(const char* json, size_t jsonLength, View* view,
        MaterialInstance* const* materials, size_t materialCount, IndirectLight* ibl,
        utils::Entity sunlight, LightManager* lm, Scene* scene, Renderer* renderer) {
    JsonSerializer serializer;
    if (!serializer.readJson(json, jsonLength, mSettings)) {
        std::string jsonWithTerminator(json, json + jsonLength);
        slog.e << "Badly formed JSON:\n" << jsonWithTerminator.c_str() << io::endl;
        return;
    }
    viewer::applySettings(mSettings->view, view);
    for (size_t i = 0; i < materialCount; i++) {
        viewer::applySettings(mSettings->material, materials[i]);
    }
    viewer::applySettings(mSettings->lighting, ibl, sunlight, lm, scene);
    Camera* camera = &view->getCamera();
    Skybox* skybox = scene->getSkybox();
    viewer::applySettings(mSettings->viewer, camera, skybox, renderer);
}

ColorGrading* AutomationEngine::getColorGrading(Engine* engine) {
    if (mSettings->view.colorGrading != mColorGradingSettings) {
        mColorGradingSettings = mSettings->view.colorGrading;
        if (mColorGrading) {
            mColorGradingEngine->destroy(mColorGrading);
        }
        mColorGrading = createColorGrading(mColorGradingSettings, engine);
        mColorGradingEngine = engine;
    }
    return mColorGrading;
}

ViewerOptions AutomationEngine::getViewerOptions() const {
    ViewerOptions options = mSettings->viewer;
    const auto dofOptions = mSettings->view.dof;
    if (dofOptions.enabled) {
        options.cameraFocalLength = Camera::computeEffectiveFocalLength(
                options.cameraFocalLength / 1000.0,
                std::max(0.1f, options.cameraFocusDistance)) * 1000.0;

    }
    return mSettings->viewer;
}

void AutomationEngine::tick(View* view, MaterialInstance* const* materials, size_t materialCount,
        Renderer* renderer, float deltaTime) {
    const auto activateTest = [this, view, materials, materialCount]() {
        mElapsedTime = 0;
        mElapsedFrames = 0;
        mSpec->get(mCurrentTest, mSettings);
        viewer::applySettings(mSettings->view, view);
        for (size_t i = 0; i < materialCount; i++) {
            viewer::applySettings(mSettings->material, materials[i]);
        }
        if (mOptions.verbose) {
            utils::slog.i << "Running test " << mCurrentTest << utils::io::endl;
        }
    };

    if (!mIsRunning) {
        if (mRequestStart) {
            if ((mBatchModeEnabled && mBatchModeAllowed) || !mBatchModeEnabled) {
                mIsRunning = true;
                mRequestStart = false;
                mCurrentTest = 0;
                activateTest();
            }
        }
        return;
    }

    mElapsedTime += deltaTime;
    mElapsedFrames++;

    if (mElapsedTime < mOptions.sleepDuration || mElapsedFrames < mOptions.minFrameCount) {
        return;
    }

    const bool isLastTest = mCurrentTest == mSpec->size() - 1;

    const int digits = (int) log10 ((double) mSpec->size()) + 1;
    std::ostringstream stringStream;
    stringStream << mSpec->getName(mCurrentTest)
            << std::setfill('0') << std::setw(digits) << mCurrentTest;
    std::string prefix = stringStream.str();

    if (mOptions.exportSettings) {
        std::string filename = prefix + ".json";
        exportSettings(*mSettings, filename.c_str());
    }

    if (mOptions.exportScreenshots) {
        exportScreenshot(view, renderer, prefix + ".ppm", isLastTest, this);
    }

    if (isLastTest) {
        mIsRunning = false;
        if (mBatchModeEnabled && !mOptions.exportScreenshots) {
            mShouldClose = true;
        }
        return;
    }

    // Increment the case number and apply the next round of settings.
    mCurrentTest++;
    activateTest();
}

const char* AutomationEngine::getStatusMessage() const {
    return gStatus.c_str();
}

} // namespace viewer
} // namespace filament
