/**
 * BangleJS Pong game
 *
 * Original Author: Frederic Rousseau https://github.com/fredericrous
 * Created: April 2020
 *
 * Inspired by:
 * - Let's make pong, One Man Army Studios, Youtube
 * - Pong.js, KanoComputing, Github
 * - Coding Challenge #67: Pong!, The Coding Train, Youtube
 * - Pixl.js Multiplayer Pong, espruino website
 */

const SCREEN_WIDTH = 240;
const FPS = 16;
const MAX_SCORE = 11;
let scores = [0, 0];
let aiSpeedRandom = 0;
let winnerMessage = '';

const sound = {
  ping: () => Bangle.beep(8, 466),
  pong: () => Bangle.beep(8, 220),
  fall: () => Bangle.beep(16*3, 494).then(_ => Bangle.beep(32*3, 3322))
};

function Vector(x, y) {
  this.x = x;
  this.y = y;
}
Vector.prototype.add = function (x) {
  this.x += x.x || 0;
  this.y += x.y || 0;
  return this;
};

const constrain = (n, low, high) => Math.max(Math.min(n, high), low);
const random = (min, max) => Math.random() * (max - min) + min;
const intersects = (circ, rect, right) => {
  var c = circ.pos;
  var r = circ.r;
  if (c.y - r < rect.pos.y + rect.height && c.y + r > rect.pos.y) {
    if (right) {
      return c.x + r > rect.pos.x - rect.width*2 && c.x < rect.pos.x + rect.width
    } else {
      return c.x - r < rect.pos.x + rect.width*2 && c.x > rect.pos.x - rect.width
    }
  }
  return false;
}

/////////////////////////////  Ball  //////////////////////////////////////////

function Ball() {
  this.r = 4;
  this.prevPos = null;
  this.originalSpeed = 4;
  this.maxSpeed = 6;

  this.reset();
}
Ball.prototype.reset = function() {
  this.speed = this.originalSpeed;
  var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed;
  var bounceAngle = Math.PI/6;
  this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle));
  this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH));
  this.ballReturn = 0;
};
Ball.prototype.restart = function() {
  this.reset();
  ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2);
  player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2);
  this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2);
};
Ball.prototype.show = function (invert) {
  if (this.prevPos != null) {
    g.setColor(invert ? -1 : 0);
    g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r);
  }
  g.setColor(invert ? 0 : -1);
  g.fillCircle(this.pos.x, this.pos.y, this.r);
  this.prevPos = {
    x: this.pos.x,
    y: this.pos.y,
    r: this.r
  };
};
function bounceAngle(playerY, ballY, playerHeight, maxHangle) {
  let relativeIntersectY = (playerY + (playerHeight/2)) - ballY;
  let normalizedRelativeIntersectionY = relativeIntersectY / (playerHeight/2);
  let bounceAngle = normalizedRelativeIntersectionY * maxHangle;
  return { x: Math.cos(bounceAngle), y: -Math.sin(bounceAngle) };
}
Ball.prototype.bouncePlayer = function (directionX, directionY, player) {
  this.ballReturn++;
  this.speed = constrain(this.speed + 2, this.originalSpeed, this.maxSpeed);
  var MAX_BOUNCE_ANGLE = 4 * Math.PI/12;
  var angle = bounceAngle(player.pos.y, this.pos.y, player.height, MAX_BOUNCE_ANGLE)
  this.velocity.x = this.speed * angle.x * directionX;
  this.velocity.y = this.speed * angle.y * directionY;
  this.ballReturn % 2 === 0 ? sound.ping() : sound.pong();
};
Ball.prototype.bounce = function (directionX, directionY, player) {
  if (player)
    return this.bouncePlayer(directionX, directionY, player);

  if (directionX) {
    this.velocity.x = Math.abs(this.velocity.x) * directionX;
  }
  if (directionY) {
    this.velocity.y = Math.abs(this.velocity.y) * directionY;
  }
};
Ball.prototype.fall = function (playerId) {
  scores[playerId]++;
  if (scores[playerId] >= MAX_SCORE) {
    this.restart();
    state = 3;
    if (playerId === 1) {
      winnerMessage = startOption === 0 ? "AI Wins!" : "Player 2 Wins!";
    } else {
      winnerMessage = startOption === 0 ? "You Win!" : "Player 1 Wins!";
    }
  } else {
    sound.fall();
    this.reset();
  }
};
Ball.prototype.wallCollision = function () {
  if (this.pos.y < 0) {
    this.bounce(0, 1);
  } else if (this.pos.y > SCREEN_WIDTH) {
    this.bounce(0, -1);
  } else if (this.pos.x < 0) {
    this.fall(1);
  } else if (this.pos.x > SCREEN_WIDTH) {
    this.fall(0);
  } else {
    return false;
  }
  return true;
};
Ball.prototype.playerCollision = function (player) {
  if (intersects(this, player)) {
    if (this.pos.x < SCREEN_WIDTH/2) {
      this.bounce(1, 1, player);
      this.pos.add(new Vector(this.width, 0));
      aiSpeedRandom = random(-1.6, 1.6);
    } else {
      this.bounce(-1, 1, player);
      this.pos.add(new Vector(-(this.width / 2 + 1), 0));
    }
    return true;
  }
  return false;
};
Ball.prototype.collisions = function () {
  return this.wallCollision() || this.playerCollision(player) || this.playerCollision(ai);
};
Ball.prototype.updatePosition = function () {
  var elapsed = new Date().getTime() - this.lastUpdate;
  var x = (elapsed / 50) * this.velocity.x;
  var y = (elapsed / 50) * this.velocity.y;
  this.pos.add(new Vector(x, y));
};
Ball.prototype.update = function () {
  this.updatePosition();
  this.lastUpdate = new Date().getTime();
  this.collisions();
};

