package twilightforest.client.texture;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.util.ResourceLocation;
import twilightforest.TwilightForestMod;

import java.util.Arrays;
import java.util.Collection;
import java.util.function.Function;

import static net.minecraft.util.math.MathHelper.sqrt;

public class GradientMappedTexture extends TextureAtlasSprite {
    private final ResourceLocation textureDependency;

    private boolean shouldNormalize;
    private final GradientNode[] GRADIENT_MAP;
    private float minimumValue;
    private float maximumValue;

    public GradientMappedTexture(ResourceLocation textureDependency, ResourceLocation spriteName, boolean shouldNormalize, GradientNode[] gradient_map) {
        super(spriteName.toString());
        this.textureDependency = textureDependency;
        this.GRADIENT_MAP = gradient_map;
        this.shouldNormalize = shouldNormalize;
    }

    @Override
    public boolean hasCustomLoader(IResourceManager manager, ResourceLocation location) {
        return true;
    }

    @Override
    public Collection<ResourceLocation> getDependencies() {
        return ImmutableList.of(textureDependency);
    }

    @Override
    public boolean load(IResourceManager manager, ResourceLocation location, Function<ResourceLocation, TextureAtlasSprite> textureGetter) {
        final TextureAtlasSprite sprite = textureGetter.apply(textureDependency);

        // Copy all coord data from texture we're mapping
        copyFrom(sprite);
        this.animationMetadata = sprite.animationMetadata;

        this.framesTextureData = Lists.newArrayList();

        int minimumValue = 255;
        int maximumValue = 0;

        // Copy all textures from texture dependency
        for (int i = 0; i < sprite.getFrameCount(); i++) {
            // getFrameTextureData() is oddly named; the param passed in is the frame index
            // Returned is an array of mipmaps of each actual texture, from largest to smallest mips.
            final int[][] textureFrom = sprite.getFrameTextureData(i);
            int [][] textureTo = new int[textureFrom.length][];

            for (int j = 0; j < textureFrom.length; j++) {
                textureTo[j] = Arrays.copyOf(textureFrom[j], textureFrom[j].length);
            }

            // Graying out textures will usually produce float ranges that are only a fragment the 0.0 -> 1.0 range.
            // Resulting grayscale texture will not fully capture the entire gradient map which are designed for
            // the 0.0 -> 1.0 range. Test all animation frames for the darkest and lightest value.
            if (this.shouldNormalize) {
                for (int pixel : textureTo[0]) {
                    if ((pixel >> 24 & 0xFF) == 0) continue; // Fully transparent pixels should not be considered

                    minimumValue = Math.min(minimumValue, getPerceptualBrightness(pixel));
                    maximumValue = Math.max(maximumValue, getPerceptualBrightness(pixel));
                }
            }

            this.framesTextureData.add(i, textureTo);
        }

        if (shouldNormalize) {
            if (minimumValue > maximumValue) {
                this.minimumValue = maximumValue / 255f;
                this.maximumValue = minimumValue / 255f;
            } else {
                this.minimumValue = minimumValue / 255f;
                this.maximumValue = maximumValue / 255f;
            }

            // foreach iterates 'texture frames' in framesTextureData, l iterates each mipmap, j iterates each pixel
            for (int[][] texture : framesTextureData) {
                for (int l = 0; l < texture.length; l++) {
                    for (int j = 0; j < texture[l].length; j++) {
                        texture[l][j] = getGradient(texture[l][j], GRADIENT_MAP, this.minimumValue, this.maximumValue);
                    }
                }
            }

            TwilightForestMod.LOGGER.debug("Autogenerated {} from {} with min value {} and max value {}", this.getIconName(), this.textureDependency, this.minimumValue, this.maximumValue);
        } else {
            // foreach iterates 'texture frames', l iterates each mipmap, j iterates each pixel
            for (int[][] texture : framesTextureData) {
                for (int l = 0; l < texture.length; l++) {
                    for (int j = 0; j < texture[l].length; j++) {
                        texture[l][j] = getGradient(texture[l][j], GRADIENT_MAP, 0.0f, 1.0f);
                    }
                }
            }

            TwilightForestMod.LOGGER.debug("Autogenerated {} from {}", this.getIconName(), this.textureDependency);
        }

        return false;
    }

