package makeo.gadomancy.common.entities;

import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
import io.netty.buffer.ByteBuf;
import makeo.gadomancy.client.effect.EffectHandler;
import makeo.gadomancy.client.effect.fx.EntityFXFlowPolicy;
import makeo.gadomancy.client.effect.fx.FXFlow;
import makeo.gadomancy.client.effect.fx.Orbital;
import makeo.gadomancy.common.data.config.ModConfig;
import makeo.gadomancy.common.items.ItemAuraCore;
import makeo.gadomancy.common.network.PacketHandler;
import makeo.gadomancy.common.network.packets.PacketStartAnimation;
import makeo.gadomancy.common.registration.RegisteredItems;
import makeo.gadomancy.common.utils.MiscUtils;
import makeo.gadomancy.common.utils.PrimalAspectList;
import makeo.gadomancy.common.utils.Vector3;
import net.minecraft.block.Block;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.init.Blocks;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.ChunkCoordinates;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.util.WeightedRandom;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.research.ScanResult;
import thaumcraft.common.lib.research.ScanManager;
import thaumcraft.common.lib.utils.BlockUtils;

import java.awt.Color;
import java.util.*;

/**
 * This class is part of the Gadomancy Mod
 * Gadomancy is Open Source and distributed under the
 * GNU LESSER GENERAL PUBLIC LICENSE
 * for more read the LICENSE file
 *
 * Created by HellFirePvP @ 16.11.2015 14:42
 */
public class EntityAuraCore extends EntityItem implements IEntityAdditionalSpawnData {

    private static final String SPLIT = ";";

    private static final int PRE_GATHER_EFFECT_LENGTH = 50;
    private static final int GATHER_EFFECT_LENGTH = 500; //20 + 430
    private static final int GATHER_RANGE = 4;
    private static final int CLUSTER_WEIGHT = 10; //Counts as 10 'blocks' 'scanned' with the given aspect.
    public static final int CLUSTER_RANGE = 10; //Defines how close the clicked cluster has to be.
    private static final int REQUIRED_BLOCKS = (int) Math.round(Math.pow(GATHER_RANGE*2+1, 3) * 0.15);

    public PrimalAspectList internalAuraList = new PrimalAspectList();
    public Orbital auraOrbital = null;

    //Effect stuff ResidentSleeper
    private Aspect[] effectAspects = new Aspect[6];
    private Orbital.OrbitalRenderProperties[] effectProperties = new Orbital.OrbitalRenderProperties[6];
    private FXFlow[] flows = new FXFlow[6];

    private String oldAspectDataSent = null;
    public ChunkCoordinates activationLocation = null;

    private int blockCount;

    public EntityAuraCore(World world) {
        super(world);
    }

    public EntityAuraCore(World world, double x, double y, double z, ItemStack stack, ChunkCoordinates startingCoords, Aspect[] aspects) {
        super(world, x, y, z, stack);
        this.activationLocation = startingCoords;
        if(aspects.length == 1) {
            this.internalAuraList.add(aspects[0], CLUSTER_WEIGHT * 6);
        } else {
            for(Aspect a : aspects) {
                if(a == null) continue;
                this.internalAuraList.add(a, CLUSTER_WEIGHT);
            }
        }
        sendAspectData(electParliament());

        initGathering();
    }

    @Override
    protected void entityInit() {
        super.entityInit();
        getDataWatcher().addObjectByDataType(ModConfig.entityAuraCoreDatawatcherAspectsId, 4);

        getDataWatcher().updateObject(ModConfig.entityAuraCoreDatawatcherAspectsId, "");
    }

