/*
    Breakout for LPC2104 Color LCD Game
    
    2007 - www.FullyFledged.com tos@fullyfledged.com (Tomas Soderqvist)
    
    Embedded Artists AB may redistribute, sell, modify this code freely.
    
    
    Enjoy /ToS
   
   ---------------------------------------------------------------------------
*/
#include "../pre_emptive_os/api/osapi.h"
#include "../pre_emptive_os/api/general.h"
#include <ea_init.h>
#include <stdlib.h>
#include "key.h"
#include "hw.h"
#include "lcd.h"
/*
    Defines...    
   ---------------------------------------------------------------------------
*/
#define MAXEVENTS 10
#define XMAX 129
#define YMAX 129
#define PADDLE_STEP 7
#define PADDLE_YCOR 1
#define BPROW 10
#define BROWS 4
#define BROWHEIGHT 10
#define BALL_UPDATE_INTERVALL 40
#define PLIPPTIME 10
#define NUM_COLORS 14
#define BLACK   0x00      // 000000000
#define BLUE1   0x07      // 000000111
#define BLUE2   0x06      // 000000110
#define BLUE3   0x05      // 000000100
#define GREEN1  0x38      // 000111000
#define GREEN2  0x30      // 000110000
#define GREEN3  0x20      // 000100000
#define CYAN    0x3F      // 000111111
#define RED1    0x1C1     // 111000000
#define RED2    0x180     // 110000000
#define RED3    0x100     // 100000000
#define MAGENTA 0x1C7     // 111111000
#define YELLOW  0x1F8     // 000111111
#define GREY2   0xDB      // 011011011
#define GREY1   0x1B6     // 110110110
#define WHITE   0x1FF     // 111111111
/*
    Enumberations...    
   ---------------------------------------------------------------------------
*/
// 
enum g_PaddleHit_t {
                 NO,            // No hit
                 CENTER,        // Hit center at paddle
                 LEFT_EDGE,     // Hit left edge at paddle
                 RIGHT_EDGE,    // Hit right edge at paddle
                 SITTING        // Sitting on paddle with speed 0
                };

enum g_BallHit_t {
                 NO_HIT,        // No hit
                 LEFT_HIT,      // Left side of ball hit block
                 RIGHT_HIT,     // Right side of ball hit block
                 TOP_HIT,       // Top side of ball hit block
                 BOTTOM_HIT     // Bottom side of ball hit block
                 };                 
 
enum g_State_t { 
                 INIT,          // Init app.
                 IDLE,          // Idle, wait for start
                 GOVER,         // GameOver
                 PLAY,          // During game
                 STOP           // Stop app.
                 };

enum g_Event_t {
                  NONE,         // No event in queue
                  LEFT,         // Stick left
                  RIGHT,        // Stick right
                  FIRE,         // Stick press
                  START,        // Do start app
                  PLIPP,
                  LEVLA,        // Do advance level
                  NOBALLS,      // No balls left
                  QUIT          // User selected quit
                 };
/*
    Structs...    
   ---------------------------------------------------------------------------
*/
typedef struct {
  tU8 xCor;
  tU8 yCor;
  tS8 xSpeed;
  tS8 ySpeed;
  tU8 count;
  tU8 updateIntervall;
} s_Ball;                       // Ball related data

typedef struct {
  tU8 xCor;
  tU8 yCor;
  tU8 width;
} s_Paddle;                     // Paddle related data


static tU8 color[NUM_COLORS]={  // Color scheme for flashing txt and rows.
(tU8)BLACK, 
(tU8)BLUE1,
(tU8)GREEN1,
(tU8)RED1,
(tU8)YELLOW, 
(tU8)BLUE2,
(tU8)RED2,
(tU8)GREEN2,
(tU8)BLUE3,
(tU8)GREEN3,
(tU8)RED3, 
(tU8)MAGENTA, 
(tU8)GREY1, 
(tU8)GREY2
};

