 /**
  *  GameCanvas contains the Canvas for the Epsilon game.
  *  Epsilon is a simple space-shooter-vertical-scroller game.
  *
  *  Epsilon is available for non-commercial use only!
  *  You can learn from this sources and you can modify them
  *  for learning purposes.
  * 
  *  @author      Ziga Hajdukovic
  *  @version     1.0
  *
  *  CHANGES (01 --> 02)
  *  + new player sprite, p.png
  *  + added boss sprite boss1.png
  *  + added tileset t04.png
  *  + added enemy sprites e1.png, e2.png, e3.png
  *  + added fire.png, removed pf.png
  *  + added explosion sprite, exp.png
  *  + moved Image objects for sprites to spriteImage array
  *  + added tileImage
  *  + moved image loading to loadImages()
  *  + setClip in paintSprite
  *  + player sprite frames for left and right movement
  *  + multiple fire sprites + fire_cooldown_wait_frames
  *  + 20 FPS fixed frame rate
  *  + Vector explosionSprites
  *  + updateExplosionSprites()
  *  + doCollision() -> add explosion on collision
  *  + paint explosionSprites
  *  + Level class
  *  + loadLevel(), tile map, and sprite positions
  *  + space and tiles y and vy
  *  + load sprites positions
  *  + level paint
  *  +   (scrolling background space layer)
  *  +   (scrolling tile layer)
  *  + new (but still random) enemies
  *  + player game over explosion
  *  
 */

import java.util.*;

import javax.microedition.lcdui.*;

//import com.nokia.mid.ui.FullCanvas;
//
//class GameCanvas extends FullCanvas implements Runnable
class GameCanvas extends Canvas implements Runnable
{
	private final Game midlet;
	
	public static int canvasWidth;
	public static int canvasHeight;

	public static boolean gameRunning;

	public static final int SOFTKEY_1 = -6;
	public static final int SOFTKEY_2 = -7;
	
	public static boolean menu_command_1;
	public static boolean menu_command_2;
	
	public static boolean command_up;
	public static boolean command_down;
	public static boolean command_left;
	public static boolean command_right;
	public static boolean command_fire;
	
	public static byte mode;
	public static final byte MODE_INTRO = 0;
	public static final byte MODE_GAME = 1;
	public static final byte MODE_MENU = 2;
	
	public static int loadingCounter;

	public static Image imgIntro;
	public static Image imgBackground;

	public static Image[] spriteImages;
	public static final byte SPRITE_IMAGE_PLAYER = 0;
	public static final byte SPRITE_IMAGE_FIRE = 1;
	public static final byte SPRITE_IMAGE_BOSS1 = 2;
	public static final byte SPRITE_IMAGE_ENEMY1 = 3;
	public static final byte SPRITE_IMAGE_ENEMY2 = 4;
	public static final byte SPRITE_IMAGE_ENEMY3 = 5;
	public static final byte SPRITE_IMAGE_EXPLOSION = 6;
	public static final byte SPRITE_IMAGE_MAX = 7;
	
	public static final byte SPRITE_FRAME_PLAYER_LEFT = 0;
	public static final byte SPRITE_FRAME_PLAYER_CENTER = 1;
	public static final byte SPRITE_FRAME_PLAYER_RIGHT = 2;
	
	public static final byte FIRE_COOLDOWN_WAIT_FRAMES = 10;
	public static byte fire_cooldown_wait_frames_counter;
	
	public static Image tileImage;

	public static int menuScreen;
	public static final byte MENU_SCR_MAIN = 0; 
	public static final byte MENU_SCR_TEXT_ABOUT = 1;
	public static final byte MENU_SCR_TEXT_HELP = 2;
	public static final byte MENU_SCR_TEXT_GAME_OVER = 3;

	public static int menuSelectedOption;
	public static final byte MENU_OPTION_MAIN_NEW_GAME = 0;
	public static final byte MENU_OPTION_MAIN_ABOUT = 1;
	public static final byte MENU_OPTION_MAIN_HELP = 2;
	public static final byte MENU_OPTION_MAIN_MAX = 2;
	
