contract ClashHash is Ownable {
    using SafeMath for uint256;
    using RLPReader for bytes;
    using RLPReader for RLPReader.RLPItem;

    struct Round {
        BetStorage records;
        uint256 betsCount;
        uint256 totalReward;
        address winner;
    }

    uint256 public minBet = 0.001 ether;
    uint256 constant public MIN_BLOCKS_BEFORE_ROUND = 10;
    uint256 constant public MIN_BLOCKS_AFTER_ROUND = 10;
    uint256 constant public MAX_BLOCKS_AFTER_ROUND = 256;

    uint256 public adminFee = 5;
    uint256 public referrerFee = 50;

    mapping(address => address) public referrers;
    mapping(uint256 => Round) public rounds;
    BlockHash public _blockStorage;

    //

    event RoundCreated(uint256 indexed blockNumber, address contractAddress);
    event RoundBetAdded(uint256 indexed blockNumber, address indexed user, address indexed option, uint256 value);
    event RoundFinalized(uint256 indexed blockNumber, address indexed option);
    event RewardClaimed(uint256 indexed blockNumber, address indexed user, address indexed winner, uint256 reward);

    event NewReferral(address indexed user, address indexed referrer);
    event ReferralReward(address indexed user, address indexed referrer, uint256 value);

    event AdminFeeUpdate(uint256 oldFee, uint256 newFee);
    event ReferrerFeeUpdate(uint256 oldFee, uint256 newFee);
    event MinBetUpdate(uint256 oldMinBet, uint256 newMinBet);

    //

    constructor (BlockHash blockStorage) public {
        _blockStorage = blockStorage;
    }

    //

    function setReferrerFee(uint256 newFee) public onlyOwner {
        emit ReferrerFeeUpdate(referrerFee, newFee);
        referrerFee = newFee;
    }

    function setAdminFee(uint256 newFee) public onlyOwner {
        emit AdminFeeUpdate(adminFee, newFee);
        adminFee = newFee;
    }

    function setMinBet(uint256 newMinBet) public onlyOwner {
        emit MinBetUpdate(minBet, newMinBet);
        minBet = newMinBet;
    }

    /**
     * @param referrer Who has invited the user.
     */
    function addReferral(address referrer) public {
        require(referrer != address(0), "Invalid referrer address");
        require(referrer != msg.sender, "Different addresses required");
        require(referrers[msg.sender] == address(0), "User has referrer already");

        referrers[msg.sender] = referrer;
        emit NewReferral(msg.sender, referrer);
    }

    function addBet(uint256 blockNumber, address option) public payable {
        require(msg.value >= minBet, "Bet amount is too low");
        require(block.number <= blockNumber.sub(MIN_BLOCKS_BEFORE_ROUND), "It's too late");

        Round storage round = rounds[blockNumber];
        if (round.records == address(0)) {
            round.records = new BetStorage();
            emit RoundCreated(blockNumber, round.records);
        }

        round.betsCount += 1;
        round.totalReward = round.totalReward.add(msg.value);
        round.records.addBet.value(msg.value)(msg.sender, option);

        emit RoundBetAdded(
            blockNumber,
            msg.sender,
            option,
            msg.value
        );
    }

    function addBetWithReferrer(
        uint256 blockNumber,
        address option,
        address referrer
    )
        public
        payable
    {
        addReferral(referrer);
        addBet(blockNumber, option);
    }

    function claimRewardWithBlockData(uint256 blockNumber, bytes blockData) public {
        if (blockData.length > 0 && rounds[blockNumber].winner == address(0)) {
            addBlockData(blockNumber, blockData);
        }

        claimRewardForUser(blockNumber, msg.sender);
    }

    function claimRewardForUser(uint256 blockNumber, address user) public {
        Round storage round = rounds[blockNumber];
        require(round.winner != address(0), "Round not yet finished");
        require(address(round.records).balance > 0, "Round prizes are already distributed");

        (uint256 userReward, uint256 referrerReward,) = round.records.rewards(
            user,
            referrers[user],
            referrerFee,
            adminFee
        );
        round.records.claimReward(user, owner(), adminFee, referrers[user], referrerFee);

        emit RewardClaimed(blockNumber, user, round.winner, userReward);

        if (referrerReward > 0) {
            emit ReferralReward(user, referrers[user], referrerReward);
        }
    }

    function addBlockData(uint256 blockNumber, bytes blockData) public {
        Round storage round = rounds[blockNumber];

        require(round.winner == address(0), "Winner was already submitted");
        require(block.number <= blockNumber.add(MAX_BLOCKS_AFTER_ROUND), "It's too late, 256 blocks gone");
        require(block.number >= blockNumber.add(MIN_BLOCKS_AFTER_ROUND), "Wait at least 10 blocks");

        address blockBeneficiary = _readBlockBeneficiary(blockNumber, blockData);

        round.winner = blockBeneficiary;
        round.records.finalize(blockBeneficiary, owner());
        emit RoundFinalized(blockNumber, blockBeneficiary);
    }

    function _readBlockBeneficiary(
        uint256 blockNumber,
        bytes blockData
    )
        internal
        view
        returns(address)
    {
        require(keccak256(blockData) == _blockStorage.blockhashes(blockNumber), "Block data isn't valid");
        RLPReader.RLPItem[] memory items = blockData.toRlpItem().toList();
        return items[2].toAddress();
    }
}
/**
 * @title ClashHash
 * This product is protected under license.  Any unauthorized copy, modification, or use without
 * express written consent from the creators is prohibited.
 */
function setReferrerFee(uint256 newFee) public onlyOwner {
    emit ReferrerFeeUpdate(referrerFee, newFee);
    referrerFee = newFee;
}

//