// --------------------------------------------------------------------------------------------------
//  Copyright (c) 2016 Microsoft Corporation
//  
//  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
//  associated documentation files (the "Software"), to deal in the Software without restriction,
//  including without limitation the rights to use, copy, modify, merge, publish, distribute,
//  sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//  
//  The above copyright notice and this permission notice shall be included in all copies or
//  substantial portions of the Software.
//  
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
//  NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// --------------------------------------------------------------------------------------------------

package com.microsoft.Malmo.MissionHandlers;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;

import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.init.Blocks;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;

import com.microsoft.Malmo.MissionHandlerInterfaces.IWorldDecorator;
import com.microsoft.Malmo.Schemas.AgentSection;
import com.microsoft.Malmo.Schemas.BlockType;
import com.microsoft.Malmo.Schemas.Colour;
import com.microsoft.Malmo.Schemas.MissionInit;
import com.microsoft.Malmo.Schemas.PosAndDirection;
import com.microsoft.Malmo.Schemas.SnakeBlock;
import com.microsoft.Malmo.Schemas.SnakeDecorator;
import com.microsoft.Malmo.Schemas.Variation;
import com.microsoft.Malmo.Utils.BlockDrawingHelper;
import com.microsoft.Malmo.Utils.MinecraftTypeHelper;

public class SnakeDecoratorImplementation extends HandlerBase implements IWorldDecorator
{
	private int buildX = -1;
	private int buildY = -1;
	private int buildZ = -1;
	private int buildDirection = 0;
	private int stairs = 0;
	private int timeSinceLastBuild = 0;
	private Random randomBuilder = new Random();
	private Random randomBlocks = new Random();
	ArrayList<BlockPos> path = new ArrayList<BlockPos>();
	private boolean pendingBlock = false;
	private IBlockState freshBlock = null;
	private IBlockState staleBlock = null;
	private int consecutiveGaps = 0;

	// Parameters:
	private int speedInTicks = 6;
	private int minYPos = 32;
	private int maxYPos = 250;
	private float chanceOfChangeOfDirection = 0.01f;
	private float chanceOfStairs = 0.08f;
	private float chanceOfGap = 0.04f;
	private int maxNumberOfStairs = 20;
	private int maxPathLength = 30;
	private String freshBlockName = "glowstone";
	private String staleBlockName = "air";
	private SnakeDecorator snakeParams;
	
	public SnakeDecoratorImplementation()
	{
		Block fresh = (Block)Block.REGISTRY.getObject(new ResourceLocation(this.freshBlockName));
		Block stale = (Block)Block.REGISTRY.getObject(new ResourceLocation(this.staleBlockName));
		this.freshBlock = (fresh != null) ? fresh.getDefaultState() : Blocks.GLOWSTONE.getDefaultState();
		this.staleBlock = (stale != null) ? stale.getDefaultState() : Blocks.AIR.getDefaultState();
	}
	
    @Override
    public boolean parseParameters(Object params)
    {
        if (params == null || !(params instanceof SnakeDecorator))
            return false;
        this.snakeParams = (SnakeDecorator)params;
        return true;
    }
	
	@Override
	public void update(World world)
	{
		this.timeSinceLastBuild++;
		if (this.timeSinceLastBuild > this.speedInTicks && !this.pendingBlock)
			updatePath();

		if (this.path.size() > 0 && this.pendingBlock)
		{
    		BlockPos bp = this.path.get(this.path.size() - 1);
    		// Create the block, or a gap if we are leaving a gap:
       		world.setBlockState(bp, this.consecutiveGaps == 0 ? this.freshBlock : Blocks.AIR.getDefaultState());
			this.pendingBlock = false;
    		
    		// Create space above and below this block (even if we are leaving a gap):
			BlockPos bpUp = bp;
			BlockPos bpDown = bp;
    		for (int i = 0; i < 3; i++) {
        		bpUp = bpUp.add(0, 1, 0);
        		bpDown = bpDown.add(0, -1, 0);
        		world.setBlockToAir(bpUp);
        		world.setBlockToAir(bpDown);
    		}
   		
    		// Now remove block at the other end of the path, if need be:
    		if (this.path.size() > this.maxPathLength) {
    			bp = this.path.remove(0);
    			world.setBlockState(bp, this.staleBlock);
    		}
    	}
	}
	
	private void updatePath()
	{
		this.pendingBlock = true;
		this.timeSinceLastBuild = 0;

		// Update block position:
		this.buildX += ((this.buildDirection % 2) == 0) ? this.buildDirection - 1 : 0;
		this.buildZ += ((this.buildDirection % 2) == 1) ? this.buildDirection - 2 : 0;

		// We can add a gap, unless we've already added one, or we are going up:
		boolean addGap = (this.consecutiveGaps == 0 && this.stairs <= 0 && this.randomBuilder.nextFloat() < chanceOfGap);

		// Update the Y position:
		if (this.stairs > 0)
		{
			this.buildY++;
			this.stairs--;
		}
		else if (this.stairs < 0)
		{
			this.buildY--;
			this.stairs++;
		}

		// Clamp Y:
		this.buildY = (this.buildY < this.minYPos) ? this.minYPos : (this.buildY > this.maxYPos) ? this.maxYPos : this.buildY;

		// Add the block to our path:
		BlockPos bp = new BlockPos(this.buildX, this.buildY, this.buildZ);
		this.path.add(bp);

		if (!addGap)
		{
			this.consecutiveGaps = 0;
			
			// Update the deltas randomly:
			scrambleDirections();
		}
		else
		{
			this.consecutiveGaps++;
		}
	}
	
