/* Blinker
   Copyright 2011-2019 Stefan Schuermans <stefan@blinkenarea.org>
   Copyleft GNU public license - http://www.gnu.org/copyleft/gpl.html
   a blinkenarea.org project */

#include <cmath>
#include <stdlib.h>
#include <string>
#include <vector>

#include <BlinkenLib/BlinkenFrame.h>

#include "File.h"
#include "Format.h"
#include "FormatFile.h"
#include "Game.h"
#include "Mgrs.h"
#include "Module.h"
#include "NameFile.h"
#include "OpConn.h"
#include "OpConnIf.h"
#include "OpReqIf.h"
#include "OutStreamFile.h"
#include "Tetris.h"
#include "Time.h"
#include "TimeCallee.h"
#include "UIntFile.h"

namespace Blinker {

/**
 * @brief constructor
 * @param[in] name module name
 * @param[in] mgrs managers
 * @param[in] dirBase base directory
 */
Tetris::Tetris(const std::string &name, Mgrs &mgrs, const Directory &dirBase):
  Game(name, mgrs, dirBase),
  m_fileStoneColor(dirBase.getFile("stoneColor")),
  m_fileDelay(dirBase.getFile("delay")),
  m_fileStartSound(dirBase.getFile("startSound")),
  m_stoneColor(),
  m_delay(c_delayDescr.default_),
  m_pConn(NULL),
  m_stone(-1), m_rot(-1), m_posX(-1), m_posY(-1), m_field()
{
  // open operator connection interfaces for player
  m_mgrs.m_opMgr.open(m_name, this);
}

/// virtual destructor
Tetris::~Tetris()
{
  // close operator connection interface
  m_mgrs.m_opMgr.close(m_name);

  // close open operator connection
  if (m_pConn) {
    m_pConn->close();
    m_pConn = NULL;
  }
}

/// check for update of configuration (derived game), return true on update
bool Tetris::updateConfigGame()
{
  bool ret = false;

  // color file was modified -> convert color, return true for update
  // cfg value file was updated -> read new cfg value, return true for update
  if (colorUpdate(m_fileStoneColor, m_stoneColor) ||
      valueUpdate(m_fileDelay, c_delayDescr, m_delay)) {
    ret = true;
  }

  // sound name file was modified -> re-read sound name, no other update needed
  soundUpdate(m_fileStartSound);

  return ret;
}

/**
 * @brief check if accepting new operator connection is possible
 * @param[in] name operator interface name
 * @return if accepting new connection is possible
 */
bool Tetris::acceptNewOpConn(const std::string &name)
{
  (void)name;

  // accept player if no one there yet
  return ! m_pConn;
}

/**
 * @brief new operator connection
 * @param[in] name operator interface name
 * @param[in] pConn operator connection object
 *
 * The new connection may not yet be used for sending inside this callback.
 */
void Tetris::newOpConn(const std::string &name, OpConn *pConn)
{
  (void)name;

  // player arrives and starts game
  if (! m_pConn) {
    m_pConn = pConn;
    requestOpConnSound(m_pConn, m_fileStartSound);
    activate();
  }
  // close imcoming connection as soon as possible, nothing else happens
  else {
    requestOpConnClose(pConn);
    return;
  }
}

/**
 * @brief key command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] key key that was pressed
 */
void Tetris::opConnRecvKey(OpConn *pConn, char key)
{
  // hash -> hang up
  if (key == '#') {
    opConnClose(pConn);
    pConn->close();
    return;
  }

  // star -> inform player about game
  if (key == '*') {
    playOpConnSound(pConn, m_fileStartSound);
    return;
  }

  // normal keys for controlling game

  // move left
  if (key == '4') {
    if (checkStoneFit(m_stone, m_rot, m_posY, m_posX - 1)) {
      wipeStone(m_stone, m_rot, m_posY, m_posX);
      m_posX -= 1;
      drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color
      sendFrame();
    }
    return;
  }

  // move right
  if (key == '6') {
    if (checkStoneFit(m_stone, m_rot, m_posY, m_posX + 1)) {
      wipeStone(m_stone, m_rot, m_posY, m_posX);
      m_posX += 1;
      drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color
      sendFrame();
    }
    return;
  }

  // rotate left
  if (key == '1') {
    int new_rot = m_rot - 1;
    if (new_rot < 0) {
      new_rot = c_rotCnt - 1;
    }
    if (checkStoneFit(m_stone, new_rot, m_posY, m_posX)) {
      wipeStone(m_stone, m_rot, m_posY, m_posX);
      m_rot = new_rot;
      drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color
      sendFrame();
    }
    return;
  }

  // rotate right
  if (key == '2' || key == '3') {
    int new_rot = m_rot + 1;
    if (new_rot >= c_rotCnt) {
      new_rot = 0;
    }
    if (checkStoneFit(m_stone, new_rot, m_posY, m_posX)) {
      wipeStone(m_stone, m_rot, m_posY, m_posX);
      m_rot = new_rot;
      drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color
      sendFrame();
    }
    return;
  }

  // drop stone
  if (key == '8') {
    // TODO
    return;
  }
}

/**
 * @brief play command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] sound name of sound to play
 */
void Tetris::opConnRecvPlay(OpConn *pConn, const std::string &sound)
{
  (void)pConn;
  (void)sound;
}

/**
 * @brief operator connection is closed
 * @param[in] pConn operator connection object
 *
 * The connection may not be used for sending any more in this callback.
 */
void Tetris::opConnClose(OpConn *pConn)
{
  // remove coperator connection from requests (if it was in)
  forgetOpConn(pConn);

  // player leaves -> deactivate game
  if (pConn == m_pConn) {
    m_pConn = NULL;
    deactivate();
  }
}

/// re-initialize game (e.g. due to config change)
void Tetris::reinitialize()
{
  // convert colors
  color2data(m_fileStoneColor, m_stoneColor);
  // get values
  valueFromFile(m_fileDelay, c_delayDescr, m_delay);

  // initialize field: empty
  m_field.clear();
  m_field.resize(m_height * m_width, -1);

  // start with new stone
  newStone();

  // redraw image and send frame
  redraw();

  // request first time step if needed
  planTimeStep();
}

/// redraw current game image, expected to call sendFrame() at end
void Tetris::redraw()
{
  // draw background
  rectFill(0, m_height, 0, m_width, m_backgroundColor);

  // draw fixed pixels
  for (int y = 0, i = 0; y < m_height; ++y) {
    for (int x = 0; x < m_width; ++x, ++i) {
      if (m_field.at(i) >= 0) {
        pixel(y, x, m_stoneColor);
      }
    }
  }

  // draw current stone
  drawStone(m_stone, m_rot, m_posY, m_posX);

  // send updated image buffer as frame
  sendFrame();
}

/// process next time step of game
void Tetris::timeStep()
{
  // stone can move down by one pixel
  if (checkStoneFit(m_stone, m_rot, m_posY + 1, m_posX)) {
    // move stone down by one pixel
    wipeStone(m_stone, m_rot, m_posY, m_posX);
    m_posY += 1;
    drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: active color
  }

  // stone cannot move down by one pixel
  else {
    // add stone permanently to field at current position
    freezeStone(m_stone, m_rot, m_posY, m_posX);
    drawStone(m_stone, m_rot, m_posY, m_posX); // TODO: frozen color
    // prepare new stone
    newStone();
  }

  // send updated image buffer as frame
  sendFrame();

  // request next time step
  planTimeStep();
}

/// set up a new stone
void Tetris::newStone()
{
  // random stone, random rotation
  m_stone = rand() % c_stoneCnt;
  m_rot = rand() % c_rotCnt;

  // postion: two pixels above top middle
  m_posX = (m_width - 1) / 2;
  m_posY = -2;
}

/// set time for next time step of game - or unset if not needed
void Tetris::planTimeStep()
{
  // no time call needed if not active
  if (! isActive()) {
    unsetTimeStep();
    return;
  }

  // compute interval based on score and bounce count
  float interval = 1e-3f * m_delay;

  // request next time call
  Time stepTime;
  stepTime.fromFloatSec(interval);
  setTimeStep(Time::now() + stepTime);
}

/// get rotatation of stone from stone/rotation index (or NULL in invalid)
Tetris::RotStone const * Tetris::getRotStone(int stone, int rot)
{
  if (! checkLimitInt(stone, 0, c_stoneCnt -1) ||
      ! checkLimitInt(rot, 0, c_rotCnt - 1)) {
    return NULL; // invalid stone or rotation
  }
  return &c_stones[stone].rot[rot];
}

/// check if stone fits at position
bool Tetris::checkStoneFit(int stone, int rot, int y, int x) const
{
  // get rotation of stone
  RotStone const *rotStone = getRotStone(stone, rot);
  if (! rotStone) {
    return false; // invalid stone or rotation -> does not fit
  }

  // check pixels
  for (int p = 0; p < c_pixelCnt; ++p) {
    int py = y + rotStone->pixels[p].y;
    int px = x + rotStone->pixels[p].x;
    if (py > m_height - 1 || ! checkLimitInt(px, 0, m_width - 1)) {
      return false; // outside field (except at top) -> does not fit
    }
    if (py >= 0) { // do not check above top
      int pi = py * m_width + px;
      if (m_field.at(pi) >= 0) {
        return false; // occupixed pixel -> does not fit
      }
    }
  }

  // all checks passed -> stone fits
  return true;
}

/// freeze stone to field at position
void Tetris::freezeStone(int stone, int rot, int y, int x)
{
  // get rotation of stone
  RotStone const *rotStone = getRotStone(stone, rot);
  if (! rotStone) {
    return; // invalid stone or rotation -> nothing to do
  }

  // add pixels to field
  for (int p = 0; p < c_pixelCnt; ++p) {
    int py = y + rotStone->pixels[p].y;
    int px = x + rotStone->pixels[p].x;
    if (checkLimitInt(py, 0, m_height - 1) &&
        checkLimitInt(px, 0, m_width - 1)) {
      int pi = py * m_width + px;
      m_field.at(pi) = stone; // mark pixel in field with stone index
    }
  }
}

/// draw a stone to image buffer
void Tetris::drawStone(int stone, int rot, int y, int x)
{
  colorStone(stone, rot, y, x, m_stoneColor);
}

/// wipe a stone from image buffer (i.e. replace it with background color)
void Tetris::wipeStone(int stone, int rot, int y, int x)
{
  colorStone(stone, rot, y, x, m_backgroundColor);
}

/// set shape of stone to color in image buffer
void Tetris::colorStone(int stone, int rot, int y, int x,
                        ColorData const &color)
{
  // get rotation of stone
  RotStone const *rotStone = getRotStone(stone, rot);
  if (! rotStone) {
    return; // invalid stone or rotation -> nothing to do
  }

  // color pixels
  for (int p = 0; p < c_pixelCnt; ++p) {
    pixel(y + rotStone->pixels[p].y, x + rotStone->pixels[p].x, color);
  }
}

/// stone data
Tetris::Stone const Tetris::c_stones[7] = {
  // the I
  { {
      { { { -2,  0 }, { -1,  0 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -2 }, {  0, -1 }, {  0,  0 }, {  0,  1 } } },
      { { { -2,  0 }, { -1,  0 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -2 }, {  0, -1 }, {  0,  0 }, {  0,  1 } } },
  } },
  // the L
  { {
      { { {  1, -1 }, { -1,  0 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -1 }, {  0,  0 }, {  0,  1 }, {  1,  1 } } },
      { { { -1,  0 }, {  0,  0 }, {  1,  0 }, { -1,  1 } } },
      { { { -1, -1 }, {  0, -1 }, {  0,  0 }, {  0,  1 } } },
  } },
  // the J
  { {
      { { { -1, -1 }, { -1,  0 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -1 }, {  1, -1 }, {  0,  0 }, {  0,  1 } } },
      { { { -1,  0 }, {  0,  0 }, {  1,  0 }, {  1,  1 } } },
      { { {  0, -1 }, {  0,  0 }, { -1,  1 }, {  0,  1 } } },
  } },
  // the T
  { {
      { { {  0, -1 }, { -1,  0 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -1 }, {  0,  0 }, {  1,  0 }, {  0,  1 } } },
      { { { -1,  0 }, {  0,  0 }, {  1,  0 }, {  0,  1 } } },
      { { {  0, -1 }, { -1,  0 }, {  0,  0 }, {  0,  1 } } },
  } },
  // the O
  { {
      { { {  0, -1 }, {  1, -1 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -1 }, {  1, -1 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -1 }, {  1, -1 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -1 }, {  1, -1 }, {  0,  0 }, {  1,  0 } } },
  } },
  // the Z
  { {
      { { { -1, -1 }, {  0, -1 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -1 }, { -1,  0 }, {  0,  0 }, { -1,  1 } } },
      { { { -1, -1 }, {  0, -1 }, {  0,  0 }, {  1,  0 } } },
      { { {  0, -1 }, { -1,  0 }, {  0,  0 }, { -1,  1 } } },
  } },
  // the S
  { {
      { { {  0, -1 }, {  1, -1 }, { -1,  0 }, {  0,  0 } } },
      { { { -1, -1 }, { -1,  0 }, {  0,  0 }, {  0,  1 } } },
      { { {  0, -1 }, {  1, -1 }, { -1,  0 }, {  0,  0 } } },
      { { { -1, -1 }, { -1,  0 }, {  0,  0 }, {  0,  1 } } },
  } },
};

/// number of stones
int const Tetris::c_stoneCnt = sizeof(Tetris::c_stones) /
                               sizeof(Tetris::c_stones[0]);
/// number of rotations per stone
int const Tetris::c_rotCnt = sizeof(Tetris::Stone::rot) /
                             sizeof(Tetris::Stone::rot[0]);
/// number of pixels per stone
int const Tetris::c_pixelCnt = sizeof(Tetris::RotStone::pixels) /
                               sizeof(Tetris::RotStone::pixels[0]);

/// descriptor for delay value
Tetris::ValueDescr const Tetris::c_delayDescr = { 400, 200, 1000 };

} // namespace Blinker

