contract BlackJack is CroupierRole {
    using SafeMath for *;

    uint constant THIS_DIVISOR = 1000;
    uint constant LOCK_RATIO = 2500;
    uint8 constant MAX_CUT_CARD = 15;

    mapping (address => uint256) private _balances;
    mapping (address => uint256) private _locked;

    mapping (address => uint256) private _last_apply_time;
    mapping (address => uint256) private _apply_amount;

    uint256 private _totalBalance = 0;

    uint256 public expireBlocks = 255;
    uint256 public statedPeriod = 30 minutes;
    uint256 public maxBet = 1 ether;
    uint256 public minBet = 0.1 ether;
    uint256 public feeRatio = 10;

    struct Bet {
        // gambler's address, 20 bytes.
        address gambler;
        // cut card position, the number range is 0-15.
        uint8 cutCard;
        // gambler's action list, per 4-bit representing an action, push-down storage.
        /**
        * action encoding rules:
        * 0 - reserved
        * 1 - Get
        * 2 - Hit
        * 3 - Stand
        * 4 - Double
        * 5 - Split
        * 6 - Insurance
        * 7 - Surrender
        * 8 - timeout
         */
        bytes11 actions;
        // betting amount, 128 bits number is enough.
        uint128 amount;
        // block number of deal.
        uint128 dealBlockNumber;
    }

    mapping (uint256 => Bet) public bets;

    event Deposit(address indexed from, uint256 value);
    event Withdraw(address indexed from, uint256 value);
    event Apply(address indexed from, uint256 value);
    event Deal(uint256 indexed commit);
    event Settle(uint256 indexed commit);
    event Refund(uint256 indexed commit, uint128 amount);
    
    /**
     * @dev constructor
     */
    constructor() public payable{        
    }

    /**
    * @dev Fallback function. It's another entry for deposit. While owner transfer ether to 
    *      this contract, it means increase the pot.
     */
    function () public payable {
        if(!isOwner(msg.sender)){
            _deposit(msg.sender, msg.value);
        }        
    }

    /**
    * @dev Total number of tokens deposit by gamblers.
    * @return An uint256 representing the total amount owned by gamblers.
    */
    function totalBalance() public view returns (uint256) {
        return _totalBalance;
    }

    /**
    * @dev Gets the balance of specified address.
    * @param owner The address to query the balance of.
    * @return An uint256 representing the amount owned by the passed address.
    */
    function balanceOf(address owner) public view returns (uint256) {
        return _balances[owner];
    }

    /**
    * @dev Gets the locked value of specified address.
    * @param owner The address to query the locked amount of.
    * @return An uint256 representing the amount locked by the passed address.
    */
    function lockedOf(address owner) public view returns (uint256) {
        return _locked[owner];
    }

    /**
    * @dev Gets the last apply-withdraw time of specified address.
    * @param owner The address to query the last apply time of.
    * @return An uint256 representing the last apply time by the passed address.
    */
    function lastApplyTime(address owner) public view returns (uint256) {
        return _last_apply_time[owner];
    }

    /**
    * @dev Gets the apply-withdraw amount of specified address.
    * @param owner The address to query the apply amount of.
    * @return An uint256 representing the apply amount by the passed address.
    */
    function applyAmount(address owner) public view returns (uint256) {
        return _apply_amount[owner];
    }

    /**
    * @dev Deal action to start a new game with proxy mode, submit by croupier bot.
    * @param gambler gambler's address.
    * @param commit generated by keccak of 2 256-bit reveals, used to unique identify a deck.
    *               gambler get commit but don't know the deck, dealer can't change the deck because of keccak is one-way irreversible.
    * @param amount 128-bit number of bet amount.
    * @param cutCard cut card position, gambler set it after receive the commit, so this process can guarantee fairness.
    * @param v 
    * @param r 
    * @param s v, r,s are components of ECDSA signature. Ensure the deck is signed by the gambler himself.
     */
    function deal(address gambler, uint256 commit, uint128 amount, uint8 cutCard, uint8 v, bytes32 r, bytes32 s) 
        public 
        onlyCroupier
    {
        // verify signature.
        bytes32 signatureHash = keccak256(abi.encodePacked(amount, cutCard, commit));        
        require (gambler == ecrecover(signatureHash, v, r, s), "ECDSA signature is not valid.");

        _dealCore(gambler, commit, amount, cutCard);
    }

    /**
    * @dev Settle a deck by croupier.
    * @param reveal_1 Per byte of 1-26 bytes in reveal_1, reveal_2 representing a single card, 2 256-bit reveal combine a 52 cards deck. 
    * Single card coding rules:
    * low 4-bit : 0001-1010 points of single card(1-10).
    * 5-6 bit   : suit, 00 - spades, 01 - hearts, 10 - clubs, 11 - diamonds.
    * 7-8 bit   : face cards, 00 - 10, 01 - Jack, 10 - Queen, 11 - King.
    * @param reveal_2 same as reveal_1.
    * @param actions gambler's actions.
    * @param win true - gambler win, false - lose.
    * @param amount winnings or losses amount.
     */
    function settle(uint256 reveal_1, uint256 reveal_2, bytes11 actions, bool win, uint128 amount) 
        public 
        onlyCroupier 
    {
        uint commit = uint(keccak256(abi.encodePacked(reveal_1, reveal_2)));
        Bet storage bet = bets[commit];

        // verify commit.
        address gambler = bet.gambler;
        uint256 value = uint256(bet.amount);
        require(gambler != address(0) && value > 0, "Bet should be in 'active' state.");

        // verify bet is not expired.
        require(block.number > bet.dealBlockNumber, "Settle in the same block as placeBet, or before.");
        require(block.number <= uint256(bet.dealBlockNumber).add(expireBlocks), "Bet expired.");        

        // Store actions.
        bet.actions = actions;
        bet.amount = 0;

        // unlock.
        uint256 lockValue = value.mul(LOCK_RATIO).div(THIS_DIVISOR);
        _locked[gambler] = _locked[gambler].sub(lockValue);

        // calculate balance.
        if(win) {
            _balances[gambler] = _balances[gambler].add(uint256(amount));
            _totalBalance = _totalBalance.add(uint256(amount));
        }
        else{
            _balances[gambler] = _balances[gambler].sub(uint256(amount));
            _totalBalance = _totalBalance.sub(uint256(amount));
        }

        emit Settle(commit);
    }

    /**
    * @dev Refund a commit while it's expired.
    * @param commit which one bet been refunded.
     */
    function refund(uint256 commit) public onlyCroupier {
        // Verify that bet is in 'active' state.
        Bet storage bet = bets[commit];
        uint256 value = uint256(bet.amount);
        address gambler = bet.gambler;
        require(gambler != address(0) && value > 0, "Bet should be in 'active' state.");

        // Verify that bet has already expired.
        require (block.number > uint256(bet.dealBlockNumber).add(expireBlocks), "Bet not yet expired.");

        //unlock.
        uint256 lockValue = value.mul(LOCK_RATIO).div(THIS_DIVISOR);  
        _locked[gambler] = _locked[gambler].sub(lockValue);

        bet.amount = 0;

        emit Refund(commit, uint128(value));
    }

    /**
    * @dev Deposit in this contract.
     */
    function deposit() public payable returns (bool){
        _deposit(msg.sender, msg.value);
        return true;
    }

    /**
    * @dev apply for withdrawal.
    * @param amount the amount to apply for withdrawal, should be less than balance subtract locked.
     */
    function apply(uint256 amount) public returns (bool){
        require(amount <= _balances[msg.sender].sub(_locked[msg.sender]), "Not enough balance.");

        _last_apply_time[msg.sender] = now;
        _apply_amount[msg.sender] = amount;

        emit Apply(msg.sender, amount);
        return true;
    }

    /**
    * @dev Withdraw from this contract. Should apply at first, and withdraw after the stated apply period.
     */
    function withdraw() public returns (bool){
        require(_apply_amount[msg.sender] > 0, "");
        require(now >= _last_apply_time[msg.sender].add(statedPeriod), "");

        _withdraw(msg.sender, _apply_amount[msg.sender]);

        _apply_amount[msg.sender] = 0;
        return true;
    }

    /**
    * @dev Withdraw all by croupier in special cases, such as contract upgrade.
    * @param from The address to withdraw.
     */
    function withdrawProxy(address from) public onlyCroupier returns(bool) {        
        uint256 amount = balanceOf(from);
        _withdraw(from, amount);
        return true;
    }

    /**
    * @dev Deposit for a specified address, internal function.
    * @param from The address to deposit.
    * @param value The amount to be deposited. 
     */
    function _deposit(address from, uint256 value) internal {
        require(from != address(0), "Invalid address.");

        _balances[from] = _balances[from].add(value);
        _totalBalance = _totalBalance.add(value);
        emit Deposit(from, value);
    }

    /**
    * @dev Withdraw for a specified address, internal function. Due to house edge of blackjack can't cover the cost of gas, 
    *      platform charges 1% fee while withdraw.  
    * @param from The address to withdraw.
    * @param value The amount to be withdrawed, should be less than balance subtract locked, and this contract can afford. 
     */
    function _withdraw(address from, uint256 value) internal {
        require(from != address(0), "Invalid address.");
        require(value <= _balances[from].sub(_locked[from]), "Not enough balance.");

        _balances[from] = _balances[from].sub(value);
        _totalBalance = _totalBalance.sub(value);        
        
        uint256 fee = value.mul(feeRatio).div(THIS_DIVISOR);
        require(value.sub(fee) <= address(this).balance, "Can't afford.");
        from.transfer(value.sub(fee));
        
        emit Withdraw(from, value);
    }

    /**
    * @dev Check uint256-uint128 type conversion is safe
     */
    function _safeTypeConversion(uint256 a, uint128 b) internal pure returns(bool) {
        require(a == uint256(b) && uint128(a) == b, "Not safe type conversion.");
        return true;
    }

    /**
    * @dev Deal action core.
     */
    function _dealCore(address gambler, uint256 commit, uint128 amount, uint8 cutCard) internal {
        // verify commit is "Clean".
        Bet storage bet = bets[commit];
        require(bet.gambler == address(0), "Bet should be in 'clean' state.");

        // verify cut card position.
        require(cutCard <= MAX_CUT_CARD, "Cut card position is not valid.");
    
        //verify bet amount range.
        uint256 value = uint256(amount);
        require(_safeTypeConversion(value, amount), "Not safe type conversion");

        require(value >= minBet && value <= maxBet, "Bet amount is out of range.");

        uint256 lockValue = value.mul(LOCK_RATIO).div(THIS_DIVISOR);        
        require(lockValue <= balanceOf(gambler).sub(lockedOf(gambler)), "Balance is not enough for locked.");

        // Store bet parameters on blockchain.
        _locked[gambler] = _locked[gambler].add(lockValue);

        bet.gambler = gambler;
        bet.cutCard = cutCard;
        bet.amount = amount;
        bet.dealBlockNumber = uint128(block.number);

        emit Deal(commit);        
    }

    /**
    * @dev Set max bet amount.
    * @param input in wei.
     */
    function setMaxBet(uint256 input) public onlyOwner {
        maxBet = input;
    }

    /**
    * @dev Set min bet amount.
    * @param input in wei.
     */
    function setMinBet(uint256 input) public onlyOwner {
        minBet = input;
    }

    /**
    * @dev Set fee ratio.
    * @param input new fee ratio, div by 1000.
     */
    function setFeeRatio(uint256 input) public onlyOwner {
        feeRatio = input;
    }

    /**
    * @dev Set expiration blocks.
    * @param input new number of expiration blocks.
     */
    function setExpireBlocks(uint256 input) public onlyOwner {
        expireBlocks = input;
    }

    /**
    * @dev Set stated apply period.
    * @param input new number of stated apply period.
     */
    function setStatedPeriod(uint256 input) public onlyOwner {
        statedPeriod = input;
    }

    /**
    * @dev Withdraw funds to cover costs of operation.
    * @param amount should ensure the total balances of palyers.
     */
    function withdrawFunds(uint256 amount) public onlyOwner {
        require(amount <= address(this).balance.sub(_totalBalance), "Not enough funds.");
        msg.sender.transfer(amount);
    }

    /**
    * @dev kill this contract while upgraded.
     */
    function kill() public onlyOwner {
        require(_totalBalance == 0, "All of gambler's balances need to be withdrawn.");
        selfdestruct(msg.sender);
    }
}
function balanceOf(address owner) public view returns (uint256) {
    return _balances[owner];
}

/**
 * @dev Gets the balance of specified address.
 * @param owner The address to query the balance of.
 * @return An uint256 representing the amount owned by the passed address.
 */