/* 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 "Pong.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
 */
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_fileComputerColor(dirBase.getFile("computerColor")),
  m_fileScoreColor(dirBase.getFile("scoreColor")),
  m_fileGoalColor(dirBase.getFile("goalColor")),
  m_fileDelay(dirBase.getFile("delay")),
  m_fileMaxScore(dirBase.getFile("maxScore")),
  m_fileLeftPlayerSound(dirBase.getFile("leftPlayerSound")),
  m_fileRightPlayerSound(dirBase.getFile("rightPlayerSound")),
  m_fileScoreSound(dirBase.getFile("scoreSound")),
  m_fileOtherScoreSound(dirBase.getFile("otherScoreSound")),
  m_fileWinSound(dirBase.getFile("winSound")),
  m_fileLoseSound(dirBase.getFile("loseSound")),
  m_ballColor(), m_lineColor(), m_padColor(), m_computerColor(),
  m_scoreColor(), m_goalColor(),
  m_delay(c_delayDescr.default_), m_maxScore(c_maxScoreDescr.default_),
  m_pConnLeft(NULL), m_pConnRight(NULL),
  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),
  m_goalDelay(0), m_bounceCnt(0), m_scoreLeft(0), m_scoreRight(0)
{
  // open operator connection interfaces for left and right player
  m_mgrs.m_opMgr.open(m_name + c_opConnSuffixLeft, this);
  m_mgrs.m_opMgr.open(m_name + c_opConnSuffixRight, this);
}

/// virtual destructor
Pong::~Pong()
{
  // close operator connection interfaces
  m_mgrs.m_opMgr.close(m_name + c_opConnSuffixLeft);
  m_mgrs.m_opMgr.close(m_name + c_opConnSuffixRight);

  // close open operator connections
  if (m_pConnLeft) {
    m_pConnLeft->close();
    m_pConnLeft = NULL;
  }
  if (m_pConnRight) {
    m_pConnRight->close();
    m_pConnRight = NULL;
  }
}

/**
 * @brief check for update of configuration (derived game)
 * @param[in,out] doReinit set to true to ask for reinitialization
 * @param[in,out] doRedraw set to true to ask for redrawing
 */
void Pong::updateConfigGame(bool &doReinit, bool &doRedraw)
{
  (void)doReinit;

  // color file was modified -> convert color, ask for redraw
  if (colorUpdate(m_fileBallColor, 255, m_ballColor)) { doRedraw = true; }
  if (colorUpdate(m_fileLineColor, 128, m_lineColor)) { doRedraw = true; }
  if (colorUpdate(m_filePadColor, 255, m_padColor)) { doRedraw = true; }
  if (colorUpdate(m_fileComputerColor, 192, m_computerColor)) { doRedraw = true; }
  if (colorUpdate(m_fileScoreColor, 128, m_scoreColor)) { doRedraw = true; }
  if (colorUpdate(m_fileGoalColor, 255, m_goalColor)) { doRedraw = true; }

  // cfg value file was updated -> read new cfg value, no reinit/redraw
  valueUpdate(m_fileDelay, c_delayDescr, m_delay);
  valueUpdate(m_fileMaxScore, c_maxScoreDescr, m_maxScore);

  // sound name file was modified -> re-read sound name, no other update needed
  soundUpdate(m_fileLeftPlayerSound);
  soundUpdate(m_fileRightPlayerSound);
  soundUpdate(m_fileScoreSound);
  soundUpdate(m_fileOtherScoreSound);
  soundUpdate(m_fileWinSound);
  soundUpdate(m_fileLoseSound);
}

/**
 * @brief check if accepting new operator connection is possible
 * @param[in] name operator interface name
 * @return if accepting new connection is possible
 */
bool Pong::acceptNewOpConn(const std::string &name)
{
  // game cannot be activates -> reject connection
  if (! canActivate()) {
    return false;
  }
  // left player can join if none there yet
  if (name == m_name + c_opConnSuffixLeft && ! m_pConnLeft) {
    return true;
  }
  // right player can join if none there yet
  if (name == m_name + c_opConnSuffixRight && ! m_pConnRight) {
    return true;
  }
  // default: reject connection
  return false;
}

