package com.amadornes.framez.tile;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.Packet;
import net.minecraft.network.play.server.S35PacketUpdateTileEntity;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.ChatComponentText;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import uk.co.qmunity.lib.helper.RedstoneHelper;
import uk.co.qmunity.lib.misc.Pair;
import uk.co.qmunity.lib.vec.IWorldLocation;
import uk.co.qmunity.lib.vec.Vec3i;

import com.amadornes.framez.api.IDebuggable;
import com.amadornes.framez.api.Priority;
import com.amadornes.framez.api.Priority.PriorityEnum;
import com.amadornes.framez.api.modifier.IMotorModifier;
import com.amadornes.framez.api.movement.BlockMovementType;
import com.amadornes.framez.api.movement.IMotor;
import com.amadornes.framez.api.movement.IMovable;
import com.amadornes.framez.api.movement.IMovement;
import com.amadornes.framez.api.movement.IMovingBlock;
import com.amadornes.framez.api.movement.MotorSetting;
import com.amadornes.framez.movement.MovementHelper;
import com.amadornes.framez.movement.MovementScheduler;
import com.amadornes.framez.movement.MovingBlock;
import com.amadornes.framez.movement.MovingStructure;
import com.amadornes.framez.network.NetworkHandler;
import com.amadornes.framez.network.PacketMotorSetting;
import com.amadornes.framez.util.MotorCache;
import com.amadornes.framez.util.ThreadBlockChecking;
import com.amadornes.framez.util.Timing;

import cpw.mods.fml.common.gameevent.TickEvent.Phase;

public abstract class TileMotor extends TileEntity implements IMotor, IDebuggable, IMovable {

    protected MovingStructure structure;
    protected List<Vec3i> blocking = new ArrayList<Vec3i>();

    private boolean redstoneInput = false;
    private boolean scheduled = false;

    private ForgeDirection face = ForgeDirection.DOWN;
    private HashSet<MotorSetting> settings = new HashSet<MotorSetting>();
    private List<IMotorModifier> modifiers = new ArrayList<IMotorModifier>();

    private double powerStorageSize = 1000000;
    private double storedPower = 0;

    @Override
    public World getWorld() {

        return getWorldObj();
    }

    @Override
    public int getX() {

        return xCoord;
    }

    @Override
    public int getY() {

        return yCoord;
    }

    @Override
    public int getZ() {

        return zCoord;
    }

    @Override
    public Collection<IMotorModifier> getModifiers() {

        if (modifiers == null)
            modifiers = new ArrayList<IMotorModifier>();

        return modifiers;
    }

    @Override
    public boolean canWork() {

        return true;
    }

    @Override
    public boolean isWorking() {

        return structure != null && structure.getProgress() < 1;
    }

