/*
 *  Entangle: Tethered Camera Control & Capture
 *
 *  Copyright (C) 2009-2018 Daniel P. Berrangé
 *
 *  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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include <string.h>

#include <lcms2.h>

#include "entangle-colour-profile.h"

#include "entangle-debug.h"

#define DEBUG_CMS 0

/**
 * SECTION:entangle-colour-profile
 * @Short_description: a colour management profile for a device
 * @Title: EntangleColourProfile
 *
 * The #EntangleColourProfile object provides colour management
 * profile for a device.
 */

struct _EntangleColourProfile
{
    GObject parent;
    GMutex lock;
    GByteArray *data;
    char *filename;
    cmsHPROFILE *profile;
    gboolean dirty;
};

struct _EntangleColourProfileTransform
{
    GObject parent;
    EntangleColourProfile *srcProfile;
    EntangleColourProfile *dstProfile;
    EntangleColourProfileIntent renderIntent;
};

G_DEFINE_TYPE(EntangleColourProfile, entangle_colour_profile, G_TYPE_OBJECT);
G_DEFINE_TYPE(EntangleColourProfileTransform,
              entangle_colour_profile_transform,
              G_TYPE_OBJECT);

enum
{
    PROP_0,
    PROP_FILENAME,
    PROP_DATA,
};

enum
{
    PROP_00,
    PROP_SRC_PROFILE,
    PROP_DST_PROFILE,
    PROP_RENDERING_INTENT,
};

