package game;

import game.mobs.Ghost;
import game.obj.Bomb;
import game.obj.BombPreview;
import lime.ui.GamepadButton;
import lime.ui.KeyCode;
import lime.ui.KeyModifier;
import menu.Pause;
import motion.Actuate;
import openfl.Assets;
import openfl.Lib;
import openfl.display.Bitmap;
import openfl.display.BitmapData;
import openfl.display.Sprite;
import openfl.geom.Point;
import openfl.media.SoundChannel;
import openfl.media.SoundTransform;
import raycaster.Raycaster;

using Math;
using Std;

class Game extends State {
	static final ROTATION_SPEED:Float = Math.PI;
	static final MOVE_SPEED:Float = 1.8;
	static final RUN_SPEED:Float = 3.2;

	public static var current:Game;

	public var ghostSpawned:Bool = false;
	public var chaseEndTime:Null<Int> = null;
	public var currentGhost:Ghost = null;
	public var canSpawnGhost:Bool = true;

	public var map:GameMap;

	public var rc:Raycaster;

	var layBomb(get, never):Bool;

	var _shakeAmp:Float = 0;
	var _shakePhase:Float = 0;
	var _shakeTime:Float = 0;
	var _shakeTimeTotal:Float = 0;

	var bombPreviewSprite:BombPreview = new BombPreview();
	var cameraTraveledDistance:Float = 0;

	var stepAlt:Int = 1;

	var putCellX:Int;
	var putCellY:Int;

	var _chasing:Bool = false;

	var chasingSound:SoundChannel;
	var ambientSound:SoundChannel;

	var coinsHud:Bitmap = new Bitmap(new BitmapData(32, 1, true, 0x00000000));

	function get_layBomb():Bool
		return switch (inputMode) {
			case KEYBOARD: inputKeys[KeyCode.SPACE];
			case GAMEPAD: gamepadButtons[GamepadButton.A];
		};

	public function new(?level:String = 'assets/levels/level1.json') {
		super();

		current = this;

		map = GameMap.fromJson(Assets.getText(level));

		rc = new Raycaster(40, 40, 100, map);
		rc.x = rc.y = -4;
		rc.camera.angle = Math.PI / 100;

		for (mob in map.mobs) {
			rc.addSprite(mob);
		}

		for (sprite in map.sprites)
			rc.addSprite(sprite);

		addChild(rc);

		rc.addSprite(bombPreviewSprite);
		bombPreviewSprite.visible = false;

		var _mask = new Sprite();
		_mask.graphics.beginFill(0x0);
		_mask.graphics.drawRect(0, 0, 32, 32);
		_mask.graphics.endFill();

		mask = _mask;
		addChild(_mask);

		coinsHud.alpha = 0.5;
		addChild(coinsHud);

		ambientSound = playSound('assets/sound/ambience.ogg', 0, 9999);
	}

	public function end() {
		endChase();

		canSpawnGhost = false;

		Actuate.tween(this, 3, {alpha: 0}).delay(2).onComplete(() -> {
			Main.instance.setState(new Survived());
		});
	}

	/**
		A*
	**/
	public function pathToPlayer(start:CellDef):Array<CellDef> {
		var end = map.tiles[rc.camera.position.x.int()][rc.camera.position.y.int()].cell;

		var d:CellDef->CellDef->Float = (c1:CellDef, c2:CellDef) -> {
			return (c2.cellX - c1.cellX) * (c2.cellX - c1.cellX) + (c2.cellY - c1.cellY) * (c2.cellY - c1.cellY);
		};

		var h = (c:CellDef) -> {
			return d(c, end);
		}

		var openSet:Array<CellDef> = [start];

		var cameFrom:Map<CellDef, CellDef> = [];

		var gScore:Map<CellDef, Float> = [];

		gScore[start] = 0;

		var fScore:Map<CellDef, Float> = [];

		fScore[start] = h(start);

		var getLowest = (fromSet:Array<CellDef>) -> {
			var min = Math.POSITIVE_INFINITY;
			var result:CellDef = null;

			for (item in fromSet) {
				if (fScore.exists(item)) {
					if (fScore[item] < min) {
						min = fScore[item];
						result = item;
					}
				}
			}

			return result;
		};

		while (openSet.length != 0) {
			var current = getLowest(openSet);

			if (current == end) {
				var total_path:Array<CellDef> = [];

				while (cameFrom.exists(current)) {
					current = cameFrom[current];
					total_path.unshift(current);
				}

				return total_path;
			}

			openSet.remove(current);

			var neighbors:Array<CellDef> = [];

			for (c in [[-1, 0], [1, 0], [0, 1], [0, -1]]) {
				var tile = map.getTile(current.cellX + c[0], current.cellY + c[1]);

				if (tile != null && !tile.collision) {
					neighbors.push(map.tiles[current.cellX + c[0]][current.cellY + c[1]].cell);
				}
			}

			for (neighbor in neighbors) {
				var tentative_gScore = gScore[current] + d(current, neighbor);
				if (!gScore.exists(neighbor))
					gScore[neighbor] = Math.POSITIVE_INFINITY;

				if (tentative_gScore < gScore[neighbor]) {
					cameFrom[neighbor] = current;
					gScore[neighbor] = tentative_gScore;
					fScore[neighbor] = gScore[neighbor] + h(neighbor);

					if (openSet.indexOf(neighbor) == -1)
						openSet.push(neighbor);
				}
			}
		}

		return [];
	}