////////////////////////////  Player  /////////////////////////////////////////

function Player(right) {
  this.width = 4;
  this.height = 30;
  this.pos = new Vector(right ? SCREEN_WIDTH-this.width : this.width, SCREEN_WIDTH/2 - this.height/2);
  this.acc = new Vector(0, 0);
  this.speed = 15;
  this.maxSpeed = 25;
  this.prevPos = null;
  this.right = right;
}
Player.prototype.show = function () {
  if (this.prevPos != null) {
    g.setColor(0);
    g.fillRect(this.prevPos.x1, this.prevPos.y1, this.prevPos.x2, this.prevPos.y2);
  }
  g.setColor(-1);
  g.fillRect(this.pos.x, this.pos.y, this.pos.x+this.width, this.pos.y+this.height);
  this.prevPos = {
    x1: this.pos.x,
    y1: this.pos.y,
    x2: this.pos.x+this.width,
    y2: this.pos.y+this.height
  };
};
Player.prototype.up = function () {
  this.acc.y -= this.speed;
};
Player.prototype.down = function () {
  this.acc.y += this.speed;
};
Player.prototype.stop = function () {
  this.acc.y = 0;
};
Player.prototype.update = function () {
  this.acc.y = constrain(this.acc.y, -this.maxSpeed, this.maxSpeed);
  this.pos.add(this.acc);
  this.pos.y = constrain(this.pos.y, 0, SCREEN_WIDTH-this.height);
};

//////////////////////////////  AI  ///////////////////////////////////////////

function AI() {
  Player.call(this);
  this.pos = new Vector(SCREEN_WIDTH-this.width*2, SCREEN_WIDTH/2 - this.height/2);
}
AI.prototype = Object.create(Player.prototype);
AI.prototype.constructor = Player;
AI.prototype.update = function () {
  var y = ball.pos.y - this.height/2;
  var randomizedY = ball.ballReturn < 3 ? y : y + (aiSpeedRandom * this.height/2);
  var yConstrained = constrain(randomizedY, 0, SCREEN_WIDTH-this.height);
  this.pos = new Vector(this.pos.x, yConstrained);
};

/////////////////////////////// Scenes ////////////////////////////////////////

function net() {
  var dashSize = 5;
  for (let y = dashSize/2; y < SCREEN_WIDTH; y += dashSize*2) {
    g.setColor(-1);
    let halfScreen = SCREEN_WIDTH/2;
    g.fillRect(halfScreen-dashSize/2, y, halfScreen+dashSize/2, y+dashSize);
  }
}

function drawScores() {
  let x1 = SCREEN_WIDTH/4-5;
  let x2 = SCREEN_WIDTH*3/4-5;

  g.setColor(0);
  g.setFont('Vector', 20);
  g.drawString(prevScores[0], x1, 7);
  g.drawString(prevScores[1], x2, 7);
  g.setColor(-1);
  g.setFont('Vector', 20);
  g.drawString(scores[0], x1, 7);
  g.drawString(scores[1], x2, 7);
  prevScores = scores.slice();
}

function drawGameOver() {
  g.setFont("Vector", 20);
  g.drawString(winnerMessage, startOption === 0 ? 55 : 75, SCREEN_WIDTH/2 - 10);
}