    @Override
    public void onUpdate() {
        super.onUpdate();

        if(auraOrbital == null && !MiscUtils.getPositionVector(this).equals(Vector3.ZERO)
                && worldObj.isRemote) {
            auraOrbital = new Orbital(MiscUtils.getPositionVector(this), worldObj);
        }
        if(auraOrbital != null && worldObj.isRemote) {
            if(!auraOrbital.registered) {
                EffectHandler.getInstance().registerOrbital(auraOrbital);
            }
            auraOrbital.updateCenter(MiscUtils.getPositionVector(this));
            if(ticksExisted > PRE_GATHER_EFFECT_LENGTH) {
                int part = ticksExisted - PRE_GATHER_EFFECT_LENGTH;
                float perc = ((float) GATHER_EFFECT_LENGTH - part) / ((float) GATHER_EFFECT_LENGTH);
                auraOrbital.reduceAllOffsets(perc);
            }
        }

        if (this.age + 5 >= this.lifespan) {
            this.age = 0;
        }

        if(!worldObj.isRemote) {
            if(ticksExisted > GATHER_EFFECT_LENGTH) {
                finishCore();
            } else if(ticksExisted > PRE_GATHER_EFFECT_LENGTH) {
                auraGatherCycle();
            }
        } else {
            boolean changed = recieveAspectData();
            for (int i = 0; i < effectProperties.length; i++) {
                Orbital.OrbitalRenderProperties node = effectProperties[i];
                if(node == null) {
                    if(effectAspects[i] == null) {
                        continue;
                    }

                    node = new Orbital.OrbitalRenderProperties(Orbital.Axis.persisentRandomAxis(), 1D);//rand.nextDouble()
                    node.setColor(new Color(effectAspects[i].getColor())).setTicksForFullCircle(120 + rand.nextInt(40));
                    node.setOffsetTicks(rand.nextInt(80));
                    Color c = getSubParticleColor(effectAspects[i]);
                    node.setSubParticleColor(c);
                    node.setParticleSize(0.1f);
                    node.setSubSizeRunnable(new Orbital.OrbitalSubSizeRunnable() {
                        @Override
                        public float getSubParticleSize(Random rand, int orbitalExisted) {
                            return 0.05F + (rand.nextBoolean() ? 0.0F : 0.025F);
                        }
                    });
                    //node.setMultiplier()
                    effectProperties[i] = node;
                }

                if(flows[i] == null && ticksExisted < PRE_GATHER_EFFECT_LENGTH) {
                    Vector3 v = new Vector3(activationLocation.posX + 0.5D, activationLocation.posY + 0.5D, activationLocation.posZ + 0.5D);
                    flows[i] = EffectHandler.getInstance().effectFlow(worldObj,
                            v, new FXFlow.EntityFlowProperties().setPolicy(EntityFXFlowPolicy.Policies.DEFAULT)
                                    .setTarget(auraOrbital.getOrbitalStartPoints(node)[0])
                                    .setColor(new Color(effectAspects[i].getColor())).setFading(getSubParticleColor(effectAspects[i])));
                    flows[i].setLivingTicks(PRE_GATHER_EFFECT_LENGTH - ticksExisted);
                }

                if(flows[i] != null) flows[i].applyTarget(auraOrbital.getOrbitalStartPoints(node)[0]);

                if(changed) {
                    if(effectProperties[i] != null) {
                        effectProperties[i].setColor(new Color(effectAspects[i].getColor()));
                        Color c = getSubParticleColor(effectAspects[i]);
                        node.setSubParticleColor(c);
                    }

                    if(flows[i] != null) {
                        flows[i].setColor(getSubParticleColor(effectAspects[i]));
                        flows[i].setColor(new Color(effectAspects[i].getColor()));
                    }
                }
            }
            if(ticksExisted >= (PRE_GATHER_EFFECT_LENGTH - 1)) {
                if(auraOrbital.orbitalsSize() == 0) {
                    for (int i = 0; i < 6; i++) {
                        Orbital.OrbitalRenderProperties node = effectProperties[i];
                        auraOrbital.addOrbitalPoint(node);
                    }
                }
            } else {
                for (int i = 0; i < 6; i++) {
                    if(flows[i] != null) {
                        flows[i].lastUpdateCall = System.currentTimeMillis();
                    }
                }
            }
        }
    }

    private void finishCore() {
        ItemStack auraCore = new ItemStack(RegisteredItems.itemAuraCore, 1, 0);
        boolean success = false;
        if(blockCount >= REQUIRED_BLOCKS) {
            double avg = ((double) this.internalAuraList.visSize()) / ((double) this.internalAuraList.size());
            Aspect[] sortedHtL = this.internalAuraList.getAspectsSortedAmount();
            AspectList al = new AspectList();
            for(Aspect a : sortedHtL) {
                if(a == null) return;
                int am = this.internalAuraList.getAmount(a);
                if(am >= avg) {
                    al.add(a, am);
                }
            }

            List<AspectWRItem> rand = new ArrayList<AspectWRItem>();
            for(Aspect a : al.getAspects()) {
                if(a == null) continue;
                rand.add(new AspectWRItem(al.getAmount(a), a));
            }

            Aspect aura = ((AspectWRItem) WeightedRandom.getRandomItem(worldObj.rand, rand)).getAspect();
            for(ItemAuraCore.AuraCoreType type : ItemAuraCore.AuraCoreType.values()) {
                if(type.isAspect() && type.getAspect().equals(aura)) {
                    RegisteredItems.itemAuraCore.setCoreType(auraCore, type);
                    success = true;
                }
            }
        }

        PacketStartAnimation animationPacket;
        if(!success) {
            animationPacket = new PacketStartAnimation(PacketStartAnimation.ID_SMOKE_SPREAD, (int) posX, (int) posY, (int) posZ, Float.floatToIntBits(2F));
        } else {
            animationPacket = new PacketStartAnimation(PacketStartAnimation.ID_SPARKLE_SPREAD, (int) posX, (int) posY, (int) posZ, (byte) 0);
        }
        PacketHandler.INSTANCE.sendToAllAround(animationPacket, MiscUtils.getTargetPoint(worldObj, this, 32));

        EntityItem ei = new EntityItem(worldObj, posX, posY, posZ, auraCore);
        ei.motionX = 0;
        ei.motionY = 0;
        ei.motionZ = 0;
        worldObj.spawnEntityInWorld(ei);
        setDead();
    }

