// SPDX-License-Identifier: MIT

pragma solidity ^0.8.9;

contract BettingGame {
    uint256 public MIN_DEPOSIT_AMOUNT = 0.005 ether;
    uint256 public MAX_DEPOSIT_AMOUNT = 0.05 ether;

    uint256 private rewardFee = 3;
    uint256 private bettingFee = 5;

    uint256 public gameId = 0;
    address public adminWallet;
    mapping(uint256 => Game) public games;

    struct Game{
        uint256 id;
        uint256 bet;
        uint256 seed;
        uint256 amount;
        uint256 winAmount;
        uint256 time;
        address payable player;
    }

    uint16 private _randomIndex = 0;
    uint private _randomCalls = 0;
    mapping(uint16 => address) private _randomSource;
    mapping(address => bool) public managers;

    event Result(uint256 id, uint256 bet, uint256 randomSeed, uint256 amount, address player, uint256 winAmount, uint256 randomResult, uint256 time);

    constructor (address _admin) {
        // Fill random source addresses
        _randomSource[0] = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
        _randomSource[1] = 0x3cD751E6b0078Be393132286c442345e5DC49699;
        _randomSource[2] = 0xb5d85CBf7cB3EE0D56b3bB207D5Fc4B82f43F511;
        _randomSource[3] = 0xC098B2a3Aa256D2140208C3de6543aAEf5cd3A94;
        _randomSource[4] = 0x28C6c06298d514Db089934071355E5743bf21d60;
        _randomSource[5] = 0x2FAF487A4414Fe77e2327F0bf4AE2a264a776AD2;
        _randomSource[6] = 0x267be1C1D684F78cb4F6a176C4911b741E4Ffdc0;

        managers[msg.sender] = true;
        adminWallet = _admin;
    }

    function fund() external payable {}

    /**
    * Taking bets function.
    * By winning, user 2x his betAmount.
    * Chances to win and lose are the same.
    */
    function game(uint256 bet, uint256 seed) public payable returns (bool) {

        /** !UPDATE
        * 
        * Checking if msg.value is higher or equal than $1.
        */
        require(msg.value>=MIN_DEPOSIT_AMOUNT, 'Error, msg.value must be bigger than minimum amount');
        require(msg.value<=MAX_DEPOSIT_AMOUNT, 'Error, msg.value must be smaller than maximum amount');
        
        //bet=0 is back
        //bet=1 is front
        require(bet<=1, 'Error, accept only 0 and 1');

        //vault balance must be at least equal to msg.value
        require(address(this).balance>=msg.value, 'Error, insufficent vault balance');
        
        //each bet has unique id
        games[gameId] = Game(gameId, bet, seed, msg.value, 0, 0, payable(msg.sender));

        //increase gameId for the next bet
        gameId = gameId+1;

        //seed is auto-generated by DApp
        uint random = getSomeRandomNumber(seed, 2);
        verdict(gameId-1, random);

        return true;
    }

    function updateRandomIndex() internal {
        _randomIndex += 1;
        _randomCalls += 1;
        if (_randomIndex > 6) _randomIndex = 0;
    }

    function getSomeRandomNumber(uint _seed, uint _limit) internal returns (uint16) {
        uint extra = 0;
        for (uint16 i = 0; i < 7; i++) {
            extra += _randomSource[_randomIndex].balance;
        }

        uint random = uint(
            keccak256(
                abi.encodePacked(
                    _seed,
                    blockhash(block.number - 1),
                    block.coinbase,
                    block.difficulty,
                    msg.sender,
                    gameId,
                    extra,
                    _randomCalls,
                    _randomIndex
                )
            )
        );

        _randomIndex = uint16(random % 6);
        updateRandomIndex();

        return uint16(random % _limit);
    }

    /**
    * Send rewards to the winners.
    */
    function verdict(uint256 id, uint256 random) internal {
        //reset winAmount for current user
        uint256 winAmount = 0;

        uint256 rewardFeeAmount = getRewardFee(games[id].amount);
        payable(adminWallet).transfer(rewardFeeAmount);
        
        //if user wins, then receives 2x of their betting amount
        if(random == games[id].bet) {
            uint256 bettingFeeAmount = getBettingFee(games[id].amount);
            winAmount = (games[id].amount - bettingFeeAmount) * 2;
            games[id].player.transfer(winAmount);
        }
        games[id].winAmount = winAmount;
        games[id].time = block.timestamp;
        emit Result(games[id].id, games[id].bet, games[id].seed, games[id].amount, games[id].player, games[id].winAmount, random, games[id].time);
    }

    /**
    * Withdraw Ether from this contract (manager option).
    */
    function withdrawEther(uint256 amount) external {
        require(address(this).balance>=amount, 'Error, contract has insufficent balance');
        require(managers[msg.sender] == true, 'Error, You are not allowed');
        payable(msg.sender).transfer(amount);
    }

    function changeAdminWallet(address _addr) external {
        require(managers[msg.sender] == true,  'Error, You are not allowed');
        adminWallet = _addr;
    }

    function getRewardFee(uint256 amount) private view returns(uint256) {
        return amount * rewardFee / 100; 
    }

    function getBettingFee(uint256 amount) private view returns(uint256) {
        return amount * bettingFee / 100;
    }

    function setMinAmount(uint256 newAmount) external {
        require(managers[msg.sender] == true,  'Error, You are not allowed');
        MIN_DEPOSIT_AMOUNT = newAmount;
    }

    function setMaxAmount(uint256 newAmount) external {
        require(managers[msg.sender] == true,  'Error, You are not allowed');
        MAX_DEPOSIT_AMOUNT = newAmount;
    }
}