/*
 * Copyright 2017-2020 Pranav Pandey
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.pranavpandey.android.dynamic.utils;

import android.graphics.Color;

import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.core.graphics.ColorUtils;

import java.util.Random;

/**
 * Helper class to change colors dynamically.
 */
public class DynamicColorUtils {

    /**
     * Visible contrast between the two colors.
     */
    private static final float VISIBLE_CONTRAST = 0.35f;

    /**
     * Amount to calculate the contrast color.
     */
    private static final float CONTRAST_FACTOR = 0.65f;

    /**
     * Generate a random rgb color.
     *
     * @return The randomly generated color.
     *
     * @see Random
     * @see Color#HSVToColor(float[])
     */
    public static @ColorInt int getRandomColor() {
        Random random = new Random();
        float hue = (float) random.nextInt(360);
        float saturation = random.nextFloat();
        float lightness = random.nextFloat();

        return ColorUtils.HSLToColor(new float[]{hue, saturation, lightness});
    }

    /**
     * Generate a random rgb color by comparing a given color.
     *
     * @param color The color to compare.
     *
     * @return The randomly generated color.
     */
    public static @ColorInt int getRandomColor(@ColorInt int color) {
        @ColorInt int newColor = getRandomColor();

        if (newColor != color) {
            return newColor;
        } else {
            return getRandomColor();
        }
    }

    /**
     * Adjust alpha of a color according to the given parameter.
     *
     * @param color The color whose alpha to be adjusted.
     * @param factor Factor in float by which adjust the alpha.
     *
     * @return The color with adjusted alpha.
     */
    public static @ColorInt int adjustAlpha(@ColorInt int color, float factor) {
        int alpha = Math.min(255, (int) (Color.alpha(color) * factor));
        int red = Color.red(color);
        int green = Color.green(color);
        int blue = Color.blue(color);

        return Color.argb(alpha, red, green, blue);
    }

    /**
     * Checks whether the color has alpha component.
     *
     * @param color The color to check the alpha component.
     *
     * @return {@code true} if the color has alpha component.
     */
    public static boolean isAlpha(@ColorInt int color) {
        return Color.alpha(color) != 255;
    }

    /**
     * Set alpha to a color.
     *
     * @param color The color whose alpha to be set.
     *
     * @return The color with alpha.
     */
    public static @ColorInt int setAlpha(@ColorInt int color,
            @IntRange(from = 0, to = 255) int alpha) {
        return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
    }

    /**
     * Remove alpha from a color.
     *
     * @param color The color whose alpha to be removed.
     *
     * @return The color without alpha.
     */
    public static @ColorInt int removeAlpha(@ColorInt int color) {
        return Color.rgb(Color.red(color), Color.green(color), Color.blue(color));
    }

