/* 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 <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 "OutStreamFile.h"
#include "Pong.h"
#include "Time.h"
#include "TimeCallee.h"

namespace Blinker {

/**
 * @brief constructor
 * @param[in] name module name
 * @param[in] mgrs managers
 * @param[in] dirBase base directory
 */
Pong::Pong(const std::string &name, Mgrs &mgrs, const Directory &dirBase):
  Game(name, mgrs, dirBase),
  m_fileBallColor(dirBase.getFile("ballColor")),
  m_fileLineColor(dirBase.getFile("lineColor")),
  m_filePadColor(dirBase.getFile("padColor")),
  m_ballColor(), m_lineColor(), m_padColor(),
  m_ballPosX(-1), m_ballPosY(-1), m_ballDirX(0), m_ballDirY(0),
  m_padSize(0), m_leftPosY(0), m_rightPosY(0), m_leftDelay(0), m_rightDelay(0)
{
  // FIXME: activate at begin for initial development only
  activate();
}

/// virtual destructor
Pong::~Pong()
{
  // cancel time callback request
  m_mgrs.m_callMgr.cancelTimeCall(this);
}

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

  // color file was modified -> convert color, return true for update
  if (colorUpdate(m_fileBallColor, m_ballColor) ||
      colorUpdate(m_fileLineColor, m_lineColor) ||
      colorUpdate(m_filePadColor, m_padColor)) {
    ret = true;
  }

  return ret;
}

/// re-initialize game (e.g. due to config change)
void Pong::reinitialize()
{
  // compute parameters
  m_padSize = (m_height + 1) / 3;
  m_leftPosY = (m_height - m_padSize) / 2;
  m_rightPosY = (m_height - m_padSize) / 2;
  m_leftDelay = 0;
  m_rightDelay = 0;

  // convert colors
  color2data(m_fileBallColor, m_ballColor);
  color2data(m_fileLineColor, m_lineColor);
  color2data(m_filePadColor, m_padColor);

  // FIXME: start ball for development
  startBall();
}

