package codechicken.lib.render;

import codechicken.lib.colour.ColourRGBA;
import codechicken.lib.lighting.LC;
import codechicken.lib.lighting.LightMatrix;
import codechicken.lib.util.Copyable;
import codechicken.lib.vec.Rotation;
import codechicken.lib.vec.Transformation;
import codechicken.lib.vec.Vector3;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.WorldRenderer;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.renderer.vertex.VertexFormat;
import net.minecraft.util.BlockPos;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.IBlockAccess;

import java.util.ArrayList;

/**
 * The core of the CodeChickenLib render system.
 * Rendering operations are written to avoid object allocations by reusing static variables.
 */
public class CCRenderState
{
    private static int nextOperationIndex;

    public static int registerOperation() {
        return nextOperationIndex++;
    }

    public static int operationCount() {
        return nextOperationIndex;
    }

    /**
     * Represents an operation to be run for each vertex that operates on and modifies the current state
     */
    public static interface IVertexOperation
    {
        /**
         * Load any required references and add dependencies to the pipeline based on the current model (may be null)
         * Return false if this operation is redundant in the pipeline with the given model
         */
        public boolean load();

        /**
         * Perform the operation on the current render state
         */
        public void operate();

        /**
         * Get the unique id representing this type of operation. Duplicate operation IDs within the pipeline may have unexpected results.
         * ID shoulld be obtained from CCRenderState.registerOperation() and stored in a static variable
         */
        public int operationID();
    }

    private static ArrayList<VertexAttribute<?>> vertexAttributes = new ArrayList<VertexAttribute<?>>();
    private static int registerVertexAttribute(VertexAttribute<?> attr) {
        vertexAttributes.add(attr);
        return vertexAttributes.size()-1;
    }

    public static VertexAttribute<?> getAttribute(int index) {
        return vertexAttributes.get(index);
    }

    /**
     * Management class for a vertex attrute such as colour, normal etc
     * This class should handle the loading of the attrute from an array provided by IVertexSource.getAttributes or the computation of this attrute from others
     * @param <T> The array type for this attrute eg. int[], Vector3[]
     */
    public static abstract class VertexAttribute<T> implements IVertexOperation
    {
        public final int attributeIndex = registerVertexAttribute(this);
        private final int operationIndex = registerOperation();
        /**
         * Set to true when the attrute is part of the pipeline. Should only be managed by CCRenderState when constructing the pipeline
         */
        public boolean active = false;

        /**
         * Construct a new array for storage of vertex attrutes in a model
         */
        public abstract T newArray(int length);

        @Override
        public int operationID() {
            return operationIndex;
        }
    }

    public static void arrayCopy(Object src, int srcPos, Object dst, int destPos, int length) {
        System.arraycopy(src, srcPos, dst, destPos, length);
        if(dst instanceof Copyable[]) {
            Object[] oa = (Object[])dst;
            Copyable<Object>[] c = (Copyable[])dst;
            for(int i = destPos; i < destPos+length; i++)
                if(c[i] != null)
                    oa[i] = c[i].copy();
        }
    }

    public static <T> T copyOf(VertexAttribute<T> attr, T src, int length) {
        T dst = attr.newArray(length);
        arrayCopy(src, 0, dst, 0, ((Object[])src).length);
        return dst;
    }

    public static interface IVertexSource
    {
        public Vertex5[] getVertices();

        /**
         * Gets an array of vertex attrutes
         * @param attr The vertex attrute to get
         * @param <T> The attrute array type
         * @return An array, or null if not computed
         */
        public <T> T getAttributes(VertexAttribute<T> attr);

        /**
         * @return True if the specified attrute is provided by this model, either by returning an array from getAttributes or by setting the state in prepareVertex
         */
        public boolean hasAttribute(VertexAttribute<?> attr);

        /**
         * Callback to set CCRenderState for a vertex before the pipeline runs
         */
        public void prepareVertex();
    }

