package worldGen;

import java.awt.Rectangle;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.LinkedList;

import engine.Game;
import engine.Handler;
import interfaces.Biomes;
import player.Player;
import utils.Runner;
import utils.Stack;
import utils.Utils;
import world.Tile;

/*  ------------- Nihilne ------------
    * Created by Mitch Weaver - 2016 *
      ~~~~ All Rights Reserved ~~~~ */

@SuppressWarnings("unchecked")
public final class WorldGenerator implements Biomes {
	
	public static boolean WORLD_GENERATED, beginTileCreation;
	public static Stack<Stack<Tile>> workerStacks;
	private static long tilesThen, then;
	public static float stackSize;
	public static volatile int workerNumber;
	
	public static final void generateWorld() {
				final int threads; if(Game.threads > 2) threads = Game.threads - 2; else threads = 1;
				final long generateWorldInitThen = then = System.currentTimeMillis(); workerNumber = 0;
				generateWorldSections(); beginTileCreation = false; percentage = 0;
				if(workerStacks != null) workerStacks.clear(); workerStacks = new Stack<Stack<Tile>>();
				for(int i = 0; i < threads; i++) workerStacks.push(new Stack<Tile>());
				Handler.numSectionsRowsColsHalved = Handler.numSectionsRowsCols/2;
				System.out.println("World Gen Preparation Complete --- Time Taken: " + Float.toString((float)(System.currentTimeMillis() - generateWorldInitThen)/1000f)+ " seconds");
			
			final Stack<Tile> tiles = new Stack<Tile>();
			
				try {   long getMapDataThen = System.currentTimeMillis();
						 InputStream fis = WorldGenerator.class.getResourceAsStream("/mapsdata.nihil");
						ObjectInputStream ois = new ObjectInputStream(fis);
						LinkedList<byte[]> bytes = (LinkedList<byte[]>) ois.readObject();
						byte[] mapToGenerate = (byte[])bytes.get(Utils.rand.nextInt(bytes.size()));
						for (int i = 0, count = 0; i < Handler.numRowsCols; i++)
							for (int j = 0; j < Handler.numRowsCols; j++) 
								tiles.push(new Tile(i * Game.TILE_SIZE, j * Game.TILE_SIZE, mapToGenerate[count++]));
						fis.close(); ois.close(); stackSize = tiles.size(); bytes.clear(); bytes = null; fis = null; ois = null; mapToGenerate = null;
						System.out.println("Map Data Retrieved --- Time Taken: " + Float.toString((float)(System.currentTimeMillis() - getMapDataThen)/1000f)+ " seconds");
				} catch (Exception e) { e.printStackTrace(); System.exit(0); }
			
			generateWorldEntities(tiles);
			
			beginTileCreation = true; tilesThen = System.currentTimeMillis();
			
			System.gc();
			
			for(int i = 0; i < workerStacks.size() - 1; i++)
				for(int j = 0; j < stackSize / threads; j++)
					workerStacks.get(i).push(tiles.pop());
			while(!tiles.isEmpty()) workerStacks.peek().push(tiles.pop());
			
			System.gc();
			
			for(int i = 0; i < workerStacks.size(); i++) {
				final int temp = i; 
				Game.pool.runTask(new Runner(() -> fillSectionsWithTiles(workerStacks.get(temp))));
			}
	}
	
	private static final synchronized void generateWorldEntities(Stack<Tile> tiles){
		final long then = System.currentTimeMillis();
		EntityGenerator.generate(tiles); int temp, biome;
		while(true){ /* set player somewhere random on the map, except in water / rare / dangeous place */
			temp = Utils.rand.nextInt(tiles.size()); biome = tiles.get(temp).biome;
			switch(biome){
				case OCEAN: break;
				case LAKE: break;
				case RIVER: break;
				case VOLCANIC: break;
				case COASTLINE: break;
				case BACK_OF_VOLCANO: break;
				case HIGHLIGHTED: break;
				case SNOW_TUNDA: break;
				case TAIGA: break;
				case BARREN_ROCK: break;
				default: { Player.pos.setLocation(tiles.get(temp).x + Game.TILE_SIZE/2, tiles.get(temp).y + Game.TILE_SIZE/2);
				System.out.println("World Entities Generated --- Time Taken: " + Float.toString((float)(System.currentTimeMillis() - then)/1000f)+ " seconds");
				return; }
			}
		}
	}
	
	private static final void generateWorldSections(){
		Handler.sections = null;
		Handler.SECTION_SIZE_WIDTH_HEIGHT = determineSectionSize();
		Handler.numSectionsRowsCols = (Handler.numRowsCols * Game.TILE_SIZE) / Handler.SECTION_SIZE_WIDTH_HEIGHT;
		Handler.AREA_RECT.width = Handler.AREA_RECT.height = Handler.SECTION_SIZE_WIDTH_HEIGHT*3;
		
		/* Create and fill the array of rectangles for sections of map */
		Handler.sections = new Rectangle[Handler.numSectionsRowsCols][Handler.numSectionsRowsCols];
		for(int i = 0; i < Handler.numSectionsRowsCols; i++)
			for(int j = 0; j < Handler.numSectionsRowsCols; j++)
				Handler.sections[i][j] = new Rectangle(i*Handler.SECTION_SIZE_WIDTH_HEIGHT, j*Handler.SECTION_SIZE_WIDTH_HEIGHT, Handler.SECTION_SIZE_WIDTH_HEIGHT, Handler.SECTION_SIZE_WIDTH_HEIGHT);
		
		/* Create and fill the arraylist of arraylists of arraylist of hashsets for tiles objects in map */
		Handler.world = new ArrayList<ArrayList<ArrayList<Stack<Tile>>>>(Handler.numSectionsRowsCols);
		
		for(int i = 0; i < Handler.numSectionsRowsCols; i++){
			Handler.world.add(new ArrayList<ArrayList<Stack<Tile>>>(Handler.numSectionsRowsCols)); /* ROW */
				for(int j = 0; j < Handler.numSectionsRowsCols; j++){
					Handler.world.get(i).add(new ArrayList<Stack<Tile>>()); 					   /* COL */
						for(int k = 0; k < Handler.NUM_TYPES_OF_HASHSETS; k++)
							Handler.world.get(i).get(j).add(new Stack<Tile>());			     /* TYPE */
				}
		}
	}
	
