package dk.itu.mario.level;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import dk.itu.mario.MarioInterface.GamePlay;
import dk.itu.mario.MarioInterface.LevelInterface;
import dk.itu.mario.engine.DataRecorder;
import dk.itu.mario.engine.sprites.Enemy;
import dk.itu.mario.engine.sprites.SpriteTemplate;
import dk.itu.mario.level.grammar.GrammarTuner;
import dk.itu.mario.level.grammar.LevelGrammar;
import dk.itu.mario.level.grammar.LevelGrammarFactory;
import dk.itu.mario.level.grammar.LevelParseTree;
import dk.itu.mario.level.matcher.ArchetypeMatcher;
import dk.itu.mario.level.matcher.LevelArchetype;
import dk.itu.mario.level.matcher.PlayerProfile;
import dk.itu.mario.level.matcher.ProfileMatcher;

public class PCGLevel extends Level {
	public enum MazeLevel {
		BOT, MID, TOP
	}

	public static long lastSeed;
	private static Random levelSeedRandom = new Random();

	public int BLOCKS_COINS = 0; // the number of coin blocks
	public int BLOCKS_EMPTY = 0; // the number of empty blocks
	public int BLOCKS_POWER = 0; // the number of power blocks
	public int COINS = 0; // These are the coins in boxes that Mario collect

	private DataRecorder dataRecorder;
	private int difficulty;
	// Store information about the level
	public int ENEMIES = 0; // the number of enemies the level contains
	private int gaps;
	private int type;
	private Random random;

	public PCGLevel(int width, int height) {
		super(width, height);
	}

	public PCGLevel(int width, int height, long seed, int difficulty,
			int type, GamePlay playerMetrics) {
		this(width, height);

		System.out
				.println("Generating level based on previous GamePlay metrics ONLY.");
		this.dataRecorder = DataRecorder.BLANK_RECORD;

		generateLevel(seed, playerMetrics);
	}

	public PCGLevel(int width, int height, long seed, int difficulty,
			int type, GamePlay playerMetrics, DataRecorder dataRecorder) {
		this(width, height);

		System.out
				.println("Generating level based on previous GamePlay AND DataRecorder metrics.");
		this.dataRecorder = dataRecorder;

		generateLevel(seed, playerMetrics);
	}

	private void generateLevel(long seed, GamePlay playerMetrics) {

		PlayerProfile profile = ProfileMatcher.getMatchingProfile(
				playerMetrics, dataRecorder);
		System.out.println("PlayerProfile: " + profile);

		LevelArchetype archetype = ArchetypeMatcher.getMatchingArchetype(
				playerMetrics, dataRecorder);
		System.out.println("LevelArchetype: " + archetype);

		System.out.println("Creating level grammar");
		LevelGrammar grammar = LevelGrammarFactory.createGrammar(profile,
				archetype);

		System.out
				.println("Tuning grammar for PlayerProfile & LevelArchetype using RETE");
		grammar = GrammarTuner.tune(grammar, profile, archetype);

		System.out.println("Creating level.");
		create(seed, profile, archetype, grammar);
	}

	@Override
	public RandomLevel clone() throws CloneNotSupportedException {

		RandomLevel clone = new RandomLevel(width, height);

		clone.xExit = xExit;
		clone.yExit = yExit;
		byte[][] map = getMap();
		SpriteTemplate[][] st = getSpriteTemplate();

		for (int i = 0; i < map.length; i++)
			for (int j = 0; j < map[i].length; j++) {
				clone.setBlock(i, j, map[i][j]);
				clone.setSpriteTemplate(i, j, st[i][j]);
			}
		clone.BLOCKS_COINS = BLOCKS_COINS;
		clone.BLOCKS_EMPTY = BLOCKS_EMPTY;
		clone.BLOCKS_POWER = BLOCKS_POWER;
		clone.ENEMIES = ENEMIES;
		clone.COINS = COINS;

		return clone;

	}

