contract Dice is usingOraclize {

    //Definition of Constant======================================================================================================
    uint constant INVALID_BETPRICE_ID = 999999;//Invalid bet amount ID
    uint constant INVALID_BET_ID = 0;//Invalid bet ID

    //Bet Status
    uint constant BET_STATE_WAITPAIR = 0;//Wait for a challenger
    uint constant BET_STATE_WAITORACLIZE = 1;//Wait for the result
    uint constant BET_STATE_END = 2;//End successfully
    uint constant BET_STATE_CANCEL_BY_PLAYER = 3;//Canceled by the player
    uint constant BET_STATE_CANCEL_BY_OWNER = 4;//Canceled by the contract owner
    uint constant BET_STATE_CANCEL_BY_ORACLIZE_ERROR_RANDOM_NUMBER = 5;//Canceled due to random number not being verified
    uint constant BET_STATE_CANCEL_BY_ORACLIZE_ERROR_FEE = 6;//Canceled due to service fee is more than the bet amount
    uint constant BET_STATE_CANCEL_BY_RANDOM_NUMBER_A_EUQAL_B = 7;//Canceled due to ending in a draw

    
    //Variables of Contract Management===================================================================================================
    address public owner;//Contract Owner
    bool public isStopped;//Pause the Contract

    //Variables of Bet===================================================================================================
    //Definition of Bet Storage Structure
    struct Bet {
        uint betPrice;//Bet Amount
        uint betState;//Bet Status
        address playerAddressA;//Address of Player A
        address playerAddressB;//Address of Player B
        uint numberRolled;//Returned random numbers from Oraclize
        uint oraclizeFee;//Service fee of random number
    }

    uint public betFee = 200; //Bet Transaction Fee (10000 = 100%)
    uint[] public betPrices = [  200 finney , 500 finney, 1000 finney];//Bet Amount Option
    uint[] public waitPairBetIDs = [ INVALID_BET_ID , INVALID_BET_ID ,INVALID_BET_ID];//Current Bet ID that waiting for matching of each option
    uint public oraclizeCallbackGasLimit = 200000;//GasLimit of Oraclize
    Bet[] public bets ;//Save all bet data in chronological order
    mapping (bytes32 => uint) public oraclizeQueryID2BetID;// Mapping of random number query ID to Bet ID
    mapping (address => uint) public address2SendErrorValue;// Mapping of address to SendErrorValue

    //Event Notification======================================================================================================
    event LOG_ORACLIZE_CALLBACK(uint indexed betid , bytes32 _queryId, string _result, bytes _proof);
    event LOG_SEND_ERROR(uint indexed betid , address indexed player, uint value);
    event LOG_SET_SEND_ERROR_VALUE(address indexed player, uint value);

    //Constructor =====================================================================================================
    function Dice() {
        oraclize_setProof(proofType_Ledger); // sets the Ledger authenticity proof in the constructor
        owner = msg.sender;
        bets.push(Bet(0,0,address(0x0),address(0x0),0,0));//Bet 0 is invalid
    }

    //Function Modification=====================================================================================================
    modifier onlyIfNotStopped {
        if (isStopped) throw;
        _;
    }

    modifier onlyOwner {
        if (owner != msg.sender) throw;
        _;
    }

    modifier onlyBetCanCancel(uint betid) {
        if((bets[betid].betState != BET_STATE_WAITPAIR)&&
        (bets[betid].betState != BET_STATE_WAITORACLIZE)
        ) throw;//Cannot cancel
        _;
    }

    //Contract Management=====================================================================================================

    function stopContract() public
        onlyOwner {
        isStopped = true;
    }

    function resumeContract() public
        onlyOwner {
        isStopped = false;
    }

    function changeOwnerAddress(address newOwner) public
        onlyOwner {
        if (newOwner == address(0x0)) throw;
        owner = newOwner;
    }

    //Bet Management=====================================================================================================    
    function setBetFee(uint newfee) public
        onlyOwner
    {
        betFee = newfee;
    }

    function setOraclizeCallbackGasLimit(uint newgaslimit) public
        onlyOwner
    {
        oraclizeCallbackGasLimit = newgaslimit;
    }


    function setOraclizeCallbackGasPrice(uint newgasprice) public
		onlyOwner
	{
        oraclize_setCustomGasPrice(newgasprice);
    }     
    

    //Contract Owner withdraws the profit from the contract and sent to the specific account
    function getProfitToAddress(uint profit , address receiver) public
        onlyOwner
    {
        //Cannot withdraw the pending bets
        if(this.balance - profit < getBetWaitEndEther()) throw;

        receiver.transfer(profit);
    }

    //Withdraw all profit to the owner
    function getProfit() public
        onlyOwner
    {
        owner.transfer(this.balance - getBetWaitEndEther());
    }

    //Cancel all pending bets to modify the bet options
    function clearWaitPairBets() public
        onlyOwner
    {
        for( uint i = 0 ;i<waitPairBetIDs.length;i++){
            //Cancel all pending bets of this amount option
            while(waitPairBetIDs[i] != INVALID_BET_ID){
                cancelBetByOwner(waitPairBetIDs[i]);
            }
        }
    }


    function setBetPrices(uint[] newprices) public
        onlyOwner
    {
        //Set new amount option only while no bet pending
        uint i=0;
        for( ;i<waitPairBetIDs.length;i++){
            if(waitPairBetIDs[i] != INVALID_BET_ID)
                throw;
        }

        //Set new bet amount option
        betPrices = newprices;
        //Reset pending bet ID after setting up new bet amount option
        waitPairBetIDs = new uint[](betPrices.length);
        for(i = 0 ;i<waitPairBetIDs.length;i++){
            waitPairBetIDs[i] = INVALID_BET_ID;
        }
    }


    function setSendErrorValue(address player , uint value) public
        onlyOwner
    {
        address2SendErrorValue[player] = value;
        LOG_SET_SEND_ERROR_VALUE(player,value);
    }



    //Place the Bet=====================================================================================================
    
    function() public
        payable 
    {

        bet();
    }

    function bet() public
        payable
        onlyIfNotStopped 
    {
        //Verify the Bet
        uint betpriceid = getBetPriceID(msg.value);
        if(betpriceid != INVALID_BETPRICE_ID){
            //Verified; place the bet
            doBet(betpriceid);
        }else{
            //Refund If invalid bet and not from the contract owner; if it is from the contract owner, do not refund (for funding purpose)
            if (owner != msg.sender) throw;
        }
    }

    function doBet(uint betpriceid)
        private
    {


        //Start matching if there is pending bet that not from you
        uint waitpairbetid = waitPairBetIDs[betpriceid];
        if ((waitpairbetid != INVALID_BET_ID )&&(bets[waitpairbetid].playerAddressA != msg.sender)){

            bets[waitpairbetid].betState = BET_STATE_WAITORACLIZE;
            bets[waitpairbetid].playerAddressB = msg.sender;

            uint oraclizeFee = getOraclizePrice();
            if (oraclizeFee > msg.value ) {
                //Invalid if the service fee of random number is more than the bet amount; cancel the bet without deducting the commission
                cancelBet(waitpairbetid,false,BET_STATE_CANCEL_BY_ORACLIZE_ERROR_FEE);
            }else{
                //Random Number Query
                bytes32 oraclizeQueryID = oraclize_newRandomDSQuery(0, 2, oraclizeCallbackGasLimit); // this function internally generates the correct oraclize_query and returns its queryId
                oraclizeQueryID2BetID[ oraclizeQueryID ] = waitpairbetid;//Save the mapping of Query ID to Bet ID
                bets[waitpairbetid].oraclizeFee = oraclizeFee;

                //Update waitPairBetIDs
                findNextwaitPairBetIDs(betpriceid , waitpairbetid);
            }
        }else {
            //Generate New Bet
            bets.push( Bet(
                msg.value,//Bet Amount
                BET_STATE_WAITPAIR,//Bet Status
                msg.sender,//Address of Player A
                address(0x0),//Address of Player B
                0,//Random Number
                0//Service Fee of Random Number Query
            ));

            //Update waitPairBetIDs    
            if (waitpairbetid == INVALID_BET_ID )
                waitPairBetIDs[betpriceid] = bets.length - 1;
                        
        }

    }



    function getBetPriceID(uint sendvalue)
        private
        returns (uint)
    {
        for(uint i = 0;i < betPrices.length;i++){
            if(betPrices[i]==sendvalue)
                return i;
        }
        return INVALID_BETPRICE_ID;
    }

    function findNextwaitPairBetIDs(uint betpriceid,uint betid)
        private
    {

        for(uint i = betid+1 ; i< bets.length ; i++){
            if( ( bets[i].betPrice == betPrices[betpriceid])&&(bets[i].betState == BET_STATE_WAITPAIR)){
                waitPairBetIDs[betpriceid] = i;
                return;
            }
        }

        //No new pending bet
        waitPairBetIDs[betpriceid] = INVALID_BET_ID;
    }


    //Callback of Oraclize Random Number=========================================================================================
    function __callback(bytes32 _queryId, string _result, bytes _proof) public
    { 
        //Only Oraclize can use callback
        if (msg.sender != oraclize_cbAddress()) throw;

        uint betid = oraclizeQueryID2BetID[_queryId];

        //Verify the Bet
        if(bets[betid].playerAddressA == address(0x0)) throw;
        if(bets[betid].playerAddressB == address(0x0)) throw;
        if(bets[betid].betState != BET_STATE_WAITORACLIZE) throw;

        //Record the log of Oraclize callback data
        LOG_ORACLIZE_CALLBACK(betid,_queryId,_result,_proof);

        if ( oraclize_randomDS_proofVerify__returnCode(_queryId, _result, _proof) != 0) {
            //Invalid random number; cancel the bet and deduct the service fee
            cancelBet(betid,false,BET_STATE_CANCEL_BY_ORACLIZE_ERROR_RANDOM_NUMBER);
        } else {
            // the proof verification has passed
            uint maxRange = 2**(8 * 2); // this is the highest uint we want to get. It should never be greater than 2^(8*N), where N is the number of random bytes we had asked the datasource to return
            uint randomNumber = uint(sha3(_result)) % maxRange; // this is an efficient way to get the uint out in the [0, maxRange] range
            
            //The left 8 digits is A’s result and the right 8 digits is B’s result
            //The bigger number wins; equal means a draw
            uint randomA = randomNumber >> 8;
            uint randomB = randomNumber & 0x00FF;
            
            //Save Bet Result
            bets[betid].numberRolled = randomNumber;

            //Send Prize
            //Prize = Bet Amount - Transaction Fee - Service Fee
            uint winAmount = 2 * bets[betid].betPrice  -  2 * ( bets[betid].betPrice * betFee / 10000 ) - bets[betid].oraclizeFee;
            bool senderror = false;
            if(randomA == randomB){
                //A game ended in a draw will deduct the commission
                cancelBet(betid,true,BET_STATE_CANCEL_BY_RANDOM_NUMBER_A_EUQAL_B);                
            }else{
                address win;
                address lose;
                if(randomA > randomB){
                    win = bets[betid].playerAddressA;
                    lose = bets[betid].playerAddressB;
                }else{
                    win = bets[betid].playerAddressB;
                    lose = bets[betid].playerAddressA;
                }

                //Send coin to the winner
                if(!win.send(winAmount)){
                    //Failed to send coin
                    address2SendErrorValue[win] += winAmount;
                    LOG_SEND_ERROR(betid,win,winAmount);
                }

                //Send coin to the loser
                if(!lose.send(1)){
                    //Failed to send coin
                    address2SendErrorValue[lose] += 1;
                    LOG_SEND_ERROR(betid,lose,1);                    
                }
                
                //Successfully ended
                bets[betid].betState = BET_STATE_END;
            }
        }
    }

    

    //Cancel the Bet=====================================================================================================
    //Player cancels the bet
    function cancelBetByPlayer(uint betid) public
            onlyBetCanCancel(betid)  
    {                
        if (bets[betid].playerAddressA == msg.sender) 
            cancelBetByA(betid);
        else if (bets[betid].playerAddressB == msg.sender)
            cancelBetByB(betid);
        else
            throw;
    }

    function cancelBetByA(uint betid)
        private
    {
        if(bets[betid].playerAddressB != address(0x0)){
            //If there is a Player B, the Player B turns into a Player A and waits for the matching
            bets[betid].playerAddressA = bets[betid].playerAddressB;
            bets[betid].playerAddressB = address(0x0);
            bets[betid].betState = BET_STATE_WAITPAIR;
            //Update waitPairBetIDs    
            uint betpriceid = getBetPriceID(bets[betid].betPrice);
            waitPairBetIDs[betpriceid] = betid;
        }else{
            //if there is no Player B, cancel the bet and update waitPairBetIDs
            bets[betid].betState = BET_STATE_CANCEL_BY_PLAYER;
            refreshWaitPairBetIDsByCancelBet(betid);
        }

        //Refund to Player A
        sendCancelValue(bets[betid].playerAddressA ,betid,false);


    }

    function cancelBetByB(uint betid)
        private
    {
        //If Player B cancels the bet, Player A restarts the waiting of the matching
        bets[betid].playerAddressB = address(0x0);
        bets[betid].betState = BET_STATE_WAITPAIR;
        //Update waitPairBetIDs
        uint betpriceid = getBetPriceID(bets[betid].betPrice);
        waitPairBetIDs[betpriceid] = betid;

        //Refund to Player B
        sendCancelValue(bets[betid].playerAddressB ,betid,false);
    }

    function cancelBetByOwner(uint betid) public
        onlyOwner
    {
        //Cancel the bet and update pending Bet IDs
        cancelBet(betid,false,BET_STATE_CANCEL_BY_OWNER);
    }

    function cancelBet(uint betid,bool fee,uint betstate)
        private
        onlyBetCanCancel(betid)        
    {

        //Cancel Player A
        sendCancelValue(bets[betid].playerAddressA ,betid,fee);

        //Cancel Player B if there is one
        if(bets[betid].playerAddressB != address(0x0)){
            sendCancelValue(bets[betid].playerAddressB ,betid,fee);
        }

        bets[betid].betState = betstate;

        //Update waitPairBetIDs
        refreshWaitPairBetIDsByCancelBet(betid);
    }

    function refreshWaitPairBetIDsByCancelBet(uint betid)
        private
    {
        for( uint i = 0 ;i<waitPairBetIDs.length;i++){
            if(waitPairBetIDs[i] == betid){                
                findNextwaitPairBetIDs(i , betid);
                break;
            }
        }
    }

    function sendCancelValue(address receiver,uint betid,bool fee)
        private
    {
        //Refund = Bet Amount – Transaction Fee – Service Fee
        uint cancelAmount = bets[betid].betPrice  -  bets[betid].betPrice * (fee ? betFee : 0) / 10000 - bets[betid].oraclizeFee / 2;
        if(!receiver.send(cancelAmount)){
            address2SendErrorValue[receiver] += cancelAmount;
            LOG_SEND_ERROR(betid,receiver,cancelAmount);            
        }

    }

    //Auxiliary Function of Contract Query=======================================================================================================
    //Get Bet Info
    function getBets(uint start , uint length) public
        constant
        returns(uint[])
    {   
        //Verify the Parameter
        if(start >= bets.length) throw;      
        if(length == 0) throw;  

        if(start+length > bets.length)
            length = bets.length - start;

        uint[] memory result = new uint[](length*6);
        for (uint i = 0; i < length; i++){
            result[i*6] = bets[start+i].betPrice;
	    result[i*6+1] = bets[start+i].betState;
	    result[i*6+2] = uint(bets[start+i].playerAddressA);
	    result[i*6+3] = uint(bets[start+i].playerAddressB);
	    result[i*6+4] = bets[start+i].numberRolled;
	    result[i*6+5] = bets[start+i].oraclizeFee;
	}
        return result;
    }

    function getBetsLength() public
        constant
        returns(uint)
    {
        return bets.length;
    }

    //Get the amount of pending bets
    function getBetWaitEndEther() public
        constant
        returns(uint result)
    {
        for(uint i=1; i < bets.length ; i++){
            if(  bets[i].betState == BET_STATE_WAITPAIR  ){
                result += bets[i].betPrice;
            }else if ( bets[i].betState == BET_STATE_WAITORACLIZE ){
                result += bets[i].betPrice * 2;                
            }
        }

        return result;

    }    

    //Get Service Fee of Random Number
    function getOraclizePrice() public
        constant
        returns (uint)
    {
        return oraclize_getPrice("random", oraclizeCallbackGasLimit);
    }

}
// </ORACLIZE_API>
function cancelBetByPlayer(uint betid) public
        onlyBetCanCancel(betid)  
{                
    if (bets[betid].playerAddressA == msg.sender) 
        cancelBetByA(betid);
    else if (bets[betid].playerAddressB == msg.sender)
        cancelBetByB(betid);
    else
        throw;
}

//Cancel the Bet=====================================================================================================
//Player cancels the bet