contract GameManager is IBalanceInfo, IGameManager {
    // all of the games 
    mapping (uint => GameInfo) games;
    // all of the bets in the games  
    mapping (uint => mapping (address => uint)) bets;

    struct GameInfo {
        address highestBidder; // the person who made the highest bet in this game
        address starter; // the person who started this game
        uint blockFinish; // the block when the game will be finished
        uint prize; // the prize, that will be awarded to the winner
        uint totalBets; // the amount of total bets to this game
        bool payedOut; // true, if the user has taken back his deposit
        uint lastBetBlock;
        uint addOn;
    }

    function startGameInternal(uint gameId, uint length, uint addOn, uint prize) internal {
        // there was no other game started
        require(!gameExists(gameId));
        
        // the prize should be present
        require(prize > 0);

        // the length should be nonzero
        require(length > 0);

        // calculate the prize for the game
        games[gameId].starter = msg.sender;
        games[gameId].prize = prize;

        // reserve the balance for this prize
        reserveBalance(prize);

        // the finishing block of the game
        // important question is that the actual start block of the game is the current number, and block passed is used by id
        games[gameId].blockFinish = block.number + length - 1;
        games[gameId].addOn = addOn;

        // generate an event
        emit GameStarted(gameId, msg.sender, block.number, games[gameId].blockFinish, prize);
    }

    /**
    Checks that this user can start round (due to nonce issues)
     */
    function betInGameInternal(uint gameId, uint bet) internal {
        require(bet > 0, "Bet should be not zero");

        // the game was started
        require(gameExists(gameId), "No such game");
        require(!finishedGame(gameId), "Game is finished");
        uint newBet = bets[gameId][msg.sender] + bet;
        
        // update the highest bidder
        if (newBet > bets[gameId][games[gameId].highestBidder]) {
            games[gameId].highestBidder = msg.sender;
            games[gameId].lastBetBlock = block.number;
        } 

        bets[gameId][msg.sender] = newBet;
        // update the total bets
        games[gameId].totalBets += bet;
        emit GameBet(gameId, msg.sender, games[gameId].highestBidder, bets[gameId][games[gameId].highestBidder], addonEndsIn(gameId), newBet);
    }

    function withdrawPrize(uint gameId) public {
        withdrawPrizeInternal(gameId, 0);
    }

    function withdrawPrizeInternal(uint gameId, uint additional) internal {
        require(finishedGame(gameId), "Game not finished");
        require(msg.sender == games[gameId].highestBidder, "You are not the winner");
        require(!games[gameId].payedOut, "Game already payed");
        games[gameId].payedOut = true;

        // the invariant stays the same
        freeBalance(games[gameId].prize);
        msg.sender.transfer(games[gameId].prize + additional);
        emit GamePrizeTaken(gameId, msg.sender);
    }


    function gameExists(uint gameId) public view returns (bool) {
        return games[gameId].blockFinish != 0;
    }

    function getWinner(uint gameId) public view returns(address) {
        return games[gameId].highestBidder;
    }

    function finishedGame(uint gameId) public view returns(bool) {
        if (!gameExists(gameId)) 
            return false;
        return addonEndsIn(gameId) < block.number;
    }

    // returns that the game is payed out
    function payedOut(uint gameId) public view returns(bool) {
        return games[gameId].payedOut;
    }

    // returns the prize for this game
    function prize(uint gameId) public view returns(uint) {
        return games[gameId].prize;
    }

    function lastBetBlock(uint gameId) public view returns(uint) {
        return games[gameId].lastBetBlock;
    }

    function getBet(uint gameId, address better) public view returns(uint) {
        return bets[gameId][better];
    }

    // returns the block number, when the game is finished
    function endsIn(uint gameId) public view returns(uint) {
        return games[gameId].blockFinish;
    }

    function addonEndsIn(uint gameId) public view returns(uint) {
        uint addonB = games[gameId].lastBetBlock + games[gameId].addOn;
        if (addonB >= games[gameId].blockFinish) {
            return addonB;
        } else {
            return games[gameId].blockFinish;
        }
    }

    // returns the total amount of bets in the game
    function totalBets(uint gameId) public view returns(uint) {
        return games[gameId].totalBets;
    }

    function gameProfited(uint gameId) public view returns(bool) {
        return games[gameId].totalBets >= games[gameId].prize;
    }
}
function payedOut(uint gameId) public view returns(bool) {
    return games[gameId].payedOut;
}

// returns that the game is payed out