	private void create(long seed, PlayerProfile profile,
			LevelArchetype archetype, LevelGrammar grammar) {
		if (dataRecorder == DataRecorder.BLANK_RECORD) {
			System.out
					.println("DataRecorder record is BLANK - using GamePlay metrics only.");
		}

		this.type = archetype.getTypeInt();
		this.difficulty = archetype.getDifficultyLevel();

		lastSeed = seed;
		random = new Random(seed);

		System.out.println("Generating level for component list: ");
		LevelParseTree parseTree = grammar.generateRandomTree(seed, width);
		List<LevelComponent> levelTemplate = parseTree.getLevelTemplate();
		
		int length = 0;
		
		for (LevelComponent lcomp : levelTemplate) {
			LevelComponent.TYPE lctype = lcomp.getType();
			System.out.println("Building for: " + lcomp);
			switch (lctype) {
				case FLAT:
					while (length < Math.min(width-64,lcomp.getEnd())) {
						length += buildStraight(length, lcomp.getEnd(), true);
					}
				break;
				default  :
				System.out.println("Cannot build level segment for unrecognized LevelComponent type: " + type);
			}
		}
		
		System.out.println("Total length built: " + length);
		
		/* Original non-PCG code:
		// create the start location
		int length = buildStraight(0, width, true);
		length += buildPipeJump(length, width - length);
		// length += buildMaze(length, width - length);

		// create all of the medium sections
		while (length < width - 64) {
			length += buildStraight(length, width - length, true);
		}
		*/
		// set the end piece
		int floor = height - 1 - random.nextInt(4);

		xExit = length + 8;
		yExit = floor;
		
		fillEndPiece(length, floor);
	}

	private void fillEndPiece(int length, int floor) {
		// fills the end piece
		for (int x = length; x < width; x++) {
			for (int y = 0; y < height; y++) {
				if (y >= floor) {
					setBlock(x, y, GROUND);
				}
			}
		}

		if (type == LevelInterface.TYPE_CASTLE
				|| type == LevelInterface.TYPE_UNDERGROUND) {
			int ceiling = 0;
			int run = 0;
			for (int x = 0; x < width; x++) {
				if (run-- <= 0 && x > 4) {
					ceiling = random.nextInt(4);
					run = random.nextInt(4) + 4;
				}
				for (int y = 0; y < height; y++) {
					if ((x > 4 && y <= ceiling) || x < 1) {
						setBlock(x, y, GROUND);
					}
				}
			}
		}

		fixWalls();
	}
	private void addEnemyLine(int x0, int x1, int y) {
		for (int x = x0; x < x1; x++) {
			if (random.nextInt(35) < difficulty + 1) {
				int type = random.nextInt(4);

				if (difficulty < 1) {
					type = Enemy.ENEMY_GOOMBA;
				} else if (difficulty < 3) {
					type = random.nextInt(3);
				}

				setSpriteTemplate(x, y,
						new SpriteTemplate(type,
								random.nextInt(35) < difficulty));
				ENEMIES++;
			}
		}
	}