	public static final String[] menuScreenMainOptions = { "New game", "About", "Help"};
	
	public static final String[][] menuScreensTextLines = {
		{ "" },
		{ "About", "", "Epsilon v0.1", "(c) 2005 zigah." },
		{ "Help", "", "Press LEFT, RIGHT, ", "UP or DOWN", "to move and", "FIRE to shoot." },
		{ "Game Over", "", "Well done!"},
	};
	
	public static GameSprite player;
	public static Vector playerFireSprites;
	public static int player_score;
	public static int player_gameover_explosion_counter;
	public static int PLAYER_GAMEOVER_EXPLOSION_COUNTER = 40;

	public static final byte MAX_ENERGY_PLAYER = 100;
	public static final byte MAX_ENERGY_PLAYER_FIRE = 10;
	public static final byte MAX_ENERGY_ENEMY = 10;
	
	public static Vector enemySprites;
	
	public static Vector explosionSprites;

	public static byte max_enemy_sprites = 3;
	
	public static final short FIXED_FRAME_TIME = 60;

	public static int global_frame_counter;
	
	public static Random random = new Random();
	
	GameCanvas(Game midlet)
	{
		this.midlet = midlet;

		canvasWidth = this.getWidth();
		canvasHeight = this.getHeight();

		loadingCounter = 0;
		menuScreen = MENU_SCR_MAIN;
		menuSelectedOption = MENU_OPTION_MAIN_NEW_GAME;
		mode = MODE_INTRO;
		gameRunning = true;
	}

	public void run()
	{
		while(gameRunning)
		{
			long frameStartTime = System.currentTimeMillis();
			global_frame_counter++;
			
			repaint();
			serviceRepaints();

			switch (mode) {
			case MODE_INTRO:

				try
				{
					if (loadingCounter == 0)
					{
						imgIntro = Image.createImage("/intro.png");
					}
					else if (loadingCounter == 1)
					{
						loadImages();
					}
					else
					{
						// view the intro splash image for a while
						// it would be better to let user skip intro by pressing a key
						// but, in a full game project, loading would take forever, anyway, so...
						try	{
							Thread.sleep(1000);
						} catch (Exception e) {};
						// switch mode
						mode = MODE_MENU;
					}
					loadingCounter++;
				}
				catch (Exception e)	{
					// ignore loading exception	
				};
				break;
				
			case MODE_MENU:

				if (command_up) {
					command_up = false;
					menuSelectedOption--;
					if (menuSelectedOption < 0)
						menuSelectedOption = 0;
				}
				if (command_down) {
					command_down = false;
					menuSelectedOption++;
					if ((menuScreen == MENU_SCR_MAIN) &&
							(menuSelectedOption > MENU_OPTION_MAIN_MAX))
						menuSelectedOption = MENU_OPTION_MAIN_MAX;
				}
				
				if (menu_command_1)
				{
					menu_command_1 = false;
					if (menuScreen == MENU_SCR_MAIN)
					{
						switch(menuSelectedOption)
						{
						case MENU_OPTION_MAIN_NEW_GAME:
							startGame();
							break;
							
						case MENU_OPTION_MAIN_ABOUT:
							menuScreen = MENU_SCR_TEXT_ABOUT;
							break;
							
						case MENU_OPTION_MAIN_HELP:
							menuScreen = MENU_SCR_TEXT_HELP;
							break;
						}
					} else {
						// all other menu screens go to main menu when OK is selected
						menuScreen = MENU_SCR_MAIN;
					}
				}
				if (menu_command_2)
				{
					menu_command_2 = false;
					
					// exit when in main menu
					if (menuScreen == MENU_SCR_MAIN)
						midlet.exitRequested();
				}

				// delay, so the keypress events get their turn...
				try	{
					Thread.sleep(40);
				} catch (Exception e) {};
				break;
				
			case MODE_GAME:

				// scroll space and tile background
				Level.space_scroll_y += Level.space_scroll_vy;
				Level.tiles_scroll_y += Level.tiles_scroll_vy;

				// top of the level (boss) reached, stop scrolling
				if (Level.tiles_scroll_y < 0)
					Level.tiles_scroll_y = 0;
				
				int tiles_scroll_x_range = (Level.LEVEL_WIDTH * Level.TILE_SIZE - canvasWidth);
				int player_x_range = canvasWidth - player.w;
				Level.tiles_scroll_x = player.x * tiles_scroll_x_range / player_x_range;
//				System.out.print("tx: "+Level.tiles_scroll_x);
//				System.out.println(", ty: "+Level.tiles_scroll_y);
				
				updateEnemySprites();

				updatePlayerSprite();
				
				updatePlayerFireSprites();
				
				updateExplosionSprites();
				
				// check player <-> enemy <-> player fire collision
				doCollision();
				
				// check for game end
				if (menu_command_2 || 
						( (player.energy <= 0) &&
								(player_gameover_explosion_counter == 0))
						)
				{
					menu_command_2 = false;
					// on softkey_2 end game and return to main menu..
					endGame();
				}

				// calc. delay
				long frameTime = System.currentTimeMillis() - frameStartTime;
				if (frameTime < FIXED_FRAME_TIME)
				{
					try	{
						Thread.sleep( FIXED_FRAME_TIME - frameTime );
					} catch (Exception e) {};
				}
				break;
			}
		}
	}
	
