/*******************************************************************************
 * Copyright 2015 See AUTHORS file.
 * <p/>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.mygdx.game.steerers;

import com.badlogic.gdx.ai.GdxAI;
import com.badlogic.gdx.ai.steer.SteeringAcceleration;
import com.badlogic.gdx.ai.steer.behaviors.FollowPath;
import com.badlogic.gdx.ai.steer.utils.paths.LinePath;
import com.badlogic.gdx.ai.steer.utils.paths.LinePath.LinePathParam;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.Ray;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Bits;
import com.mygdx.game.GameEngine;
import com.mygdx.game.GameRenderer;
import com.mygdx.game.GameScreen;
import com.mygdx.game.objects.GameObject;
import com.mygdx.game.objects.SteerableBody;
import com.mygdx.game.pathfinding.NavMeshGraphPath;
import com.mygdx.game.pathfinding.NavMeshPointPath;
import com.mygdx.game.pathfinding.Triangle;
import com.mygdx.game.settings.GameSettings;
import com.mygdx.game.utilities.Entity;
import com.mygdx.game.utilities.MyShapeRenderer;

/**
 * A steerer to follow a path while avoiding collisions. 
 * 
 * @author jsjolund
 * @author davebaol
 */
public class FollowPathSteerer extends CollisionAvoidanceSteererBase {

	/**
	 * Path of triangles on the navigation mesh. Used to construct path points.
	 */
	public final NavMeshGraphPath navMeshGraphPath = new NavMeshGraphPath();

	/**
	 * Path points on the navigation mesh, which the steerable will follow.
	 */
	public final NavMeshPointPath navMeshPointPath = new NavMeshPointPath();

	/**
	 * Path which is rendered on screen
	 */
	public final Array<Vector3> pathToRender = new Array<Vector3>();

	/**
	 * Steering behaviour for path following
	 */
	public final FollowPath<Vector3, LinePath.LinePathParam> followPathSB;

	/**
	 * Holds the path segments for steering behaviour
	 */
	protected final LinePath<Vector3> linePath;

	/**
	 * Path segment index the steerable is currently following.
	 */
	private int currentSegmentIndex = -1;

	/**
	 * Points from which to construct the path segments the steerable should follow
	 */
	private final Array<Vector3> centerOfMassPath = new Array<Vector3>();

	private Vector3 tmpVec1 = new Vector3();
	private Ray stationarityRayLow = new Ray();
	private Ray stationarityRayHigh = new Ray();
	private float stationarityRayLength;
	private Color stationarityRayColor;

	public FollowPathSteerer(final SteerableBody steerableBody) {
		super(steerableBody);

		// At least two points are needed to construct a line path
		Array<Vector3> waypoints = new Array<Vector3>(new Vector3[]{new Vector3(), new Vector3(1, 0, 1)});
		this.linePath = new LinePath<Vector3>(waypoints, true);
		this.followPathSB = new FollowPath<Vector3, LinePath.LinePathParam>(steerableBody, linePath, 1);

		this.prioritySteering.add(followPathSB);
	}

	public boolean calculateNewPath(Ray ray, Bits visibleLayers) {
		if (GameScreen.screen.engine.getScene().navMesh.getPath(
				steerableBody.getCurrentTriangle(),
				steerableBody.getGroundPosition(tmpVec1),
				ray, visibleLayers,
				GameSettings.CAMERA_PICK_RAY_DST,
				navMeshGraphPath)) {

			calculateNewPath0();
			return true;
		}
		return false;
	}

	public boolean calculateNewPath(Triangle targetTriangle, Vector3 targetPoint) {
		if (GameScreen.screen.engine.getScene().navMesh.getPath(
				steerableBody.getCurrentTriangle(),
				steerableBody.getGroundPosition(tmpVec1),
				targetTriangle,
				targetPoint,
				navMeshGraphPath)) {

			calculateNewPath0();
			return true;
		}
		return false;
	}

