/**
  General definitions common for Pokitto raycasting demos.

  The demos use mode 13: 1 byte per pixel = 256 colors. Bitmaps (textures,
  sprites, ...) are also in this format (use the provided python script to
  convert png images).

  author: Miloslav "drummyfish" Ciz
  license: CC0 1.0
*/

#ifndef RAYCAST_DEMO_GENERAL_HPP
#define RAYCAST_DEMO_GENERAL_HPP

#include "stdio.h" // for debugging raycastlibg

#define VERTICAL_FOV UNITS_PER_SQUARE // redefine camera vertical FOV

#define PIXEL_FUNCTION pixelFunc
/* ^ This has to be defined to the name of the function that will render
     pixels. */

#include "raycastlib.h"
#include "Pokitto.h"

Pokitto::Core pokitto;

#ifndef FPS
#define FPS 30
#endif

#ifndef PLAYER_SPEED
#define PLAYER_SPEED (4 * UNITS_PER_SQUARE)
#endif

#ifndef PLAYER_ROTATION_SPEED
#define PLAYER_ROTATION_SPEED (UNITS_PER_SQUARE / 2)
#endif

#ifndef PLAYER_JUMP_SPEED
#define PLAYER_JUMP_SPEED 500
#endif

#ifndef HEAD_BOB_HEIGHT
#define HEAD_BOB_HEIGHT 100
#endif

#ifndef HEAD_BOB_STEP
#define HEAD_BOB_STEP 10
#endif

#ifndef GRAVITY_ACCELERATION
#define GRAVITY_ACCELERATION ((3 * UNITS_PER_SQUARE) / 2)
#endif

#define SCREEN_WIDTH 110
#define SCREEN_HEIGHT 88

#define MIDDLE_ROW (SCREEN_HEIGHT / 2)
#define MIDDLE_COLUMN (SCREEN_WIDTH / 2)

#ifndef SUBSAMPLE
#define SUBSAMPLE 2
#endif

#define SUBSAMPLED_WIDTH (SCREEN_WIDTH / SUBSAMPLE)

#define TRANSPARENT_COLOR 0b00000111 /// Transparent color for sprites and GUI.

Unit zBuffer[SUBSAMPLED_WIDTH]; ///< 1D z-buffer for visibility determination.

unsigned short palette[256];

// helper macro for fast pixel drawing
#ifdef POK_SIM
  #define putSubsampledPixel\
    pokitto.display.drawPixel(pixel->position.x * SUBSAMPLE,pixel->position.y,c);\
    pokitto.display.drawPixel(pixel->position.x * SUBSAMPLE + 1,pixel->position.y,c);
#else
  // this code breaks the simulator
  #define putSubsampledPixel\
    uint8_t *buf = pokitto.display.screenbuffer;\
    buf += pixel->position.x * SUBSAMPLE;\
    buf += pixel->position.y * SCREEN_WIDTH;\
    for (uint8_t i = 0; i < SUBSAMPLE - 1; ++i)\
      *buf++ = c;\
    *buf = c;
#endif

/**
  Gets (the index of) color by specified RGB components. 

  @param r red, 3 bits (0 to 7)
  @param g green, 3 bits (0 to 7)
  @param b blue, 2 bits (0 to 3)
  @return palette index of the color
*/
inline uint8_t rgbToIndex(uint8_t r, uint8_t g, uint8_t b)
{
  return (r & 0b00000111) | ((g & 0b00000111) << 3) | ((b & 0b00000011) << 6);
}

/**
  Inits and loads a general 256 color palette.
*/
void initPalette()
{
  for (uint8_t r = 0; r < 8; ++r)
    for (uint8_t g = 0; g < 8; ++g)
      for (uint8_t b = 0; b < 4; ++b)
        palette[rgbToIndex(r,g,b)] =
          pokitto.display.RGBto565(36 * r, 36 * g, 85 * b);

  pokitto.display.load565Palette(palette);
}

/**
  Adds given intensity to a color.

  @param color input color
  @param intensity intensity to add, 3 bit (0 to 7)
  @return new color
*/
inline uint8_t addIntensity(uint8_t color, int16_t intensity)
{
  uint8_t r = color & 0b00000111;
  uint8_t g = (color & 0b00111000) >> 3;
  uint8_t b = (color & 0b11000000) >> 6;

  if (intensity >= 0)
  {
    r += intensity;
    r = r > 7 ? 7 : r;

    g += intensity;
    g = g > 7 ? 7 : g;

    b += intensity / 2;
    b = b > 3 ? 3 : b;
  }
  else
  {
    intensity *= -1;
    r = (intensity > r) ? 0 : r - intensity;
    g = (intensity > g) ? 0 : g - intensity;
    intensity /= 2;
    b = intensity > b ? 0 : b - intensity;
  }

  return rgbToIndex(r,g,b);
}

