contract SwappStaking is ReentrancyGuard {
    using SafeMath for uint256;
    using SafeMath for uint128;

    uint128 constant private BASE_MULTIPLIER = uint128(1 * 10 ** 18);
    uint256 constant private CALC_MULTIPLIER = 1000000;

    // timestamp for the epoch 1
    // everything before that is considered epoch 0 which won't have a reward but allows for the initial stake
    uint256 public epoch1Start;

    // duration of each epoch
    uint256 public epochDuration;

    // holds the current balance of the user for each token
    mapping(address => mapping(address => uint256)) private balances;
    
    address constant private swapp = 0x8CB924583681cbFE487A62140a994A49F833c244;
	address constant private minter = 0xBC1f9993ea5eE2C77909bf43d7a960bB8dA8C9B9;
    address constant private staking = 0x245a551ee0F55005e510B239c917fA34b41B3461;
	address public farm;
    
    struct Pool {
        uint256 size;
        bool set;
    }

    // for each token, we store the total pool size
    mapping(address => mapping(uint256 => Pool)) private poolSize;

    // a checkpoint of the valid balance of a user for an epoch
    struct Checkpoint {
        uint128 epochId;
        uint128 multiplier;
        uint256 startBalance;
        uint256 newDeposits;
    }

    // balanceCheckpoints[user][token][]
    mapping(address => mapping(address => Checkpoint[])) private balanceCheckpoints;

    mapping(address => uint128) private lastWithdrawEpochId;


    //referrals
    uint256 public firstReferrerRewardPercentage;
    uint256 public secondReferrerRewardPercentage;

    struct Referrer {
        // uint256 totalReward;
        uint256 referralsCount;
        mapping(uint256 => address) referrals;
    }

    // staker to referrer
    mapping(address => address) public referrals;
    // referrer data
    mapping(address => Referrer) public referrers;

	uint256 constant public NR_OF_EPOCHS = 60;
	
	struct Topup {
	    uint256 totalTopups;
        mapping(uint256 => uint128) epochs;
        mapping(uint256 => uint256) amounts;
    }
	
	struct Stake {
		uint128 startEpoch;
		uint256 startTimestamp;
		uint128 endEpoch;
		uint128 duration;
		bool active;
	}
	
	mapping(address => Stake) public stakes;
    mapping(address => Topup) public topups;
	uint256 public stakedSwapp;
	
	modifier onlyOwner() {
        require(msg.sender == _owner, "Only owner can perfrom this action");
        _;
    }
    
    modifier whenNotPaused() {
        require(!paused(), "Staking: contract is paused");
        _;
    }

    event Deposit(address indexed user, address indexed tokenAddress, uint256 amount, uint256 endEpoch);
    event Withdraw(address indexed user, address indexed tokenAddress, uint256 amount, uint256 penalty);
    event ManualEpochInit(address indexed caller, uint128 indexed epochId, address[] tokens);
    event EmergencyWithdraw(address indexed user, address indexed tokenAddress, uint256 amount);
    event RegisteredReferer(address indexed referral, address indexed referrer, uint256 amount);
    event Penalty(address indexed staker, uint128 indexed epochId, uint256 amount);
    event PrepareMigration(address indexed staker, uint256 balance);

    address public _owner;
    address private _migration;
    bool private _paused = false;
    bool emergencyWithdrawAllowed = false;

    constructor () {
        epoch1Start = 1626652800;
        epochDuration = 2419200; // 28 days

        _owner = msg.sender;

        firstReferrerRewardPercentage = 1000;
        secondReferrerRewardPercentage = 500;
    }

    /*
     * Stores `amount` of `tokenAddress` tokens for the `user` into the vault
     */
    function deposit(address tokenAddress, uint256 amount, address referrer, uint128 endEpoch) public nonReentrant whenNotPaused {
        require(amount > 0, "Staking: Amount must be > 0");
		require(tokenAddress == swapp, "This pool accepts only Swapp token");
        require(IERC20(tokenAddress).allowance(msg.sender, address(this)) >= amount, "Staking: Token allowance too small");
		
		uint128 currentEpoch = getCurrentEpoch();
        require(endEpoch > currentEpoch && endEpoch <= NR_OF_EPOCHS.add(1), "Staking: not acceptable end of stake");
        
        IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount);
        TokenInterface(tokenAddress).burn(amount);
        stakedSwapp = stakedSwapp.add(amount);

        if (referrer != address(0)) {
            processReferrals(referrer, amount);
        }

        balances[msg.sender][tokenAddress] = balances[msg.sender][tokenAddress].add(amount);
        
        handleStakeDuration(endEpoch, amount);

        // epoch logic
        uint128 currentMultiplier = currentEpochMultiplier();

        if (!epochIsInitialized(tokenAddress, currentEpoch)) {
            address[] memory tokens = new address[](1);
            tokens[0] = tokenAddress;
            manualEpochInit(tokens, currentEpoch);
        }

        // update the next epoch pool size
        Pool storage pNextEpoch = poolSize[tokenAddress][currentEpoch + 1];
        
        pNextEpoch.size = stakedSwapp;
        pNextEpoch.set = true;

        Checkpoint[] storage checkpoints = balanceCheckpoints[msg.sender][tokenAddress];

        uint256 balanceBefore = getEpochUserBalance(msg.sender, tokenAddress, currentEpoch);

        // if there's no checkpoint yet, it means the user didn't have any activity
        // we want to store checkpoints both for the current epoch and next epoch because
        // if a user does a withdraw, the current epoch can also be modified and
        // we don't want to insert another checkpoint in the middle of the array as that could be expensive
        if (checkpoints.length == 0) {
            checkpoints.push(Checkpoint(currentEpoch, currentMultiplier, 0, amount));

            // next epoch => multiplier is 1, epoch deposits is 0
            checkpoints.push(Checkpoint(currentEpoch + 1, BASE_MULTIPLIER, amount, 0));
        } else {
            uint256 last = checkpoints.length - 1;

            // the last action happened in an older epoch (e.g. a deposit in epoch 3, current epoch is >=5)
            if (checkpoints[last].epochId < currentEpoch) {
                uint128 multiplier = computeNewMultiplier(
                    getCheckpointBalance(checkpoints[last]),
                    BASE_MULTIPLIER,
                    amount,
                    currentMultiplier
                );
                checkpoints.push(Checkpoint(currentEpoch, multiplier, getCheckpointBalance(checkpoints[last]), amount));
                checkpoints.push(Checkpoint(currentEpoch + 1, BASE_MULTIPLIER, balances[msg.sender][tokenAddress], 0));
            }
            // the last action happened in the previous epoch
            else if (checkpoints[last].epochId == currentEpoch) {
                checkpoints[last].multiplier = computeNewMultiplier(
                    getCheckpointBalance(checkpoints[last]),
                    checkpoints[last].multiplier,
                    amount,
                    currentMultiplier
                );
                checkpoints[last].newDeposits = checkpoints[last].newDeposits.add(amount);

                checkpoints.push(Checkpoint(currentEpoch + 1, BASE_MULTIPLIER, balances[msg.sender][tokenAddress], 0));
            }
            // the last action happened in the current epoch
            else {
                if (last >= 1 && checkpoints[last - 1].epochId == currentEpoch) {
                    checkpoints[last - 1].multiplier = computeNewMultiplier(
                        getCheckpointBalance(checkpoints[last - 1]),
                        checkpoints[last - 1].multiplier,
                        amount,
                        currentMultiplier
                    );
                    checkpoints[last - 1].newDeposits = checkpoints[last - 1].newDeposits.add(amount);
                }

                checkpoints[last].startBalance = balances[msg.sender][tokenAddress];
            }
        }

        uint256 balanceAfter = getEpochUserBalance(msg.sender, tokenAddress, currentEpoch);

        poolSize[tokenAddress][currentEpoch].size = poolSize[tokenAddress][currentEpoch].size.add(balanceAfter.sub(balanceBefore));

        emit Deposit(msg.sender, tokenAddress, amount, endEpoch);
    }
    
    function handleStakeDuration(uint128 endEpoch, uint256 amount) internal {
        Stake storage stake = stakes[msg.sender];
        uint128 currentEpoch = getCurrentEpoch();
        
		if (!stake.active) {
			stake.startEpoch = currentEpoch;
			stake.startTimestamp = block.timestamp;
			stake.endEpoch = endEpoch;
			stake.duration = endEpoch - currentEpoch;
			stake.active = true;
		}

        Topup storage topupData = topups[msg.sender];

        uint256 currentTopup = topupData.totalTopups + 1;
        topupData.totalTopups = currentTopup;
        topupData.epochs[currentTopup] = currentEpoch;
        topupData.amounts[currentTopup] = amount;
    }
    
    function getTopupById(address staker, uint256 id) public view returns (uint128 epochId, uint256 amount) {
        return (topups[staker].epochs[id], topups[staker].amounts[id]);
    }
    
    function calcDurationBonusMultiplier(uint128 epochId, address staker) external view returns (uint256) {
        Topup storage topupData = topups[staker];
        // only if there were topups
        if (topupData.totalTopups > 0) {
            uint256 dividend = 0;
            uint256 divider = 0;
            for (uint256 i = 1; i <= topupData.totalTopups; i++) {
                // Topup storage topup = topups[staker];
                uint128 startEpoch = topupData.epochs[i];
                uint256 amount = topupData.amounts[i];
                // correct multiplier only for epoch from topup starts
                if (epochId < startEpoch) {
                    continue;
                }
                // correct multiplier only for epoch from current stake starts
                if (epochId < stakes[msg.sender].startEpoch) {
                    continue;
                }
                
                dividend += epochId.sub(startEpoch).mul(CALC_MULTIPLIER).div(epochId) * amount;
                divider += amount;
            }

            if (divider > 0) {
                return dividend.div(divider);
            }
        }
        return 0;
    }
    
    function stakeData(address staker) external view returns (uint256 startEpoch, uint256 endEpoch, bool active) {
        Stake memory stake = stakes[staker];
        return (stake.startEpoch, stake.endEpoch, stake.active);
    }
    
    function setFarm(address _farm) external onlyOwner {
        farm = _farm;
    }

    // must be in bases point ( 1,5% = 150 bp)
    function updateReferrersPercentage(uint256 first, uint256 second) external onlyOwner {
        firstReferrerRewardPercentage = first;
        secondReferrerRewardPercentage = second;
    }
    
    function allowEmergencyWithdraw() external onlyOwner{
        emergencyWithdrawAllowed = true;
    }
    
    function disallowEmergencyWithdraw() external onlyOwner{
        emergencyWithdrawAllowed = false;
    }

    function processReferrals(address referrer, uint256 amount) internal {
        //get referrer from first staking pool
        address firstReferrer = Staking(staking).referrals(msg.sender);
        if(firstReferrer != address(0)) {
            referrer = firstReferrer;
        }
        
        //Return if sender has referrer alredy or referrer is contract or self ref
        if (hasReferrer(msg.sender) || !notContract(referrer) || referrer == msg.sender) {
            return;
        }

        //check cross refs 
        if (referrals[referrer] == msg.sender || referrals[referrals[referrer]] == msg.sender) {
            return;
        }
        
        //check if already has stake, do not add referrer if has
        if (balanceOf(msg.sender, swapp) > 0) {
            return;
        }

        referrals[msg.sender] = referrer;

        Referrer storage refData = referrers[referrer];

        refData.referralsCount = refData.referralsCount.add(1);
        refData.referrals[refData.referralsCount] = msg.sender;
        emit RegisteredReferer(msg.sender, referrer, amount);
    }

    function hasReferrer(address addr) public view returns(bool) {
        return referrals[addr] != address(0);
    }

    function getReferralById(address referrer, uint256 id) public view returns (address) {
        return referrers[referrer].referrals[id];
    }
    
    /*
     * Removes the deposit of the user and sends the amount of `tokenAddress` back to the `user`
     */
    function withdraw(address tokenAddress, uint256 amount) public nonReentrant whenNotPaused {
        require(balances[msg.sender][tokenAddress] >= amount, "Staking: balance too small");
        uint128 currentEpoch = getCurrentEpoch();
        Stake storage stake = stakes[msg.sender];
        require(currentEpoch > stake.startEpoch, "Staking: withdraw is not allowed on stake start epoch");
        
        if (currentEpoch < stake.endEpoch) {
            uint256 userLastEpochHarvested = SwappYieldFarm(farm).getUserLastEpochHarvested(msg.sender);
            require(userLastEpochHarvested == currentEpoch.sub(1), "Staking: withdraw allowed only after all epoch before current epoch are harvested");
        }
        
        balances[msg.sender][tokenAddress] = balances[msg.sender][tokenAddress].sub(amount);
        
        if (balances[msg.sender][tokenAddress] == 0) {
    		if (stake.active) {
    			stake.active = false;
    		}
    		
    		if (currentEpoch < stake.endEpoch) {
    		    SwappYieldFarm(farm).clearDurationBonus(msg.sender);
    		}
        } else {
            if (currentEpoch < stake.endEpoch) {
                uint256 balanceBefore = balances[msg.sender][tokenAddress].add(amount);
                uint256 reduceMultiplier = balances[msg.sender][tokenAddress].mul(CALC_MULTIPLIER).div(balanceBefore);
    		    SwappYieldFarm(farm).reduceDurationBonus(msg.sender, reduceMultiplier);
    		}
        }
        
        uint256 penalty = calcPenalty(amount);
        uint256 amountToMint = amount.sub(penalty);
        stakedSwapp = stakedSwapp.sub(amount);
        
        if (penalty > 0) {
            emit Penalty(msg.sender, currentEpoch, penalty);
        }
        
        if (amountToMint > 0) {
		    Minter(minter).mint(msg.sender, amountToMint);
        }
        
        // epoch logic
        
        lastWithdrawEpochId[tokenAddress] = currentEpoch;

        if (!epochIsInitialized(tokenAddress, currentEpoch)) {
            address[] memory tokens = new address[](1);
            tokens[0] = tokenAddress;
            manualEpochInit(tokens, currentEpoch);
        }

        // update the pool size of the next epoch to its current balance
        Pool storage pNextEpoch = poolSize[tokenAddress][currentEpoch + 1];
        
        pNextEpoch.size = stakedSwapp;
        pNextEpoch.set = true;

        Checkpoint[] storage checkpoints = balanceCheckpoints[msg.sender][tokenAddress];
        uint256 last = checkpoints.length - 1;

        // note: it's impossible to have a withdraw and no checkpoints because the balance would be 0 and revert

        // there was a deposit in an older epoch (more than 1 behind [eg: previous 0, now 5]) but no other action since then
        if (checkpoints[last].epochId < currentEpoch) {
            checkpoints.push(Checkpoint(currentEpoch, BASE_MULTIPLIER, balances[msg.sender][tokenAddress], 0));

            poolSize[tokenAddress][currentEpoch].size = poolSize[tokenAddress][currentEpoch].size.sub(amount);
        }
        // there was a deposit in the `epochId - 1` epoch => we have a checkpoint for the current epoch
        else if (checkpoints[last].epochId == currentEpoch) {
            checkpoints[last].startBalance = balances[msg.sender][tokenAddress];
            checkpoints[last].newDeposits = 0;
            checkpoints[last].multiplier = BASE_MULTIPLIER;

            poolSize[tokenAddress][currentEpoch].size = poolSize[tokenAddress][currentEpoch].size.sub(amount);
        }
        // there was a deposit in the current epoch
        else {
            Checkpoint storage currentEpochCheckpoint = checkpoints[last - 1];

            uint256 balanceBefore = getCheckpointEffectiveBalance(currentEpochCheckpoint);

            // in case of withdraw, we have 2 branches:
            // 1. the user withdraws less than he added in the current epoch
            // 2. the user withdraws more than he added in the current epoch (including 0)
            if (amount < currentEpochCheckpoint.newDeposits) {
                uint128 avgDepositMultiplier = uint128(
                    balanceBefore.sub(currentEpochCheckpoint.startBalance).mul(BASE_MULTIPLIER).div(currentEpochCheckpoint.newDeposits)
                );

                currentEpochCheckpoint.newDeposits = currentEpochCheckpoint.newDeposits.sub(amount);

                currentEpochCheckpoint.multiplier = computeNewMultiplier(
                    currentEpochCheckpoint.startBalance,
                    BASE_MULTIPLIER,
                    currentEpochCheckpoint.newDeposits,
                    avgDepositMultiplier
                );
            } else {
                currentEpochCheckpoint.startBalance = currentEpochCheckpoint.startBalance.sub(
                    amount.sub(currentEpochCheckpoint.newDeposits)
                );
                currentEpochCheckpoint.newDeposits = 0;
                currentEpochCheckpoint.multiplier = BASE_MULTIPLIER;
            }

            uint256 balanceAfter = getCheckpointEffectiveBalance(currentEpochCheckpoint);

            poolSize[tokenAddress][currentEpoch].size = poolSize[tokenAddress][currentEpoch].size.sub(balanceBefore.sub(balanceAfter));

            checkpoints[last].startBalance = balances[msg.sender][tokenAddress];
        }

        emit Withdraw(msg.sender, tokenAddress, amount, penalty);
    }
    
    function calcPenalty(uint256 amount) public view returns (uint256) {
        Stake memory stake = stakes[msg.sender];
        uint256 currentEpoch = getCurrentEpoch();
        if (currentEpoch >= stake.endEpoch) {
            return 0;
        } else {
            uint256 staked = currentEpoch - stake.startEpoch;
            uint256 promised = stake.duration;
            uint256 k = 10000 - staked * 10000 / promised; 
            return amount * k / 10000;
        }
    }
    
    function isStakeFinished(address staker) public view returns (bool) {
        Stake memory stake = stakes[staker];
        uint256 currentEpoch = getCurrentEpoch();
        
        return currentEpoch >= stake.endEpoch;
    }
    
    function stakeEndEpoch(address staker) external view returns (uint128) {
        return stakes[staker].endEpoch;
    }

    /*
     * manualEpochInit can be used by anyone to initialize an epoch based on the previous one
     * This is only applicable if there was no action (deposit/withdraw) in the current epoch.
     * Any deposit and withdraw will automatically initialize the current and next epoch.
     */
    function manualEpochInit(address[] memory tokens, uint128 epochId) public {
        require(epochId <= getCurrentEpoch(), "can't init a future epoch");

        for (uint i = 0; i < tokens.length; i++) {
            Pool storage p = poolSize[tokens[i]][epochId];

            if (epochId == 0) {
                p.size = uint256(0);
                p.set = true;
            } else {
                require(!epochIsInitialized(tokens[i], epochId), "Staking: epoch already initialized");
                require(epochIsInitialized(tokens[i], epochId - 1), "Staking: previous epoch not initialized");

                p.size = poolSize[tokens[i]][epochId - 1].size;
                p.set = true;
            }
        }

        emit ManualEpochInit(msg.sender, epochId, tokens);
    }

    function emergencyWithdraw(address tokenAddress) public {
        require(emergencyWithdrawAllowed == true, "Emergency withdrawal not allowed");
        require((getCurrentEpoch() - lastWithdrawEpochId[tokenAddress]) >= 10, "At least 10 epochs must pass without success");

        uint256 totalUserBalance = balances[msg.sender][tokenAddress];
        require(totalUserBalance > 0, "Amount must be > 0");

        balances[msg.sender][tokenAddress] = 0;
        stakedSwapp = stakedSwapp - totalUserBalance;
        
		Minter(minter).mint(msg.sender, totalUserBalance);

        emit EmergencyWithdraw(msg.sender, tokenAddress, totalUserBalance);
    }

    /*
     * Returns the valid balance of a user that was taken into consideration in the total pool size for the epoch
     * A deposit will only change the next epoch balance.
     * A withdraw will decrease the current epoch (and subsequent) balance.
     */
    function getEpochUserBalance(address user, address token, uint128 epochId) public view returns (uint256) {
        Checkpoint[] storage checkpoints = balanceCheckpoints[user][token];

        // if there are no checkpoints, it means the user never deposited any tokens, so the balance is 0
        if (checkpoints.length == 0 || epochId < checkpoints[0].epochId) {
            return 0;
        }

        uint min = 0;
        uint max = checkpoints.length - 1;

        // shortcut for blocks newer than the latest checkpoint == current balance
        if (epochId >= checkpoints[max].epochId) {
            return getCheckpointEffectiveBalance(checkpoints[max]);
        }

        // binary search of the value in the array
        while (max > min) {
            uint mid = (max + min + 1) / 2;
            if (checkpoints[mid].epochId <= epochId) {
                min = mid;
            } else {
                max = mid - 1;
            }
        }

        return getCheckpointEffectiveBalance(checkpoints[min]);
    }

    /*
     * Returns the amount of `token` that the `user` has currently staked
     */
    function balanceOf(address user, address token) public view returns (uint256) {
        return balances[user][token];
    }

    /*
     * Returns the id of the current epoch derived from block.timestamp
     */
    function getCurrentEpoch() public view returns (uint128) {
        if (block.timestamp < epoch1Start) {
            return 0;
        }

        return uint128((block.timestamp - epoch1Start) / epochDuration + 1);
    }

    /*
     * Returns the total amount of `tokenAddress` that was locked from beginning to end of epoch identified by `epochId`
     */
    function getEpochPoolSize(address tokenAddress, uint128 epochId) public view returns (uint256) {
        // Premises:
        // 1. it's impossible to have gaps of uninitialized epochs
        // - any deposit or withdraw initialize the current epoch which requires the previous one to be initialized
        if (epochIsInitialized(tokenAddress, epochId)) {
            return poolSize[tokenAddress][epochId].size;
        }

        // epochId not initialized and epoch 0 not initialized => there was never any action on this pool
        if (!epochIsInitialized(tokenAddress, 0)) {
            return 0;
        }

        // epoch 0 is initialized => there was an action at some point but none that initialized the epochId
        // which means the current pool size is equal to the current balance of token held by the staking contract
        return stakedSwapp;
    }

    /*
     * Returns the percentage of time left in the current epoch
     */
    function currentEpochMultiplier() public view returns (uint128) {
        uint128 currentEpoch = getCurrentEpoch();
        uint256 currentEpochEnd = epoch1Start + currentEpoch * epochDuration;
        uint256 timeLeft = currentEpochEnd - block.timestamp;
        uint128 multiplier = uint128(timeLeft * BASE_MULTIPLIER / epochDuration);

        return multiplier;
    }

    function computeNewMultiplier(uint256 prevBalance, uint128 prevMultiplier, uint256 amount, uint128 currentMultiplier) public pure returns (uint128) {
        uint256 prevAmount = prevBalance.mul(prevMultiplier).div(BASE_MULTIPLIER);
        uint256 addAmount = amount.mul(currentMultiplier).div(BASE_MULTIPLIER);
        uint128 newMultiplier = uint128(prevAmount.add(addAmount).mul(BASE_MULTIPLIER).div(prevBalance.add(amount)));

        return newMultiplier;
    }

    /*
     * Checks if an epoch is initialized, meaning we have a pool size set for it
     */
    function epochIsInitialized(address token, uint128 epochId) public view returns (bool) {
        return poolSize[token][epochId].set;
    }

    function getCheckpointBalance(Checkpoint memory c) internal pure returns (uint256) {
        return c.startBalance.add(c.newDeposits);
    }

    function getCheckpointEffectiveBalance(Checkpoint memory c) internal pure returns (uint256) {
        return getCheckpointBalance(c).mul(c.multiplier).div(BASE_MULTIPLIER);
    }

    function notContract(address _addr) internal view returns (bool) {
        uint32 size;
        assembly { size := extcodesize(_addr) }
        return (size == 0);
    }
    
    function paused() public view returns (bool) {
        return _paused;
    }
    
    function pause() external onlyOwner {
        _paused = true;
    }
    
    function unpause() external onlyOwner {
        _paused = false;
    }
    
    function setMigration(address migration) external onlyOwner{
        _migration = migration;
    }
    
    function prepareMigration(address staker) public returns (uint256 balance) {
        require(_migration != address(0), "Migration is not initialised");
        require(msg.sender == _migration, "Only migration contract can perform this action");
        require(balances[staker][swapp] > 0, "Balance too small");
        
        uint256 _balance = balances[staker][swapp];
        balances[staker][swapp] = 0;
        stakedSwapp = stakedSwapp.sub(_balance);
        
        Stake storage stake = stakes[staker];
        stake.active = false;
        stake.endEpoch = getCurrentEpoch();
        
        emit PrepareMigration(staker, _balance);
        
        return _balance;
    }
}
function balanceOf(address user, address token) public view returns (uint256) {
    return balances[user][token];
}

/*
 * Returns the amount of `token` that the `user` has currently staked
 */