/*****************************************************************************
 * Copyright (C) 2022 VLC authors and VideoLAN
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * ( at your option ) any later version.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

// original code from the Chromium project

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "gtk_util.h"
#include "gtk_compat.h"
#include <gdk/gdk.h>

#include <locale.h>
#include <stddef.h>

#include <memory>
#include <cmath>

namespace gtk {

namespace {

GtkCssContext AppendCssNodeToStyleContextImpl(
        GtkCssContext context,
        GType gtype,
        const std::string& name,
        const std::string& object_name,
        const std::vector<std::string>& classes,
        GtkStateFlags state,
        float scale) {
#if 0 //FIXME GTK4
    if (GtkCheckVersion(4)) {
        // GTK_TYPE_BOX is used instead of GTK_TYPE_WIDGET because:
        // 1. Widgets are abstract and cannot be created directly.
        // 2. The widget must be a container type so that it unrefs child widgets
        //    on destruction.
        auto* widget_object = object_name.empty()
                ? g_object_new(GTK_TYPE_BOX, nullptr)
                : g_object_new(GTK_TYPE_BOX, "css-name",
                               object_name.c_str(), nullptr);
        auto widget = TakeGObject(GTK_WIDGET(widget_object));

        if (!name.empty())
            gtk_widget_set_name(widget, name.c_str());

        std::vector<const char*> css_classes;
        css_classes.reserve(classes.size() + 1);
        for (const auto& css_class : classes)
            css_classes.push_back(css_class.c_str());
        css_classes.push_back(nullptr);
        gtk_widget_set_css_classes(widget, css_classes.data());

        gtk_widget_set_state_flags(widget, state, false);

        if (context)
            gtk_widget_set_parent(widget, context.widget());

        gtk_style_context_set_scale(gtk_widget_get_style_context(widget), scale);

        return GtkCssContext(widget, context ? context.root() : widget);
    } else
#endif
    {
        GtkWidgetPath* path =
                context ? gtk_widget_path_copy(gtk_style_context_get_path(context))
                        : gtk_widget_path_new();
        gtk_widget_path_append_type(path, gtype);

        if (!object_name.empty()) {
            if (GtkCheckVersion(3, 20))
                gtk_widget_path_iter_set_object_name(path, -1, object_name.c_str());
            else
                gtk_widget_path_iter_add_class(path, -1, object_name.c_str());
        }

        if (!name.empty())
            gtk_widget_path_iter_set_name(path, -1, name.c_str());

        for (const auto& css_class : classes)
            gtk_widget_path_iter_add_class(path, -1, css_class.c_str());

        if (GtkCheckVersion(3, 14))
            gtk_widget_path_iter_set_state(path, -1, state);

        GtkCssContext child_context(TakeGObject(gtk_style_context_new()));
        gtk_style_context_set_path(child_context, path);
        if (GtkCheckVersion(3, 14)) {
            gtk_style_context_set_state(child_context, state);
        } else {
            GtkStateFlags child_state = state;
            if (context) {
                child_state = static_cast<GtkStateFlags>(
                            child_state | gtk_style_context_get_state(context));
            }
            gtk_style_context_set_state(child_context, child_state);
        }

        gtk_style_context_set_scale(child_context, scale);

        gtk_style_context_set_parent(child_context, context);

        gtk_widget_path_unref(path);
        return GtkCssContext(std::move(child_context));
    }
}

GtkWidget* CreateDummyWindow() {
    GtkWidget* window =   gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_widget_realize(window);
    return window;
}

}  // namespace

bool GtkCheckVersion(uint32_t major, uint32_t minor, uint32_t micro) {
    if (major < gtk_get_major_version())
        return true;
    else if (major > gtk_get_major_version())
        return false;

    if (minor < gtk_get_minor_version())
        return true;
    else if (minor > gtk_get_minor_version())
        return false;

    return micro <= gtk_get_micro_version();
}

const char* GtkCssMenu() {
    return GtkCheckVersion(4) ? "#popover.background.menu #contents"
                              : "GtkMenu#menu";
}

const char* GtkCssMenuItem() {
    return GtkCheckVersion(4) ? "#modelbutton.flat" : "GtkMenuItem#menuitem";
}

const char* GtkCssMenuScrollbar() {
    return GtkCheckVersion(4) ? "#scrollbar #range"
                              : "GtkScrollbar#scrollbar #trough";
}

bool GtkInitFromCommandLine(int* argc, char** argv) {
    // Callers should have already called setlocale(LC_ALL, "") and
    // setlocale(LC_NUMERIC, "C") by now. Chrome does this in
    // service_manager::Main.
    assert(strcmp(setlocale(LC_NUMERIC, nullptr), "C") == 0);
    // This prevents GTK from calling setlocale(LC_ALL, ""), which potentially
    // overwrites the LC_NUMERIC locale to something other than "C".
    gtk_disable_setlocale();
    return gtk_init_check(argc, &argv);
}


CairoSurface::CairoSurface(VLCPicturePtr& bitmap) 
{
    picture_t* pic = bitmap.get();
    surface_ = (cairo_image_surface_create_for_data(
                   static_cast<unsigned char*>(pic->p[0].p_pixels),
                   CAIRO_FORMAT_ARGB32,
                   pic->format.i_visible_width,
                   pic->format.i_visible_height,
                   pic->p[0].i_pitch));
    cairo_ = cairo_create(surface_);
}

CairoSurface::CairoSurface(const MySize& size)
    : surface_(cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
                                          size.width(),
                                          size.height())),
      cairo_(cairo_create(surface_)) {
    assert(cairo_surface_status(surface_) == CAIRO_STATUS_SUCCESS);
    // Clear the surface.
    cairo_save(cairo_);
    cairo_set_source_rgba(cairo_, 0, 0, 0, 0);
    cairo_set_operator(cairo_, CAIRO_OPERATOR_SOURCE);
    cairo_paint(cairo_);
    cairo_restore(cairo_);
}

CairoSurface::~CairoSurface() {
    // `cairo_destroy` and `cairo_surface_destroy` decrease the reference count on
    // `cairo_` and `surface_` objects respectively. The underlying memory is
    // freed if the reference count goes to zero. We use ExtractAsDangling() here
    // to avoid holding a briefly dangling ptr in case the memory is freed.
    cairo_destroy(cairo_);
    cairo_surface_destroy(surface_);
}



GdkRGBA CairoSurface::GetAveragePixelValue(bool frame) {
    cairo_surface_flush(surface_);
    unsigned char* data =cairo_image_surface_get_data(surface_);
    int width = cairo_image_surface_get_width(surface_);
    int height = cairo_image_surface_get_height(surface_);
    int stride = cairo_image_surface_get_stride(surface_);
    assert(cairo_image_surface_get_format(surface_) == CAIRO_FORMAT_ARGB32);
    long a = 0, r = 0, g = 0, b = 0;
    uint32_t max_alpha = 0;
    for (int line = 0; line < height; line++)
    {
        uint32_t* rgbaLine = (uint32_t*)(&data[line*stride]);
        for (int i = 0; i < width; i++) {
            uint32_t color =  rgbaLine[i];
            max_alpha = std::max((color >> 24) & 0xFF, max_alpha);
            a += (color >> 24) & 0xFF;
            r += (color >> 16) & 0xFF;
            g += (color >> 8) & 0xFF;
            b += (color)  & 0xFF;
        }
    }

    GdkRGBA out = {0,0,0,0};
    if (a == 0) {
        return out;
    }
    out.red = gdouble(r) / a;
    out.green = gdouble(g) / a;
    out.blue = gdouble(b) / a;
    out.alpha = frame ? (max_alpha / 255.) : gdouble(a) / (width * height * 255);
    return out;
}

GtkCssContext::GtkCssContext(GtkWidget* widget, GtkWidget* root)
    : widget_(widget), root_(WrapGObject(root)) {
    assert(GtkCheckVersion(4));
}

GtkCssContext::GtkCssContext(GtkStyleContext* context)
    : context_(WrapGObject(context)) {
    assert(!GtkCheckVersion(4));
}

GtkCssContext::GtkCssContext() = default;
GtkCssContext::GtkCssContext(const GtkCssContext&) = default;
GtkCssContext::GtkCssContext(GtkCssContext&&) = default;
GtkCssContext& GtkCssContext::operator=(const GtkCssContext&) = default;
GtkCssContext& GtkCssContext::operator=(GtkCssContext&&) = default;
GtkCssContext::~GtkCssContext() = default;

GtkCssContext::operator GtkStyleContext*() {
    if (GtkCheckVersion(4))
        return widget_ ? gtk_widget_get_style_context(widget_) : nullptr;
    return context_;
}

GtkCssContext GtkCssContext::GetParent() {
    if (GtkCheckVersion(4)) {
        return GtkCssContext(WrapGObject(gtk_widget_get_parent(widget_)),
                             root_ == widget_ ? ScopedGObject<GtkWidget>() : root_);
    }
    return GtkCssContext(WrapGObject(gtk_style_context_get_parent(context_)));
}

GtkWidget* GtkCssContext::widget() {
    assert(GtkCheckVersion(4));
    return widget_;
}

GtkWidget* GtkCssContext::root() {
    assert(GtkCheckVersion(4));
    return root_;
}


GtkCssContext AppendCssNodeToStyleContext(GtkCssContext context,
                                          const std::string& css_node) {
    enum {
        CSS_TYPE,
        CSS_NAME,
        CSS_OBJECT_NAME,
        CSS_CLASS,
        CSS_PSEUDOCLASS,
        CSS_NONE,
    } part_type = CSS_TYPE;

    static const struct {
        const char* name;
        GtkStateFlags state_flag;
    } pseudo_classes[] = {
    {"active", GTK_STATE_FLAG_ACTIVE},
    {"hover", GTK_STATE_FLAG_PRELIGHT},
    {"selected", GTK_STATE_FLAG_SELECTED},
    {"disabled", GTK_STATE_FLAG_INSENSITIVE},
    {"indeterminate", GTK_STATE_FLAG_INCONSISTENT},
    {"focus", GTK_STATE_FLAG_FOCUSED},
    {"backdrop", GTK_STATE_FLAG_BACKDROP},
    {"link", GTK_STATE_FLAG_LINK},
    {"visited", GTK_STATE_FLAG_VISITED},
    {"checked", GTK_STATE_FLAG_CHECKED},
};

    GType gtype = G_TYPE_NONE;
    std::string name;
    std::string object_name;
    std::vector<std::string> classes;
    GtkStateFlags state = GTK_STATE_FLAG_NORMAL;

    auto it = css_node.cbegin();

    //base::StringTokenizer t(css_node, ".:#()");
    //t.set_options(base::StringTokenizer::RETURN_DELIMS);
    while (it != css_node.cend()) {

        auto found = std::find_if(it, css_node.cend(), [](const char c) {
            return (c == '.') || (c == ':') || (c == '#') || (c == '(') || (c == ')');
        });

        std::string token(it, found);

        if (token != "")
        {
            switch (part_type) {
            case CSS_NAME:
                name = token;
                break;
            case CSS_OBJECT_NAME:
                object_name = token;
                break;
            case CSS_TYPE: {
                if (!GtkCheckVersion(4)) {
                    gtype = g_type_from_name(token.c_str());
                    assert(gtype);
                }
                break;
            }
            case CSS_CLASS:
                classes.push_back(std::move(token));
                break;
            case CSS_PSEUDOCLASS: {
                GtkStateFlags state_flag = GTK_STATE_FLAG_NORMAL;
                for (const auto& pseudo_class_entry : pseudo_classes) {
                    if (strcmp(pseudo_class_entry.name, token.c_str()) == 0) {
                        state_flag = pseudo_class_entry.state_flag;
                        break;
                    }
                }
                state = static_cast<GtkStateFlags>(state | state_flag);
                break;
            }
            case CSS_NONE:
                assert(false);
            }
        }

        if (found == css_node.cend())
            break;

        switch (*found) {
        case '(':
            part_type = CSS_NAME;
            break;
        case ')':
            part_type = CSS_NONE;
            break;
        case '#':
            part_type = CSS_OBJECT_NAME;
            break;
        case '.':
            part_type = CSS_CLASS;
            break;
        case ':':
            part_type = CSS_PSEUDOCLASS;
            break;
        default:
            assert("unreachable" == 0);
        }

        it = found + 1;
    }

    // Always add a "chromium" class so that themes can style chromium
    // widgets specially if they want to.
    classes.push_back("vlc");

    float scale = std::round(GetDeviceScaleFactor());

    return AppendCssNodeToStyleContextImpl(std::move(context), gtype, name, object_name,
                                           classes, state, scale);
}

GtkCssContext GetStyleContextFromCss(const std::string& css_selector) {
    // Prepend a window node to the selector since all widgets must live
    // in a window, but we don't want to specify that every time.
    auto context = AppendCssNodeToStyleContext({}, "GtkWindow#window.background");

    auto it = css_selector.cbegin();
    while (it != css_selector.cend())
    {
        auto found = std::find(it, css_selector.cend(), ' ');
        std::string widget_type(it, found);
        if (widget_type != "")
            context = AppendCssNodeToStyleContext(context, widget_type);

        if (found == css_selector.cend())
            break;

        it = found + 1;
    }
    return context;
}

GdkRGBA GetBgColorFromStyleContext(GtkCssContext context) {
    // Backgrounds are more general than solid colors (eg. gradients),
    // but chromium requires us to boil this down to one color.  We
    // cannot use the background-color here because some themes leave it
    // set to a garbage color because a background-image will cover it
    // anyway.  So we instead render the background into a 24x24 bitmap,
    // removing any borders, and hope that we get a good color.
    ApplyCssToContext(context,
                      "* {"
                      "border-radius: 0px;"
                      "border-style: none;"
                      "box-shadow: none;"
                      "}");
    MySize size(24, 24);
    CairoSurface surface(size);
    RenderBackground(size, surface.cairo(), std::move(context));
    return surface.GetAveragePixelValue(false);
}

GdkRGBA GtkStyleContextGetColor(GtkStyleContext* context) {
    GdkRGBA color;
    gtk_style_context_get_color(context, gtk_style_context_get_state(context), &color );
    return color;
}


GdkRGBA GetFgColor(const std::string& css_selector) {
    return GtkStyleContextGetColor(GetStyleContextFromCss(css_selector));
}



ScopedCssProvider GetCssProvider(const std::string& css) {
    auto provider = TakeGObject(gtk_css_provider_new());

    gtk_css_provider_load_from_data(provider, css.c_str(), -1, nullptr);
    return provider;
}

void ApplyCssProviderToContext(GtkCssContext context,
                               GtkCssProvider* provider) {
    while (context) {
        gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider),
                                       G_MAXUINT);
        context = context.GetParent();
    }
}

void ApplyCssToContext(GtkCssContext context, const std::string& css) {
    auto provider = GetCssProvider(css);
    ApplyCssProviderToContext(std::move(context), provider);
}

void RenderBackground(const MySize& size,
                      cairo_t* cr,
                      GtkCssContext context) {
    if (!context)
        return;
    RenderBackground(size, cr, context.GetParent());
    gtk_render_background(context, cr, 0, 0, size.width(), size.height());
}

GdkRGBA GetBgColor(const std::string& css_selector) {
    return GetBgColorFromStyleContext(GetStyleContextFromCss(css_selector));
}

GdkRGBA GetBorderColor(const std::string& css_selector) {
    // Borders have the same issue as backgrounds, due to the
    // border-image property.
    auto context = GetStyleContextFromCss(css_selector);
    MySize size(24, 24);
    CairoSurface surface(size);
    gtk_render_frame(context, surface.cairo(), 0, 0, size.width(), size.height());
    return surface.GetAveragePixelValue(true);
}

GdkRGBA GetFocusColor(const std::string& css_selector)
{
    auto context = GetStyleContextFromCss(css_selector);
    MySize size(24, 24);
    CairoSurface surface(size);
    gtk_render_focus(context, surface.cairo(), 0, 0, size.width(), size.height());
    return surface.GetAveragePixelValue(true);
}

GdkRGBA GetSelectionBgColor(const std::string& css_selector) {
    auto context = GetStyleContextFromCss(css_selector);
    return GetBgColorFromStyleContext(std::move(context));
}



bool ContextHasClass(GtkCssContext context, const std::string& style_class) {
    bool has_class = gtk_style_context_has_class(context, style_class.c_str());
    if (!GtkCheckVersion(4)) {
        has_class |= gtk_widget_path_iter_has_class(
                    gtk_style_context_get_path(context), -1, style_class.c_str());
    }
    return has_class;
}

GdkRGBA GetSeparatorColor(const std::string& css_selector) {
    auto context = GetStyleContextFromCss(css_selector);
    bool horizontal = ContextHasClass(context, "horizontal");

    int w = 1, h = 1;
    if (GtkCheckVersion(4)) {
        auto size = GetSeparatorSize(horizontal);
        w = size.width();
        h = size.height();
    } else {
        gtk_style_context_get(context, gtk_style_context_get_state(context), "min-width", &w, "min-height", &h, nullptr);
    }

    MyInset border = GtkStyleContextGetBorder(context);
    MyInset padding = GtkStyleContextGetPadding(context);

    w += border.left() + padding.left() + padding.right() + border.right();
    h += border.top() + padding.top() + padding.bottom() + border.bottom();

    if (horizontal) {
        w = 24;
        h = std::max(h, 1);
    } else {
        assert(ContextHasClass(context, "vertical"));
        h = 24;
        w = std::max(w, 1);
    }

    CairoSurface surface(MySize(w, h));
    gtk_render_background(context, surface.cairo(), 0, 0, w, h);
    gtk_render_frame(context, surface.cairo(), 0, 0, w, h);
    return surface.GetAveragePixelValue(false);
}

std::string GetGtkSettingsStringProperty(GtkSettings* settings,
                                         const gchar* prop_name) {
    GValue layout = G_VALUE_INIT;
    g_value_init(&layout, G_TYPE_STRING);
    g_object_get_property(G_OBJECT(settings), prop_name, &layout);
    assert(G_VALUE_HOLDS_STRING(&layout));
    std::string prop_value(g_value_get_string(&layout));
    g_value_unset(&layout);
    return prop_value;
}

GtkIconTheme* GetDefaultIconTheme() {
#if 0 //FIXME GTK4
    return GtkCheckVersion(4)
            ? gtk_icon_theme_get_for_display(gdk_display_get_default())
            : gtk_icon_theme_get_default();
#endif
    return gtk_icon_theme_get_default();
}

void GtkWindowDestroy(GtkWidget* widget) {
#if 0 //FIXME GTK4
    if (GtkCheckVersion(4))
        gtk_window_destroy(GTK_WINDOW(widget));
#endif
    gtk_widget_destroy(widget);
}

GtkWidget* GetDummyWindow() {
    static GtkWidget* window = CreateDummyWindow();
    return window;
}

MySize GetSeparatorSize(bool horizontal) {
    auto widget = TakeGObject(gtk_separator_new(
                                  horizontal ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL));
    GtkRequisition natural_size;
    gtk_widget_get_preferred_size(widget, nullptr, &natural_size);
    return {natural_size.width, natural_size.height};
}

static float g_deviceScaleFactor =  1.0f;

void SetDeviceScaleFactor(float scaleFactor) {
    g_deviceScaleFactor = scaleFactor;
}

float GetDeviceScaleFactor() {
    return g_deviceScaleFactor;
}


}  // namespace gtk