	public function startChase() {
		if (!_chasing) {
			ambientSound.stop();
			chasingSound = playSound('assets/sound/chase_music.ogg', 0, 999, new SoundTransform(0));
			playSound('assets/sound/piano_hit.ogg');
			Actuate.transform(chasingSound, 1).sound(1);
			_chasing = true;
		}
	}

	public function endChase() {
		if (_chasing) {
			if (currentGhost != null) {
				rc.removeSprite(currentGhost);
				ghostSpawned = false;
				chaseEndTime = Lib.getTimer();
				currentGhost = null;
			}

			Actuate.transform(chasingSound, 3).sound(0, 0).onComplete(() -> {
				chasingSound.stop();
			});

			ambientSound = playSound('assets/sound/ambience.ogg', 0, 9999);

			_chasing = false;
		}
	}

	function pause() {
		Main.instance.setState(new Pause(this));
	}

	public function shake(amp:Float, time:Float = 1) {
		_shakeAmp = amp;
		_shakeTime = _shakeTimeTotal = time;
	}

	override function keyDown(keyCode:KeyCode, keyModifier:KeyModifier) {
		if (keyCode == ESCAPE)
			pause();
		else
			super.keyDown(keyCode, keyModifier);
	}

	override function keyUp(keyCode:KeyCode, keyModifier:KeyModifier) {
		if (keyCode == SPACE && layBomb) {
			putBomb();
		}

		super.keyUp(keyCode, keyModifier);
	}

	override function gamepadButtonDown(button:GamepadButton) {
		if (button == START)
			pause();
		else
			super.gamepadButtonDown(button);
	}

	override function gamepadButtonUp(button:GamepadButton) {
		if (button == A && layBomb) {
			putBomb();
		}

		super.gamepadButtonUp(button);
	}

	function putBomb() {
		var bomb = new Bomb(putCellX + 0.5, putCellY + 0.5);
		rc.addSprite(bomb);
	}

	override function update(delta:Float) {
		if (_shakeTime > 0) {
			var _shakeT:Float = _shakeTime / _shakeTimeTotal;
			_shakeTime = Math.max(0, _shakeTime - delta);
			_shakePhase += Math.PI * 100 * Math.random() * delta;

			var tamp:Float = 8 * _shakeAmp;

			rc.x = Math.floor(-(tamp / 2) + tamp * _shakeT * Math.sin(_shakePhase));
			rc.y = Math.floor(-(tamp / 2) + tamp * _shakeT * Math.cos(_shakePhase));
		}

		for (sprite in rc.sprites) {
			if (sprite.is(GameSprite)) {
				cast(sprite, GameSprite).update(delta);
				if (cast(sprite, GameSprite).remove) {
					rc.sprites.remove(sprite);
				}
			}
		}

		var p = rc.camera.position.clone();

		var speed = (inputKeys[LEFT_SHIFT] || gamepadButtons[RIGHT_SHOULDER]) ? RUN_SPEED : MOVE_SPEED;

		rc.camera.angle += ROTATION_SPEED * inputDirectionX() * delta;
		rc.camera.position.x += rc.camera.direction.x * speed * inputMovementY() * delta;
		rc.camera.position.y += rc.camera.direction.y * speed * inputMovementY() * delta;

		rc.camera.position.x -= Math.cos(rc.camera.angle - Math.PI / 2) * speed * inputMovementX() * delta;
		rc.camera.position.y -= Math.sin(rc.camera.angle - Math.PI / 2) * speed * inputMovementX() * delta;

		rc.cameraCollision();

		var dp = new Point(rc.camera.position.x - p.x, rc.camera.position.y - p.y);

		cameraTraveledDistance += dp.length.abs();

		if (cameraTraveledDistance >= 0.8) {
			playSound('assets/sound/step_${stepAlt}.ogg', new SoundTransform(1, stepAlt == 1 ? -0.2 : 0.2));
			stepAlt = stepAlt == 1 ? 2 : 1;
			cameraTraveledDistance = 0;
		}

		if (layBomb) {
			putCellX = Std.int(rc.camera.position.x + rc.camera.direction.x);
			putCellY = Std.int(rc.camera.position.y + rc.camera.direction.y);

			var tile = rc.map.getTile(putCellX, putCellY);

			if (tile == null || tile.collision) {
				putCellX = Std.int(rc.camera.position.x);
				putCellY = Std.int(rc.camera.position.y);
			}

			bombPreviewSprite.x = putCellX + 0.5;
			bombPreviewSprite.y = putCellY + 0.5;
			bombPreviewSprite.visible = true;
		} else {
			bombPreviewSprite.visible = false;
		}
	}

	public function updateCoinsHUD() {
		coinsHud.bitmapData.lock();
		for (px in 0...(Math.floor(32 * (map.collectedCoins / map.numCoins)))) {
			coinsHud.bitmapData.setPixel32(px, 0, 0xffffff00);
		}
		coinsHud.bitmapData.unlock();
	}

	override function render() {
		rc.render();
	}
}