	private void blockify(Level level, boolean[][] blocks, int width, int height) {
		int to = 0;
		if (type == LevelInterface.TYPE_CASTLE) {
			to = 4 * 2;
		} else if (type == LevelInterface.TYPE_UNDERGROUND) {
			to = 4 * 3;
		}

		boolean[][] b = new boolean[2][2];

		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				for (int xx = x; xx <= x + 1; xx++) {
					for (int yy = y; yy <= y + 1; yy++) {
						int _xx = xx;
						int _yy = yy;
						if (_xx < 0)
							_xx = 0;
						if (_yy < 0)
							_yy = 0;
						if (_xx > width - 1)
							_xx = width - 1;
						if (_yy > height - 1)
							_yy = height - 1;
						b[xx - x][yy - y] = blocks[_xx][_yy];
					}
				}

				if (b[0][0] == b[1][0] && b[0][1] == b[1][1]) {
					if (b[0][0] == b[0][1]) {
						if (b[0][0]) {
							level.setBlock(x, y, (byte) (1 + 9 * 16 + to));
						} else {
							// KEEP OLD BLOCK!
						}
					} else {
						if (b[0][0]) {
							// down grass top?
							level.setBlock(x, y, (byte) (1 + 10 * 16 + to));
						} else {
							// up grass top
							level.setBlock(x, y, (byte) (1 + 8 * 16 + to));
						}
					}
				} else if (b[0][0] == b[0][1] && b[1][0] == b[1][1]) {
					if (b[0][0]) {
						// right grass top
						level.setBlock(x, y, (byte) (2 + 9 * 16 + to));
					} else {
						// left grass top
						level.setBlock(x, y, (byte) (0 + 9 * 16 + to));
					}
				} else if (b[0][0] == b[1][1] && b[0][1] == b[1][0]) {
					level.setBlock(x, y, (byte) (1 + 9 * 16 + to));
				} else if (b[0][0] == b[1][0]) {
					if (b[0][0]) {
						if (b[0][1]) {
							level.setBlock(x, y, (byte) (3 + 10 * 16 + to));
						} else {
							level.setBlock(x, y, (byte) (3 + 11 * 16 + to));
						}
					} else {
						if (b[0][1]) {
							// right up grass top
							level.setBlock(x, y, (byte) (2 + 8 * 16 + to));
						} else {
							// left up grass top
							level.setBlock(x, y, (byte) (0 + 8 * 16 + to));
						}
					}
				} else if (b[0][1] == b[1][1]) {
					if (b[0][1]) {
						if (b[0][0]) {
							// left pocket grass
							level.setBlock(x, y, (byte) (3 + 9 * 16 + to));
						} else {
							// right pocket grass
							level.setBlock(x, y, (byte) (3 + 8 * 16 + to));
						}
					} else {
						if (b[0][0]) {
							level.setBlock(x, y, (byte) (2 + 10 * 16 + to));
						} else {
							level.setBlock(x, y, (byte) (0 + 10 * 16 + to));
						}
					}
				} else {
					level.setBlock(x, y, (byte) (0 + 1 * 16 + to));
				}
			}
		}
	}

	private int buildMaze(int xo, int maxLength) {
		int length = random.nextInt(maxLength - 19) + 20;
		int soFar = 0;
		int next;
		// MazeLevel last = MazeLevel.BOT;

		class Stretch {
			public int len;
			public PCGLevel.MazeLevel lvl;

			public Stretch(int lngth) {
				len = lngth;

				switch (random.nextInt(3)) {
				case 0:
					lvl = MazeLevel.TOP;
					break;
				case 1:
					lvl = MazeLevel.MID;
					break;
				default:
					lvl = MazeLevel.BOT;
				}
			}
		}

		ArrayList<Stretch> maze = new ArrayList<Stretch>();

		while (soFar < length - 3) {
			next = random.nextInt(length / 2) + 1;

			if (soFar + next > length - 3) {
				next = length - soFar;
			}

			maze.add(new Stretch(next));

			soFar += next;
		}

		soFar = 0;
		setBlock(xo, this.height - 1, Level.GROUND);
		setBlock(xo + 1, this.height - 1, Level.GROUND);
		setBlock(xo + 2, this.height - 1, Level.GROUND);

		for (Stretch str : maze) {
			for (int x = 0; x < str.len; x++) {
				setBlock(xo + 3 + soFar + x, this.height - 1, Level.GROUND);
				setBlock(xo + 3 + soFar + x, this.height - 4, Level.BLOCK_EMPTY);
				setBlock(xo + 3 + soFar + x, this.height - 7, Level.BLOCK_EMPTY);

				if (x == str.len / 2) {

					if (str.lvl != MazeLevel.BOT) {
						setBlock(xo + 3 + soFar + x, this.height - 2,
								Level.BLOCK_EMPTY);
						setBlock(xo + 3 + soFar + x, this.height - 3,
								Level.BLOCK_EMPTY);
					}
					if (str.lvl != MazeLevel.MID) {
						setBlock(xo + 3 + soFar + x, this.height - 5,
								Level.BLOCK_EMPTY);
						setBlock(xo + 3 + soFar + x, this.height - 6,
								Level.BLOCK_EMPTY);
					}
					if (str.lvl != MazeLevel.TOP) {
						setBlock(xo + 3 + soFar + x, this.height - 8,
								Level.BLOCK_EMPTY);
						setBlock(xo + 3 + soFar + x, this.height - 9,
								Level.BLOCK_EMPTY);
						setBlock(xo + 3 + soFar + x, this.height - 10,
								Level.BLOCK_EMPTY);
						setBlock(xo + 3 + soFar + x, this.height - 11,
								Level.BLOCK_EMPTY);
						setBlock(xo + 3 + soFar + x, this.height - 12,
								Level.BLOCK_EMPTY);
						setBlock(xo + 3 + soFar + x, this.height - 13,
								Level.BLOCK_EMPTY);
						setBlock(xo + 3 + soFar + x, this.height - 14,
								Level.BLOCK_EMPTY);
					}
				}
			}
		}

		return length;
	}

	private int buildPipeJump(int xo, int maxLength) {
		int numPipes = 4;
		int length = numPipes * 2;
		int lastHeight = 4;
		int localHeight;
		int pipeHeight;
		int gap = 0;

		while (length > maxLength) {
			numPipes--;
			length = numPipes * 2;
		}

		if (length == 0) {
			return length;
		}

		for (int i = 0; i < numPipes && length < maxLength; i++) {
			length += random.nextInt(4);
		}

		if (length > maxLength) {
			length = maxLength;
		}

		for (int soFar = 0; numPipes > 0; numPipes--) {
			localHeight = (soFar == 0) ? random.nextInt(2) + 4 : random
					.nextInt(7) + 4;

			while (Math.abs(localHeight - lastHeight) > 3) {
				localHeight += localHeight > lastHeight ? -1 : 1;
			}

			lastHeight = localHeight;
			pipeHeight = localHeight > 5 ? 4 + random.nextInt(localHeight - 4)
					: 4;

			for (int y = 0; y < localHeight; y++) {
				setBlock(xo + soFar, this.height - 1 - y, Level.GROUND);
				setBlock(xo + soFar + 1, this.height - 1 - y, Level.GROUND);

				if (y == localHeight - 1) {
					setBlock(xo + soFar, this.height - 1 - y,
							Level.TUBE_TOP_LEFT);
					setBlock(xo + soFar + 1, this.height - 1 - y,
							Level.TUBE_TOP_RIGHT);
				} else if (y > localHeight - pipeHeight) {
					setBlock(xo + soFar, this.height - 1 - y,
							Level.TUBE_SIDE_LEFT);
					setBlock(xo + soFar + 1, this.height - 1 - y,
							Level.TUBE_SIDE_RIGHT);
				}
			}

			gap = random.nextInt(4);
			while (soFar + gap + 2 > length && gap >= 0) {
				gap--;
			}

			soFar += (2 + gap);
		}

		return length;
	}

	private int buildStraight(int xo, int maxLength, boolean safe) {
		int length = random.nextInt(10) + 2;

		if (safe)
			length = 10 + random.nextInt(5);

		if (length > maxLength)
			length = maxLength;

		int floor = height - 1 - random.nextInt(4);

		// runs from the specified x position to the length of the segment
		for (int x = xo; x < xo + length; x++) {
			for (int y = 0; y < height; y++) {
				if (y >= floor) {
					setBlock(x, y, GROUND);
				}
			}
		}

		if (!safe) {
			if (length > 5) {
				decorate(xo, xo + length, floor);
			}
		}

		return length;
	}

	private void decorate(int xStart, int xLength, int floor) {
		// if its at the very top, just return
		if (floor < 1)
			return;

		// boolean coins = random.nextInt(3) == 0;
		boolean rocks = true;

		// add an enemy line above the box
		addEnemyLine(xStart + 1, xLength - 1, floor - 1);

		int s = random.nextInt(4);
		int e = random.nextInt(4);

		if (floor - 2 > 0) {
			if ((xLength - 1 - e) - (xStart + 1 + s) > 1) {
				for (int x = xStart + 1 + s; x < xLength - 1 - e; x++) {
					setBlock(x, floor - 2, COIN);
					COINS++;
				}
			}
		}

		s = random.nextInt(4);
		e = random.nextInt(4);

		// this fills the set of blocks and the hidden objects inside them
		if (floor - 4 > 0) {
			if ((xLength - 1 - e) - (xStart + 1 + s) > 2) {
				for (int x = xStart + 1 + s; x < xLength - 1 - e; x++) {
					if (rocks) {
						if (x != xStart + 1 && x != xLength - 2
								&& random.nextInt(3) == 0) {
							if (random.nextInt(4) == 0) {
								setBlock(x, floor - 4, BLOCK_POWERUP);
								BLOCKS_POWER++;
							} else { // the fills a block with a hidden coin
								setBlock(x, floor - 4, BLOCK_COIN);
								BLOCKS_COINS++;
							}
						} else if (random.nextInt(4) == 0) {
							if (random.nextInt(4) == 0) {
								setBlock(x, floor - 4, (byte) (2 + 1 * 16));
							} else {
								setBlock(x, floor - 4, (byte) (1 + 1 * 16));
							}
						} else {
							setBlock(x, floor - 4, BLOCK_EMPTY);
							BLOCKS_EMPTY++;
						}
					}
				}
			}
		}
	}

	private void fixWalls() {
		boolean[][] blockMap = new boolean[width + 1][height + 1];

		for (int x = 0; x < width + 1; x++) {
			for (int y = 0; y < height + 1; y++) {
				int blocks = 0;
				for (int xx = x - 1; xx < x + 1; xx++) {
					for (int yy = y - 1; yy < y + 1; yy++) {
						if (getBlockCapped(xx, yy) == GROUND) {
							blocks++;
						}
					}
				}
				blockMap[x][y] = blocks == 4;
			}
		}
		blockify(this, blockMap, width + 1, height + 1);
	}

}