    public static VertexAttribute<Vector3[]> normalAttrib = new VertexAttribute<Vector3[]>() {
        private Vector3[] normalRef;

        @Override
        public Vector3[] newArray(int length) {
            return new Vector3[length];
        }

        @Override
        public boolean load() {
            normalRef = model.getAttributes(this);
            if(model.hasAttribute(this))
                return normalRef != null;

            if(model.hasAttribute(sideAttrib)) {
                pipeline.addDependency(sideAttrib);
                return true;
            }
            throw new IllegalStateException("Normals requested but neither normal or side attrutes are provided by the model");
        }

        @Override
        public void operate() {
            if(normalRef != null)
                setNormal(normalRef[vertexIndex]);
            else
                setNormal(Rotation.axes[side]);
        }
    };
    public static VertexAttribute<int[]> colourAttrib = new VertexAttribute<int[]>() {
        private int[] colourRef;

        @Override
        public int[] newArray(int length) {
            return new int[length];
        }

        @Override
        public boolean load() {
            colourRef = model.getAttributes(this);
            return colourRef != null || !model.hasAttribute(this);
        }

        @Override
        public void operate() {
            if(colourRef != null)
                setColour(ColourRGBA.multiply(baseColour, colourRef[vertexIndex]));
            else
                setColour(baseColour);
        }
    };
    public static VertexAttribute<int[]> lightingAttrib = new VertexAttribute<int[]>() {
        private int[] colourRef;

        @Override
        public int[] newArray(int length) {
            return new int[length];
        }

        @Override
        public boolean load() {
            if(!computeLighting || !useColour || !model.hasAttribute(this))
                return false;

            colourRef = model.getAttributes(this);
            if(colourRef != null) {
                pipeline.addDependency(colourAttrib);
                return true;
            }
            return false;
        }

        @Override
        public void operate() {
            setColour(ColourRGBA.multiply(colour, colourRef[vertexIndex]));
        }
    };
    public static VertexAttribute<int[]> sideAttrib = new VertexAttribute<int[]>() {
        private int[] sideRef;

        @Override
        public int[] newArray(int length) {
            return new int[length];
        }

        @Override
        public boolean load() {
            sideRef = model.getAttributes(this);
            if(model.hasAttribute(this))
                return sideRef != null;

            pipeline.addDependency(normalAttrib);
            return true;
        }

        @Override
        public void operate() {
            if(sideRef != null)
                side = sideRef[vertexIndex];
            else
                side = CCModel.findSide(normal);
        }
    };
    /**
     * Uses the position of the lightmatrix to compute LC if not provided
     */
    public static VertexAttribute<LC[]> lightCoordAttrib = new VertexAttribute<LC[]>() {
        private LC[] lcRef;
        private Vector3 vec = new Vector3();//for computation
        private Vector3 pos = new Vector3();

        @Override
        public LC[] newArray(int length) {
            return new LC[length];
        }

        @Override
        public boolean load() {
            lcRef = model.getAttributes(this);
            if(model.hasAttribute(this))
                return lcRef != null;

            pos.set(lightMatrix.pos.x, lightMatrix.pos.y, lightMatrix.pos.z);
            pipeline.addDependency(sideAttrib);
            pipeline.addRequirement(Transformation.operationIndex);
            return true;
        }

        @Override
        public void operate() {
            if(lcRef != null)
                lc.set(lcRef[vertexIndex]);
            else
                lc.compute(vec.set(vert.vec).sub(pos), side);
        }
    };

    //pipeline state
    public static IVertexSource model;
    public static int firstVertexIndex;
    public static int lastVertexIndex;
    public static int vertexIndex;
    public static CCRenderPipeline pipeline = new CCRenderPipeline();

    //context
    public static int baseColour;
    public static int alphaOverride;
    public static boolean useNormals;
    public static boolean computeLighting;
    public static boolean useColour;
    public static LightMatrix lightMatrix = new LightMatrix();

    //vertex outputs
    public static Vertex5 vert = new Vertex5();
    public static boolean hasNormal;
    public static Vector3 normal = new Vector3();
    public static boolean hasColour;
    public static int colour;
    public static boolean hasBrightness;
    public static int brightness;