/// redraw current game image, expected to call sendFrame() at end
void Pong::redraw()
{
  int y, x;

  // draw background
  rectFill(0, m_height, 0, m_width, m_backgroundColor);

  // draw middle line: single line on odd width, two dashed lines at even width
  for (y = 0; y < m_height; ++y) {
    x = (m_width - (y & 1)) / 2;
    pixel(y, x, m_lineColor);
  }

  // draw pads
  lineVert(m_leftPosY, m_leftPosY + m_padSize - 1, 0, m_padColor);
  lineVert(m_rightPosY, m_rightPosY + m_padSize - 1, m_width - 1, m_padColor);

  // draw ball
  pixel(m_ballPosY, m_ballPosX, m_ballColor);

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

/// callback when requested time reached
void Pong::timeCall()
{
  // computer player: move pads
  computerLeft();
  computerRight();

  // bounce ball
  bounceBall();

  // move ball
  m_ballPosX += m_ballDirX;
  m_ballPosY += m_ballDirY;

  // draw and send frame
  redraw();

  // request next call if needed
  planTimeCall();
}

/**
 * @brief delay processing for computer players
 * @param[in,out] delay delay variable of computer player
 * @return whether computer player is allowed to move
 */
bool Pong::computerDelay(int &delay) const

{
  // zero delay: generate new delay
  if (delay <= 0) {
    int avg_steps = (m_height - m_padSize) / 2;
    int delay_range = avg_steps > 1 ? m_width / avg_steps: m_width;
    delay = rand() % delay_range + delay_range;
  }

  // count down delay
  --delay;

  // moving allowd if delay expired
  return delay <= 0;
}

/**
 * @brief computation of ideal pad position for computer players
 * @param[in] padBallX x coordinate of position of ball when hitting the pad
 * @param[in] padY current y coordinate of pad
 * @param[out] padYmin minimum ideal y position of pad
 * @param[out] padYmax maximum ideal y position of pad
 */
void Pong::computerComputePadPos(int padBallX, int padY,
                                 int &padYmin, int &padYmax) const
{
  // ball not moving towards pad
  if ((padBallX - m_ballPosX) * m_ballDirX <= 0) {
    // do not move if ball is still close to pad
    if (abs(padBallX - m_ballPosX) <= 2) {
      padYmin = padY;
      padYmax = padY;
      return;
    }
    // move pad to middle (might be 2 pixels wide)
    padYmin = (m_height - m_padSize) / 2;
    padYmax = (m_height - m_padSize + 1) / 2;
    return;
  }

  // compute expected ball position at pad
  int ballPosX = m_ballPosX; // simulate were ball is going
  int ballPosY = m_ballPosY;
  int ballDirY = m_ballDirY;
  while ((padBallX - ballPosX) * m_ballDirX > 0) { // while moving to pad
    int deltaX = padBallX - ballPosX;
    int deltaY = deltaX * m_ballDirX * ballDirY;
    if (deltaY < -ballPosY) {
      deltaY = -ballPosY;
      ballPosX += deltaY * m_ballDirX * ballDirY;
      ballPosY = 0;
      ballDirY = 1;
    }
    else if (deltaY > m_height - 1 - ballPosY) {
      deltaY = m_height - 1 - ballPosY;
      ballPosX += deltaY * m_ballDirX * ballDirY;
      ballPosY = m_height - 1;
      ballDirY = -1;
    } else {
      ballPosX += deltaX;
      ballPosY += deltaY;
    }
  }

  // compute pad position to hit ball with center of pad
  padYmin = ballPosY - m_padSize / 2;
  padYmax = ballPosY - (m_padSize - 1) / 2;
}

/**
 * @brief move pad for computer players
 * @param[in] padYmin minimum desired y position of pad
 * @param[in] padYmax maximum desired y position of pad
 * @param[in,out] padPosY y position of pad
 */
void Pong::computerMovePad(int padYmin, int padYmax, int &padPosY) const
{
  // move pad, do not move pad out of field
  if (padPosY > padYmax && padPosY > 0) {
    --padPosY;
  }
  else if (padPosY < padYmin && padPosY < m_height - m_padSize) {
    ++padPosY;
  }
}

/// computer player for left pad
void Pong::computerLeft()
{
  if (computerDelay(m_leftDelay)) {
    int padYmin, padYmax;
    computerComputePadPos(1, m_leftPosY, padYmin, padYmax);
    computerMovePad(padYmin, padYmax, m_leftPosY);
  }
}

/// computer player for right pad
void Pong::computerRight()
{
  if (computerDelay(m_rightDelay)) {
    int padYmin, padYmax;
    computerComputePadPos(m_width - 2, m_rightPosY, padYmin, padYmax);
    computerMovePad(padYmin, padYmax, m_rightPosY);
  }
}

/// bounce ball
void Pong::bounceBall()
{
  bounceBallSide(); // must be done before player bounce to be safe
  bounceBallLeft();
  bounceBallRight();
  bounceBallSide(); // must also be done after player bounce to be safe
}

/// bounce ball at sides
void Pong::bounceBallSide()
{
  if (m_ballPosY <= 0 && m_ballDirY < 0) {
    m_ballDirY = 1;
  }
  if (m_ballPosY >= m_height - 1 && m_ballDirY > 0) {
    m_ballDirY = -1;
  }
  if (m_ballPosX <= 0 && m_ballDirX < 0) {
    m_ballDirX = 1;
  }
  if (m_ballPosX >= m_width - 1 && m_ballDirX > 0) {
    m_ballDirX = -1;
  }
}

/// bounce ball at left pad
void Pong::bounceBallLeft()
{
  if (m_ballPosX != 1 || m_ballDirX >= 0) {
    return;
  }

  // top corner
  if (m_ballPosY == m_leftPosY - 1 && m_ballDirY > 0) {
    m_ballDirX = 1;
    m_ballDirY = -1;
  }

  // bottom corner
  else if (m_ballPosY == m_leftPosY + m_padSize && m_ballDirY < 0) {
    m_ballDirX = 1;
    m_ballDirY = 1;
  }

  // pad edge
  else if (m_ballPosY >= m_leftPosY &&
           m_ballPosY < m_leftPosY + m_padSize) {
    m_ballDirX = 1;
  }
}

/// bounce ball at right pad
void Pong::bounceBallRight()
{
  if (m_ballPosX != m_width - 2 || m_ballDirX <= 0) {
    return;
  }

  // top corner
  if (m_ballPosY == m_rightPosY - 1 && m_ballDirY > 0) {
    m_ballDirX = -1;
    m_ballDirY = -1;
  }

  // bottom corner
  else if (m_ballPosY == m_rightPosY + m_padSize && m_ballDirY < 0) {
    m_ballDirX = -1;
    m_ballDirY = 1;
  }

  // pad edge
  else if (m_ballPosY >= m_rightPosY &&
           m_ballPosY < m_rightPosY + m_padSize) {
    m_ballDirX = -1;
  }
}

/// request next time call - or cancel request if not needed
void Pong::planTimeCall()
{
  if (m_ballPosX < 0 ||  m_ballPosY < 0) {
    m_mgrs.m_callMgr.cancelTimeCall(this);
    return;
  }

  Time stepTime;
  stepTime.fromMs(100);
  m_mgrs.m_callMgr.requestTimeCall(this, Time::now() + stepTime);
}

/// move ball out of the field and halt it
void Pong::hideBall()
{
  m_ballPosX = -1;
  m_ballPosY = -1;
  m_ballDirX = 0;
  m_ballDirY = 0;

  // update time call request
  planTimeCall();
}

/// start ball
void Pong::startBall()
{
  // ball starts horizontally at middle of field, vertically random
  m_ballPosX = (m_width - (rand() & 1)) / 2;
  m_ballPosY = rand() % m_height;
  // random diagonal direction
  m_ballDirX = (rand() & 1) * 2 - 1;
  m_ballDirY = (rand() & 1) * 2 - 1;

  // request first time call if needed
  planTimeCall();
}

} // namespace Blinker