/**
 * @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 Pong::newOpConn(const std::string &name, OpConn *pConn)
{
  // left player joins if none there yet
  if (name == m_name + c_opConnSuffixLeft && ! m_pConnLeft) {
    m_pConnLeft = pConn;
    requestOpConnSound(m_pConnLeft, m_fileLeftPlayerSound);
  }
  // right player joins if none there yet
  else if (name == m_name + c_opConnSuffixRight && ! m_pConnRight) {
    m_pConnRight = pConn;
    requestOpConnSound(m_pConnRight, m_fileRightPlayerSound);
  }
  // close imcoming connection as soon as possible, nothing else happens
  else {
    requestOpConnClose(pConn);
    return;
  }

  // already active
  if (isActive()) {
    redraw(); // player color is different for phone / computer
  }
  // first player joined -> activate
  else {
    if (! activate()) {
      // activation failed (interlock), close connection as soon as possible
      requestOpConnClose(pConn);
      m_pConnLeft = nullptr;
      m_pConnRight = nullptr;
    }
  }
}

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

  // star -> inform player about it side
  if (key == '*') {
    if (pConn == m_pConnLeft) {
      playOpConnSound(pConn, m_fileLeftPlayerSound);
    } else if (pConn == m_pConnRight) {
      playOpConnSound(pConn, m_fileRightPlayerSound);
    }
    return;
  }

  // normal keys for controlling game

  // left player
  if (pConn == m_pConnLeft) {
    processKey(key, m_leftPosY);
  }
  // right player
  else if (pConn == m_pConnRight) {
    processKey(key, m_rightPosY);
  }
}

/**
 * @brief play command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] sound name of sound to play
 */
void Pong::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 Pong::opConnClose(OpConn *pConn)
{
  // remove coperator connection from requests (if it was in)
  forgetOpConn(pConn);

  // left player leaves
  if (pConn == m_pConnLeft) {
    m_pConnLeft = NULL;
  }
  // right player leaves
  else if (pConn == m_pConnRight) {
    m_pConnRight = NULL;
  }
  // nothing happens
  else {
    return;
  }

  // still a phone player there
  if (m_pConnLeft || m_pConnRight) {
    redraw(); // player color is different for phone / computer
  }
  // no phone players left
  else {
    deactivate();
  }
}

/// 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, 255, m_ballColor);
  color2data(m_fileLineColor, 128, m_lineColor);
  color2data(m_filePadColor, 255, m_padColor);
  color2data(m_fileComputerColor, 192, m_computerColor);
  color2data(m_fileScoreColor, 128, m_scoreColor);
  color2data(m_fileGoalColor, 255, m_goalColor);
  // get values
  valueFromFile(m_fileDelay, c_delayDescr, m_delay);
  valueFromFile(m_fileMaxScore, c_maxScoreDescr, m_maxScore);

  // reset scores
  m_scoreLeft = 0;
  m_scoreRight = 0;

  // start first ball
  m_ballDirX = 0; // start at random side
  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 score
  ColorData const &scoreColor = m_goalDelay <= 0 ? m_scoreColor : m_goalColor;
  y = (m_height - 1) / 2;
  x = m_width / 4;
  number3x5(y, x, 0, 0, m_scoreLeft, scoreColor);
  number3x5(y, m_width - 1 - x, 0, 0, m_scoreRight, scoreColor);

  // draw pads
  lineVert(m_leftPosY, m_leftPosY + m_padSize - 1, 0,
           m_pConnLeft ? m_padColor : m_computerColor);
  lineVert(m_rightPosY, m_rightPosY + m_padSize - 1, m_width - 1,
           m_pConnRight ? m_padColor : m_computerColor);

  // draw ball (blinking if goal delay is active)
  if (m_goalDelay <= 0 || (m_goalDelay & 4) == 0) {
    pixel(m_ballPosY, m_ballPosX, m_ballColor);
  }

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

/// process next time step of game
void Pong::timeStep()
{
  // game is running
  if (m_goalDelay <= 0) {

    // computer player: move pads
    if (! m_pConnLeft) {
      computerLeft();
    }
    if (! m_pConnRight) {
      computerRight();
    }

    // bounce ball
    bounceBall();

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

    // detect goal
    detectGoal();

  }
  // goal delay (blinking ball)
  else if (m_goalDelay > 0) {

    --m_goalDelay;

    // delay is over
    if (m_goalDelay <= 0) {
        // maximum score reached
        if (m_scoreLeft >= (int)m_maxScore ||
            m_scoreRight >= (int)m_maxScore) {
          gameOver();
          return; // gameOver() calls deactivate(), no need to redraw
        }
        // game continues with new ball
        else {
          startBall();
          return; // startBall() calls redraw() and planTimeStep()
        }
    }

  }

  // draw and send frame
  redraw();

  // request next time step
  planTimeStep();
}