	/**
	 * Calculate the navigation mesh point path, then assign this steering provider to the owner
	 */
	private void calculateNewPath0() {
		navMeshPointPath.calculateForGraphPath(navMeshGraphPath);

		pathToRender.clear();
		pathToRender.addAll(navMeshPointPath.getVectors());

		centerOfMassPath.clear();
		// Since the navmesh path is on the ground, we need to translate
		// it to align with body origin
		for (Vector3 v : navMeshPointPath) {
			centerOfMassPath.add(new Vector3(v).add(0, steerableBody.halfExtents.y, 0));
		}
		linePath.createPath(centerOfMassPath);
		
		followPathSB.setTimeToTarget(steerableBody.steerSettings.getTimeToTarget())
				.setArrivalTolerance(steerableBody.steerSettings.getArrivalTolerance())
				.setDecelerationRadius(steerableBody.steerSettings.getDecelerationRadius())
				.setPredictionTime(steerableBody.steerSettings.getPredictionTime())
				.setPathOffset(steerableBody.steerSettings.getPathOffset());
		steerableBody.setZeroLinearSpeedThreshold(steerableBody.steerSettings.getZeroLinearSpeedThreshold());
		currentSegmentIndex = -1;

		collisionAvoidanceSB.setEnabled(true);

		deadlockDetection = false;

		// Make this steerer active
		steerableBody.steerer = this;
	}

	/**
	 * Path segment index the steerable is currently following.
	 */
	public int getCurrentSegmentIndex() {
		return currentSegmentIndex;
	}

	@Override
	public void startSteering() {
	}

	@Override
	public boolean stopSteering() {
		// Clear path
		pathToRender.clear();
		navMeshPointPath.clear();
		navMeshGraphPath.clear();
		return false;
	}

	boolean deadlockDetection;
	float deadlockDetectionStartTime;
	float collisionDuration;
	private static final float DEADLOCK_TIME = .5f;
	private static final float MAX_NO_COLLISION_TIME = DEADLOCK_TIME + .5f;

	@Override
	public boolean processSteering(SteeringAcceleration<Vector3> steering) {

		// Check if steering target path segment changed.
		LinePathParam pathParam = followPathSB.getPathParam();
		int traversedSegment = pathParam.getSegmentIndex();
		if (traversedSegment > currentSegmentIndex) {
			currentSegmentIndex = traversedSegment;
		}

		if (prioritySteering.getSelectedBehaviorIndex() == 0) {
			/*
			 * Collision avoidance management
			 */
			float pr = proximity.getRadius() * 1.5f;
			if (linePath.getEndPoint().dst2(steerableBody.getPosition()) <= pr * pr) {
				// Disable collision avoidance near the end of the path since the obstacle
				// will likely prevent the entity from reaching the target.
				collisionAvoidanceSB.setEnabled(false);
				deadlockDetectionStartTime = Float.POSITIVE_INFINITY;
			} else if (deadlockDetection) {
				// Accumulate collision time during deadlock detection
				collisionDuration += GdxAI.getTimepiece().getDeltaTime();

				if (GdxAI.getTimepiece().getTime() - deadlockDetectionStartTime > DEADLOCK_TIME && collisionDuration > DEADLOCK_TIME * .6f) {
					// Disable collision avoidance since most of the deadlock detection period has been spent on collision avoidance
					collisionAvoidanceSB.setEnabled(false);
				}
			} else {
				// Start deadlock detection
				deadlockDetectionStartTime = GdxAI.getTimepiece().getTime();
				collisionDuration = 0;
				deadlockDetection = true;
			}
			return true;
		}

		/*
		 * Path following management
		 */
		float dst2FromPathEnd = steerableBody.getPosition().dst2(linePath.getEndPoint());

		// Check to see if the entity has reached the end of the path
		if (steering.isZero() && dst2FromPathEnd < followPathSB.getArrivalTolerance() * followPathSB.getArrivalTolerance()) {
			return false;
		}

		// Check if collision avoidance must be re-enabled
		if (deadlockDetection && !collisionAvoidanceSB.isEnabled() && GdxAI.getTimepiece().getTime() - deadlockDetectionStartTime > MAX_NO_COLLISION_TIME) {
				collisionAvoidanceSB.setEnabled(true);
				deadlockDetection = false;
		}
		
		// If linear speed is very low and the entity is colliding something at his feet, like a step of the stairs
		// for instance, we have to increase the acceleration to make him go upstairs. 
		float minVel = .2f;
		if (steerableBody.getLinearVelocity().len2() > minVel * minVel) {
			stationarityRayColor = null;
		} else {
			steerableBody.getGroundPosition(stationarityRayLow.origin).add(0, 0.05f, 0);
			steerableBody.getDirection(stationarityRayLow.direction).scl(1f, 0f, 1f).nor();
			stationarityRayLength = steerableBody.getBoundingRadius() + 0.4f;
			Entity hitEntityLow = GameScreen.screen.engine.rayTest(stationarityRayLow, null, GameEngine.ALL_FLAG, GameEngine.PC_FLAG, stationarityRayLength, null);
			if (hitEntityLow instanceof GameObject) {
				stationarityRayColor = Color.RED;
				stationarityRayHigh.set(stationarityRayLow);
				stationarityRayHigh.origin.add(0, .8f, 0);
				Entity hitEntityHigh = GameScreen.screen.engine.rayTest(stationarityRayHigh, null, GameEngine.ALL_FLAG, GameEngine.PC_FLAG, stationarityRayLength, null);
				if (hitEntityHigh == null) {
					// The entity is touching a small obstacle with his feet like a step of the stairs.
					// Increase the acceleration to make him go upstairs.
					steering.linear.scl(8);
				}
				else if (hitEntityHigh instanceof GameObject) {
					// The entity is touching a higher obstacle like a tree, a column or something.
					// Here we should invent something to circumvent this kind of obstacles :)
					//steering.linear.rotateRad(Constants.V3_UP, Constants.PI0_25);
				}
			} else {
				stationarityRayColor = Color.BLUE;
			}
		}

		return true;
	}

