abstract contract LUCKYRNG is Ownable {
    /**
    * Tiers
    * 0 - Platinum
    * 1 - Gold
    * 2 - Silver
    * 3 - Bronze
     */
    using SafeMath for uint256;
    using EnumerableSet for EnumerableSet.AddressSet;

    address payable public platinumWinner;
    address payable public goldWinner;
    address payable public silverWinner;
    address payable public bronzeWinner;
    
    EnumerableSet.AddressSet platinumSet;
    EnumerableSet.AddressSet goldSet;
    EnumerableSet.AddressSet silverSet;
    EnumerableSet.AddressSet bronzeSet;

    EnumerableSet.AddressSet[] gamblingWallets;

    uint256 public platinumMinWeight = 2 * 10 ** 5;
    uint256 public goldMinWeight = 10 ** 5;
    uint256 public silverMinWeight = 5 * 10 ** 4;

    mapping(address => uint256) public gamblingWeights;
    mapping(address => uint256) public ethAmounts;
    mapping(address => bool) public excludedFromGambling;
    mapping(address => bool) public isEthAmountNegative;

    IUniswapV2Router02 public uniswapV2Router;

    uint256 public feeMin = 0.1 * 10 ** 18;
    uint256 public feeMax = 0.3 * 10 ** 18;
    uint256 internal lastTotalFee;

    uint256 public ethWeight = 10 ** 10;

    mapping(address => bool) isGoverner;
    address[] governers;

    event newWinnersSelected(uint256 timestamp, address platinumWinner, address goldWinner, address silverWinner, address bronzeWinner, 
        uint256 platinumEthAmount, uint256 goldEthAmount, uint256 silverEthAmount, uint256 bronzeEthAmount,
        uint256 platinumGShibaAmount, uint256 goldGShibaAmount, uint256 silverGShibaAmount, uint256 bronzeGShibaAmount,
        uint256 lastTotalFee);

    modifier onlyGoverner() {
        require(isGoverner[_msgSender()], "Not governer");
        _;
    }

    constructor(address payable _initialWinner) public
    {
        platinumWinner = _initialWinner;
        goldWinner = _initialWinner;
        silverWinner = _initialWinner;
        bronzeWinner = _initialWinner;
        
        platinumSet.add(_initialWinner);
        goldSet.add(_initialWinner);
        silverSet.add(_initialWinner);
        bronzeSet.add(_initialWinner);

        gamblingWallets.push(platinumSet);
        gamblingWallets.push(goldSet);
        gamblingWallets.push(silverSet);
        gamblingWallets.push(bronzeSet);

        uniswapV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); // UniswapV2 for Ethereum network

        isGoverner[owner()] = true;
        governers.push(owner());
    }

    function checkTierFromWeight(uint256 weight)
        public
        view
        returns(uint256)
    {
        if (weight > platinumMinWeight) {
            return 0;
        }
        if (weight > goldMinWeight) {
            return 1;
        }
        if (weight > silverMinWeight) {
            return 2;
        }
        return 3;
    }

    function calcWeight(uint256 ethAmount, uint256 gShibaAmount) public view returns(uint256) {
        return ethAmount.div(10 ** 13) + gShibaAmount.div(10 ** 13).div(ethWeight);
    }

    function addNewWallet(address _account, uint256 tier) internal {
        gamblingWallets[tier].add(_account);
    }

    function removeWallet(address _account, uint256 tier) internal {
        gamblingWallets[tier].remove(_account);
    }

    function addWalletToGamblingList(address _account, uint256 _amount) internal {
        if (!excludedFromGambling[_account]) {
            address[] memory path = new address[](2);
            path[0] = uniswapV2Router.WETH();
            path[1] = address(this);
            
            uint256 ethAmount = uniswapV2Router.getAmountsIn(_amount, path)[0];
            
            uint256 oldWeight = gamblingWeights[_account];

            if (isEthAmountNegative[_account]) {
                if (ethAmount > ethAmounts[_account]) {
                    ethAmounts[_account] = ethAmount - ethAmounts[_account];
                    isEthAmountNegative[_account] = false;

                    gamblingWeights[_account] = calcWeight(ethAmounts[_account], IERC20(address(this)).balanceOf(_account) + _amount);
                } else {
                    ethAmounts[_account] = ethAmounts[_account] - ethAmount;
                    gamblingWeights[_account] = 0;
                }
            } else {
                ethAmounts[_account] += ethAmount;

                gamblingWeights[_account] = calcWeight(ethAmounts[_account], IERC20(address(this)).balanceOf(_account) + _amount);
            }

            if (!isEthAmountNegative[_account]) {
                uint256 oldTier = checkTierFromWeight(oldWeight);
                uint256 newTier = checkTierFromWeight(gamblingWeights[_account]);

                if (oldTier != newTier) {
                    removeWallet(_account, oldTier);
                }

                addNewWallet(_account, newTier);
            }
        }
    }

    function removeWalletFromGamblingList(address _account, uint256 _amount) internal {
        if (!excludedFromGambling[_account]) {
            address[] memory path = new address[](2);
            path[0] = uniswapV2Router.WETH();
            path[1] = address(this);
            
            uint256 ethAmount = uniswapV2Router.getAmountsIn(_amount, path)[0];

            uint256 oldWeight = gamblingWeights[_account];

            if (isEthAmountNegative[_account]) {
                ethAmounts[_account] += ethAmount;
                gamblingWeights[_account] = 0;
            } else if (ethAmounts[_account] >= ethAmount) {
                ethAmounts[_account] -= ethAmount;
                gamblingWeights[_account] = calcWeight(ethAmounts[_account], IERC20(address(this)).balanceOf(_account));
            } else {
                ethAmounts[_account] = ethAmount - ethAmounts[_account];
                isEthAmountNegative[_account] = true;
                gamblingWeights[_account] = 0;
            }

            uint256 oldTier = checkTierFromWeight(oldWeight);
            removeWallet(_account, oldTier);
        }
    }

    function rand(uint256 max)
        private
        view
        returns(uint256)
    {
        if (max == 1) {
            return 0;
        }

        uint256 seed = uint256(keccak256(abi.encodePacked(
            block.timestamp + block.difficulty +
            ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)) +
            block.gaslimit +
            ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)) +
            block.number
        )));

        return (seed - ((seed / (max - 1)) * (max - 1))) + 1;
    }

    function checkAndChangeGamblingWinner() internal {
        uint256 randFee = rand(feeMax - feeMin) + feeMin;

        if (lastTotalFee >= randFee) {
            uint256 platinumWinnerIndex = rand(gamblingWallets[0].length());
            uint256 goldWinnerIndex = rand(gamblingWallets[1].length());
            uint256 silverWinnerIndex = rand(gamblingWallets[2].length());
            uint256 bronzeWinnerIndex = rand(gamblingWallets[3].length());

            platinumWinner = payable(gamblingWallets[0].at(platinumWinnerIndex));
            goldWinner = payable(gamblingWallets[1].at(goldWinnerIndex));
            silverWinner = payable(gamblingWallets[2].at(silverWinnerIndex));
            bronzeWinner = payable(gamblingWallets[3].at(bronzeWinnerIndex));

            emit newWinnersSelected(
                block.timestamp, platinumWinner, goldWinner, silverWinner, bronzeWinner, 
                ethAmounts[platinumWinner], ethAmounts[goldWinner], ethAmounts[silverWinner], ethAmounts[bronzeWinner],
                IERC20(address(this)).balanceOf(platinumWinner), IERC20(address(this)).balanceOf(goldWinner), IERC20(address(this)).balanceOf(silverWinner), IERC20(address(this)).balanceOf(bronzeWinner),
                lastTotalFee
            );
        }
    }

    /**
    * Mutations
     */

    function setEthWeight(uint256 _ethWeight) external onlyGoverner {
        ethWeight = _ethWeight;
    }

    function setTierWeights(uint256 _platinumMin, uint256 _goldMin, uint256 _silverMin) external onlyGoverner {
        require(_platinumMin > _goldMin && _goldMin > _silverMin, "Weights should be descending order");

        platinumMinWeight = _platinumMin;
        goldMinWeight = _goldMin;
        silverMinWeight = _silverMin;
    }

    function setFeeMinMax(uint256 _feeMin, uint256 _feeMax) external onlyGoverner {
        require(_feeMin < _feeMax, "feeMin should be smaller than feeMax");

        feeMin = _feeMin;
        feeMax = _feeMax;
    }

    function addGoverner(address _governer) public onlyGoverner {
        if (!isGoverner[_governer]) {
            isGoverner[_governer] = true;
            governers.push(_governer);
        }
    }

    function removeGoverner(address _governer) external onlyGoverner {
        if (isGoverner[_governer]) {
            isGoverner[_governer] = false;

            for (uint i = 0; i < governers.length; i ++) {
                if (governers[i] == _governer) {
                    governers[i] = governers[governers.length - 1];
                    governers.pop();
                    break;
                }
            }
        }
    }

    function addV1Users(address[] memory _users) external onlyOwner {
        uint256 len = _users.length;

        for (uint i = 0; i < len; i ++) {
            address user = _users[i];

            uint256 gShibabalance = IERC20(address(this)).balanceOf(user);
            uint256 ethAmount = gShibabalance.div(10 ** 10);

            uint256 weight = calcWeight(ethAmount, gShibabalance);
            uint256 tier = checkTierFromWeight(weight);

            gamblingWallets[tier].add(user);
            ethAmounts[user] = ethAmount;
            gamblingWeights[user] = weight;
        }
    }
}
// 
function setEthWeight(uint256 _ethWeight) external onlyGoverner {
    ethWeight = _ethWeight;
}

/**
 * Mutations
 */