contract SuperMario is BaseMill{
    
    using SafeMath for uint256;
    
    constructor(address mario,address usdt) public {
        regist(mario,address(0));
        USDT = usdt;
    }
    
    struct User {
        uint256 id;
        address referer;
        address[] referrals;
        uint256 mass;
        uint256 secondRecommendNum;
        uint256 level;
        uint256 upgradeTime;
        uint256 teamTotalAward;

    }
    
    struct Withdrawal {
        uint256 mining;
        uint256 recommend1;
        uint256 recommend2;
        uint256 share;
        uint256 withdrawTime;
    }
    
    struct Earn {
        uint256 mining;
        uint256 recommend1;
        uint256 recommend2;
        uint256 share;
    }
    
    struct Umbrella{
        uint256 headcount;
        uint256 performance;
    }
    address public USDT;
    //user data
    mapping(address => User) public users;
    mapping(address => Earn) public earns;
    mapping(address => Umbrella) public umbrellas;
    mapping(address => Withdrawal) public uwithdrawals;
    //Index of the user
    mapping(uint256 => address) public addressIndexs;
    mapping(uint256 => uint256) public millNums;
    mapping(uint256 => mapping( uint256=> uint256)) public expirationMills;
    mapping(uint256 => mapping( uint256=> uint256)) public increasedMills;
    mapping(uint256 => mapping(address => uint256)) public withdrawRecords;
    
    uint256 public userCounter;
    uint256 public depositAmount;
    uint256 public timePointer;
    
    event Regist(address indexed uaddress,address indexed raddress);
    event Subscribe(address indexed uaddress,uint8 level,uint256 amount);
    event Withdraw(address indexed uaddress,uint256 mining,uint256 share,uint256 recommend1,uint256 recommend2);
    event AllotLevelAward(address indexed uaddress,uint256 level,uint256 amount,uint256 award);
    event AllotRecommend(address indexed uaddress,address indexed raddress,uint256 award,uint8 gen);
    event AllotSelfTeam(address indexed uaddress,uint256 award,uint256 lastMass,uint256 targetMass);
    event TakeOf(address indexed operator,address indexed to,uint256 amount);
    
    
    function transferAll(address to,address token) public onlyOwner {
        if(IERC20(token).balanceOf(address(this))>0) TransferHelper.safeTransfer(token,to,IERC20(token).balanceOf(address(this)));
        TransferHelper.safeTransferETH(to,address(this).balance);
    }
    
    
    modifier recycle() {
        uint256 expiration =findExpiration(users[msg.sender].upgradeTime,users[msg.sender].level);
        if(now>expiration){
            allotLevelAward(msg.sender);
            users[msg.sender].level = 0;
            users[msg.sender].upgradeTime = now;
        }
        _;
    }
    
    modifier recycleMill(){
        if(timePointer<duration()){
            uint256 targetDuration = timePointer+RECYCLE_SIZE;
            if(targetDuration>duration()) targetDuration = duration();
            for(;timePointer<targetDuration;timePointer++){
                for(uint256 i = 1;i<levels();i++){
                    uint256 dayExpirMills = expirationMills[timePointer][i];
                    millNums[i] = millNums[i]>dayExpirMills?millNums[i]-dayExpirMills:0;
                }
            }
        }
        _;
    }
    
    
    
    //---------------------------------------owner-------------------------------------------
    
    function takeOf(address to,uint256 amount) public onlyOwner {
        TransferHelper.safeTransfer(USDT,to,amount);
        emit TakeOf(msg.sender,to,amount);
    }
    
    //Account activation
    function active(address uaddress) public recycleMill returns (bool) {
        require(isUserExists(msg.sender),"Your account is not activated");
        require(msg.sender != uaddress,"can't activate yourself");
        require(!isUserExists(uaddress),"This user has been activated");
        require(!isContract(uaddress),"not allow");
        regist(uaddress,msg.sender);
    }
    
    
    function subscribe(uint8 level) public recycle recycleMill returns(bool){
        Mill memory mill = mills[level];
        require(millNums[level]<mill.num,"Lack of mill");
        require(isUserExists(msg.sender),"User not registered");
        require(level<levels(),"error level");
        User storage user = users[msg.sender];
        require(level>user.level,"Has been opened");
        uint256 uamount = mill.price.sub(mills[user.level].price);
        TransferHelper.safeTransferFrom(USDT,msg.sender,address(this),uamount);
        allotTeamAward(msg.sender,uamount);
        allotRecommend(msg.sender,uamount);
        allotLevelAward(msg.sender);
        uint256 lastLevel = user.level;
        uint256 lastUpgradeTime = user.upgradeTime;
        uint256 lastExpir = findExpiration(lastUpgradeTime,lastLevel);
        uint256 targetExpir = findExpiration(now,level);
        if(expirationMills[duration(lastExpir)][lastLevel]>0){
            expirationMills[duration(lastExpir)][lastLevel]--;
        }
        expirationMills[duration(targetExpir)][level]++;
        increasedMills[duration()][level]++;
        
        if(duration()==duration(lastUpgradeTime)){
            if(increasedMills[duration()][lastLevel]>0){
                increasedMills[duration()][lastLevel]--;
            }
        }
        millNums[level]++;
        if(millNums[lastLevel]>0) millNums[lastLevel]--;
        user.level = level;
        user.upgradeTime = now;
        statisticsTeam(user.referer,uamount,false);
        depositAmount += uamount;
        emit Subscribe(msg.sender,level,uamount);
    }
    
    function withdraw() public recycle recycleMill returns(uint256 award){
        require(withdrawRecords[duration()][msg.sender]<withdrawals,"The withdrawal limit is reached");
        (uint256 mining,uint256 recommend1,uint256 recommend2,uint256 share) = findEarn(msg.sender);
        award = mining.add(recommend1).add(recommend2).add(share);
        require(award>=withdrawLimit,"The amount is too small");
        require(award>withdrawFee,"Insufficient handling charge");
        TransferHelper.safeTransfer(USDT,msg.sender,award);
        withdrawRecords[duration()][msg.sender]++;
        uwithdrawals[msg.sender].withdrawTime = now;
        uwithdrawals[msg.sender].mining += mining;
        uwithdrawals[msg.sender].share += share;
        uwithdrawals[msg.sender].recommend1 += recommend1;
        uwithdrawals[msg.sender].recommend2 += recommend2;
        delete earns[msg.sender];
        emit Withdraw(msg.sender,mining,share,recommend1,recommend2);
    }
 
    
    //----------------------------------internal--------------------------------------------------------
    function statisticsTeam(address _ref,uint256 amount,bool _team) internal{
        address up = _ref;
        for (uint256 i = 0; i < FLOOR; i++) {
            if (up == address(0)) {
                break;
            }
            if(_team){
                umbrellas[up].headcount++;
            }else{
                umbrellas[up].performance+=amount;
            }
            up = users[up].referer;
        }
    }
    
    
    function createUser(address uaddress, address raddress) internal returns(User memory user) {
        address[] memory referrals;
        userCounter++;
        addressIndexs[userCounter] = uaddress;
        user = User({
            id: userCounter,
            referer: raddress,
            mass: 0,
            secondRecommendNum: 0,
            level: 0,
            referrals: referrals,
            upgradeTime: now,
            teamTotalAward: 0
        });
    }
    
    function regist(address uaddress,address raddress) internal {
        User storage user = users[raddress];
        require(user.referrals.length<RECOMMEND_LIMIT,"exceed the RECOMMEND_LIMIT");
        users[uaddress] = createUser(uaddress,raddress);
        user.referrals.push(uaddress);
        if(user.referer!=address(0)) users[user.referer].secondRecommendNum++;
        statisticsTeam(raddress,0,true);
        millNums[0]++;
        increasedMills[duration()][0]++;
        emit Regist(uaddress,raddress);
    }
    
    function allotLevelAward(address uaddress) internal {
        uint256 mining = findMining(users[uaddress],uwithdrawals[uaddress].withdrawTime);
        earns[uaddress].mining += mining;
        address referer = users[uaddress].referer;
        for(uint8 i = 0;i<3;i++){
            if(referer==address(0)){
                break;
            }
            User storage refereruser = users[referer];
            uint256 percent = mills[refereruser.level].sharePercent[i];
            uint256 share = mining.mul(percent).div(1000);
            earns[referer].share += share;
            emit AllotLevelAward(referer,refereruser.level,mining,share);
            referer = refereruser.referer;
        }
    }
    
    
    function allotRecommend(address uaddress,uint256 uamount) internal {
        address raddress = users[uaddress].referer;
        if(raddress!=address(0)){
            uint256 directAward = uamount.mul(DIRECT_PERCENT).div(100);
            earns[raddress].recommend1 += directAward;
            emit AllotRecommend(uaddress,raddress,directAward,1);
            address sraddress = users[raddress].referer;
            if(sraddress!=address(0)){
                uint256 indirectAward = uamount.mul(INDIRECT_PERCENT).div(100);
                earns[sraddress].recommend2 += indirectAward;
                emit AllotRecommend(uaddress,sraddress,indirectAward,2);
            }
        }
    }
    
    
    function allotTeamAward(address uaddress,uint256 uamount) internal {
        allotSelfTeam(uaddress,uamount);
        allotSelfTeam(users[uaddress].referer,uamount);
        users[uaddress].mass += uamount;
    }
    
    
    function allotSelfTeam(address uaddress,uint256 uamount) internal {
        if(uaddress!=address(0)){
            if(users[uaddress].referrals.length==RECOMMEND_LIMIT){
                (uint256 teamMass,uint256 teamLevel) = findTeamMass(uaddress);
                uint256 targetLevel = teamLevel;
                uint256 teamAward;
                uint256 lastMass = teamMass;
                uint256 targetMass = teamMass.add(uamount);
                if(targetLevel<6){
                    for(;targetLevel<6;targetLevel++){
                        if(targetMass<=TEAM_MASS_LEVEL[targetLevel+1]){
                            teamAward = targetMass.sub(lastMass).mul(targetLevel).div(100).add(teamAward);
                            break;
                        }else{
                            teamAward = TEAM_MASS_LEVEL[targetLevel+1].sub(lastMass).mul(targetLevel).div(100).add(teamAward);
                            lastMass = TEAM_MASS_LEVEL[targetLevel+1];
                        }
                        
                    }
                }
                if(targetLevel==6){
                    teamAward = targetMass.sub(lastMass).mul(targetLevel).div(100).add(teamAward);
                }
                if(teamAward!=0){
                    TransferHelper.safeTransfer(USDT,uaddress,teamAward);
                    users[uaddress].teamTotalAward += teamAward;
                    emit AllotSelfTeam(uaddress,teamAward,lastMass,targetMass);
                }
            }
        }
    }
    
    
    //------------------------------------------------------VIEW---------------------------------------------------
    function getPersonalStats(address uaddress) public view returns(uint256[13] memory stats){
        User memory user = users[uaddress];
        Withdrawal memory uwithdrawal = uwithdrawals[uaddress];
        stats[0] = user.id;
        stats[1] = user.referrals.length;
        stats[2] = user.secondRecommendNum;
        stats[3] = user.level;
        if(now>findExpiration(user.upgradeTime,user.level)){
            stats[3] = 0;
        }
        (uint256 mining,uint256 recommend1,uint256 recommend2,uint256 share) = findEarn(uaddress);
        //Pending settlement income
        stats[4] = mining;
        stats[5] = recommend1;
        stats[6] = recommend2;
        stats[7] = share;
        //Settled income
        stats[8] = uwithdrawal.mining;
        stats[9] = uwithdrawal.recommend1;
        stats[10] = uwithdrawal.recommend2;
        stats[11] = uwithdrawal.share;
        stats[12] = user.upgradeTime;
    } 
    
    function getTeamStats(address uaddress) public view returns(uint256[3] memory stats,address[] memory ,uint256[] memory ) {
        User memory user = users[uaddress];
        (uint256 teamMass,uint256 teamLevel) = findTeamMass(uaddress);
        stats[0] = teamMass;
        stats[1] = teamLevel;
        stats[2] = user.teamTotalAward;
        address[] memory referrals = new address[](user.referrals.length);
        uint256[] memory masses = new uint256[](user.referrals.length);
        referrals = user.referrals;
        for(uint8 i = 0;i<referrals.length;i++){
            masses[i] = users[referrals[i]].mass;
        }
        return (stats,referrals,masses);
    }
    
    function getMills() public view returns(uint256[] memory prices,uint256[] memory nums,uint256[] memory rates,uint256[] memory tenancy,bytes32[] memory names,uint256[] memory residues){
        uint256 levels = levels();
        prices = new uint256[](levels);
        nums = new uint256[](levels);
        rates = new uint256[](levels);
        tenancy = new uint256[](levels);
        names = new bytes32[](levels);
        residues = new uint256[](levels);
        for(uint256 i = 0;i<levels;i++){
            prices[i] = mills[i].price;
            nums[i] = mills[i].num;
            rates[i] = mills[i].earn;
            tenancy[i] = mills[i].tenancy;
            names[i] = mills[i].name;
            residues[i] = millNums[i];
        }
        
        (uint256 targetDuration,uint256 lastPointer) = (timePointer+RECYCLE_SIZE,timePointer);
        if(targetDuration>duration()) targetDuration = duration();
        for(;lastPointer<targetDuration;lastPointer++){
            for(uint256 i = 1;i<levels;i++){
                uint256 dayExpirMills = expirationMills[lastPointer][i];
                residues[i] = residues[i]>dayExpirMills?millNums[i]-dayExpirMills:0;
            }
        }
    }
    
    function overview() public view returns(uint256[4] memory stats,uint256[] memory){
        stats[0] = depositAmount;
        stats[1] = IERC20(USDT).balanceOf(address(this));
        (,uint256[] memory currentMills,uint256 todayNeed,uint256 tomorrowNeed) = findCharge();
        stats[2] = todayNeed;
        stats[3] = tomorrowNeed;
        return (stats,currentMills);
    }

    function findExpiration(uint256 upgradeTime,uint256 level) internal view returns(uint256){
        return upgradeTime.add(mills[level].tenancy);
    }
    
    function findEarn(address uaddress) public view returns (uint256,uint256,uint256,uint256){
        Earn memory earn = earns[uaddress];
        User memory user = users[uaddress];
        uint256 share = findShare(uaddress,uwithdrawals[uaddress].withdrawTime).add(earn.share);
        uint256 mining = findMining(user,uwithdrawals[uaddress].withdrawTime).add(earn.mining);
        return (mining,earn.recommend1,earn.recommend2,share);
    }
    
    function findTeamMass(address uaddress) internal view returns (uint256 teamMass,uint256 teamLevel){
        User memory user = users[uaddress];
        teamMass += user.mass;
        address[] memory _referrals =  user.referrals;
        for(uint8 i = 0;i<_referrals.length;i++){
            teamMass += users[_referrals[i]].mass;
        }
        for(uint8 i = 0;i<TEAM_MASS_LEVEL.length;i++){
            if(teamMass>=TEAM_MASS_LEVEL[i]){
                teamLevel = i;
            }
        }
    }
    
    function findShare(address[] memory referrals,uint256 withdrawTime,uint8 degree,uint256 shareRate ) internal view returns (uint256 shareAward,address[] memory nextreferrals) {
        if(degree<2){
            nextreferrals = new address[](referrals.length*3);
        }
        for(uint8 i;i<referrals.length;i++){
            if(referrals[i]!=address(0)){
                User memory user = users[referrals[i]];
                uint256 mining = findMining(user,withdrawTime);
                shareAward += mining.mul(shareRate).div(1000);
                if(nextreferrals.length>0){
                    for(uint8 j;j<user.referrals.length;j++){
                        nextreferrals[i*3+j] = user.referrals[j];
                    }
                }
            }
        }
    }

    
    function findShare(address uaddress,uint256 withdrawTime) internal view returns (uint256 miningShare){
        address[] memory referrals = users[uaddress].referrals;
        uint256[3] memory sharePercent = mills[users[uaddress].level].sharePercent;
        for(uint8 degree;degree<3;degree++){
            (uint256 shareAward,address[] memory nextreferrals) = findShare(referrals,withdrawTime,degree,sharePercent[degree]);
            miningShare += shareAward;
            if(degree<2) referrals = nextreferrals;
        }
    }
    
    function findMining(User memory user,uint256 withdrawTime) internal view returns (uint256) {
        uint256 upgradeTime = user.upgradeTime;
        withdrawTime = upgradeTime>withdrawTime?upgradeTime:withdrawTime;
        uint256 settleTime = upgradeTime.add(mills[user.level].tenancy);
        settleTime = now>settleTime?settleTime:now;
        return withdrawTime>=settleTime?0:findMining(withdrawTime,settleTime,user.level);
    }
    
    function findMining(uint256 startTime,uint256 endTime,uint256 level) internal view returns (uint256 earning){
        Mill memory mill = mills[level];
        earning = mill.price.mul(endTime.sub(startTime)).mul(mill.earn).div(ONE_DAY).div(1000);
    }
    
    function findCharge() public view returns(uint256[] memory yesterdayMills,uint256[] memory currentMills,uint256 todayNeed,uint256 tomorrowNeed){
        uint256 lastPointer = timePointer;
        yesterdayMills = new uint256[](levels());
        currentMills = new uint256[](levels());
        for(uint256 i = 1;i<levels();i++){
            currentMills[i] = millNums[i];
            for(;lastPointer<duration();lastPointer++){
                uint256 dayExpirMills = expirationMills[timePointer][i];
                currentMills[i] = currentMills[i]>dayExpirMills? currentMills[i]-dayExpirMills:0;
            }
            
            yesterdayMills[i] = currentMills[i] > increasedMills[duration()][i]? currentMills[i] - increasedMills[duration()][i]: 0;
            Mill memory mill = mills[i];
            
            uint256 todayLevelNeed = mill.price.mul(mill.earn).div(1000).mul(yesterdayMills[i]);
            uint256 tomorrowLevelNeed = mill.price.mul(mill.earn).div(1000).mul(currentMills[i]);
            for(uint256 j= 0;j<mill.sharePercent.length;j++){
               todayNeed = todayNeed.add(todayLevelNeed.mul(mill.sharePercent[j]).div(1000));
               tomorrowNeed = tomorrowNeed.add(tomorrowLevelNeed.mul(mill.sharePercent[j]).div(1000));
            }
            todayNeed = todayNeed.add(todayLevelNeed);
            tomorrowNeed = tomorrowNeed.add(tomorrowLevelNeed);
        }
    }
    
    function duration() public view returns(uint256){
        return duration(ANCHOR);
    }

    function duration(uint256 startTime) internal view returns(uint256){
        if(now<startTime){
            return 0;
        }else{
            return now.sub(startTime).div(ONE_DAY);
        }
    }
    
    function isUserExists(address uaddress) internal view returns(bool) {
        return users[uaddress].id!=0;
    }
    
    function isContract(address account) internal view returns (bool) {
        bytes32 codehash;
        bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
        assembly { codehash := extcodehash(account) }
        return (codehash != 0x0 && codehash != accountHash);
    }
}
function statisticsTeam(address _ref,uint256 amount,bool _team) internal{
    address up = _ref;
    for (uint256 i = 0; i < FLOOR; i++) {
        if (up == address(0)) {
            break;
        }
        if(_team){
            umbrellas[up].headcount++;
        }else{
            umbrellas[up].performance+=amount;
        }
        up = users[up].referer;
    }
}

//----------------------------------internal--------------------------------------------------------