    /**
     * Calculate darkness of a color.
     *
     * @param color The color whose darkness to be calculated.
     *
     * @return The darkness of color (less than or equal to 1).
     *         <p>{@code 0} for white and {@code 1} for black.
     */
    public static double getColorDarkness(@ColorInt int color) {
        return 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color)
                + 0.114 * Color.blue(color)) / 255;
    }

    /**
     * Detect light or dark color.
     *
     * @param color The color whose darkness to be calculated.
     *
     * @return {@code true} if color is dark.
     */
    public static boolean isColorDark(@ColorInt int color) {
        return getColorDarkness(color) >= 0.5;
    }

    /**
     * Calculate luma value according to XYZ color space of a color.
     *
     * @param color The color whose XyzLuma to be calculated.
     *
     * @return The luma value according to XYZ color space in the range {@code 0.0 - 1.0}.
     */
    private static float calculateXyzLuma(@ColorInt int color) {
        return (0.2126f * Color.red(color)
                + 0.7152f * Color.green(color)
                + 0.0722f * Color.blue(color)) / 255f;
    }

    /**
     * Lightens a color by a given amount.
     *
     * @param color The color to lighten.
     * @param amount The amount to lighten the color.
     *               <p>{@code 0} will leave the color unchanged.
     *               <p>{@code 1} will make the color completely white.
     *
     * @return The lighter color.
     */
    public static @ColorInt int getLighterColor(@ColorInt int color, float amount) {
        float[] hsv = new float[3];
        Color.colorToHSV(color, hsv);
        if (hsv[2] == 0) {
            hsv[2] = VISIBLE_CONTRAST / 10;
            color = Color.HSVToColor(Color.alpha(color), hsv);
        }

        int red = (int) ((Color.red(color) * (1 - amount) / 255 + amount) * 255);
        int green = (int) ((Color.green(color) * (1 - amount) / 255 + amount) * 255);
        int blue = (int) ((Color.blue(color) * (1 - amount) / 255 + amount) * 255);

        return Color.argb(Color.alpha(color), red, green, blue);
    }

    /**
     * Darkens a color by a given amount.
     *
     * @param color The color to darken.
     * @param amount The amount to darken the color.
     *               <p>{@code 0} will leave the color unchanged.
     *               <p>{@code 1} will make the color completely black.
     *
     * @return The darker color.
     */
    public static @ColorInt int getDarkerColor(@ColorInt int color, float amount) {
        int red = (int) ((Color.red(color) * (1 - amount) / 255) * 255);
        int green = (int) ((Color.green(color) * (1 - amount) / 255) * 255);
        int blue = (int) ((Color.blue(color) * (1 - amount) / 255) * 255);

        return Color.argb(Color.alpha(color), red, green, blue);
    }

    /**
     * Shift a color according to the given parameter.
     * <p>Useful to create different color states.
     *
     * @param color The color to be shifted.
     * @param by The factor in float by which shift the color.
     *
     * @return The shifted color.
     */
    public static @ColorInt int shiftColor(@ColorInt int color,
            @FloatRange(from = 0.0f, to = 2.0f) float by) {
        int alpha = Color.alpha(color);
        if (by == 1f) {
            return color;
        }

        float[] hsv = new float[3];
        Color.colorToHSV(color, hsv);
        hsv[2] *= by;
        return Color.HSVToColor(alpha, hsv);
    }

    /**
     * Shift a color according to the supplied parameters.
     * <p>The shifted color will be lighter for a dark color and vice versa.
     *
     * @param color The color to be shifted.
     * @param shiftLightBy The factor in float by which shift the light color.
     * @param shiftDarkBy The factor in float by which shift the dark color.
     *
     * @return The shifted color.
     */
    public static @ColorInt int shiftColor(@ColorInt int color,
            @FloatRange(from = 0.0f, to = 2.0f) float shiftLightBy,
            @FloatRange(from = 0.0f, to = 2.0f) float shiftDarkBy) {
        return shiftColor(color, isColorDark(color) ? shiftDarkBy : shiftLightBy);
    }

    /**
     * Calculate accent color based on the given color for android theme generation.
     * <p>Still in beta so, sometimes may be inaccurate colors.
     *
     * @param color The color whose accent color to be calculated.
     *
     * @return The accent color based on the given color.
     */
    public static @ColorInt int getAccentColor(@ColorInt int color) {
        int finalColor;
        int a = Color.alpha(color);
        int r = Color.red(color);
        int g = Color.green(color);
        int b = Color.blue(color);
        double Y = ((r * 299) + (g * 587) + (b * 114)) / 1000d;

        int rc = b ^ 0x55;
        int gc = g & 0xFA;
        int bc = r ^ 0x55;

        finalColor = Color.argb(a, rc, gc, bc);
        int r1 = Color.red(finalColor);
        int g1 = Color.green(finalColor);
        int b1 = Color.blue(finalColor);
        double YC = ((r1 * 299) + (g1 * 587) + (b1 * 114)) / 1000d;

        int CD = (Math.max(r, r1) - Math.min(r, r1)) + (Math.max(g, g1)
                - Math.min(g, g1)) + (Math.max(b, b1) - Math.min(b, b1));
        if ((Y - YC <= 50) && CD <= 200) {
            rc = b ^ 0xFA;
            gc = g & 0x55;
            bc = r ^ 0x55;
        }

        finalColor = Color.argb(a, rc, gc, bc);
        return finalColor;
    }

    /**
     * Calculate color contrast difference between two colors based
     * on luma value according to XYZ color space.
     *
     * @param color1 The first color to calculate the contrast difference.
     * @param color2 The second color to calculate the contrast difference.
     *
     * @return The color contrast between the two colors.
     *
     * @see #calculateXyzLuma(int)
     */
    public static float calculateContrast(@ColorInt int color1, @ColorInt int color2) {
        return Math.abs(calculateXyzLuma(color1) - calculateXyzLuma(color2));
    }

    /**
     * Calculate tint based on a given color for better readability.
     *
     * @param color The color whose tint to be calculated.
     *
     * @return The tint of the given color.
     */
    public static @ColorInt int getTintColor(@ColorInt int color) {
        return getContrastColor(color, color);
    }

    /**
     * Lightens or darkens a color by a given amount.
     *
     * @param color The color to be lighten or darken.
     * @param lightenBy The amount to lighten the color.
     *                  <p>{@code 0} will leave the color unchanged.
     *                  <p>{@code 1} will make the color completely white.
     * @param darkenBy The amount to darken the color.
     *                 <p>{@code 0} will leave the color unchanged.
     *                 <p>{@code 1} will make the color completely black.
     *
     * @return The state color.
     */
    public static @ColorInt int getStateColor(@ColorInt int color,
            @FloatRange(from = 0.0f, to = 1.0f) float lightenBy,
            @FloatRange(from = 0.0f, to = 1.0f) float darkenBy) {
        return isColorDark(color) ? getLighterColor(color, lightenBy)
                : getDarkerColor(color, darkenBy);
    }

    /**
     * Calculate contrast of a color based on the given base color so that it will always
     * be visible on top of the base color.
     *
     * @param color The color whose contrast to be calculated.
     * @param contrastWith The background color to calculate the contrast.
     *
     * @return The contrast of the given color according to the base color.
     */
    public static @ColorInt int getContrastColor(@ColorInt int color, @ColorInt int contrastWith) {
        float contrast = calculateContrast(color, contrastWith);
        if (contrast < VISIBLE_CONTRAST) {
            if (isColorDark(contrastWith)) {
                return getLighterColor(color,
                        Math.max(VISIBLE_CONTRAST + contrast, CONTRAST_FACTOR));
            } else {
                return getDarkerColor(color,
                        Math.max(VISIBLE_CONTRAST + contrast, CONTRAST_FACTOR));
            }
        }

        return color;
    }

    /**
     * Calculate less visible color of a given color.
     * <p>Useful to create unselected or disabled color states.
     *
     * @param color The color whose less visible color to be calculated.
     *
     * @return The less visible color by shifting the color.
     */
    public static @ColorInt int getLessVisibleColor(@ColorInt int color) {
        return shiftColor(color, isColorDark(color) ? 0.6f : 1.6f);
    }

    /**
     * Get hexadecimal string from the color integer.
     *
     * @param color The color to get the hex code.
     * @param includeAlpha {@code true} to include alpha in the string.
     * @param includeHash {@code true} to include {@code #} in the string.
     *
     * @return The hexadecimal string equivalent of the supplied color integer.
     */
    public static String getColorString(@ColorInt int color, boolean includeAlpha,
            boolean includeHash) {
        String colorString;
        if (includeAlpha) {
            colorString = String.format("%08X", color);
        } else {
            colorString = String.format("%06X", 0xFFFFFF & color);
        }

        if (includeHash) {
            colorString = "#" + colorString;
        }

        return colorString;
    }
}