	public void paint(Graphics g)
	{
		switch(mode)
		{
		case MODE_INTRO:

			g.setColor(0x000000);
			g.fillRect(0, 0, canvasWidth, canvasHeight);
			if (imgIntro != null) {
				g.drawImage(imgIntro, canvasWidth/2, canvasHeight/2, Graphics.VCENTER | Graphics.HCENTER);
			}

//			g.setColor(0xFFFFFF);
//			g.drawString(""+loadingCounter,0,0,Graphics.LEFT | Graphics.TOP);

			break;
			
		case MODE_MENU:

			Level.paintSpaceBackground(g);

			switch(menuScreen)
			{
			case MENU_SCR_MAIN:
				paintMainMenu(g);
				break;
				
			case MENU_SCR_TEXT_ABOUT:
			case MENU_SCR_TEXT_HELP:
			case MENU_SCR_TEXT_GAME_OVER:
				paintTextMenu(g, menuScreen);
				break;
			}
			break;
			
		case MODE_GAME:
			
			Level.paintSpaceBackground(g);
			Level.paintTiles(g);
			
			paintSprite( g, player );
			
			for (int i=0; i < playerFireSprites.size(); i++)
				paintSprite( g, (GameSprite) playerFireSprites.elementAt(i));
			
			for (int i=0; i < enemySprites.size(); i++)
				paintSprite( g, (GameSprite) enemySprites.elementAt(i) );
			
			for (int i=0; i < explosionSprites.size(); i++)
				paintSprite( g, (GameSprite) explosionSprites.elementAt(i) );

			paintStatusBars(g);
			
			break;		
		}
	}
	
	public void keyPressed(int key)
	{
		switch(key) {
		case SOFTKEY_1:
			menu_command_1 = true;
			break;
			
		case SOFTKEY_2:
			menu_command_2 = true;
			break;
		}
		
		int game_action = getGameAction(key);
		
		switch(game_action)
		{
		case UP:
			command_up = true;
			break;

		case DOWN:
			command_down = true;
			break;

		case LEFT:
			command_left = true;
			break;

		case RIGHT:
			command_right = true;
			break;

		case FIRE:
			command_fire = true;
			menu_command_1 = true;
			break;
		}
	}
	
	public void keyReleased(int key)
	{
		int game_action = getGameAction(key);

		switch(game_action)
		{
		case UP:
			command_up = false;
			break;

		case DOWN:
			command_down = false;
			break;

		case LEFT:
			command_left = false;
			break;

		case RIGHT:
			command_right = false;
			break;

		case FIRE:
			command_fire = false;
			menu_command_1 = false;
			break;
		}
	}

