package codechicken.lib.vec;

import codechicken.lib.math.MathHelper;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class Rotation extends Transformation
{
    /**
     * Clockwise pi/2 about y looking down
     */
    public static Transformation[] quarterRotations = new Transformation[]{
        new RedundantTransformation(),
        new VariableTransformation(new Matrix4(0, 0,-1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1)){
            @Override public void apply(Vector3 vec){
                double d1 = vec.x; double d2 = vec.z;
                vec.x = -d2; vec.z = d1;
            } @Override public Transformation inverse(){
                return quarterRotations[3];
            }},
        new VariableTransformation(new Matrix4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1)){
            @Override public void apply(Vector3 vec){
                vec.x = -vec.x; vec.z = -vec.z;
            } @Override public Transformation inverse(){
                return this;
            }},
        new VariableTransformation(new Matrix4(0, 0, 1, 0, 0, 1, 0, 0,-1, 0, 0, 0, 0, 0, 0, 1)){
            @Override public void apply(Vector3 vec){
                double d1 = vec.x; double d2 = vec.z;
                vec.x = d2; vec.z = -d1;
            } @Override public Transformation inverse(){
                return quarterRotations[1];
            }}
        };
    
    public static Transformation[] sideRotations = new Transformation[]{
        new RedundantTransformation(),
        new VariableTransformation(new Matrix4(1, 0, 0, 0, 0,-1, 0, 0, 0, 0,-1, 0, 0, 0, 0, 1)){
            @Override public void apply(Vector3 vec){
                vec.y = -vec.y; vec.z = -vec.z;
            } @Override public Transformation inverse(){
                return this;
            }},
        new VariableTransformation(new Matrix4(1, 0, 0, 0, 0, 0,-1, 0, 0, 1, 0, 0, 0, 0, 0, 1)){
            @Override public void apply(Vector3 vec){
                double d1 = vec.y; double d2 = vec.z;
                vec.y = -d2; vec.z = d1;
            } @Override public Transformation inverse(){
                return sideRotations[3];
            }},
        new VariableTransformation(new Matrix4(1, 0, 0, 0, 0, 0, 1, 0, 0,-1, 0, 0, 0, 0, 0, 1)){
            @Override public void apply(Vector3 vec){
                double d1 = vec.y; double d2 = vec.z;
                vec.y = d2; vec.z = -d1;
            } @Override public Transformation inverse(){
                return sideRotations[2];
            }},
        new VariableTransformation(new Matrix4(0, 1, 0, 0,-1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)){
            @Override public void apply(Vector3 vec){
                double d0 = vec.x; double d1 = vec.y;
                vec.x = d1; vec.y = -d0;
            } @Override public Transformation inverse(){
                return sideRotations[5];
            }},
        new VariableTransformation(new Matrix4(0,-1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)){
            @Override public void apply(Vector3 vec){
                double d0 = vec.x; double d1 = vec.y;
                vec.x = -d1; vec.y = d0;
            } @Override public Transformation inverse(){
                return sideRotations[4];
            }}
        };
    
    public static Vector3[] axes = new Vector3[]{
        new Vector3( 0,-1, 0),
        new Vector3( 0, 1, 0),
        new Vector3( 0, 0,-1),
        new Vector3( 0, 0, 1),
        new Vector3(-1, 0, 0),
        new Vector3( 1, 0, 0)};
    
    public static int[] sideRotMap = new int[]{
        3,4,2,5,
        3,5,2,4,
        1,5,0,4,
        1,4,0,5,
        1,2,0,3,
        1,3,0,2};
    
    public static int[] rotSideMap = new int[]{
        -1,-1, 2, 0, 1, 3,
        -1,-1, 2, 0, 3, 1,
         2, 0,-1,-1, 3, 1,
         2, 0,-1,-1, 1, 3,
         2, 0, 1, 3,-1,-1,
         2, 0, 3, 1,-1,-1};
    
    /**
     * Rotate pi/2 * this offset for [side] about y axis before rotating to the side for the rotation indicies to line up
     */
    public static int[] sideRotOffsets = new int[]{0, 2, 2, 0, 1, 3};

    public static int rotateSide(int s, int r) {
        return sideRotMap[s << 2 | r];
    }

    /**
     * Reverse of rotateSide
     */
    public static int rotationTo(int s1, int s2) {
        if ((s1 & 6) == (s2 & 6))
            throw new IllegalArgumentException("Faces " + s1 + " and " + s2 + " are opposites");
        return rotSideMap[s1 * 6 + s2];
    }

    /**
     * @param player The placing player, used for obtaining the look vector
     * @param side The side of the block being placed on
     * @return The rotation for the face == side^1
     */
    public static int getSidedRotation(EntityPlayer player, int side) {
        Vector3 look = new Vector3(player.getLook(1));
        double max = 0;
        int maxr = 0;
        for (int r = 0; r < 4; r++) {
            Vector3 axis = Rotation.axes[rotateSide(side ^ 1, r)];
            double d = look.scalarProject(axis);
            if (d > max) {
                max = d;
                maxr = r;
            }
        }
        return maxr;
    }

    /**
     * @return The rotation quat for side 0 and rotation 0 to side s with rotation r
     */
    public static Transformation sideOrientation(int s, int r) {
        return quarterRotations[(r + sideRotOffsets[s]) % 4].with(sideRotations[s]);
    }

    /**
     * @param entity The placing entity, used for obtaining the look vector
     * @return The side towards which the entity is most directly looking.
     */
    public static int getSideFromLookAngle(EntityLivingBase entity) {
        Vector3 look = new Vector3(entity.getLook(1));
        double max = 0;
        int maxs = 0;
        for (int s = 0; s < 6; s++) {
            double d = look.scalarProject(axes[s]);
            if (d > max) {
                max = d;
                maxs = s;
            }
        }
        return maxs;
    }

    public double angle;
    public Vector3 axis;

    private Quat quat;

    public Rotation(double angle, Vector3 axis) {
        this.angle = angle;
        this.axis = axis;
    }

    public Rotation(double angle, double x, double y, double z) {
        this(angle, new Vector3(x, y, z));
    }

    public Rotation(Quat quat) {
        this.quat = quat;

        angle = Math.acos(quat.s) * 2;
        if (angle == 0) {
            axis = new Vector3(0, 1, 0);
        } else {
            double sa = Math.sin(angle * 0.5);
            axis = new Vector3(quat.x / sa, quat.y / sa, quat.z / sa);
        }
    }

    @Override
    public void apply(Vector3 vec) {
        if (quat == null)
            quat = Quat.aroundAxis(axis, angle);

        vec.rotate(quat);
    }

    @Override
    public void applyN(Vector3 normal) {
        apply(normal);
    }

    @Override
    public void apply(Matrix4 mat) {
        mat.rotate(angle, axis);
    }

    public Quat toQuat() {
        if (quat == null)
            quat = Quat.aroundAxis(axis, angle);
        return quat;
    }
    
    @Override
    @SideOnly(Side.CLIENT)
    public void glApply() {
        GlStateManager.rotate((float) (angle * MathHelper.todeg), (float) axis.x, (float) axis.y, (float) axis.z);
    }

    @Override
    public Transformation inverse() {
        return new Rotation(-angle, axis);
    }

    @Override
    public Transformation merge(Transformation next) {
        if(next instanceof Rotation) {
            Rotation r = (Rotation)next;
            if(r.axis.equalsT(axis))
                return new Rotation(angle+r.angle, axis);
            
            return new Rotation(toQuat().copy().multiply(r.toQuat()));
        }
        
        return null;
    }
    
    @Override
    public boolean isRedundant() {
        return MathHelper.between(-1E-5, angle, 1E-5);
    }

    @Override
    public String toString() {
        MathContext cont = new MathContext(4, RoundingMode.HALF_UP);
        return "Rotation(" + new BigDecimal(angle, cont) + ", " + new BigDecimal(axis.x, cont) + ", " +
                new BigDecimal(axis.y, cont) + ", " + new BigDecimal(axis.z, cont) + ")";
    }
}