/*
 * Copyright 2014 Hannes Janetzek
 * Copyright 2019 Gustl22
 *
 * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.oscim.utils;

import org.oscim.backend.canvas.Color;
import org.oscim.utils.math.Vec3;

import static org.oscim.backend.canvas.Color.a;
import static org.oscim.backend.canvas.Color.b;
import static org.oscim.backend.canvas.Color.g;
import static org.oscim.backend.canvas.Color.r;
import static org.oscim.utils.FastMath.clamp;

public class ColorUtil {

    private static final Vec3 TMP_VEC = new Vec3();

    public static synchronized int desaturate(int color) {
        Vec3 hsl = TMP_VEC;
        rgbToHsl(r(color), g(color), b(color), hsl);
        return hslToRgb(hsl.x, 0, hsl.z);
    }

    public static synchronized int saturate(int color, double saturation) {
        Vec3 hsl = TMP_VEC;
        rgbToHsv(r(color), g(color), b(color), hsl);
        return hsvToRgb(hsl.x, saturation, hsl.z);
    }

    public static synchronized int setHue(int color, double hue) {
        Vec3 hsl = TMP_VEC;
        rgbToHsv(r(color), g(color), b(color), hsl);
        return hsvToRgb(hue, hsl.y, hsl.z, null);
    }

    public static synchronized int shiftHue(int color, double hue) {
        Vec3 hsv = TMP_VEC;
        rgbToHsv(r(color), g(color), b(color), hsv);
        hsv.x += hue;
        hsv.x -= Math.floor(hsv.x);

        return hsvToRgb(clamp(hsv.x, 0, 1), hsv.y, hsv.z, null);
    }

    public static synchronized int saturate(int color, double saturation, boolean relative) {
        Vec3 hsl = TMP_VEC;
        rgbToHsv(r(color), g(color), b(color), hsl);
        return hsvToRgb(hsl.x, clamp(saturation * hsl.y, 0, 1), hsl.z);
    }

    /**
     * @param hue        the hue from 0 to 1 (exclusive)
     *                   0: no color shift
     *                   0.5: opposite hue
     * @param saturation the saturation
     *                   0 to 1: desaturate
     *                   1 to 2: saturate
     * @param value      the lightness
     *                   0 to 1: darken
     *                   1 to 2: lighten
     * @param relative   indicate if colors are modified relative to their values
     *                   (e.g black not changes if relative)
     */
    public static synchronized int modHsv(int color, double hue, double saturation, double value,
                                          boolean relative) {
        if ((hue == 0 || hue == 1) && saturation == 1 && value == 1)
            return color;
        Vec3 hsl = TMP_VEC;
        rgbToHsv(r(color), g(color), b(color), hsl);
        return Color.setA(hsvToRgb(clamp((hue + hsl.x) % 1, 0, 1),
                clamp(relative || saturation <= 1 ? saturation * hsl.y :
                        hsl.y + (saturation - 1) * (1 - hsl.y), 0, 1),
                clamp(relative || value <= 1 ? value * hsl.z :
                        hsl.z + (value - 1) * (1 - hsl.z), 0, 1)), a(color));
    }

    // functions ported from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c

    /**
     * Converts an RGB color value to HSL. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
     * Assumes r, g, and b are contained in the set [0, 255] and
     * returns h, s, and l in the set [0, 1].
     *
     * @param Number r The red color value
     * @param Number g The green color value
     * @param Number b The blue color value
     * @return Array The HSL representation
     */
    public static Vec3 rgbToHsl(double r, double g, double b, Vec3 out) {
        r /= 255d;
        g /= 255d;
        b /= 255d;

        double max = Math.max(r, Math.max(g, b));
        double min = Math.min(r, Math.min(g, b));

        double h = 0, s = 0, l = (max + min) / 2;

        if (max != min) {
            double d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            if (max == r)
                h = (g - b) / d + (g < b ? 6 : 0);
            else if (max == g)
                h = (b - r) / d + 2;
            else
                h = (r - g) / d + 4;

            h /= 6;
        }

        out.set(h, s, l);

        return out;
    }

    public static Vec3 rgbToHsl(double r, double g, double b) {
        return rgbToHsl(r, g, b, new Vec3());
    }

    /**
     * Converts an HSL color value to RGB. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
     * Assumes h, s, and l are contained in the set [0, 1] and
     * returns r, g, and b in the set [0, 255].
     *
     * @param Number h The hue
     * @param Number s The saturation
     * @param Number l The lightness
     * @return Array The RGB representation
     */
    public static int hslToRgb(double h, double s, double l, Vec3 out) {
        double r, g, b;

        if (s == 0) {
            r = g = b = l; // achromatic
        } else {
            double q = l < 0.5 ? l * (1 + s) : l + s - l * s;
            double p = 2 * l - q;
            r = hue2rgb(p, q, h + 1 / 3);
            g = hue2rgb(p, q, h);
            b = hue2rgb(p, q, h - 1 / 3);
        }

        if (out != null)
            out.set(r, g, b);

        return Color.get(r, g, b);
    }

    static double hue2rgb(double p, double q, double t) {
        if (t < 0)
            t += 1;
        if (t > 1)
            t -= 1;
        if (t < 1 / 6)
            return p + (q - p) * 6 * t;
        if (t < 1 / 2)
            return q;
        if (t < 2 / 3)
            return p + (q - p) * (2 / 3 - t) * 6;
        return p;
    }

    /**
     * Converts an RGB color value to HSV. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
     * Assumes r, g, and b are contained in the set [0, 255] and
     * returns h, s, and v in the set [0, 1].
     *
     * @param Number r The red color value
     * @param Number g The green color value
     * @param Number b The blue color value
     * @return Array The HSV representation
     */
    public static Vec3 rgbToHsv(double r, double g, double b, Vec3 out) {
        r /= 255d;
        g /= 255d;
        b /= 255d;

        double max = Math.max(r, Math.max(g, b));
        double min = Math.min(r, Math.min(g, b));

        double h = 0, s, v = max;

        double d = max - min;
        s = max == 0 ? 0 : d / max;

        if (max != min) {
            if (max == r)
                h = (g - b) / d + (g < b ? 6 : 0);
            else if (max == g)
                h = (b - r) / d + 2;
            else if (max == b)
                h = (r - g) / d + 4;
            h /= 6;
        }

        out.set(h, s, v);

        return out;
    }

    public static Vec3 rgbToHsv(double r, double g, double b) {
        return rgbToHsv(r, g, b, new Vec3());
    }

    /**
     * Converts an HSV color value to RGB. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
     * Assumes h, s, and v are contained in the set [0, 1] and
     * returns r, g, and b in the set [0, 255].
     *
     * @param h   The hue
     * @param s   The saturation
     * @param v   The value
     * @param out result rgb, may be ommited
     * @return Array The RGB representation
     */
    public static int hsvToRgb(double h, double s, double v, Vec3 out) {
        double r = 0, g = 0, b = 0;

        int i = (int) Math.floor(h * 6);
        double f = h * 6 - i;
        double p = v * (1 - s);
        double q = v * (1 - f * s);
        double t = v * (1 - (1 - f) * s);

        switch (i % 6) {
            case 0:
                r = v;
                g = t;
                b = p;
                break;
            case 1:
                r = q;
                g = v;
                b = p;
                break;
            case 2:
                r = p;
                g = v;
                b = t;
                break;
            case 3:
                r = p;
                g = q;
                b = v;
                break;
            case 4:
                r = t;
                g = p;
                b = v;
                break;
            case 5:
                r = v;
                g = p;
                b = q;
                break;
        }

        if (out != null)
            out.set(r, g, b);

        return Color.get(r, g, b);
    }

    public static int hsvToRgb(double h, double s, double v) {
        return hsvToRgb(h, s, v, null);
    }

    public static int hslToRgb(double h, double s, double l) {
        return hslToRgb(h, s, l, null);
    }

    /**
     * Blend two colors.
     *
     * @param color1 the first color
     * @param color2 the second color
     * @param mix    the mixing proportion in range 0 to 1
     * @return the blended color
     */
    public static int blend(int color1, int color2, float mix) {
        float mix2 = 1f - mix;
        return Color.get(
                (int) ((((color2 >>> 24) & 0xff) * mix) + (((color1 >>> 24) & 0xff) * mix2)),
                (int) ((((color2 >>> 16) & 0xff) * mix) + (((color1 >>> 16) & 0xff) * mix2)),
                (int) ((((color2 >>> 8) & 0xff) * mix) + (((color1 >>> 8) & 0xff) * mix2)),
                (int) ((((color2 >>> 0) & 0xff) * mix) + (((color1 >>> 0) & 0xff) * mix2))
        );
    }
}