inline uint8_t addRGB(uint8_t color, int16_t red, int16_t green, int16_t blue)
{
  int8_t r = color & 0b00000111;
  int8_t g = (color & 0b00111000) >> 3;
  int8_t b = (color & 0b11000000) >> 6;

  r = clamp(r + red,0,7);
  g = clamp(g + green,0,7);
  b = clamp(b + blue,0,3);

  return rgbToIndex(r,g,b);
}

/**
  Samples an image by normalized coordinates - each coordinate is in range
  0 to UNITS_PER_SQUARE (from raycastlib).
*/ 
inline uint8_t sampleImage(const unsigned char *image, Unit x, Unit y)
{
  // TODO: optimize

  x = wrap(x,UNITS_PER_SQUARE);
  y = wrap(y,UNITS_PER_SQUARE);

  int32_t index =
    image[1] * ((image[0] * x) / UNITS_PER_SQUARE) + (image[0] * y) /
    UNITS_PER_SQUARE;

  return image[2 + index];
}

/**
  Draws a scaled sprite on screen in an optimized way. The sprite has to be
  square in resolution.
*/
void inline drawSpriteSquare(const unsigned char *sprite, int16_t x, int16_t y, Unit depth, int16_t size)
{
  if (size < 0 || size > 200 || // let's not mess up with the incoming array
      sprite[0] != sprite[1])   // only draw square sprites
    return;

  int16_t samplingIndices[size];

  // optimization: precompute the indices

  for (Unit i = 0; i < size; ++i)
    samplingIndices[i] = (i * sprite[0]) / size;

  x -= size / 2;
  y -= size / 2;

  uint8_t c;

  int16_t jTo = size - max(0,y + size - 88);
  int16_t iTo = size - max(0,x + size - 110);

  for (Unit i = max(-1 * x,0); i < iTo; ++i)
  {
    int16_t xPos = x + i;

    if (zBuffer[xPos / SUBSAMPLE] <= depth)
      continue;

    int16_t columnLocation = 2 + samplingIndices[i] * sprite[0];

    for (Unit j = max(-1 * y,0); j < jTo; ++j)
    {
      c = sprite[columnLocation + samplingIndices[j]];
     
      if (c != TRANSPARENT_COLOR)
        pokitto.display.drawPixel(xPos,y + j,c);
    }
  }
}

/// Faster than drawSprite.
void drawImage(const unsigned char *image, int16_t x, int16_t y)
{
  // TODO: optimize

  for (int16_t i = 0; i < image[0]; ++i)
  {
    int16_t xPos = x + i;
    int16_t column = 2 + i * image[1];

    for (int16_t j = 0; j < image[1]; ++j)
    {
      char c = image[column + j];

      if (c != TRANSPARENT_COLOR)
        pokitto.display.drawPixel(xPos,y + j,image[column + j]);
    }
  }
}

class Player
{
public:
  Camera mCamera;
  Unit mVericalSpeed;
  bool mRunning;
  Unit mHeadBob;
  bool mHeadBobUp;

  Player()
  {
    initCamera(&mCamera);

    mCamera.position.x = 0;
    mCamera.position.y = 0;
    mCamera.direction = 0;
    mCamera.height = UNITS_PER_SQUARE * 3;
    mCamera.resolution.x = SCREEN_WIDTH / SUBSAMPLE;
    mCamera.resolution.y = SCREEN_HEIGHT;
    mCamera.shear = 0;
    mVericalSpeed = 0;
    mRunning = false;
    mHeadBob = 0;
    mHeadBobUp = true;
  }

  void setPosition(Unit x, Unit y)
  {
    mCamera.position.x = x;
    mCamera.position.y = y;
  }

  void setPosition(Unit x, Unit y, Unit z, Unit direction)
  {
    mCamera.position.x = x;
    mCamera.position.y = y;
    mCamera.height = z;
    mCamera.direction = direction;
  }

  void setPositionSquare(int16_t squareX, int16_t squareY)
  {
    setPosition(
      squareX * UNITS_PER_SQUARE + UNITS_PER_SQUARE / 2,
      squareY * UNITS_PER_SQUARE + UNITS_PER_SQUARE / 2);
  }