	@Override
	public void draw(GameRenderer gameRenderer) {
		super.draw(gameRenderer);
		
		if (pathToRender.size > 0 && currentSegmentIndex >= 0) {
			MyShapeRenderer shapeRenderer = gameRenderer.shapeRenderer;
			shapeRenderer.setProjectionMatrix(gameRenderer.viewport.getCamera().combined);

			// Draw path target position
			Vector3 t = gameRenderer.vTmpDraw1.set(followPathSB.getInternalTargetPosition());
			t.y -= steerableBody.halfExtents.y;
			float size = .05f;
			float offset = size / 2;
			shapeRenderer.begin(MyShapeRenderer.ShapeType.Filled);
			shapeRenderer.setColor(Color.CORAL);
			shapeRenderer.box(t.x - offset, t.y - offset, t.z + offset, size, size, size);

			// Draw path
			shapeRenderer.set(MyShapeRenderer.ShapeType.Line);
			Vector3 p = t;
			int i = getCurrentSegmentIndex() + 1;
			if (i + 1 < pathToRender.size && linePath.calculatePointSegmentSquareDistance(gameRenderer.vTmpDraw2, pathToRender.get(i), pathToRender.get(i + 1), p) < 0.0001)
				i++;
			while (i < pathToRender.size) {
				Vector3 q = pathToRender.get(i++);
				shapeRenderer.line(p, q);
				p = q;
			}			

			// Draw stationarity rays
			if (stationarityRayColor != null) {
				shapeRenderer.setColor(stationarityRayColor);
				shapeRenderer.line(stationarityRayLow.origin, tmpVec1.set(stationarityRayLow.origin).mulAdd(stationarityRayLow.direction, stationarityRayLength));
				shapeRenderer.line(stationarityRayHigh.origin, tmpVec1.set(stationarityRayHigh.origin).mulAdd(stationarityRayHigh.direction, stationarityRayLength));
			}

			shapeRenderer.end();
		}
	}

}