package org.sunflow.image;

import org.sunflow.math.MathUtils;

public final class Color {

    private float r, g, b;
    public static final RGBSpace NATIVE_SPACE = RGBSpace.SRGB;
    public static final Color BLACK = new Color(0, 0, 0);
    public static final Color WHITE = new Color(1, 1, 1);
    public static final Color RED = new Color(1, 0, 0);
    public static final Color GREEN = new Color(0, 1, 0);
    public static final Color BLUE = new Color(0, 0, 1);
    public static final Color YELLOW = new Color(1, 1, 0);
    public static final Color CYAN = new Color(0, 1, 1);
    public static final Color MAGENTA = new Color(1, 0, 1);
    public static final Color GRAY = new Color(0.5f, 0.5f, 0.5f);

    public static Color black() {
        return new Color();
    }

    public static Color white() {
        return new Color(1, 1, 1);
    }
    private static final float[] EXPONENT = new float[256];

    static {
        EXPONENT[0] = 0;
        for (int i = 1; i < 256; i++) {
            float f = 1.0f;
            int e = i - (128 + 8);
            if (e > 0) {
                for (int j = 0; j < e; j++) {
                    f *= 2.0f;
                }
            } else {
                for (int j = 0; j < -e; j++) {
                    f *= 0.5f;
                }
            }
            EXPONENT[i] = f;
        }
    }

    public Color() {
    }

    public Color(float gray) {
        r = g = b = gray;
    }

    public Color(float r, float g, float b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }

    public Color toNonLinear() {
        r = NATIVE_SPACE.gammaCorrect(r);
        g = NATIVE_SPACE.gammaCorrect(g);
        b = NATIVE_SPACE.gammaCorrect(b);
        return this;
    }

    public Color toLinear() {
        r = NATIVE_SPACE.ungammaCorrect(r);
        g = NATIVE_SPACE.ungammaCorrect(g);
        b = NATIVE_SPACE.ungammaCorrect(b);
        return this;
    }

    public Color(Color c) {
        r = c.r;
        g = c.g;
        b = c.b;
    }

    public Color(int rgb) {
        r = ((rgb >> 16) & 0xFF) / 255.0f;
        g = ((rgb >> 8) & 0xFF) / 255.0f;
        b = (rgb & 0xFF) / 255.0f;
    }

    public Color copy() {
        return new Color(this);
    }

    public final Color set(float r, float g, float b) {
        this.r = r;
        this.g = g;
        this.b = b;
        return this;
    }

    public final Color set(Color c) {
        r = c.r;
        g = c.g;
        b = c.b;
        return this;
    }

    public final Color setRGB(int rgb) {
        r = ((rgb >> 16) & 0xFF) / 255.0f;
        g = ((rgb >> 8) & 0xFF) / 255.0f;
        b = (rgb & 0xFF) / 255.0f;
        return this;
    }

    public final Color setRGBE(int rgbe) {
        float f = EXPONENT[rgbe & 0xFF];
        r = f * ((rgbe >>> 24) + 0.5f);
        g = f * (((rgbe >> 16) & 0xFF) + 0.5f);
        b = f * (((rgbe >> 8) & 0xFF) + 0.5f);
        return this;
    }

    public final boolean isBlack() {
        return r <= 0 && g <= 0 && b <= 0;
    }

    public final float getLuminance() {
        return (0.2989f * r) + (0.5866f * g) + (0.1145f * b);
    }

    public final float getMin() {
        return MathUtils.min(r, g, b);
    }

    public final float getMax() {
        return MathUtils.max(r, g, b);
    }

    public final float getAverage() {
        return (r + g + b) / 3.0f;
    }

    public final float[] getRGB() {
        return new float[]{r, g, b};
    }

    public final int toRGB() {
        int ir = (int) (r * 255 + 0.5);
        int ig = (int) (g * 255 + 0.5);
        int ib = (int) (b * 255 + 0.5);
        ir = MathUtils.clamp(ir, 0, 255);
        ig = MathUtils.clamp(ig, 0, 255);
        ib = MathUtils.clamp(ib, 0, 255);
        return (ir << 16) | (ig << 8) | ib;
    }

    public final int toRGBA(float a) {
        int ir = (int) (r * 255 + 0.5);
        int ig = (int) (g * 255 + 0.5);
        int ib = (int) (b * 255 + 0.5);
        int ia = (int) (a * 255 + 0.5);
        ir = MathUtils.clamp(ir, 0, 255);
        ig = MathUtils.clamp(ig, 0, 255);
        ib = MathUtils.clamp(ib, 0, 255);
        ia = MathUtils.clamp(ia, 0, 255);
        return (ia << 24) | (ir << 16) | (ig << 8) | ib;
    }

    public final int toRGBE() {
        // encode the color into 32bits while preserving HDR using Ward's RGBE
        // technique
        float v = MathUtils.max(r, g, b);
        if (v < 1e-32f) {
            return 0;
        }

        // get mantissa and exponent
        float m = v;
        int e = 0;
        if (v > 1.0f) {
            while (m > 1.0f) {
                m *= 0.5f;
                e++;
            }
        } else if (v <= 0.5f) {
            while (m <= 0.5f) {
                m *= 2.0f;
                e--;
            }
        }
        v = (m * 255.0f) / v;
        int c = (e + 128);
        c |= ((int) (r * v) << 24);
        c |= ((int) (g * v) << 16);
        c |= ((int) (b * v) << 8);
        return c;
    }