    @Override
    public boolean move() {

        try {

            if (getWorld() == null || getWorld().isRemote)
                return false;

            IMovement movement = getMovement();
            if (movement == null)
                return false;

            ForgeDirection face = getFace();

            Pair<List<MovingBlock>, List<Vec3i>> p = MovementHelper.findMovedBlocks(getWorld(), getX() + face.offsetX, getY()
                    + face.offsetY, getZ() + face.offsetZ, face.getOpposite(), movement);

            List<MovingBlock> blocks = p.getKey();
            blocks.remove(new MovingBlock(new Vec3i((IWorldLocation) this), null, null));

            if (blocks.size() == 0)
                return false;

            List<Vec3i> old = blocking;
            blocking = p.getValue();
            boolean send = false;
            if ((old.size() != blocking.size())) {
                send = true;
            } else {
                for (Vec3i v : old) {
                    if (!blocking.contains(v)) {
                        send = true;
                        break;
                    }
                }
                if (!send) {
                    for (Vec3i v : blocking) {
                        if (!old.contains(v)) {
                            send = true;
                            break;
                        }
                    }
                }
            }

            double totalConsumed = 0;
            for (MovingBlock b : blocks) {
                b.snapshot();
                int parts = b.getMultiparts();
                if (parts <= b.getMaxMultiparts()) {
                    totalConsumed += parts * 10;
                } else {
                    if (!blocking.contains(new Vec3i(b))) {
                        blocking.add(new Vec3i(b));
                        send = true;
                    }
                }
                totalConsumed += 100;
                if (b.getTileEntity() != null)
                    totalConsumed += 200;
            }

            if (send)
                sendUpdatePacket();
            if (blocking.size() > 0)
                return false;

            double actuallyConsumed = drainPower(totalConsumed, true);
            if (actuallyConsumed < totalConsumed)
                return false;
            drainPower(actuallyConsumed, false);

            Timing.SECONDS = 1;
            structure = new MovingStructure(this, movement.clone(), 1 / (20 * Timing.SECONDS), blocks);
            // structure.tick(Phase.START);
            // structure.tick(Phase.END);
            MovementScheduler.instance().addStructure(structure);

            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    @Override
    public Set<MotorSetting> getSettings() {

        return settings;
    }

    @Override
    public void configure(MotorSetting setting) {

        if (getWorldObj().isRemote) {
            NetworkHandler.instance().sendToServer(new PacketMotorSetting(this, setting));
            return;
        }

        if (settings.contains(setting)) {
            settings.remove(setting);
        } else {
            for (MotorSetting s : setting.related)
                settings.remove(s);
            settings.add(setting);
        }

        sendUpdatePacket();

        onBlockUpdate();
    }

    private boolean firstTick = true;

    @Override
    public void updateEntity() {

        if (firstTick) {
            onFirstTick();
            firstTick = false;
        }

        tick();
    }

    @Override
    public void tick() {

        if (structure != null && structure.getProgress() >= 1)
            structure = null;

        if (scheduled) {
            if (!isWorking() && canWork())
                if (!move() && !getSettings().contains(MotorSetting.REDSTONE_PULSE))
                    ThreadBlockChecking.instance().addMotor(this);

            scheduled = false;
        }
    }

    @Override
    public void onFirstTick() {

    }

    public void onBlockUpdate() {

        boolean lastInput = redstoneInput;
        redstoneInput = RedstoneHelper.getInput(getWorld(), getX(), getY(), getZ()) > 0;

        if ((lastInput != redstoneInput || !getSettings().contains(MotorSetting.REDSTONE_PULSE))
                && (getSettings().contains(MotorSetting.REDSTONE_INVERTED) ? !redstoneInput : redstoneInput)) {
            scheduled = true;
        } else {
            ThreadBlockChecking.instance().removeMotor(this);
        }
    }

    @Override
    public ForgeDirection getFace() {

        return face;
    }

    public boolean setFace(ForgeDirection face) {

        this.face = face;
        sendUpdatePacket();
        return true;
    }

    public boolean rotate(ForgeDirection axis) {

        if (getMovement().rotate(this, axis)) {
            sendUpdatePacket();
            return true;
        }

        return true;
    }

    @Override
    public boolean debug(World world, int x, int y, int z, ForgeDirection face, EntityPlayer player) {

        if (!world.isRemote)
            return true;

        player.addChatMessage(new ChatComponentText("Power: " + getEnergyBuffer() + "/" + getEnergyBufferSize()));
        player.addChatMessage(new ChatComponentText("Face: " + getFace().name().toLowerCase()));
        getMovement().debug(world, x, y, z, face, player);

        return true;
    }

    public MovingStructure getStructure() {

        return structure;
    }

    public void setStructure(MovingStructure structure) {

        this.structure = structure;
    }

    @Override
    public double getEnergyBufferSize() {

        return powerStorageSize;
    }

    @Override
    public double getEnergyBuffer() {

        return storedPower;
    }

    @Override
    public double injectPower(double amount, boolean simulated) {

        double injected = Math.min(getEnergyBufferSize() - getEnergyBuffer(), amount);

        if (simulated)
            return injected;

        storedPower += injected;

        sendUpdatePacket();

        return injected;
    }

    @Override
    public double drainPower(double amount, boolean simulated) {

        double drained = Math.min(getEnergyBuffer(), amount);

        if (simulated)
            return drained;

        storedPower -= drained;

        sendUpdatePacket();

        return drained;
    }

    // NBT saving and tile updates

    @Override
    public void writeToNBT(NBTTagCompound tag) {

        super.writeToNBT(tag);

        tag.setBoolean("redstoneInput", redstoneInput);

        tag.setInteger("face", getFace().ordinal());

        NBTTagCompound movement = new NBTTagCompound();
        getMovement().writeToNBT(movement);
        tag.setTag("movement", movement);

        NBTTagList l = new NBTTagList();
        for (MotorSetting s : settings)
            l.appendTag(new NBTTagString(s.ordinal() + ""));
        tag.setTag("settings", l);

        tag.setDouble("power", getEnergyBuffer());
    }

    @Override
    public void readFromNBT(NBTTagCompound tag) {

        super.readFromNBT(tag);

        redstoneInput = tag.getBoolean("redstoneInput");

        face = ForgeDirection.getOrientation(tag.getInteger("face"));

        getMovement().readFromNBT(tag.getCompoundTag("movement"));

        settings.clear();
        NBTTagList l = tag.getTagList("settings", new NBTTagString().getId());
        for (int i = 0; i < l.tagCount(); i++)
            settings.add(MotorSetting.values()[Integer.parseInt(l.getStringTagAt(i))]);

        storedPower = tag.getDouble("power");
    }

    protected void writeToPacketNBT(NBTTagCompound tag) {

        tag.setInteger("face", getFace().ordinal());

        NBTTagCompound movement = new NBTTagCompound();
        getMovement().writeToNBT(movement);
        tag.setTag("movement", movement);

        NBTTagList l = new NBTTagList();
        for (MotorSetting s : settings)
            l.appendTag(new NBTTagString(s.ordinal() + ""));
        tag.setTag("settings", l);

        if (blocking.size() > 0) {
            NBTTagList blocking = new NBTTagList();
            for (Vec3i v : this.blocking) {
                NBTTagCompound t = new NBTTagCompound();
                t.setInteger("x", v.getX());
                t.setInteger("y", v.getY());
                t.setInteger("z", v.getZ());
                blocking.appendTag(t);
            }
            tag.setTag("blocking", blocking);
        }

        tag.setDouble("power", getEnergyBuffer());
    }

    protected void readFromPacketNBT(NBTTagCompound tag) {

        face = ForgeDirection.getOrientation(tag.getInteger("face"));

        getMovement().readFromNBT(tag.getCompoundTag("movement"));

        settings.clear();
        NBTTagList l = tag.getTagList("settings", new NBTTagString().getId());
        for (int i = 0; i < l.tagCount(); i++)
            settings.add(MotorSetting.values()[Integer.parseInt(l.getStringTagAt(i))]);

        blocking = new ArrayList<Vec3i>();
        if (tag.hasKey("blocking")) {
            NBTTagList blocking = tag.getTagList("blocking", new NBTTagCompound().getId());
            for (int i = 0; i < blocking.tagCount(); i++) {
                NBTTagCompound t = blocking.getCompoundTagAt(i);
                this.blocking.add(new Vec3i(t.getInteger("x"), t.getInteger("y"), t.getInteger("z")));
            }
        }

        storedPower = tag.getDouble("power");

        markForRenderUpdate();
    }

    @Override
    public Packet getDescriptionPacket() {

        NBTTagCompound tCompound = new NBTTagCompound();
        writeToPacketNBT(tCompound);
        return new S35PacketUpdateTileEntity(xCoord, yCoord, zCoord, 0, tCompound);
    }

    @Override
    public void onDataPacket(NetworkManager net, S35PacketUpdateTileEntity pkt) {

        readFromPacketNBT(pkt.func_148857_g());
    }

    protected void sendUpdatePacket() {

        if (!worldObj.isRemote)
            worldObj.markBlockForUpdate(xCoord, yCoord, zCoord);
    }

    protected void markForRenderUpdate() {

        if (worldObj != null)
            worldObj.markBlockRangeForRenderUpdate(xCoord, yCoord, zCoord, xCoord, yCoord, zCoord);
    }

    protected void notifyNeighborBlockUpdate() {

        worldObj.notifyBlocksOfNeighborChange(xCoord, yCoord, zCoord, getBlockType());
    }

    @Override
    @Priority(PriorityEnum.OVERRIDE)
    public BlockMovementType getMovementType(World world, int x, int y, int z, ForgeDirection side, IMovement movement) {

        if (side == getFace())
            return BlockMovementType.UNMOVABLE;

        return !isWorking() ? BlockMovementType.MOVABLE : BlockMovementType.UNMOVABLE;
    }

    @Override
    public boolean startMoving(IMovingBlock block) {

        return false;
    }

    @Override
    public boolean finishMoving(IMovingBlock block) {

        return false;
    }

    @Override
    public void onUnload() {

        MovingStructure s = getStructure();
        if (s != null)
            while (s.getProgress() < 1) {
                s.tick(Phase.START);
                s.tick(Phase.END);
            }
    }

    public List<Vec3i> getBlocking() {

        return blocking;
    }

    @Override
    public void validate() {

        super.validate();
        MotorCache.onLoad(this);
    }

    @Override
    public void invalidate() {

        super.invalidate();
        MotorCache.onUnload(this);
        onUnload();
    }

}