	/* calculates a section size that is able to be spread evenly through the map */
	private static final int determineSectionSize(){
		int length = Handler.numRowsCols * Game.TILE_SIZE; if(Utils.isOdd(length)) length++;
		int w = (int)(Game.WIDTH * 0.5f); if(Utils.isOdd(w)) w++; /* width will always be longer than height... or atleast should be may need to fix later if complaints */
		//NOTE: change this --------^ amount for smaller/larger squares --- (0.5f minimum)
		while(length % w != 0){
			if(length % w == w) {System.err.println("width is bigger than map length"); System.exit(1); } /* if the screen width is bigger than the map */
			else {
				int max = w, min = w;
				
				while(length % max != 0 && length % max != max) max++;
				
				while(length % min != min && length % min != 0 && min > ((Game.WIDTH * 0.5)-1)) min--;
				
				final int diffMax = Utils.abs(max - w);
				final int diffMin = Utils.abs(min - w);
				
				if(diffMin <= diffMax && diffMin > Game.WIDTH * 0.5f) w = min; else w = max;
			}
		}
		if(w == 0) { System.err.println("PROBLEM WITH WORLD GENERATOR -- (w == 0) ---- determineSectionSize()"); System.exit(0); }
		return w;
	}
	
	/* adds all objects from handler.tiles to stack, then erases handler.tiles, and re-adds */
	public static void regenWorldSectionsAndTiles(){
		if(Handler.world == null) return; // happens because @ start up
		final Stack<Tile> tiles = new Stack<Tile>();
		
		WORLD_GENERATED = beginTileCreation = Player.keepWalkingX = Player.keepWalkingY = Player.using = Player.attacking = false;
		Player.velX = Player.velY = 0; workerNumber = 0; percentage = 0; stackSize = 0;
		
		for(int i = 0; i < Handler.numSectionsRowsCols; i++)
			for(int j = 0; j < Handler.numSectionsRowsCols; j++)
					for(int k = 0; k < Handler.NUM_TYPES_OF_HASHSETS; k++)
						for(Tile o : Handler.world.get(i).get(j).get(k))
								tiles.push(o);
		
		stackSize = tiles.size();
		Game.pool.runTask(new Runner(() -> { generateWorldSections(); }));
		
		if(workerStacks != null) workerStacks.clear();
		workerStacks = new Stack<Stack<Tile>>();
		for(int i = 0; i < Game.threads; i++) workerStacks.push(new Stack<Tile>());
		
		for(int i = 0; i < workerStacks.size() - 1; i++)
			for(int j = 0; j < stackSize / Game.threads; j++)
				workerStacks.get(i).push(tiles.pop());
		
		while(!tiles.isEmpty()) workerStacks.get(workerStacks.size() - 1).push(tiles.pop());
		
		for(int i = 0; i < workerStacks.size(); i++) {
			final int temp = i; Game.pool.runTask(new Runner(() -> fillSectionsWithTiles(workerStacks.get(temp))));
		}
	}
	
	public static float percentage;
	public static final void fillSectionsWithTiles(Stack<Tile> stack) {
		if(stack == null || stack.isEmpty()){ System.err.println("PROBLEM fillSectionsWithTiles!"); System.exit(0); }
		
		while(!stack.isEmpty()) Handler.addTile(stack.pop());
		
		System.out.println("Worker " + ++workerNumber + " finished.");
		checkIfDone();
	}
	
	private static synchronized final void checkIfDone(){
		System.gc();
		for(final Stack<Tile> i : workerStacks) if(!i.isEmpty()) return;
		
		
		System.out.println("Tiles Generated --- Time Taken: " + Float.toString((float)(System.currentTimeMillis() - tilesThen)/1000f)+ " seconds");
		
		workerStacks.clear(); workerStacks = null;
		
		System.out.println("Tiles: " + Utils.square(Handler.numRowsCols));
		System.out.println("TREES: " + Handler.trees.size());
		System.out.println("BOULDERS: " + Handler.boulders.size());
		System.out.println("MOBS: " + Handler.mobs.size());
		System.out.println("FLOWERS: " + Handler.flowers.size());
		System.out.println("TALLGRASS: " + Handler.grass.size());
		System.out.println("SAPLINGS: " + Handler.saplings.size());
		System.err.println("~~~ World Generated --- Total Time Taken: " + Float.toString((System.currentTimeMillis() - then)/1000f)+ " seconds ~~~");
		WORLD_GENERATED = true;
		Handler.tick();
		Handler.getMapImage(); 
		System.gc();
	}
}