function showControls(hide) {
  g.setColor(hide ? 0 : -1);
  g.setFont("Vector", 8);
  var topArrowString = `
         ########
                ##
           ##  ##
        ###   ##
     ###     ##
  ###
##
`;

  var arrows = [Graphics.createImage(topArrowString), Graphics.createImage(`
               ##
                 ##
####################
                 ##
               ##
`), Graphics.createImage(topArrowString.split('\n').reverse().join('\n'))
  ];

  g.drawString('UP', 170, 50);
  g.drawImage(arrows[0], 200, 40);
  g.drawString('DOWN', 156, 120);
  g.drawImage(arrows[1], 200, 120);
  g.drawString('START', 152, 190);
  g.drawImage(arrows[2], 200, 200);
}

function drawStartScreen(hide) {
  g.setColor(hide ? 0 : -1);
  g.setFont("Vector", 10);
  g.drawString("1 PLAYER", 95, 80);
  g.drawString("2 PLAYERS", 95, 110);

  const ball1 = new Ball();
  ball1.prevPos = null;
  ball1.pos = new Vector(87, 86);
  ball1.show(hide || !(startOption === 0));

  const ball2 = new Ball();
  ball2.prevPos = null;
  ball2.pos = new Vector(87, 116);
  ball2.show(hide || !(startOption === 1));
}

function drawStartTimer(count, callback) {
  setTimeout(_ => {
    player.show();
    ai.show();
    net();
    g.setColor(0);
    g.fillRect(117-7, 115-7, 117+14, 115+14);
    if (count >= 0) {
      g.setFont("Vector", 10);
      g.drawString(count+1, 115, 115);
      g.setColor(-1);
      g.drawString(count === 0 ? 'Go!' : count, 115 - (count === 0 ? 4: 0), 115);
      drawStartTimer(count - 1, callback);
    } else {
      g.setColor(0);
      g.fillRect(117-7, 115-7, 117+14, 115+14);
      callback();
    }
  }, 800);
}

//////////////////////////////// Main /////////////////////////////////////////

function onFrame() {
  if (state === 1) {
    ball.update();
    player.update();
    ai.update();
    ball.show();
    player.show();
    ai.show();
    net();
    ball.show();
    g.flip()
  } else if (state === 3) {
    g.clear();
    g.setColor(0);
    g.fillRect(0,0,240,240);
    state++;
  } else if (state === 4) {
    drawGameOver();
  } else {
    player.show();
    ai.show();
    net();
  }
  drawScores();
}

function startThatGame() {
  player.show();
  ai.show();
  net();
  drawScores();
  drawStartTimer(3, () => setInterval(onFrame, 1000 / FPS));
}

var player = new Player();
var ai;
var ball = new Ball();
var state = 0;
var prevScores = [0, 0];
var playerBle = null;
var startOption = 0;

g.clear();
g.setColor(0);
g.fillRect(0,0,240,240);
showControls();
setTimeout(() => {
  showControls(true);
  drawStartScreen();
}, 2000);

////////////////////////////// Controls ///////////////////////////////////////

setWatch(o => {
  if (state === 0) {
    if (o.state) {
      startOption = startOption === 0 ? startOption : startOption - 1;
      drawStartScreen();
    }
  } else o.state ? player.up() : player.stop();
}, BTN1, {repeat: true, edge: 'both'});
setWatch(o => {
  if (state === 0) {
    if (o.state) {
      startOption = startOption === 1 ? startOption : startOption + 1;
      drawStartScreen();
    }
  } else o.state ? player.down() : player.stop();
}, BTN2, {repeat: true, edge: 'both'});
setWatch(o => {
  state++;
  clearInterval();
  if (state >= 2) {
    g.setColor(0);
    g.fillRect(0, 0, 240, 240);
    ball.show(true);
    scores = [0, 0];
    playerBle = null;
    ball = new Ball();
    state = 1;
    startThatGame();
  } else {
    drawStartScreen(true);
    showControls(true);
    if (startOption === 1) {
      ai = new Player(true);
      startThatGame();
    } else {
      ai = new AI();
      startThatGame();
    }
  }
}, BTN3, {repeat: true});

setWatch(o => startOption === 1 && (o.state ? ai.up() : ai.stop()), BTN4, {repeat: true, edge: 'both'});
setWatch(o => startOption === 1 && (o.state ? ai.down() : ai.stop()), BTN5, {repeat: true, edge: 'both'});