    public static int getGradient(int packedColor, GradientNode[] gradientMap, float minimumValue, float maximumValue) {
        int a = packedColor >> 24 & 0xFF;

        if (a == 0) {
            return packedColor; // Full alpha no color
        }

        // Make a gray from the colors and use it as a gradient map coordinate
        float gray = normalize(getPerceptualBrightness(packedColor) / 255.0F, minimumValue, maximumValue);

        int rTo = 0, gTo = 0, bTo = 0;

        if (gray <= gradientMap[0].node) {
            // Coord gray matches or is located before first gradient transition
            rTo = red  (gradientMap[0].color);
            gTo = green(gradientMap[0].color);
            bTo = blue (gradientMap[0].color);
        } else if (gray >= gradientMap[gradientMap.length-1].node) {
            // Coord gray matches or is located after last gradient transition
            int i = gradientMap[gradientMap.length-1].color;

            rTo = red  (i);
            gTo = green(i);
            bTo = blue (i);
        } else {
            for (int i = 0; i < gradientMap.length - 1; i++) {
                if (gray == gradientMap[i].node) {
                    // Coord gray matches a gradient node
                    rTo = red  (gradientMap[i].color);
                    gTo = green(gradientMap[i].color);
                    bTo = blue (gradientMap[i].color);
                } else if (gray >= gradientMap[i].node && gray <= gradientMap[i+1].node) {
                    return interpolateColors(normalize(gray, gradientMap[i].node, gradientMap[i+1].node), gradientMap[i].color, gradientMap[i+1].color, a);
                }
            }
        }

        return getColorFromARGB(rTo, gTo, bTo, a);
    }

    public static int interpolateColors(float placement, int color1, int color2, int alpha) {
        int r1 = red  (color1);
        int g1 = green(color1);
        int b1 = blue (color1);
        int r2 = red  (color2);
        int g2 = green(color2);
        int b2 = blue (color2);

        return getColorFromARGB(
                parabolicInterpolation(placement, r1, r2),
                parabolicInterpolation(placement, g1, g2),
                parabolicInterpolation(placement, b1, b2), alpha);
    }

    // Normally, a regular mathematical interpolation function would be linear, except these are colors.
    // Colors being linearly interpolated would produce colors that appear a little more 'dull' than
    // interpolating parabolically which makes the transition appear more vibrant.
    public static int parabolicInterpolation(float placement, float v1, float v2) {
        return (int) sqrt(((v1 * v1) * (1.0f - placement)) + ((v2 * v2) * placement));
    }

    public static float normalize(float valueIn, float minimumValue, float maximumValue) {
        return (valueIn - minimumValue) / (maximumValue - minimumValue);
    }

    static int red(int pixel) {
        return pixel >> 16 & 0xFF;
    }

    static int green(int pixel) {
        return pixel >> 8 & 0xFF;
    }

    static int blue(int pixel) {
        return pixel & 0xFF;
    }

    static int getColorFromARGB(int r, int g, int b, int a) {
        return (((a << 8) + r << 8) + g << 8) + b;
    }

    // getPerceptualBrightness will produce an accurate gray value from the colors.
    // Thanks TTFTCUTS! :)
    private static int getPerceptualBrightness(int col) {
        return getPerceptualBrightness(red(col) / 255.0, green(col) / 255.0, blue(col) / 255.0);
    }

    private static int getPerceptualBrightness(double r, double g, double b) {
        return (int) (Math.sqrt(0.241 * r * r + 0.691 * g * g + 0.068 * b * b) * 255);
    }
}