    private List<ChunkCoordinates> markedLocations;

    private void initGathering() {
        markedLocations = new ArrayList<ChunkCoordinates>((int) Math.pow(GATHER_RANGE*2+1, 3));
        for(int x = -GATHER_RANGE; x <= GATHER_RANGE; x++) {
            for(int y = -GATHER_RANGE; y <= GATHER_RANGE; y++) {
                for(int z = -GATHER_RANGE; z <= GATHER_RANGE; z++) {
                    markedLocations.add(new ChunkCoordinates(x, y, z));
                }
            }
        }
        Collections.shuffle(markedLocations, new Random(activationLocation.hashCode()));
    }

    private void auraGatherCycle() {
        if(ticksExisted >= PRE_GATHER_EFFECT_LENGTH) {
            int elapsed = ticksExisted - PRE_GATHER_EFFECT_LENGTH - 1;
            float dist = (GATHER_EFFECT_LENGTH - PRE_GATHER_EFFECT_LENGTH) / (float)markedLocations.size();

            int index = (int) (elapsed / dist);
            int lastIndex = (int)((elapsed - 1) / dist);
            if(index < markedLocations.size() && index > lastIndex) {
                int diff = index - lastIndex;
                for(int i = 0; i < diff; i++) {
                    ChunkCoordinates coord = markedLocations.get(index + i);

                    int x = (int) (coord.posX + posX);
                    int y = (int) (coord.posY + posY);
                    int z = (int) (coord.posZ + posZ);

                    Block block = worldObj.getBlock(x, y, z);
                    if(block != Blocks.air) {
                        int meta = worldObj.getBlockMetadata(x, y, z);
                        ScanResult result = null;
                        MovingObjectPosition pos = new MovingObjectPosition(x, y, z, ForgeDirection.UP.ordinal(),
                                Vec3.createVectorHelper(0, 0, 0), true);
                        ItemStack is = null;
                        try {
                            is = block.getPickBlock(pos, worldObj, x, y, z);
                        } catch (Throwable tr) {}
                        try {
                            if(is == null) {
                                is = BlockUtils.createStackedBlock(block, meta);
                            }
                        }
                        catch (Exception e) {}
                        try {
                            if (is == null) {
                                result = new ScanResult((byte)1, Block.getIdFromBlock(block), meta, null, "");
                            } else {
                                result = new ScanResult((byte)1, Item.getIdFromItem(is.getItem()), is.getItemDamage(), null, "");
                            }
                        }
                        catch (Exception e) {}

                        if(result == null) continue; //We can't scan it BibleThump

                        AspectList aspects = ScanManager.getScanAspects(result, worldObj);
                        if(aspects.size() > 0) {
                            internalAuraList.add(aspects);
                            blockCount++;
                        }
                    }
                }
            }
            sendAspectData(electParliament());
        }
    }

    private Aspect[] electParliament() {
        Aspect[] colors = new Aspect[6];

        Aspect[] aspects = internalAuraList.getAspectsSortedAmount();

        int totalSize = internalAuraList.visSize();

        int availableSeats = 6;
        for(Aspect aspect : aspects) {
            float percent = internalAuraList.getAmount(aspect) / (float)totalSize;

            int seats = (int) Math.ceil(availableSeats * percent);
            seats = Math.min(seats, availableSeats);
            availableSeats -= seats;

            for(int i = 0; i < colors.length && seats > 0; i++) {
                if(colors[i] == null) {
                    colors[i] = aspect;
                    seats--;
                }
            }

            if(availableSeats <= 0) {
                break;
            }
        }

        for(int i = 0; i < colors.length; i++) {
            if(colors[i] == null) {
                colors[i] = aspects[0];
            }
        }
        return colors;
    }