/**
 * @brief process key received from phone player
 * @param[in] key received key from player
 * @param[in,out] padPosY y position of player's pad
 */
void Pong::processKey(char key, int &padPosY)
{
  // do not move pad if goal delay is active
  if (m_goalDelay > 0) {
    return;
  }

  // move pad (2 = up, 8 = down), do not move pad out of field
  if (key == '2' && padPosY > 0) {
    --padPosY;
    redraw();
  }
  else if (key == '8' && padPosY < m_height - m_padSize) {
    ++padPosY;
    redraw();
  }
}

/**
 * @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
{
  // do not move pad if goal delay is active
  if (m_goalDelay > 0) {
    return;
  }

  // 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;
    ++m_bounceCnt;
  }

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

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

/// 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;
    ++m_bounceCnt;
  }

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

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

/// detect goal
void Pong::detectGoal()
{
  static int goalBlinkCnt = 3;
  static int goalDelay = goalBlinkCnt * 8 + 3;

  // ball at far left - goal for right player
  if (m_ballPosX == 0) {
    m_goalDelay = goalDelay;
    ++m_scoreRight;
    // play sound - score or win
    if (m_scoreRight >= (int)m_maxScore) {
      playOpConnSound(m_pConnRight, m_fileWinSound);
      playOpConnSound(m_pConnLeft, m_fileLoseSound);
    } else {
      playOpConnSound(m_pConnRight, m_fileScoreSound);
      playOpConnSound(m_pConnLeft, m_fileOtherScoreSound);
    }
  }

  // ball at far right - goal for left player
  else if (m_ballPosX == m_width - 1) {
    m_goalDelay = goalDelay;
    ++m_scoreLeft;
    // play sound - score or win
    if (m_scoreLeft >= (int)m_maxScore) {
      playOpConnSound(m_pConnLeft, m_fileWinSound);
      playOpConnSound(m_pConnRight, m_fileLoseSound);
    } else {
      playOpConnSound(m_pConnLeft, m_fileScoreSound);
      playOpConnSound(m_pConnRight, m_fileOtherScoreSound);
    }
  }
}

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

  // compute interval based on score and bounce count
  int speedup = 10 * (m_scoreLeft + m_scoreRight) + m_bounceCnt;
  float scale = 0.3f + 0.7f * expf(-0.03f * speedup);
  float interval = 1e-3f * m_delay * scale;

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

/// start ball
void Pong::startBall()
{
  // horizontal start direction opposite as before goal or random
  if (m_ballDirX > 0) {
    m_ballDirX = -1;
  } else if (m_ballDirX < 0) {
    m_ballDirX = 1;
  } else {
    m_ballDirX = (rand() & 1) * 2 - 1;
  }
  // random vertical start direction
  m_ballDirY = (rand() & 1) * 2 - 1;

  // horizontal start postion at side of field (depending on direction)
  m_ballPosX = m_ballDirX > 0 ? 0 : m_width - 1;
  // random vertical start position
  m_ballPosY = rand() % m_height;

  // no delays, ball has not bounced at pad
  m_leftDelay = 0;
  m_rightDelay = 0;
  m_goalDelay = 0;
  m_bounceCnt = 0;

  // draw and send frame
  redraw();

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

/// game over: close player connections and deactivate
void Pong::gameOver()
{
  // close open operator connections
  if (m_pConnLeft) {
    forgetOpConn(m_pConnLeft); // remove from requests (if it was in)
    m_pConnLeft->close();
    m_pConnLeft = NULL;
  }
  if (m_pConnRight) {
    forgetOpConn(m_pConnRight); // remove from requests (if it was in)
    m_pConnRight->close();
    m_pConnRight = NULL;
  }

  // deactivate game
  deactivate();
}

/// descriptor for delay value
Pong::ValueDescr const Pong::c_delayDescr = { 200, 100, 500 };
/// descriptor for maximum score value
Pong::ValueDescr const Pong::c_maxScoreDescr = { 9, 1, 99 };

/// operator connection name suffix for left player's connection
std::string const Pong::c_opConnSuffixLeft = "/left";
/// operator connection name suffix for right player's connection
std::string const Pong::c_opConnSuffixRight = "/right";

} // namespace Blinker

