/*
 * Copyright © 2016-2018 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 or 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authored by: Alan Griffiths <alan@octopull.co.uk>
 */

#include "sw_splash.h"
#include "wayland_helpers.h"

#include <wayland-client.h>

#include <sys/types.h>
#include <sys/stat.h>

#include <chrono>
#include <cstring>
#include <thread>
#include <mutex>
#include <map>

namespace
{
struct DrawContext
{
    int width = 400;
    int height = 400;

    void* content_area;
    struct wl_display* display;
    struct wl_surface* surface;
    struct wl_callback* new_frame_signal;
    struct Buffer
    {
        wl_buffer* buffer = nullptr;
        bool available = false;
    } buffers[4];
    bool waiting_for_buffer;

    uint8_t pattern[4] = { 0x14, 0x48, 0xDD, 0xFF };
};

void draw_new_stuff(void* data, struct wl_callback* callback, uint32_t time);

wl_callback_listener const frame_listener =
{
    &draw_new_stuff
};

wl_buffer* find_free_buffer(DrawContext* ctx)
{
    for (auto& b: ctx->buffers)
    {
        if (b.available)
        {
            b.available = false;
            return b.buffer;
        }
    }
    return NULL;
}

void draw_new_stuff(
    void* data,
    struct wl_callback* callback,
    uint32_t /*time*/)
{
    DrawContext* ctx = static_cast<decltype(ctx)>(data);

    wl_callback_destroy(callback);

    struct wl_buffer* buffer = find_free_buffer(ctx);
    if (!buffer)
    {
        ctx->waiting_for_buffer = false;
        return;
    }

    char* row = static_cast<decltype(row)>(ctx->content_area);
    for (int j = 0; j < ctx->height; j++)
    {
        uint32_t* pixel = (uint32_t*)row;

        for (int i = 0; i < ctx->width; i++)
            memcpy(pixel + i, ctx->pattern, sizeof pixel[i]);

        row += 4*ctx->width;
    }

    ctx->new_frame_signal = wl_surface_frame(ctx->surface);
    wl_callback_add_listener(ctx->new_frame_signal, &frame_listener, ctx);
    wl_surface_attach(ctx->surface, buffer, 0, 0);
    wl_surface_commit(ctx->surface);
}

void update_free_buffers(void* data, struct wl_buffer* buffer)
{
    DrawContext* ctx = static_cast<decltype(ctx)>(data);
    for (auto& b: ctx->buffers)
    {
        if (b.buffer == buffer)
        {
            b.available = true;
        }
    }

    if (ctx->waiting_for_buffer)
    {
        struct wl_callback* fake_frame = wl_display_sync(ctx->display);
        wl_callback_add_listener(fake_frame, &frame_listener, ctx);
    }

    ctx->waiting_for_buffer = false;
}

wl_buffer_listener const buffer_listener = {
    update_free_buffers
};
}

struct SwSplash::Self : SplashSession
{
    Self()
        : globals{
              [](auto const&) {},
              [](auto const&) {},
              [](auto const&) {}}
    {
        // Because we use the outputs exactly once, to enumerate the set of outputs at startup,
        // we don't need to care about outputs changing or disappearing.
    }

    Globals globals;

    DrawContext ctx;

    std::mutex mutable mutex;
    std::weak_ptr<mir::scene::Session> session_;

    std::shared_ptr<mir::scene::Session> session() const override
    {
        std::lock_guard<decltype(mutex)> lock{mutex};
        return session_.lock();
    }

    void operator()(struct wl_display* display);
};

SwSplash::SwSplash() : self{std::make_shared<Self>()} {}

SwSplash::~SwSplash() = default;

void SwSplash::operator()(std::weak_ptr<mir::scene::Session> const& session)
{
    std::lock_guard<decltype(self->mutex)> lock{self->mutex};
    self->session_ = session;
}

SwSplash::operator std::shared_ptr<SplashSession>() const
{
    return self;
}

void SwSplash::Self::operator()(struct wl_display* display)
{
    globals.init(display);

    ctx.display = display;
    ctx.surface = wl_compositor_create_surface(globals.compositor);

    static wl_shell_surface_listener const shell_surface_listener
    {
        // ping
        [](auto, auto, auto){},

        // configure
        [](void *data, wl_shell_surface *, uint32_t /*edges*/, int32_t width, int32_t height)
        {
            auto const ctx = static_cast<DrawContext*>(data);

            if (width > 0)
                ctx->width = width;

            if (height > 0)
                ctx->height = height;
        },

        // popup_done
        [](auto, auto){}, // popup_done
    };

    auto const shell_surface = wl_shell_get_shell_surface(globals.shell, ctx.surface);
    wl_shell_surface_add_listener(shell_surface, &shell_surface_listener, &ctx);
    wl_shell_surface_set_fullscreen(shell_surface, WL_SHELL_SURFACE_FULLSCREEN_METHOD_DEFAULT, 0, nullptr);
    wl_surface_commit(ctx.surface);
    wl_display_roundtrip(display);

    struct wl_shm_pool* shm_pool = make_shm_pool(globals.shm, ctx.width * ctx.height * 4, &ctx.content_area);

    for (auto& b: ctx.buffers)
    {
        b.buffer = wl_shm_pool_create_buffer(shm_pool, 0, ctx.width, ctx.height, ctx.width*4, WL_SHM_FORMAT_ARGB8888);
        b.available = true;
        wl_buffer_add_listener(b.buffer, &buffer_listener, &ctx);
    }

    wl_shm_pool_destroy(shm_pool);

    struct wl_callback* first_frame = wl_display_sync(display);
    wl_callback_add_listener(first_frame, &frame_listener, &ctx);

    auto const time_limit = std::chrono::steady_clock::now() + std::chrono::seconds(2);

    do
    {
        wl_display_dispatch(display);

        for (auto& x : ctx.pattern)
        {
            x =  3*x/4;
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    while (std::chrono::steady_clock::now() < time_limit);

    for (auto const& b: ctx.buffers)
    {
        if (b.buffer)
            wl_buffer_destroy(b.buffer);
    }

    wl_shell_surface_destroy(shell_surface);
    wl_surface_destroy(ctx.surface);
    globals.teardown();
    wl_display_roundtrip(display);
}

void SwSplash::operator()(struct wl_display* display)
{
    (*self)(display);
}