    public final Color constrainRGB() {
        // clamp the RGB value to a representable value
        float w = -MathUtils.min(0, r, g, b);
        if (w > 0) {
            r += w;
            g += w;
            b += w;
        }
        return this;
    }

    public final boolean isNan() {
        return Float.isNaN(r) || Float.isNaN(g) || Float.isNaN(b);
    }

    public final boolean isInf() {
        return Float.isInfinite(r) || Float.isInfinite(g) || Float.isInfinite(b);
    }

    public final Color add(Color c) {
        r += c.r;
        g += c.g;
        b += c.b;
        return this;
    }

    public static final Color add(Color c1, Color c2) {
        return Color.add(c1, c2, new Color());
    }

    public static final Color add(Color c1, Color c2, Color dest) {
        dest.r = c1.r + c2.r;
        dest.g = c1.g + c2.g;
        dest.b = c1.b + c2.b;
        return dest;
    }

    public final Color madd(float s, Color c) {
        r += (s * c.r);
        g += (s * c.g);
        b += (s * c.b);
        return this;
    }

    public final Color madd(Color s, Color c) {
        r += s.r * c.r;
        g += s.g * c.g;
        b += s.b * c.b;
        return this;
    }

    public final Color sub(Color c) {
        r -= c.r;
        g -= c.g;
        b -= c.b;
        return this;
    }

    public static final Color sub(Color c1, Color c2) {
        return Color.sub(c1, c2, new Color());
    }

    public static final Color sub(Color c1, Color c2, Color dest) {
        dest.r = c1.r - c2.r;
        dest.g = c1.g - c2.g;
        dest.b = c1.b - c2.b;
        return dest;
    }

    public final Color mul(Color c) {
        r *= c.r;
        g *= c.g;
        b *= c.b;
        return this;
    }

    public static final Color mul(Color c1, Color c2) {
        return Color.mul(c1, c2, new Color());
    }

    public static final Color mul(Color c1, Color c2, Color dest) {
        dest.r = c1.r * c2.r;
        dest.g = c1.g * c2.g;
        dest.b = c1.b * c2.b;
        return dest;
    }

    public final Color mul(float s) {
        r *= s;
        g *= s;
        b *= s;
        return this;
    }

    public static final Color mul(float s, Color c) {
        return Color.mul(s, c, new Color());
    }

    public static final Color mul(float s, Color c, Color dest) {
        dest.r = s * c.r;
        dest.g = s * c.g;
        dest.b = s * c.b;
        return dest;
    }

    public final Color div(Color c) {
        r /= c.r;
        g /= c.g;
        b /= c.b;
        return this;
    }

    public static final Color div(Color c1, Color c2) {
        return Color.div(c1, c2, new Color());
    }

    public static final Color div(Color c1, Color c2, Color dest) {
        dest.r = c1.r / c2.r;
        dest.g = c1.g / c2.g;
        dest.b = c1.b / c2.b;
        return dest;
    }

    public final Color exp() {
        r = (float) Math.exp(r);
        g = (float) Math.exp(g);
        b = (float) Math.exp(b);
        return this;
    }

    public final Color opposite() {
        r = 1 - r;
        g = 1 - g;
        b = 1 - b;
        return this;
    }

    public final Color clamp(float min, float max) {
        r = MathUtils.clamp(r, min, max);
        g = MathUtils.clamp(g, min, max);
        b = MathUtils.clamp(b, min, max);
        return this;
    }

    public static final Color blend(Color c1, Color c2, float b) {
        return blend(c1, c2, b, new Color());
    }

    public static final Color blend(Color c1, Color c2, float b, Color dest) {
        dest.r = (1.0f - b) * c1.r + b * c2.r;
        dest.g = (1.0f - b) * c1.g + b * c2.g;
        dest.b = (1.0f - b) * c1.b + b * c2.b;
        return dest;
    }

    public static final Color blend(Color c1, Color c2, Color b) {
        return blend(c1, c2, b, new Color());
    }

    public static final Color blend(Color c1, Color c2, Color b, Color dest) {
        dest.r = (1.0f - b.r) * c1.r + b.r * c2.r;
        dest.g = (1.0f - b.g) * c1.g + b.g * c2.g;
        dest.b = (1.0f - b.b) * c1.b + b.b * c2.b;
        return dest;
    }

    public static final boolean hasContrast(Color c1, Color c2, float thresh) {
        if (Math.abs(c1.r - c2.r) / (c1.r + c2.r) > thresh) {
            return true;
        }
        if (Math.abs(c1.g - c2.g) / (c1.g + c2.g) > thresh) {
            return true;
        }
        return (Math.abs(c1.b - c2.b) / (c1.b + c2.b) > thresh);
    }

    @Override
    public String toString() {
        return String.format("(%.3f, %.3f, %.3f)", r, g, b);
    }
}