  void update(int16_t moveDirection, bool strafe, int16_t turnDirection, bool jump,
    int16_t shearDirection, ArrayFunction floorHeightFunction,
    ArrayFunction ceilingHeightFunction, bool computeHeight, uint32_t dt)
  {
    Vector2D moveOffset;

    moveOffset.x = 0;
    moveOffset.y = 0;

    if (moveDirection != 0)
    {
      int16_t horizontalStep = (dt * PLAYER_SPEED * (mRunning ? 2 : 1)) / 1000 *
        (moveDirection > 0 ? 1 : -1);

      moveOffset = angleToDirection(mCamera.direction + (strafe ? UNITS_PER_SQUARE / 4 : 0));

      moveOffset.x = (moveOffset.x * horizontalStep) / UNITS_PER_SQUARE;
      moveOffset.y = (moveOffset.y * horizontalStep) / UNITS_PER_SQUARE;

      mHeadBob += mHeadBobUp ? HEAD_BOB_STEP : -HEAD_BOB_STEP;

      if (mHeadBob > HEAD_BOB_HEIGHT)
        mHeadBobUp = false;
      else if (mHeadBob < -HEAD_BOB_HEIGHT)
        mHeadBobUp = true; 
    }
    else
      mHeadBob /= 2;
 
    if (turnDirection != 0)
    {
      int16_t rotationStep = (dt * PLAYER_ROTATION_SPEED) / 1000;
      mCamera.direction = wrap(mCamera.direction + turnDirection * rotationStep,UNITS_PER_SQUARE);
    }

    Unit prevHeight = mCamera.height;

    moveCameraWithCollision(&mCamera,moveOffset,mVericalSpeed,
        floorHeightFunction, ceilingHeightFunction, computeHeight ? 1 : 0, 0);

    Unit heightDiff = mCamera.height - prevHeight;

    if (heightDiff == 0)
      mVericalSpeed = 0; // hit floor/ceiling

    if (jump && mVericalSpeed == 0)
    {
      int16_t camX = divRoundDown(mCamera.position.x,UNITS_PER_SQUARE);
      int16_t camY = divRoundDown(mCamera.position.y,UNITS_PER_SQUARE);

      if (mCamera.height - CAMERA_COLL_HEIGHT_BELOW -
        floorHeightFunction(camX,camY) < 2)
        mVericalSpeed = PLAYER_JUMP_SPEED; // jump
    }     

    if (shearDirection != 0)
      mCamera.shear = clamp(mCamera.shear + shearDirection * 10,
        -1 * mCamera.resolution.y, mCamera.resolution.y);
    else
      mCamera.shear /= 2;

    if (computeHeight)
      mVericalSpeed -= (dt * GRAVITY_ACCELERATION) / 1000; // gravity
  }
};

class Sprite
{
public:
  const unsigned char *mImage;
  Vector2D mPosition;
  Unit mHeight;
  Unit mPixelSize;

  Sprite(const unsigned char *image, int16_t squareX, int16_t squareY, Unit z,
    Unit pixelSize):
    mImage(image),
    mPixelSize(pixelSize)
  {
    mPosition.x = squareX * UNITS_PER_SQUARE + UNITS_PER_SQUARE / 2;
    mPosition.y = squareY * UNITS_PER_SQUARE + UNITS_PER_SQUARE / 2;
    mHeight = z * UNITS_PER_SQUARE + UNITS_PER_SQUARE / 2;
  }

  Sprite():
    mImage(0), mHeight(0), mPixelSize(1)
  {
    mPosition.x = 0;
    mPosition.y = 0;
  }
};

void initGeneral()
{
  pokitto.begin(); 
  pokitto.setFrameRate(FPS);
  pokitto.display.setFont(fontTiny);
  pokitto.display.persistence = 1;

  initPalette(); 

  for (uint8_t i = 0; i < SUBSAMPLED_WIDTH; ++i)
    zBuffer[i] = 0;  
}

/**
  Computes an average color of given texture.
*/
unsigned char computeAverageColor(const unsigned char *texture)
{
  uint32_t sumR = 0;
  uint32_t sumG = 0;
  uint32_t sumB = 0;
  uint32_t pixels = texture[0] * texture[1];

  for (uint16_t i = 0; i < pixels; ++i)
  {
    sumR += texture[2 + i] & 0b00000111;
    sumG += (texture[2 + i] & 0b00111000) >> 3;
    sumB += (texture[2 + i] & 0b11000000) >> 6;
  }

  return rgbToIndex(sumR / pixels,sumG / pixels,sumB / pixels);
}

#endif