	private void scrambleDirections()
	{
		if (this.randomBuilder.nextFloat() < this.chanceOfStairs)
			this.stairs = this.randomBuilder.nextInt(1 + this.maxNumberOfStairs * 2) - this.maxNumberOfStairs;
		
		if (this.randomBuilder.nextFloat() < this.chanceOfChangeOfDirection)
		{
			this.buildDirection += this.randomBuilder.nextInt(3) - 1;
			if (this.buildDirection < 0)
				this.buildDirection += 4;
			else if (this.buildDirection > 3)
				this.buildDirection -= 4;
		}
	}

    private void initBlocks()
    {
	    this.freshBlock = getBlock(this.snakeParams.getFreshBlock(), this.randomBlocks);
	    this.staleBlock = getBlock(this.snakeParams.getStaleBlock(), this.randomBlocks);
    }

    private void initRNGs()
    {
        // Initialise a RNG for this scene:
        long seed = 0;
        if (this.snakeParams.getSeed() == null || this.snakeParams.getSeed().equals("random"))
            seed = System.currentTimeMillis();
        else
            seed = Long.parseLong(this.snakeParams.getSeed());

        this.randomBuilder = new Random(seed);
        this.randomBlocks = new Random(seed);
        
        // Should we initialise a separate RNG for the block types?
        if (this.snakeParams.getMaterialSeed() != null)
        {
            long bseed = 0;
            if (this.snakeParams.getMaterialSeed().equals("random"))
                bseed = System.currentTimeMillis();
            else
                bseed = Long.parseLong(this.snakeParams.getMaterialSeed());
            this.randomBlocks = new Random(bseed);
        }
    }

    private IBlockState getBlock(SnakeBlock sblock, Random rand)
    {
        String blockName = chooseBlock(sblock.getType(), rand);
        Colour blockCol = chooseColour(sblock.getColour(), rand);
        Variation blockVar = chooseVariant(sblock.getVariant(), rand);
        return BlockDrawingHelper.applyModifications(MinecraftTypeHelper.ParseBlockType(blockName), blockCol, null, blockVar);
    }
    
    private String chooseBlock(List<BlockType> types, Random r)
    {
        if (types == null || types.size() == 0)
            return "air";
        return types.get(r.nextInt(types.size())).value();
    }
    
    private Colour chooseColour(List<Colour> colours, Random r)
    {
        if (colours == null || colours.size() == 0)
            return null;
        return colours.get(r.nextInt(colours.size()));
    }
    
    private Variation chooseVariant(List<Variation> vars, Random r)
    {
        if (vars == null || vars.size() == 0)
            return null;
        return vars.get(r.nextInt(vars.size()));
    }
    
	@Override
	public void buildOnWorld(MissionInit missionInit, World world)
	{
		initRNGs();
		initBlocks();
		initDimensionsAndBehaviour();
		setStartPoint(missionInit);
	}
	
	private void setStartPoint(MissionInit missionInit)
	{
        // Position the start point:
        PosAndDirection p = new PosAndDirection();
        p.setX(new BigDecimal(this.buildX));
        p.setY(new BigDecimal(this.buildY));
        p.setZ(new BigDecimal(this.buildZ));
        for (AgentSection as : missionInit.getMission().getAgentSection())
        {
	        p.setPitch(as.getAgentStart().getPlacement().getPitch());
	        p.setYaw(as.getAgentStart().getPlacement().getYaw());
	        as.getAgentStart().setPlacement(p);
        }
	}
	
	private float perturbProbability(float prob, float variance, Random rng)
	{
	    prob += (rng.nextFloat() - 0.5) * 2 * variance;
	    prob = prob < 0 ? 0 : (prob > 1 ? 1 : prob);
	    return prob;
	}

	private void initDimensionsAndBehaviour()
    {
    	// Get dimensions of snake:
        this.buildX = this.snakeParams.getSizeAndPosition().getXOrigin();
        this.buildY = this.snakeParams.getSizeAndPosition().getYOrigin();
        this.buildZ = this.snakeParams.getSizeAndPosition().getZOrigin();
        this.maxYPos = this.snakeParams.getSizeAndPosition().getYMax();
        this.minYPos = this.snakeParams.getSizeAndPosition().getYMin();
        
        this.chanceOfChangeOfDirection = perturbProbability(this.snakeParams.getTurnProbability().getValue().floatValue(), this.snakeParams.getTurnProbability().getVariance().floatValue(), this.randomBuilder);
        this.chanceOfGap = perturbProbability(this.snakeParams.getGapProbability().getValue().floatValue(), this.snakeParams.getGapProbability().getVariance().floatValue(), this.randomBuilder);
        this.chanceOfStairs = perturbProbability(this.snakeParams.getStairsProbability().getValue().floatValue(), this.snakeParams.getStairsProbability().getVariance().floatValue(), this.randomBuilder);

        this.speedInTicks = this.snakeParams.getSpeedInTicks();
        this.maxNumberOfStairs = this.snakeParams.getMaxStairLength();
        this.maxPathLength = this.snakeParams.getMaxLength();
    }

    @Override
    public boolean getExtraAgentHandlersAndData(List<Object> handlers, Map<String, String> data)
    {
        return false;
    }

    @Override
    public void prepare(MissionInit missionInit)
    {
    }

    @Override
    public void cleanup()
    {
    }

    @Override
    public boolean targetedUpdate(String nextAgentName)
    {
        return false;   // Does nothing.
    }

    @Override
    public void getTurnParticipants(ArrayList<String> participants, ArrayList<Integer> participantSlots)
    {
        // Does nothing.
    }
}