	public void paintMainMenu(Graphics g)
	{
		// init font
		g.setFont(Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_BOLD, Font.SIZE_LARGE));
		int menu_y = canvasHeight/2 - 45;
		for(int i=0; i <= MENU_OPTION_MAIN_MAX; i++)
		{
			// paint the main menu options
			if (i == menuSelectedOption) {
				g.setColor(0xFFCC00);
			} else {
				g.setColor(0xFFFFFF);
			}
			
			g.drawString(menuScreenMainOptions[i], canvasWidth/2, menu_y, Graphics.HCENTER | Graphics.TOP);
			
			menu_y += 30;
		}
		g.setFont(Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_BOLD, Font.SIZE_MEDIUM));
		g.setColor(0xFFFFFF);
		// paint softkey_1 label
		g.drawString("Select", 2, canvasHeight-2, Graphics.LEFT | Graphics.BOTTOM);
		// paint softkey_2 label
		g.drawString("Exit", canvasWidth-2, canvasHeight-2, Graphics.RIGHT | Graphics.BOTTOM);
	}

	public void paintTextMenu(Graphics g, int screen)
	{
		g.setFont(Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_BOLD, Font.SIZE_MEDIUM));
		g.setColor(0xFFFFFF);
		int text_line_y = 10;
		for(int line=0; line < menuScreensTextLines[screen].length; line++)
		{
			// paint all the lines of text in this menu screen
			g.drawString(menuScreensTextLines[screen][line], canvasWidth/2, text_line_y, Graphics.HCENTER | Graphics.TOP);
			
			text_line_y += 15;
		}
		if (screen == MENU_SCR_TEXT_GAME_OVER)
		{
			// print score in the game over screen
			text_line_y += 15;
			g.drawString("Score: "+ player_score, canvasWidth/2, text_line_y, Graphics.HCENTER | Graphics.TOP);
		}

		g.setFont(Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_BOLD, Font.SIZE_MEDIUM));
		// paint softkey_1 label
		g.drawString("Ok", 1, canvasHeight-1, Graphics.LEFT | Graphics.BOTTOM);
	}

	public static void paintSprite(Graphics g, GameSprite spr)
	{
		// set the clip window
		int cx = spr.x;
		int cy = spr.y;
		int cw = GameSprite.spriteImageFrameWidths[spr.imageId];
		int ch = spr.h;
		g.setClip(cx, cy, cw, ch);
		
		// calculate the x coordinate of where to start the image paint, so our frame will be seen through the clip window
		int dx = spr.x - spr.frameId * GameSprite.spriteImageFrameWidths[spr.imageId];
		
		// paint the sprites image at the calculated coordinates
		g.drawImage(spriteImages[spr.imageId], dx, spr.y, Graphics.TOP | Graphics.LEFT);

		// restore the clip window
		g.setClip(0, 0, canvasWidth, canvasHeight);
	}

	public static void paintStatusBars(Graphics g)
	{
		int max_energy_bar_width = canvasWidth/2;

		// paint the energy bar
		g.setColor(0xFFFFFF);
		g.drawRect(5, 10, max_energy_bar_width + 1, 4);
		g.setColor(0xFFCC00);
		g.fillRect(6, 11, player.energy * max_energy_bar_width / player.energyMax, 3);
		
		// paint the score count
		g.setColor(0xFFFFFF);
		g.setFont(Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_PLAIN, Font.SIZE_SMALL));
		g.drawString(""+player_score, canvasWidth-4, 8, Graphics.TOP | Graphics.RIGHT);
	}

	public void startGame()
	{
		// switch to game mode
		mode = MODE_GAME;

		player_score = 0;
		player_gameover_explosion_counter = -1;
		
		// initialize the enemy sprites list
		enemySprites = new Vector();

		// initialize the explosion sprite list
		explosionSprites = new Vector();
		
		// initialize the player fire list
		playerFireSprites = new Vector();

		Level.loadLevel((byte) 1);

		// init level tiles position to horizontal center and vertical bottom
		Level.tiles_scroll_x = Level.LEVEL_WIDTH * Level.TILE_SIZE / 2 - canvasWidth /2;
		Level.tiles_scroll_y = Level.LEVEL_HEIGHT * Level.TILE_SIZE - canvasHeight - 1;
		
	}
	
	public void endGame()
	{
		// back to menu mode, set the game over text screen
		mode = MODE_MENU;
		menuScreen = MENU_SCR_TEXT_GAME_OVER;

		// clean up
		player = null;
		enemySprites = null;
	}

	public void updateEnemySprites()
	{
		// check if a new enemy has to be generated
		if( enemySprites.size() < max_enemy_sprites )
		{
			GameSprite new_enemy;
			
			// create a new enemy
			// which one?
			byte img_id = (byte) (SPRITE_IMAGE_ENEMY1 + Math.abs(random.nextInt() % 3));
			
			new_enemy = new GameSprite(img_id, MAX_ENERGY_ENEMY);
			switch (img_id)
			{
			case SPRITE_IMAGE_ENEMY1:
				new_enemy.vx = 0;
				new_enemy.vy = 3;
				break;
			case SPRITE_IMAGE_ENEMY2:
				new_enemy.vx = 0;
				new_enemy.vy = 6;
				break;
			case SPRITE_IMAGE_ENEMY3:
				new_enemy.vx = random.nextInt() % 2;
				new_enemy.vy = 3 + random.nextInt() % 2;
				break;
			}
			new_enemy.x = Math.abs(random.nextInt()) % canvasWidth;
			new_enemy.y = -new_enemy.h;
			
			// add the new enemy to the enemy sprites list
			enemySprites.addElement(new_enemy);
		}

		// move enemy sprites
		for (int i=0; i < enemySprites.size(); i++)
		{
			GameSprite enemy = (GameSprite) enemySprites.elementAt(i);

			// animate sprite to next sprite frame every 3 frames
			if (global_frame_counter % 3 == 0)
				enemy.frameId = (byte) ( (enemy.frameId + 1) % (enemy.frameIdMax+1));
			
			// move the enemy
			enemy.x += enemy.vx;
			enemy.y += enemy.vy;
			
			// if enemy is out of screen
			if ( (enemy.x > canvasWidth) || (enemy.x+enemy.w < 0) ||
				 (enemy.y > canvasHeight) || (enemy.y+enemy.h < 0) )
			{
				enemySprites.removeElementAt(i);
			}
		}
	}

	public void updatePlayerSprite()
	{
		if (player_gameover_explosion_counter > 0)
		{
			player_gameover_explosion_counter--;
		}
		else
		{
			// move player sprite on command, and check player <-> screen edge collision
			if (command_up)
			{
				player.y -= player.vy;
				if (player.y < 0)
					player.y = 0;
			}
			if (command_down)
			{	
				player.y += player.vy;
				if (player.y > canvasHeight - player.h)
					player.y = canvasHeight - player.h;
			}
			player.frameId = SPRITE_FRAME_PLAYER_CENTER;
			if (command_left)
			{	
				player.x -= player.vx;
				if (player.x < 0)
					player.x = 0;
				player.frameId = SPRITE_FRAME_PLAYER_LEFT;
			}
			if (command_right)
			{	
				player.x += player.vx;
				if (player.x > canvasWidth - player.w)
					player.x = canvasWidth - player.w;
				player.frameId = SPRITE_FRAME_PLAYER_RIGHT;
			}
			if (command_fire)
			{
				if (fire_cooldown_wait_frames_counter <= 0)
				{
					GameSprite fire = new GameSprite(SPRITE_IMAGE_FIRE, MAX_ENERGY_PLAYER_FIRE);
					fire.x = player.x + player.w/2 - fire.w/2;
					fire.y = player.y;
					fire.vy = -4;
					playerFireSprites.addElement(fire);
	
					fire_cooldown_wait_frames_counter = FIRE_COOLDOWN_WAIT_FRAMES;
				}
			}
			if (fire_cooldown_wait_frames_counter > 0)
				fire_cooldown_wait_frames_counter--;
		}
	}

	public void doCollision()
	{
		
		int i = 0;
		while (i < enemySprites.size())
		{
			GameSprite enemy = (GameSprite) enemySprites.elementAt(i);
			
			// check player <-> enemy collision
			if (player.collidesWith(enemy))
			{
				// add explosion
				addExplosion(enemy);
				addExplosion(player);

				// player loses energy
				player.energy -= player.energyMax/4;
				if (player.energy <= 0 && player_gameover_explosion_counter < 0)
				{
					player_gameover_explosion_counter = PLAYER_GAMEOVER_EXPLOSION_COUNTER;
				}

				// enemy sprite is destroyed
				enemySprites.removeElementAt(i);
				// only increase counter, if not removed from list
				i--;

			}
			i++;
		}

		i = 0;
		while (i < enemySprites.size())
		{
			GameSprite enemy = (GameSprite) enemySprites.elementAt(i);

			// check the player fire sprites <-> enemy collision
			int j = 0;
			while (j < playerFireSprites.size())
			{
				GameSprite fire = (GameSprite) playerFireSprites.elementAt(j);
				if (fire.collidesWith(enemy) && i < enemySprites.size())
				{
					// add explosions
					addExplosion(enemy);

					// destroy player fire
					playerFireSprites.removeElementAt(j);
					// only increase counter, if not removed from list
					j--;

					// enemy sprite is destroyed
					enemySprites.removeElementAt(i);
					// only increase counter, if not removed from list
					i--;
				
					// increase player score
					player_score += 10;
				}
				j++;
			}
			i++;
		}
	}

	public void updatePlayerFireSprites()
	{
		for (int i=0; i < playerFireSprites.size(); i++)
		{
			GameSprite fire = (GameSprite) playerFireSprites.elementAt(i);
			fire.y += fire.vy;
			
			// destroy player fire when out of screen
			if (fire.y + fire.h < 0)
			{
				playerFireSprites.removeElementAt(i);
				// only increase counter, if not removed from list
				i--;
			}
		}
	}

	public void updateExplosionSprites()
	{
		for (int i=0; i < explosionSprites.size(); i++)
		{
			GameSprite exp = (GameSprite) explosionSprites.elementAt(i);
			
			// animate explosion!
			exp.frameId++;
			
			if (exp.frameId > exp.frameIdMax)
			{
				explosionSprites.removeElementAt(i);
				// only increase counter, if not removed from list
				i--;
			}
		}
		// create a big bada boom player gameover explosion
		//  7 - max explosion frames
		//  2 - add an explosion every 2nd frame
		if (player_gameover_explosion_counter > 7 &&
				player_gameover_explosion_counter % 2 == 0
				)
		{
			GameSprite new_exp = addExplosion(player);
			new_exp.x += random.nextInt() % player.w;
			new_exp.y += random.nextInt() % player.h;
		}
	}

	public static void loadImages()
	{
		try {
			
			spriteImages = new Image[SPRITE_IMAGE_MAX]; 
			spriteImages[SPRITE_IMAGE_PLAYER] = Image.createImage("/p.png"); 
			spriteImages[SPRITE_IMAGE_FIRE] = Image.createImage("/fire.png"); 
			spriteImages[SPRITE_IMAGE_BOSS1] = Image.createImage("/boss1.png"); 
			spriteImages[SPRITE_IMAGE_ENEMY1] = Image.createImage("/e1.png"); 
			spriteImages[SPRITE_IMAGE_ENEMY2] = Image.createImage("/e2.png"); 
			spriteImages[SPRITE_IMAGE_ENEMY3] = Image.createImage("/e3.png"); 
			spriteImages[SPRITE_IMAGE_EXPLOSION] = Image.createImage("/exp.png"); 
	
			tileImage = Image.createImage("/t04.png");
			
			imgBackground = Image.createImage("/bg.png");
			
		} catch (Exception e)	{};
	}

	public static GameSprite addExplosion(GameSprite exp_spr)
	{
		// add explosion
		GameSprite exp = new GameSprite(SPRITE_IMAGE_EXPLOSION, (byte) 0);
		// center the explosion position on the exploded sprite
		exp.x = exp_spr.x + exp_spr.w/2 - exp.w/2;
		exp.y = exp_spr.y + exp_spr.h/2 - exp.h/2;
		exp.vx = exp_spr.vx;
		exp.vy = exp_spr.vy;
		explosionSprites.addElement(exp);
		
		return exp;
	}
}