static void
entangle_colour_profile_get_property(GObject *object,
                                     guint prop_id,
                                     GValue *value,
                                     GParamSpec *pspec)
{
    EntangleColourProfile *profile = ENTANGLE_COLOUR_PROFILE(object);

    switch (prop_id) {
    case PROP_FILENAME:
        g_value_set_string(value, profile->filename);
        break;

    case PROP_DATA:
        g_value_set_boxed(value, profile->data);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
entangle_colour_profile_set_property(GObject *object,
                                     guint prop_id,
                                     const GValue *value,
                                     GParamSpec *pspec)
{
    EntangleColourProfile *profile = ENTANGLE_COLOUR_PROFILE(object);

    switch (prop_id) {
    case PROP_FILENAME:
        g_free(profile->filename);
        profile->filename = g_value_dup_string(value);
        profile->dirty = TRUE;
        break;

    case PROP_DATA:
        if (profile->data)
            g_byte_array_unref(profile->data);
        profile->data = g_value_dup_boxed(value);
        profile->dirty = TRUE;
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
entangle_colour_profile_transform_get_property(GObject *object,
                                               guint prop_id,
                                               GValue *value,
                                               GParamSpec *pspec)
{
    EntangleColourProfileTransform *transform =
        ENTANGLE_COLOUR_PROFILE_TRANSFORM(object);

    switch (prop_id) {
    case PROP_SRC_PROFILE:
        g_value_set_object(value, transform->srcProfile);
        break;

    case PROP_DST_PROFILE:
        g_value_set_object(value, transform->dstProfile);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
entangle_colour_profile_transform_set_property(GObject *object,
                                               guint prop_id,
                                               const GValue *value,
                                               GParamSpec *pspec)
{
    EntangleColourProfileTransform *transform =
        ENTANGLE_COLOUR_PROFILE_TRANSFORM(object);

    switch (prop_id) {
    case PROP_SRC_PROFILE:
        if (transform->srcProfile)
            g_object_unref(transform->srcProfile);
        transform->srcProfile = g_value_get_object(value);
        if (transform->srcProfile)
            g_object_ref(transform->srcProfile);
        break;

    case PROP_DST_PROFILE:
        if (transform->dstProfile)
            g_object_unref(transform->dstProfile);
        transform->dstProfile = g_value_get_object(value);
        if (transform->dstProfile)
            g_object_ref(transform->dstProfile);
        break;

    case PROP_RENDERING_INTENT:
        transform->renderIntent = g_value_get_enum(value);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
entangle_colour_profile_finalize(GObject *object)
{
    EntangleColourProfile *profile = ENTANGLE_COLOUR_PROFILE(object);

    ENTANGLE_DEBUG("Finalize profile");

    if (profile->data)
        g_byte_array_unref(profile->data);
    g_free(profile->filename);
    if (profile->profile)
        cmsCloseProfile(profile->profile);
    g_mutex_clear(&profile->lock);

    G_OBJECT_CLASS(entangle_colour_profile_parent_class)->finalize(object);
}

static void
entangle_colour_profile_transform_finalize(GObject *object)
{
    EntangleColourProfileTransform *transform =
        ENTANGLE_COLOUR_PROFILE_TRANSFORM(object);

    ENTANGLE_DEBUG("Finalize profile transform");

    if (transform->srcProfile)
        g_object_unref(transform->srcProfile);
    if (transform->dstProfile)
        g_object_unref(transform->dstProfile);

    G_OBJECT_CLASS(entangle_colour_profile_transform_parent_class)
        ->finalize(object);
}

static void
entangle_colour_profile_class_init(EntangleColourProfileClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);

    object_class->finalize = entangle_colour_profile_finalize;
    object_class->get_property = entangle_colour_profile_get_property;
    object_class->set_property = entangle_colour_profile_set_property;

    g_object_class_install_property(
        object_class, PROP_FILENAME,
        g_param_spec_string(
            "filename", "Profile filename", "Filename of the profile", NULL,
            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
                G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
    g_object_class_install_property(
        object_class, PROP_DATA,
        g_param_spec_boxed("data", "Profile data", "Raw data for the profile",
                           G_TYPE_BYTE_ARRAY,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                               G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                               G_PARAM_STATIC_BLURB));
}

static void
entangle_colour_profile_transform_class_init(
    EntangleColourProfileTransformClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);

    object_class->finalize = entangle_colour_profile_transform_finalize;
    object_class->get_property = entangle_colour_profile_transform_get_property;
    object_class->set_property = entangle_colour_profile_transform_set_property;

    g_object_class_install_property(
        object_class, PROP_SRC_PROFILE,
        g_param_spec_object("src-profile", "Source profile", "Source profile",
                            ENTANGLE_TYPE_COLOUR_PROFILE,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                                G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                                G_PARAM_STATIC_BLURB));
    g_object_class_install_property(
        object_class, PROP_DST_PROFILE,
        g_param_spec_object("dst-profile", "Destination Profile",
                            "Destination Profile", ENTANGLE_TYPE_COLOUR_PROFILE,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                                G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                                G_PARAM_STATIC_BLURB));

    g_object_class_install_property(
        object_class, PROP_RENDERING_INTENT,
        g_param_spec_enum(
            "rendering-intent", "Rendering intent", "Profile rendering intent",
            ENTANGLE_TYPE_COLOUR_PROFILE_INTENT,
            ENTANGLE_COLOUR_PROFILE_INTENT_PERCEPTUAL,
            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
                G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
}

/**
 * entangle_colour_profile_new_file:
 * @filename: (transfer none): the file holding the profile
 *
 * Create a new colour profile initializing from the contents
 * of @filename.
 *
 * Returns: (transfer full): the colour profile
 */
EntangleColourProfile *
entangle_colour_profile_new_file(const char *filename)
{
    return ENTANGLE_COLOUR_PROFILE(
        g_object_new(ENTANGLE_TYPE_COLOUR_PROFILE, "filename", filename, NULL));
}

/**
 * entangle_colour_profile_new_data:
 * @data: (transfer none): the data representing the colour profile
 *
 * Create a new colour profile initializing from @data. The @data
 * will not be copied, rather a reference taken, so the contents
 * should not be changed by the caller
 *
 * Returns: (transfer full): the colour profile
 */
EntangleColourProfile *
entangle_colour_profile_new_data(GByteArray *data)
{
    return ENTANGLE_COLOUR_PROFILE(
        g_object_new(ENTANGLE_TYPE_COLOUR_PROFILE, "data", data, NULL));
}

static gboolean
entangle_colour_profile_load(EntangleColourProfile *profile)
{
    gboolean ret;

    g_mutex_lock(&profile->lock);

    if (!profile->dirty) {
        goto cleanup;
    }

    if (profile->profile) {
        cmsCloseProfile(profile->profile);
        profile->profile = NULL;
    }
    if (profile->filename) {
        if (!(profile->profile =
                  cmsOpenProfileFromFile(profile->filename, "r")))
            ENTANGLE_DEBUG("Unable to load profile from %s", profile->filename);
    } else if (profile->data) {
        if (!(profile->profile = cmsOpenProfileFromMem(profile->data->data,
                                                       profile->data->len)))
            ENTANGLE_DEBUG("Unable to load profile from %p", profile->data);
    }

    profile->dirty = FALSE;

cleanup:
    ret = profile->profile ? TRUE : FALSE;
    g_mutex_unlock(&profile->lock);
    return ret;
}

static void
entangle_colour_profile_init(EntangleColourProfile *profile)
{
    g_mutex_init(&profile->lock);
}

static void
entangle_colour_profile_transform_init(
    EntangleColourProfileTransform *transform G_GNUC_UNUSED)
{}

/**
 * entangle_colour_profile_filename:
 * @profile: (transfer none): the colour profile
 *
 * Get the filename holding the profile on disk, if any.
 *
 * Returns: (transfer none): the filename, or NULL
 */
const char *
entangle_colour_profile_filename(EntangleColourProfile *profile)
{
    g_return_val_if_fail(ENTANGLE_IS_COLOUR_PROFILE(profile), NULL);

    return profile->filename;
}

static char *
entangle_colour_profile_get_string(EntangleColourProfile *profile,
                                   cmsInfoType info)
{
    g_return_val_if_fail(ENTANGLE_IS_COLOUR_PROFILE(profile), NULL);

    gchar *res;
    cmsUInt32Number len;

    if (!entangle_colour_profile_load(profile))
        return NULL;

    if ((len = cmsGetProfileInfoASCII(profile->profile, info, "en", "US", NULL,
                                      0)) == 0)
        return NULL;

    res = g_new0(gchar, len + 1);

    if (cmsGetProfileInfoASCII(profile->profile, info, "en", "US", res, len) ==
        0) {
        g_free(res);
        return NULL;
    }

    return res;
}

/**
 * entangle_colour_profile_description:
 * @profile: (transfer none): the colour profile
 *
 * Get the description of the profile
 *
 * Returns: (transfer full): the profile description
 */
char *
entangle_colour_profile_description(EntangleColourProfile *profile)
{
    return entangle_colour_profile_get_string(profile, cmsInfoDescription);
}

/**
 * entangle_colour_profile_manufacturer:
 * @profile: (transfer none): the colour profile
 *
 * Get the manufacturer of the profile
 *
 * Returns: (transfer full): the profile manufacturer
 */
char *
entangle_colour_profile_manufacturer(EntangleColourProfile *profile)
{
    return entangle_colour_profile_get_string(profile, cmsInfoManufacturer);
}

/**
 * entangle_colour_profile_model:
 * @profile: (transfer none): the colour profile
 *
 * Get the model of the profile
 *
 * Returns: (transfer full): the profile model
 */
char *
entangle_colour_profile_model(EntangleColourProfile *profile)
{
    return entangle_colour_profile_get_string(profile, cmsInfoModel);
}

/**
 * entangle_colour_profile_copyright:
 * @profile: (transfer none): the colour profile
 *
 * Get the copyright of the profile
 *
 * Returns: (transfer full): the profile copyright
 */
char *
entangle_colour_profile_copyright(EntangleColourProfile *profile)
{
    return entangle_colour_profile_get_string(profile, cmsInfoCopyright);
}

static int
entangle_colour_profile_pixel_type(GdkPixbuf *pixbuf)
{
    int type = COLORSPACE_SH(PT_RGB); /* GdkPixbuf only supports RGB for now */

    if (gdk_pixbuf_get_has_alpha(pixbuf)) {
        type |= CHANNELS_SH(gdk_pixbuf_get_n_channels(pixbuf) - 1);
        type |= EXTRA_SH(1);
    } else {
        type |= CHANNELS_SH(gdk_pixbuf_get_n_channels(pixbuf));
    }
    type |= BYTES_SH(gdk_pixbuf_get_bits_per_sample(pixbuf) / 8);

    return type;
}

/**
 * entangle_colour_profile_transform_new:
 * @src: (transfer none): original colour profile
 * @dst: (transfer none): target colour profile
 * @intent: rendering intent
 *
 * Create a colour profile transformation that is able to convert
 * images in the profile @src to be in the profile @dst.
 *
 * Returns: (transfer full): the colour profile transformation
 */
EntangleColourProfileTransform *
entangle_colour_profile_transform_new(EntangleColourProfile *src,
                                      EntangleColourProfile *dst,
                                      EntangleColourProfileIntent intent)
{
    g_return_val_if_fail(ENTANGLE_IS_COLOUR_PROFILE(src), NULL);
    g_return_val_if_fail(ENTANGLE_IS_COLOUR_PROFILE(dst), NULL);

    return ENTANGLE_COLOUR_PROFILE_TRANSFORM(
        g_object_new(ENTANGLE_TYPE_COLOUR_PROFILE_TRANSFORM, "src-profile", src,
                     "dst-profile", dst, "rendering-intent", intent, NULL));
}

/**
 * entangle_colour_profile_transform_apply:
 * @trans: (transfer none): the profile transformation
 * @srcpixbuf: (transfer none): the input pixbuf
 *
 * Apply the colour profile transformation @trans to the pixbuf
 * data in @srcpixbuf and return a new pixbuf whose data is
 * in the target colour profile. The contents of @srcpixbuf will
 * not be altered in any way.
 *
 * Returns: (transfer full): the transformed pixbuf
 */
GdkPixbuf *
entangle_colour_profile_transform_apply(
    EntangleColourProfileTransform *transform,
    GdkPixbuf *srcpixbuf)
{
    g_return_val_if_fail(ENTANGLE_IS_COLOUR_PROFILE_TRANSFORM(transform), NULL);
    g_return_val_if_fail(GDK_IS_PIXBUF(srcpixbuf), NULL);

    cmsHTRANSFORM cmstransform;
    GdkPixbuf *dstpixbuf;
    guchar *srcpixels;
    guchar *dstpixels;
    int type = entangle_colour_profile_pixel_type(srcpixbuf);
    int stride = gdk_pixbuf_get_rowstride(srcpixbuf);
    int height = gdk_pixbuf_get_height(srcpixbuf);
    int width = gdk_pixbuf_get_width(srcpixbuf);
    int intent = INTENT_PERCEPTUAL;

    if (!transform->srcProfile || !transform->dstProfile) {
        g_object_ref(srcpixbuf);
        return srcpixbuf;
    }

    if (!entangle_colour_profile_load(transform->srcProfile) ||
        !entangle_colour_profile_load(transform->dstProfile)) {
        g_object_ref(srcpixbuf);
        return srcpixbuf;
    }

    dstpixbuf = gdk_pixbuf_copy(srcpixbuf);

    switch (transform->renderIntent) {
    case ENTANGLE_COLOUR_PROFILE_INTENT_PERCEPTUAL:
        intent = INTENT_PERCEPTUAL;
        break;
    case ENTANGLE_COLOUR_PROFILE_INTENT_REL_COLOURIMETRIC:
        intent = INTENT_RELATIVE_COLORIMETRIC;
        break;
    case ENTANGLE_COLOUR_PROFILE_INTENT_SATURATION:
        intent = INTENT_SATURATION;
        break;
    case ENTANGLE_COLOUR_PROFILE_INTENT_ABS_COLOURIMETRIC:
        intent = INTENT_ABSOLUTE_COLORIMETRIC;
        break;
    default:
        g_warn_if_reached();
        break;
    }

    g_mutex_lock(&transform->srcProfile->lock);
    g_mutex_lock(&transform->dstProfile->lock);
    cmstransform =
        cmsCreateTransform(transform->srcProfile->profile, type,
                           transform->dstProfile->profile, type, intent, 0);
    g_mutex_unlock(&transform->srcProfile->lock);
    g_mutex_unlock(&transform->dstProfile->lock);

    srcpixels = gdk_pixbuf_get_pixels(srcpixbuf);
    dstpixels = gdk_pixbuf_get_pixels(dstpixbuf);

    /* We do it row-wise, since lcms can't cope with a
     * rowstride that isn't equal to width */
    for (int row = 0; row < height; row++)
        cmsDoTransform(
            cmstransform, srcpixels + (row * stride),
            dstpixels + (row * stride),
#if DEBUG_CMS
            /* A crude hack which causes the colour transform
             * to be applied to only half the image in a diagonal */
            (int)((double)width * (1.0 - (double)row / (double)height)));
#else
            width);
#endif

    cmsDeleteTransform(cmstransform);
    return dstpixbuf;
}

/*
 * Local variables:
 *  c-indent-level: 4
 *  c-basic-offset: 4
 *  indent-tabs-mode: nil
 *  tab-width: 8
 * End:
 */