/*
    Function prototypes...    
   ---------------------------------------------------------------------------
*/
static void drawBrick(tU8 rpw, tU8 col);
static void eraseBrick(tU8 row, tU8 col);
static void drawPaddle(tU8 oldXCor);
static void drawBall(tU8 oldXCor, tU8 oldYCor);
static void initBO(void);
static void setUpLevel(void);
static void resetBO(void);
static void pushEvent(int newEvent);
static int  popEvent();
static void scanInputs(void);
static void idle(void);
static void gameOver(void);
static int  isBallAtPaddle(void);
static tU8  isPxAtBrick(tU8 xCor, tU8 yCor);
static tU8  isBallAtBrick(void);
static void handleBall(void);
static void play(void);
int         main(void);
/*
    Globals...    
   ---------------------------------------------------------------------------
*/
static int g_eState;
static int g_EventBuffer[MAXEVENTS];
static s_Ball g_sBall;
static s_Paddle g_sPaddle;
static tU8 g_SpaceAtTop;
static tU8 g_Bricks[BROWS][BPROW];
static unsigned int g_BrickHits;
static tU8 g_BricksCleared;
static tU8 g_level;
static tU8 g_autoPilot;
static tU8 g_plippTime;
static tU8 g_colorIdx;
static tU8 g_soundMode;
/*
    draw brick... 
   ---------------------------------------------------------------------------
*/
static void drawBrick(tU8 row, tU8 col) {

  tU8 xCor;
  tU8 yCor;

  //Recalc col row to xcor, ycor
  xCor = col*((XMAX+1)/BPROW);
  yCor = row*BROWHEIGHT+(g_SpaceAtTop+1);

  //rectangle in g_Bricks[col][row] color.
  lcdRect(xCor+1, yCor, ((XMAX+1)/BPROW)-2, BROWHEIGHT-2, color[g_Bricks[row][col]]);  

}
/*
    erase brick... 
   ---------------------------------------------------------------------------
*/
static void eraseBrick(tU8 row, tU8 col) {

  tU8 xCor;
  tU8 yCor;

  //Recalc col row to xcor, ycor
  xCor = col*((XMAX+1)/BPROW);
  yCor = row*BROWHEIGHT+(g_SpaceAtTop+1);

  //rectangle in g_Bricks[col][row] color.
  lcdRect(xCor+1, yCor, ((XMAX+1)/BPROW)-2, BROWHEIGHT-2, 0x00);

}
/*
    draw paddle... 
   ---------------------------------------------------------------------------
*/
static void drawPaddle(tU8 oldXCor) {

  // Have changed pos ?
  if (g_sPaddle.xCor != oldXCor) {
    
    if ((g_sPaddle.xCor < (((g_sPaddle.width - 1) / 2)))||(g_sPaddle.xCor > XMAX))  {
      g_sPaddle.xCor = ((g_sPaddle.width - 1) / 2); 
    }
    
    if (g_sPaddle.xCor > ((XMAX-(g_sPaddle.width - 1) / 2))) {
      g_sPaddle.xCor = (XMAX-(g_sPaddle.width - 1) / 2);
    }
  
    lcdRect(oldXCor - ((g_sPaddle.width - 1) / 2), g_sPaddle.yCor, g_sPaddle.width, 2, 0x00);
    lcdRect(g_sPaddle.xCor - (((g_sPaddle.width)-1)/2), g_sPaddle.yCor, g_sPaddle.width, 2, 0xff);
  } else {
    lcdRect(g_sPaddle.xCor - (((g_sPaddle.width)-1)/2), g_sPaddle.yCor, g_sPaddle.width, 2, 0xff);
  }
}
/*
    draw ball
   ---------------------------------------------------------------------------
*/
static void drawBall(tU8 oldXCor, tU8 oldYCor) {

  // Have changed pos ?
  if ((g_sBall.xCor != oldXCor) || (g_sBall.yCor != oldYCor)) {
    //Delete old place,
      lcdRect(oldXCor-1, oldYCor-1, 3, 3, 0);    

    //Set new place,
      lcdRect(g_sBall.xCor-1, g_sBall.yCor-1, 3, 3, 0xff);    
  }
}
/*
    init, run me once... 
   ---------------------------------------------------------------------------
*/
static void initBO(void) {

  tU8 ucIdx;
  
  // Get rid of previous screen cont.
  lcdClrscr();      
  
  g_eState = IDLE;
  for (ucIdx=0;ucIdx<MAXEVENTS;ucIdx++) g_EventBuffer[ucIdx] = NONE;

  g_autoPilot = FALSE;
  g_soundMode = TRUE;
  g_plippTime = 0;
  g_colorIdx = 0;

}
/*
    setUpLevel, run me before first level and between levels.
   ---------------------------------------------------------------------------
*/
static void setUpLevel(void) {

  tU8 row;
  tU8 col;
  
  lcdClrscr();

  g_SpaceAtTop = (5 * g_level);
  
  if ((g_SpaceAtTop + (BROWS*BROWHEIGHT)) > 120) g_SpaceAtTop = 120 - (BROWS*BROWHEIGHT);
  
  for (row=0;row<BROWS;row++) {  
    for (col=0;col<BPROW;col++) {
        g_Bricks[row][col] = row + 1;        
        drawBrick(row, col);
    }
  }
  
  g_BricksCleared = 0;

  g_sPaddle.width = 25;
  g_sPaddle.xCor = (((XMAX+1)/2) - 1 + ((g_sPaddle.width-1)/2));
  g_sPaddle.yCor = YMAX-3;
  
  g_sBall.xCor = g_sPaddle.xCor;
  g_sBall.yCor = g_sPaddle.yCor - 2;
  g_sBall.xSpeed = 1;
  g_sBall.ySpeed = 0;
  g_sBall.updateIntervall = 0;
  
  drawBall(0,0);
}
/*
    reset, before start of level 1
   ---------------------------------------------------------------------------
*/
static void resetBO(void) {

  g_level = 1;
  g_BrickHits = 0;
  g_sBall.count = 3;
  setUpLevel();
}
/*
    pushEvent, add event to queue, if queue full, bitheaven
   ---------------------------------------------------------------------------
*/
static void pushEvent(int newEvent) {

  tU8 ucIdx;

  for (ucIdx=0;ucIdx<MAXEVENTS;ucIdx++) {
    if (g_EventBuffer[ucIdx] == NONE) {
      g_EventBuffer[ucIdx] = newEvent;
      break;
    }
  }
}
/*
    popEvent, get a event, or NONE if queue is empty
   ---------------------------------------------------------------------------
*/
static int popEvent() {
  
  int tmpEvent;
  tU8 ucIdx;

  tmpEvent = g_EventBuffer[0];

  for (ucIdx=1;ucIdx<MAXEVENTS;ucIdx++) g_EventBuffer[ucIdx-1] = g_EventBuffer[ucIdx];
  g_EventBuffer[MAXEVENTS-1] = NONE;
  
  return tmpEvent;
}
/*
    scanInputs, get userActions
   ---------------------------------------------------------------------------
*/
static void scanInputs(void) {

  char ch;

    ch = checkKey();

    if (ch == KEY_CENTER) pushEvent(FIRE);
    if (ch == KEY_DOWN) pushEvent(QUIT);
    if (ch == KEY_UP) pushEvent(START);

  if ((g_autoPilot == FALSE)||(g_eState != PLAY)) {
    if (ch == KEY_LEFT) pushEvent(LEFT);
    if (ch == KEY_RIGHT) pushEvent(RIGHT);
  } else {
    
    //Make the autopilot to fire away the ball at first chance.
    if (isBallAtPaddle() == SITTING) g_sBall.ySpeed = g_sBall.ySpeed - 1;  
    
    //Follow the ball
    if (g_sBall.xCor < g_sPaddle.xCor-5) pushEvent(LEFT);
    if (g_sBall.xCor > g_sPaddle.xCor+5) pushEvent(RIGHT);
  }
}
/*
    idle, wait for user to start breakout
   ---------------------------------------------------------------------------
*/
static void idle(void) {

  int currEvent;

  lcdGotoxy(25,40);
  lcdColor(0,color[g_colorIdx]);
  lcdPuts("Up  - Start");
  lcdGotoxy(25,60);
  lcdPuts("Down- Quit");
    
  lcdGotoxy(30,80);

  if (g_autoPilot == FALSE) {
    lcdColor(0,(tU8)CYAN);
    lcdPuts(" Human  ");
  } else {
    lcdColor(0,(tU8)YELLOW);
    lcdPuts("Computer");
  }
  
  lcdGotoxy(30,100);

  if (g_soundMode == FALSE) {
    lcdColor(0,(tU8)GREY1);
    lcdPuts("Sound off  ");
  } else {
    lcdColor(0,(tU8)WHITE);
    lcdPuts("Sound on   ");
  }
  
  

  currEvent = popEvent();

  switch (currEvent) {
    case NONE:
      break;
    case LEFT:
      g_autoPilot = !g_autoPilot;
      break;
    case RIGHT:
      g_soundMode = !g_soundMode;
      break;      
    case QUIT:
      g_eState = STOP;
      break;
    case START:
      resetBO();
      g_eState = PLAY;
      break;
    default:
      break;
  }
  
    g_colorIdx++;
  
  if (g_colorIdx == NUM_COLORS) g_colorIdx = 0;
}
/*
    gover, count = 0, show info, wait for response
   ---------------------------------------------------------------------------
*/
static void gameOver(void) {

  int currEvent;

  currEvent = popEvent();

  //display g_BrickHits
  lcdGotoxy(15,40);
  lcdColor(0,color[g_colorIdx]);
  lcdPuts("* GameOver *");
  
  switch (currEvent) {
    case NONE:
      break;
    case FIRE:
      g_eState = IDLE;
      lcdClrscr();      
      break;
    default:
      break;
  }
  
  g_colorIdx++;
  
  if (g_colorIdx == NUM_COLORS) g_colorIdx = 0;
}
/*
    isBallAtPaddle 
   ---------------------------------------------------------------------------
*/
static int isBallAtPaddle(void) {

  // Is the ball at right yCor ?
  if (g_sBall.yCor == g_sPaddle.yCor - 2) {
    // Right of the left end o the paddle ?    
    if (g_sBall.xCor > (g_sPaddle.xCor - (g_sPaddle.width/2))) {
      // Left of the right end o the paddle
      if (g_sBall.xCor < (g_sPaddle.xCor + (g_sPaddle.width/2))) {
        // No vertical speed, equals sitting
        if (g_sBall.ySpeed == 0) return SITTING;
        //Okey no sitting, the make blipp for bounce
        pushEvent(PLIPP);
                
        // At the left side
        if (g_sBall.xCor < (g_sPaddle.xCor - ((g_sPaddle.width-5)/2))) return LEFT_EDGE;
        if (g_sBall.xCor > (g_sPaddle.xCor + ((g_sPaddle.width-5)/2))) return RIGHT_EDGE;
      
        //Ok its on the paddle
        return CENTER;        
      }    
    }
  }
    
  return NO;
}
/*
    isPxAtBrick
   ---------------------------------------------------------------------------
*/
static tU8 isPxAtBrick(tU8 xCor, tU8 yCor) {

  tU8 row;
  tU8 col;

    //Don't bother if ball is above spaceAtTop
    if (g_sBall.yCor >= g_SpaceAtTop) {

   //Recalc to col row from xcor, ycor  
    col = (tU8)(xCor / ((XMAX+1)/BPROW));   
    row = (tU8)((yCor - g_SpaceAtTop)/BROWHEIGHT);
        
    // Don't check if ball is under the wall, col checked for debug.
    if ((row < BROWS)&&(col < BPROW))  {
      if (g_Bricks[row][col] > 0) {
        g_Bricks[row][col] = g_Bricks[row][col] - 1;
        g_BrickHits++;
        
        if (g_Bricks[row][col] == 0) {
          g_BricksCleared++;
          eraseBrick(row, col);
        } else {
          drawBrick(row, col);
        }
        
        pushEvent(PLIPP);
        return 1;
    }    
  }
  }

  return 0;
}