    //attrute storage
    public static int side;
    public static LC lc = new LC();

    public static void reset() {
        model = null;
        pipeline.reset();
        useNormals = hasNormal = hasBrightness = hasColour = false;
        useColour = computeLighting = true;
        baseColour = alphaOverride = -1;
    }

    public static void setPipeline(IVertexOperation... ops) {
        pipeline.setPipeline(ops);
    }

    public static void setPipeline(IVertexSource model, int start, int end, IVertexOperation... ops) {
        pipeline.reset();
        setModel(model, start, end);
        pipeline.setPipeline(ops);
    }

    public static void bindModel(IVertexSource model) {
        if(CCRenderState.model != model) {
            CCRenderState.model = model;
            pipeline.rebuild();
        }
    }

    public static void setModel(IVertexSource source) {
        setModel(source, 0, source.getVertices().length);
    }

    public static void setModel(IVertexSource source, int start, int end) {
        bindModel(source);
        setVertexRange(start, end);
    }

    public static void setVertexRange(int start, int end) {
        firstVertexIndex = start;
        lastVertexIndex = end;
    }

    public static void render(IVertexOperation... ops) {
        setPipeline(ops);
        render();
    }

    public static void render() {
        Vertex5[] verts = model.getVertices();
        for(vertexIndex = firstVertexIndex; vertexIndex < lastVertexIndex; vertexIndex++) {
            model.prepareVertex();
            vert.set(verts[vertexIndex]);
            runPipeline();
            writeVert();
        }
    }

    public static void runPipeline() {
        pipeline.operate();
    }

    public static void writeVert() {
        WorldRenderer r = Tessellator.getInstance().getWorldRenderer();
        
        if(hasNormal)
            r.normal((float) normal.x, (float) normal.y, (float) normal.z);
        if(hasColour)
            r.color(colour>>>24, colour>>16 & 0xFF, colour>>8 & 0xFF, alphaOverride >= 0 ? alphaOverride : colour & 0xFF);
        if(hasBrightness)
            r.lightmap(brightness >> 16 & 65535, brightness & 65535);
    }

    public static void setNormal(double x, double y, double z) {
        hasNormal = true;
        normal.set(x, y, z);
    }

    public static void setNormal(Vector3 n) {
        hasNormal = true;
        normal.set(n);
    }

    public static void setColour(int c) {
        hasColour = true;
        colour = c;
    }

    public static void setBrightness(int b) {
        hasBrightness = true;
        brightness = b;
    }

    public static void setBrightness(IBlockAccess world, BlockPos pos) {
        setBrightness(world.getBlockState(pos).getBlock().getMixedBrightnessForBlock(world, pos));
    }

    public static void pullLightmap() {
        setBrightness((int)OpenGlHelper.lastBrightnessY << 16 | (int)OpenGlHelper.lastBrightnessX);
    }

    public static void pushLightmap() {
        OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, brightness & 0xFFFF, brightness >>> 16);
    }

    /**
     * Compact helper for setting dynamic rendering context. Uses normals and doesn't compute lighting
     */
    public static void setDynamic() {
        useNormals = true;
        computeLighting = false;
    }

    public static void changeTexture(String texture) {
        changeTexture(new ResourceLocation(texture));
    }

    public static void changeTexture(ResourceLocation texture) {
        Minecraft.getMinecraft().renderEngine.bindTexture(texture);
    }

    public static WorldRenderer startDrawing() {
        return startDrawing(7, DefaultVertexFormats.POSITION_TEX);
    }

    public static WorldRenderer startDrawing(int mode, VertexFormat format) {
        WorldRenderer r = Tessellator.getInstance().getWorldRenderer();
        r.begin(mode, format);
        if(hasColour)
            r.color(colour>>>24, colour>>16 & 0xFF, colour>>8 & 0xFF, alphaOverride >= 0 ? alphaOverride : colour & 0xFF);
        if(hasBrightness)
            r.lightmap(brightness >> 16 & 65535, brightness & 65535);
        return r;
    }

    public static void draw() {
        Tessellator.getInstance().draw();
    }
}