    private Color getSubParticleColor(Aspect a) {
        Color c = null;
        if(a.equals(Aspect.AIR)) {
            c = new Color(0xFEFFC9);
        } else if(a.equals(Aspect.FIRE)) {
            c = new Color(0xFFAA3C);
        } else if(a.equals(Aspect.EARTH)) {
            c = new Color(0x7EE35F);
        } else if(a.equals(Aspect.WATER)) {
            c = new Color(0x78CFCC);
        } else if(a.equals(Aspect.ORDER)) {
            c = new Color(0xFFFFFF);
        } else if(a.equals(Aspect.ENTROPY)) {
            c = new Color(0x000000);
        }
        return c;
    }

    //Client side only.
    private boolean recieveAspectData() {
        String rec = getDataWatcher().getWatchableObjectString(ModConfig.entityAuraCoreDatawatcherAspectsId);
        if(oldAspectDataSent == null || !this.oldAspectDataSent.equals(rec)) {
            oldAspectDataSent = rec;
        } else {
            return false;
        }

        if(rec.equals("")) return false;

        String[] arr = rec.split(SPLIT);
        if(arr.length != 6) throw new IllegalStateException("Server sent wrong Aura Data! '"
                + rec + "' Please report this error to the mod authors!");
        for (int i = 0; i < arr.length; i++) {
            String s = arr[i];
            Aspect a = Aspect.getAspect(s);
            effectAspects[i] = a;
        }
        return true;
    }

    private void sendAspectData(Aspect[] aspects) {
        StringBuilder sb = new StringBuilder();
        for (Aspect aspect : aspects) {
            if (sb.length() > 0) {
                sb.append(SPLIT);
            }
            sb.append(aspect.getTag());

        }
        String toSend = sb.toString();

        if(this.oldAspectDataSent == null || !this.oldAspectDataSent.equals(toSend)) {
            this.oldAspectDataSent = toSend;
            getDataWatcher().updateObject(ModConfig.entityAuraCoreDatawatcherAspectsId, toSend);
        }
    }

    @Override
    public void readEntityFromNBT(NBTTagCompound compound) {
        super.readEntityFromNBT(compound);

        this.internalAuraList = new PrimalAspectList();

        ticksExisted = compound.getInteger("ticksExisted");
        NBTTagList list = compound.getTagList("auraList", compound.getId());
        for (int i = 0; i < list.tagCount(); i++) {
            NBTTagCompound cmp = list.getCompoundTagAt(i);
            if(cmp.hasKey("tag") && cmp.hasKey("amt")) {
                internalAuraList.add(Aspect.getAspect(cmp.getString("tag")), cmp.getInteger("amt"));
            }
        }
        if(compound.hasKey("activationVecX") && compound.hasKey("activationVecY") && compound.hasKey("activationVecZ")) {
            int x = compound.getInteger("activationVecX");
            int y = compound.getInteger("activationVecY");
            int z = compound.getInteger("activationVecZ");
            activationLocation = new ChunkCoordinates(x, y, z);
        }
        sendAspectData(electParliament());

        initGathering();

        blockCount = compound.getInteger("blockCount");
    }

    @Override
    public void writeEntityToNBT(NBTTagCompound compound) {
        super.writeEntityToNBT(compound);

        compound.setInteger("ticksExisted", ticksExisted);
        NBTTagList list = new NBTTagList();
        for(Aspect a : internalAuraList.getAspects()) {
            if(a == null) continue;
            NBTTagCompound aspectCompound = new NBTTagCompound();
            aspectCompound.setString("tag", a.getTag());
            aspectCompound.setInteger("amt", internalAuraList.getAmount(a));
            list.appendTag(aspectCompound);
        }
        compound.setTag("auraList", list);
        if(activationLocation != null) {
            compound.setInteger("activationVecX", activationLocation.posX);
            compound.setInteger("activationVecY", activationLocation.posY);
            compound.setInteger("activationVecZ", activationLocation.posZ);
        }

        compound.setInteger("blockCount", blockCount);
    }

    @Override
    public void writeSpawnData(ByteBuf buffer) {
        buffer.writeInt(ticksExisted);

        buffer.writeInt(activationLocation.posX);
        buffer.writeInt(activationLocation.posY);
        buffer.writeInt(activationLocation.posZ);
    }

    @Override
    public void readSpawnData(ByteBuf buffer) {
        ticksExisted = buffer.readInt();

        activationLocation = new ChunkCoordinates();
        activationLocation.posX = buffer.readInt();
        activationLocation.posY = buffer.readInt();
        activationLocation.posZ = buffer.readInt();
    }

    public static final class AspectWRItem extends WeightedRandom.Item {

        private final Aspect aspect;

        public AspectWRItem(int weight, Aspect aspect) {
            super(weight);
            this.aspect = aspect;
        }

        public Aspect getAspect() {
            return aspect;
        }
    }

}