/*
    isBallAtBrick
   ---------------------------------------------------------------------------
*/
static tU8 isBallAtBrick(void) {
   
  //Ball is 3x3px, check cross
  
  if (isPxAtBrick(g_sBall.xCor, g_sBall.yCor-1) == 1) return TOP_HIT;
  if (isPxAtBrick(g_sBall.xCor, g_sBall.yCor+1) == 1) return BOTTOM_HIT;
  if (isPxAtBrick(g_sBall.xCor-1, g_sBall.yCor) == 1) return LEFT_HIT;
  if (isPxAtBrick(g_sBall.xCor+1, g_sBall.yCor) == 1) return RIGHT_HIT;

  return NO_HIT;
 
}
/*
    handleBall
   ---------------------------------------------------------------------------
*/
static void handleBall(void) {

  tU8 xOld;
  tU8 yOld;
  
  xOld = g_sBall.xCor;
  yOld = g_sBall.yCor;

  if (g_sBall.ySpeed != 0) {

      g_sBall.xCor = g_sBall.xCor + g_sBall.xSpeed;
    // Don't go warping....
    if (g_sBall.xCor > XMAX+50) g_sBall.xCor = 0;
    // Beware of the dark side....
    if (g_sBall.xCor > XMAX) g_sBall.xCor = XMAX;
    
    g_sBall.yCor = g_sBall.yCor + g_sBall.ySpeed;
    
    if (g_sBall.xCor == XMAX) {
      pushEvent(PLIPP);
      g_sBall.xSpeed = - 1;
    }

    if (g_sBall.xCor == 0) {
      pushEvent(PLIPP);
      g_sBall.xSpeed = 1;
    }

    if (g_sBall.yCor == 1) {
      pushEvent(PLIPP);
      g_sBall.ySpeed = 1;
    }

    switch (isBallAtBrick()) {
      case NO_HIT:
        break;
      case TOP_HIT:
        g_sBall.ySpeed = 1;
        break;
      case BOTTOM_HIT:
        g_sBall.ySpeed = -1;
        break;
      case LEFT_HIT:
        g_sBall.xSpeed = 1;
        break;
      case RIGHT_HIT:
        g_sBall.xSpeed = -1;
        break;
    }

    switch (isBallAtPaddle()) {
      case NO:
        break;
      case CENTER:
        g_sBall.ySpeed = -1;
        break;
      case LEFT_EDGE:
        if (g_sBall.xSpeed == 0) g_sBall.xSpeed = -1;        
        if (g_sBall.xSpeed == 1) g_sBall.xSpeed = 0;        
        if (g_sBall.xSpeed == -1) g_sBall.xSpeed = -2;        
        g_sBall.ySpeed = -1;
        break;
      case RIGHT_EDGE:
        if (g_sBall.xSpeed == 0) g_sBall.xSpeed = 1;        
        if (g_sBall.xSpeed == -1) g_sBall.xSpeed = 0;        
        if (g_sBall.xSpeed == 1) g_sBall.xSpeed = 2;        
        g_sBall.ySpeed = -1;
        break;
    }

    if (g_sBall.yCor == YMAX-1) {
      g_sBall.xCor = g_sPaddle.xCor;
      g_sBall.yCor = g_sPaddle.yCor - 2;
      g_sBall.xSpeed = 1;
      g_sBall.ySpeed = 0;

      g_sBall.count--;
      if (g_sBall.count == 0) pushEvent(NOBALLS);
    }
  } else {
    if (isBallAtPaddle() == SITTING) {
      g_sBall.xCor = g_sPaddle.xCor;
    }
  }
  
  drawBall(xOld,yOld);
  
  if ((BALL_UPDATE_INTERVALL - (g_level*2)) > 1) {  
    g_sBall.updateIntervall = BALL_UPDATE_INTERVALL - (g_level*2);
  } else g_sBall.updateIntervall = 1;
  
  // If auto, go F A S T...
  if (g_autoPilot == TRUE) g_sBall.updateIntervall = 1;
  
}
/*
    play
   ---------------------------------------------------------------------------
*/
static void play(void) {
  
  int currEvent;
  
  tU8 oldXCor;    
    
  oldXCor = g_sPaddle.xCor;
  
  currEvent = popEvent();

  switch (currEvent) {
    case NONE:
      break;
    case QUIT:
      g_eState = IDLE;
      break;
    case PLIPP:
      if (g_soundMode == TRUE) {
        setBuzzer(TRUE);
      }
      g_plippTime = PLIPPTIME;
      break;
    case LEVLA:
      if (g_sBall.count < 255) g_sBall.count++;
      //Let level go to zero, looparound
      g_level++;
      if (g_level == 0) g_level = 1;
      setUpLevel();      
      break;
    case NOBALLS:
      g_eState = GOVER;
      break;
    case LEFT:
      g_sPaddle.xCor = g_sPaddle.xCor - PADDLE_STEP;
      break;
    case RIGHT:
      g_sPaddle.xCor = g_sPaddle.xCor + PADDLE_STEP;
      break;
    case FIRE:
      if (isBallAtPaddle() == SITTING) g_sBall.ySpeed = g_sBall.ySpeed - 1;
    default:
      break;
  }   

  drawPaddle(oldXCor);
  
  if (g_sBall.updateIntervall == 0) {
    handleBall();
  } else g_sBall.updateIntervall--;

  if (g_BricksCleared == (BROWS*BPROW)) {
    //levla!
    pushEvent(LEVLA);
  }
  
  if (g_plippTime > 0) {
    g_plippTime--;
    if (g_plippTime == 0) setBuzzer(FALSE);
  }
}
/*
    main
   ---------------------------------------------------------------------------
*/
int playBreakout(void) {

  g_eState = INIT;  
     
  
  while (g_eState != STOP) {

    scanInputs();
    
    switch (g_eState) {
      case INIT:
        initBO();
        break;
      case IDLE:
        idle();
        break;
      case PLAY:
        play();
        break;
      case GOVER:
        gameOver();
        break;
      default:
        g_eState = STOP;
        break;
    }
  }  

  return 0;
}

