contract Bartender is Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    // the lockup type for staked LP. affects the withdraw tax.
    enum LockType { None, ThreeDays, Week, Month, Forever}

    // Info of each user.
    struct UserInfo {
        uint256 amount;     // How many LP tokens the user has provided.
        uint256 rewardDebt; // Reward debt. See explanation below.
        LockType lockType;
        uint256 unlockDate;
        uint256 taxRewardDebt; // Reward debt. See explanation below.
        uint256 lpTaxRewardDebt; // Reward debt. See explanation below.

        //
        // We do some fancy math here. Basically, any point in time, the amount of PUBs
        // entitled to a user but is pending to be distributed is:
        //
        //   pending reward = (user.amount * pool.accPubPerShare) - user.rewardDebt
        //
        // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens:
        //   1. The pool's `accPubPerShare` (and `lastRewardBlock`) gets updated.
        //   2. User receives the pending reward sent to his/her address.
        //   3. User's `amount` gets updated.
        //   4. User's `rewardDebt` gets updated.
    }

    // Info of each pool.
    struct PoolInfo {
        IERC20 lpToken;           // Address of LP token contract.
        uint256 allocPoint;       // How many allocation points assigned to this pool. PUBs to distribute per block.
        uint256 lastRewardBlock;  // Last block number that PUBs distribution occurs.
        uint256 accPubPerShare;   // Accumulated PUBs per share, times 1e12. See below.
        uint256 accTaxPubPerShare;   // Accumulated PUBs per share, times 1e12. For Taxes
        uint256 accLPTaxPubPerShare;   // Accumulated PUBs per share, times 1e12. For LP Taxes
        uint256 accTokensForTax;
        uint256 accTokensForLPTax;

    }

    // The [new] PUB token
    PubToken public pub;
    // PUB tokens created per block.
    uint256 public pubPerBlock;
    // numerator of the owner fee
    uint256 public constant OWNER_FEE_NUMERATOR = 50;
    // denominator of the owner fee
    uint256 public constant OWNER_FEE_DENOMINATOR = 10000;

    // Info of each pool.
    PoolInfo[] public poolInfo;
    // Info of each user that stakes LP tokens.
    mapping (uint256 => mapping (address => UserInfo[])) public userInfo;
    // Total allocation points. Must be the sum of all allocation points in all pools.
    uint256 public totalAllocPoint = 0;
    // The block number when PUB mining starts.
    uint256 public startBlock;

    // accumulated tax amount
    uint256 public accumulatedTax = 0;

    // address of the old PUB token that can be 1:1 exchanged for new PUB
    IERC20 oldPub;

    event Deposit(address indexed user, uint256 indexed pid, uint256 amount);
    event Withdraw(address indexed user, uint256 indexed pid, uint256 amount);
    event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount);

    constructor(
        uint256 _startBlock,
        address _oldPub
    ) public {
        // we are going to deploy the token from within this
        // constructor to grant onlyOwner just to this contract so only Bartender can mint() tokens.
        // mint a couple tokens for the express purpose of creating the Uniswap LPs
        pub = new PubToken(250, 250, address(this), msg.sender);
        oldPub = IERC20(_oldPub);

        // in order to create the Uni Liquidity Pools we mint 5 tokens to the owner on creation.
        pub.mint(msg.sender, 5 * 10**18);

        pubPerBlock = 0; // initial value
        startBlock = _startBlock;
    }
    modifier onlyPubToken {
        require(msg.sender == address(pub), "Can only be called by PubToken contract.");
        _;
    }

    // method to return the balance of an address of the pub erc20 token associated wtih this contract
    function pubBalance(address a) external view returns (uint256){
        return pub.balanceOf(a);
    }

    // return the address of the erc20 token that gets harvested
    function pubToken() external view returns (address) {
        return address(pub);
    }

    // get the number of farms
    function poolLength() external view returns (uint256) {
        return poolInfo.length;
    }

    // get the owner of the PUB token
    function pubOwner() external view returns (address) {
        return pub.owner();
    }

    // get the PUB balance of the caller
    function myPubTokenBalance() external view returns (uint256) {
        return pub.balanceOf(msg.sender);
    }

    // view to see the pending tokens for a pool and an address
    function getUserInfo(uint256 _pid, address _address) external view returns (uint256) {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo[] storage userInfoArr = userInfo[_pid][_address];

        uint256 length = userInfoArr.length;
        uint totalAmount = 0;
        for (uint256 userInfoIndex = 0; userInfoIndex < length; ++userInfoIndex) {
            totalAmount = totalAmount.add(userInfoArr[userInfoIndex].amount);

        }
        return totalAmount;
    }

    function getUserInfoLocked(uint256 _pid, address _address) external view returns (uint256) {

        PoolInfo storage pool = poolInfo[_pid];
        UserInfo[] storage userInfoArr = userInfo[_pid][_address];


        uint256 length = userInfoArr.length;
        uint totalAmount = 0;
        for (uint256 userInfoIndex = 0; userInfoIndex < length; ++userInfoIndex) {
            UserInfo storage user =userInfoArr[userInfoIndex];
            if (user.amount > 0 && user.unlockDate > now) {

                totalAmount = totalAmount.add(user.amount);
            }
        }
        return totalAmount;
    }

    // Add a new lp to the pool. Can only be called by the owner.
    // XXX DO NOT add the same LP token more than once. Rewards will be messed up if you do.
    function add(uint256 _allocPoint, IERC20 _lpToken, bool _withUpdate) public onlyOwner {
        if (_withUpdate) {
            massUpdatePools();
        }
        uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock;
        totalAllocPoint = totalAllocPoint.add(_allocPoint);
        poolInfo.push(PoolInfo({
            lpToken: _lpToken,
            allocPoint: _allocPoint,
            lastRewardBlock: lastRewardBlock,
            accPubPerShare: 0,
            accTaxPubPerShare:0,
            accLPTaxPubPerShare:0,
            accTokensForTax:0,
            accTokensForLPTax:0
            }));
    }

    // get the current number of PUB per block
    function getPubPerBlock() public view returns (uint256){
        return pubPerBlock;
    }

    // update the number of PUB per block, with a value in wei
    function setPubPerBlock(uint256 _pubPerBlock) public onlyOwner {
        require(_pubPerBlock > 0, "_pubPerBlock must be non-zero");

        // update all pools prior to changing the block rewards
        massUpdatePools();

        // update the block rewards
        pubPerBlock = _pubPerBlock;
    }

    // Update the given pool's PUB allocation point. Can only be called by the owner.
    function set(uint256 _pid, uint256 _allocPoint, bool _withUpdate) public onlyOwner {
        if (_withUpdate) {
            massUpdatePools();
        }
        totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add(_allocPoint);
        poolInfo[_pid].allocPoint = _allocPoint;
    }

    // View function to see pending PUBs on frontend.
    function pendingPubs(uint256 _pid, address _user) external view returns (uint256) {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo[] storage userInfoArr = userInfo[_pid][msg.sender];

        uint256 length = userInfoArr.length;
        uint totalPubToTransfer = 0;
        for (uint256 userInfoIndex = 0; userInfoIndex < length; ++userInfoIndex) {
            UserInfo storage user = userInfoArr[userInfoIndex];

            if (user.amount > 0 && user.unlockDate <= now) {
                uint256 pending = user.amount.mul(pool.accPubPerShare).div(1e12).sub(user.rewardDebt);
                totalPubToTransfer = totalPubToTransfer.add(pending);

                //Distribute taxes
                if (user.lockType >= LockType.Week) {
                    uint256 pendingTax = user.amount.mul(pool.accTaxPubPerShare).div(1e12).sub(user.taxRewardDebt);
                    totalPubToTransfer = totalPubToTransfer.add(pendingTax);
                }
                //Distribute lp taxes
                if (user.lockType >= LockType.Month) {
                    uint256 pendingTax = user.amount.mul(pool.accLPTaxPubPerShare).div(1e12).sub(user.lpTaxRewardDebt);
                    totalPubToTransfer = totalPubToTransfer.add(pendingTax);
                }
            }
        }
        return totalPubToTransfer;
    }

    // View function to see pending PUBs on frontend.
    function pendingLockedPubs(uint256 _pid, address _user) external view returns (uint256) {

        PoolInfo storage pool = poolInfo[_pid];
        UserInfo[] storage userInfoArr = userInfo[_pid][msg.sender];


        uint256 length = userInfoArr.length;
        uint totalPubToTransfer = 0;
        for (uint256 userInfoIndex = 0; userInfoIndex < length; ++userInfoIndex) {
            UserInfo storage user = userInfoArr[userInfoIndex];

            if (user.amount > 0 && user.unlockDate > now) {
                uint256 pending = user.amount.mul(pool.accPubPerShare).div(1e12).sub(user.rewardDebt);
                totalPubToTransfer = totalPubToTransfer.add(pending);

                //Distribute taxes
                if (user.lockType >= LockType.Week) {
                    uint256 pendingTax = user.amount.mul(pool.accTaxPubPerShare).div(1e12).sub(user.taxRewardDebt);
                    totalPubToTransfer = totalPubToTransfer.add(pendingTax);
                }
                //Distribute lp taxes
                if (user.lockType >= LockType.Month) {
                    uint256 pendingTax = user.amount.mul(pool.accLPTaxPubPerShare).div(1e12).sub(user.lpTaxRewardDebt);
                    totalPubToTransfer = totalPubToTransfer.add(pendingTax);
                }
            }
        }
        return totalPubToTransfer;
    }

    // Update reward variables for all pools. Be careful of gas spending!
    function massUpdatePools() public {
        uint256 length = poolInfo.length;
        for (uint256 pid = 0; pid < length; ++pid) {
            updatePool(pid);
        }
    }

    // anyone can call this to update the tax distribution and gets a 1% caller bonus
    function massUpdateTaxAllocationForPools() public {
        uint callerBonus = accumulatedTax.mul(100).div(10000); // 1%
        pub.transfer(msg.sender, callerBonus);
        accumulatedTax = accumulatedTax.sub(callerBonus);
        if (accumulatedTax == 0) {
            return;
        }
        uint256 length = poolInfo.length;
        for (uint256 pid = 0; pid < length; ++pid) {
            PoolInfo storage pool = poolInfo[pid];
            uint256 lpSupply = pool.accTokensForTax;

            //handle tax distribution
            uint256 taxPubReward = accumulatedTax.mul(pool.allocPoint).div(totalAllocPoint);
            pool.accTaxPubPerShare = pool.accTaxPubPerShare.add(taxPubReward.mul(1e12).div(lpSupply));

        }
        accumulatedTax = 0;
    }

    function massUpdateLPTaxAllocationForPools(uint _amount) public {
        pub.transferFrom(msg.sender, address(this), _amount);

        uint256 length = poolInfo.length;
        for (uint256 pid = 0; pid < length; ++pid) {
            PoolInfo storage pool = poolInfo[pid];
            uint256 lpSupply = pool.accTokensForLPTax;

            //handle tax distribution
            uint256 lpTaxPubReward = _amount.mul(pool.allocPoint).div(totalAllocPoint);
            pool.accLPTaxPubPerShare = pool.accLPTaxPubPerShare.add(lpTaxPubReward.mul(1e12).div(lpSupply));
        }
    }

    // Update reward variables of the given pool to be up-to-date.
    // updates starting with the 0 index
    function updatePool(uint256 _pid) public {
        PoolInfo storage pool = poolInfo[_pid];
        if (block.number <= pool.lastRewardBlock) {
            return;
        }

        uint256 lpSupply = pool.lpToken.balanceOf(address(this));
        if (lpSupply == 0) {
            pool.lastRewardBlock = block.number;
            return;
        }

        uint256 pubReward = pubPerBlock.mul(pool.allocPoint).div(totalAllocPoint);

        if(pubReward > 0){
            pub.mint(address(this), pubReward);
        }
        pool.accPubPerShare = pool.accPubPerShare.add(pubReward.mul(1e12).div(lpSupply));

        pool.lastRewardBlock = block.number;
    }

    // claim pending yield
    function harvest(uint256 _pid) public {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo[] storage userInfoArr = userInfo[_pid][msg.sender];
        updatePool(_pid);

        uint256 length = userInfoArr.length;
        uint totalPubToTransfer = 0;
        for (uint256 userInfoIndex = 0; userInfoIndex < length; ++userInfoIndex) {
            UserInfo storage user = userInfoArr[userInfoIndex];

            if (user.amount > 0 && user.unlockDate <= now) {
                uint256 pending = user.amount.mul(pool.accPubPerShare).div(1e12).sub(user.rewardDebt);
                totalPubToTransfer = totalPubToTransfer.add(pending);
                user.rewardDebt = user.amount.mul(pool.accPubPerShare).div(1e12);

                //Distribute taxes
                if (user.lockType >= LockType.Week) {
                    uint256 pendingTax = user.amount.mul(pool.accTaxPubPerShare).div(1e12).sub(user.taxRewardDebt);
                    totalPubToTransfer = totalPubToTransfer.add(pendingTax);
                    user.taxRewardDebt = user.amount.mul(pool.accTaxPubPerShare).div(1e12);
                }
                //Distribute lp taxes
                if (user.lockType >= LockType.Month) {
                    uint256 pendingTax = user.amount.mul(pool.accLPTaxPubPerShare).div(1e12).sub(user.lpTaxRewardDebt);
                    totalPubToTransfer = totalPubToTransfer.add(pendingTax);
                    user.lpTaxRewardDebt = user.amount.mul(pool.accLPTaxPubPerShare).div(1e12);
                }
            }
        }
        safePubTransfer(msg.sender, totalPubToTransfer);
    }

    // Deposit LP tokens to Bartender for PUB allocation.
    function deposit(uint256 _pid, uint256 _amount, LockType lockType) public {
        PoolInfo storage pool = poolInfo[_pid];
        updatePool(_pid);
        if (_amount > 0) {

            pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount);

            UserInfo memory user = UserInfo(_amount,0,lockType,0,0,0);
            user.amount = _amount;
            user.rewardDebt = user.amount.mul(pool.accPubPerShare).div(1e12);
            user.lockType = lockType;
            user.taxRewardDebt = user.amount.mul(pool.accTaxPubPerShare).div(1e12);
            user.lpTaxRewardDebt = user.amount.mul(pool.accLPTaxPubPerShare).div(1e12);

            if(lockType == LockType.ThreeDays){
                user.unlockDate = now + 3 days;
            }
            else if(lockType == LockType.Week){
                user.unlockDate = now + 1 weeks;
                pool.accTokensForTax = pool.accTokensForTax.add(_amount);

            }
            else if(lockType == LockType.Month){
                user.unlockDate = now + 30 days;
                pool.accTokensForLPTax = pool.accTokensForLPTax.add(_amount);
                pool.accTokensForTax = pool.accTokensForTax.add(_amount);
            }
            else if(lockType == LockType.Forever){
                user.unlockDate = now;
                pool.accTokensForLPTax = pool.accTokensForLPTax.add(_amount);
                pool.accTokensForTax = pool.accTokensForTax.add(_amount);
            }
            else {
                user.unlockDate = now;
            }
            userInfo[_pid][msg.sender].push(user);
            emit Deposit(msg.sender, _pid, _amount);
        }
    }

    // withdraw all unlocked tokens
    function withdrawMax(uint256 _pid) public {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo[] storage userInfoArr = userInfo[_pid][msg.sender];
        updatePool(_pid);

        uint256 length = userInfoArr.length;
        uint totalPubToTransfer = 0;
        uint totalLPToTransfer = 0;
        uint totalLPFee = 0;
        for (uint256 userInfoIndex = 0; userInfoIndex < length; ++userInfoIndex) {
            UserInfo storage user = userInfoArr[userInfoIndex];

            if (user.amount > 0 && user.unlockDate <= now && user.lockType != LockType.Forever) {
                    uint256 pending = user.amount.mul(pool.accPubPerShare).div(1e12).sub(user.rewardDebt);
                    totalPubToTransfer = totalPubToTransfer.add(pending);
                    uint256 amount  = user.amount;

                    //Distribute taxes
                    if (user.lockType >= LockType.Week) {
                        uint256 pendingTax = user.amount.mul(pool.accTaxPubPerShare).div(1e12).sub(user.taxRewardDebt);
                        totalPubToTransfer = totalPubToTransfer.add(pendingTax);
                        user.taxRewardDebt = user.amount.mul(pool.accTaxPubPerShare).div(1e12);
                    }

                    //Distribute lp taxes
                    if (user.lockType >= LockType.Month) {
                        uint256 pendingTax = user.amount.mul(pool.accLPTaxPubPerShare).div(1e12).sub(user.lpTaxRewardDebt);
                        totalPubToTransfer = totalPubToTransfer.add(pendingTax);
                        user.lpTaxRewardDebt = user.amount.mul(pool.accLPTaxPubPerShare).div(1e12);
                    }

                    // lock type >= Month is 0 fee
                    uint256 fee = 0;
                    if(user.lockType == LockType.None){
                        fee = 100;
                    }
                    else if(user.lockType == LockType.ThreeDays){
                        fee = 50;
                    }
                    else if(user.lockType == LockType.Week){
                        fee = 25;
                        pool.accTokensForTax = pool.accTokensForTax.sub(amount);
                    }
                    else if(user.lockType == LockType.Month){
                        pool.accTokensForLPTax = pool.accTokensForLPTax.sub(amount);
                        pool.accTokensForTax = pool.accTokensForTax.sub(amount);
                    }

                    uint256 feeAmount = amount.mul(fee).div(OWNER_FEE_DENOMINATOR);
                    amount = amount.sub(feeAmount);
                    totalLPFee = totalLPFee.add(feeAmount);

                    totalLPToTransfer = totalLPToTransfer.add(amount);

                    user.rewardDebt = 0;
                    user.amount = 0;
                }

        }
        // surgically collapse the array
        for (uint256 userInfoIndex = 0; userInfoIndex < length;) {
            UserInfo storage user = userInfoArr[userInfoIndex];
            if(user.amount == 0){
                for (uint256 idx = userInfoIndex; idx < length-1; ++idx) {
                    userInfoArr[idx] = userInfoArr[idx+1];
                }
                length = length.sub(1);
                delete userInfoArr[length];
            }
            else{
                userInfoIndex++;
            }
        }

        // transfer all tokens that we withdrew LP from
        safePubTransfer(msg.sender, totalPubToTransfer);

        // transfer the feeAmount to the owner using deposit
        pool.lpToken.safeTransfer(address(owner()), totalLPFee);

        // withdraw, using safeTransfer
        pool.lpToken.safeTransfer(address(msg.sender), totalLPToTransfer);

        emit Withdraw(msg.sender, _pid, totalLPToTransfer);
    }

    // Withdraw without caring about rewards. EMERGENCY ONLY.
    function emergencyWithdraw(uint256 _pid) public {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo[] storage userInfoArr = userInfo[_pid][msg.sender];

        uint256 length = userInfoArr.length;
        uint totalPubToTransfer = 0;
        uint totalLPToTransfer = 0;
        uint totalLPFee = 0;
        for (uint256 userInfoIndex = 0; userInfoIndex < length; ++userInfoIndex) {
            UserInfo storage user = userInfoArr[userInfoIndex];

            if (user.amount > 0 && user.unlockDate <= now && user.lockType != LockType.Forever) {
                uint256 amount  = user.amount;


                // lock type >= Month is 0 fee
                uint256 fee = 0;
                if(user.lockType == LockType.None){
                    fee = 100;
                }
                else if(user.lockType == LockType.ThreeDays){
                    fee = 50;
                }
                else if(user.lockType == LockType.Week){
                    fee = 25;
                    pool.accTokensForTax = pool.accTokensForTax.sub(amount);

                }
                else if(user.lockType == LockType.Month){
                    pool.accTokensForLPTax = pool.accTokensForLPTax.sub(amount);
                    pool.accTokensForTax = pool.accTokensForTax.sub(amount);

                }

                uint256 feeAmount = amount.mul(fee).div(OWNER_FEE_DENOMINATOR);
                amount = amount.sub(feeAmount);
                totalLPFee = totalLPFee.add(feeAmount);

                totalLPToTransfer = totalLPToTransfer.add(amount);
            }
        }

        // transfer the feeAmount to the owner using deposit
        pool.lpToken.safeTransfer(address(owner()), totalLPFee);

        // withdraw, using safeTransfer
        pool.lpToken.safeTransfer(address(msg.sender), totalLPToTransfer);

        emit EmergencyWithdraw(msg.sender, _pid, totalLPToTransfer);
    }

    // Safe pub transfer function, just in case if rounding error causes pool to not have enough PUBs.
    function safePubTransfer(address _to, uint256 _amount) internal {
        uint256 pubBal = pub.balanceOf(address(this));
        if (_amount > pubBal) {
            pub.transfer(_to, pubBal);
        } else {
            pub.transfer(_to, _amount);
        }
    }

    function handleTaxDistribution(uint _tax,uint _devTax) external onlyPubToken{
        accumulatedTax = accumulatedTax.add(_tax);
        // transfers the dev tax to the owner
        pub.transfer(address(owner()), _devTax);
    }

    // exchange original PUB token for new PUB
    function swapPubForPub2(uint _amount) public{
        oldPub.transferFrom(msg.sender, address(0x000000000000000000000000000000000000dEaD), _amount);
        pub.mint(msg.sender, _amount);
    }

}
function deposit(uint256 _pid, uint256 _amount, LockType lockType) public {
    PoolInfo storage pool = poolInfo[_pid];
    updatePool(_pid);
    if (_amount > 0) {

        pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount);

        UserInfo memory user = UserInfo(_amount,0,lockType,0,0,0);
        user.amount = _amount;
        user.rewardDebt = user.amount.mul(pool.accPubPerShare).div(1e12);
        user.lockType = lockType;
        user.taxRewardDebt = user.amount.mul(pool.accTaxPubPerShare).div(1e12);
        user.lpTaxRewardDebt = user.amount.mul(pool.accLPTaxPubPerShare).div(1e12);

        if(lockType == LockType.ThreeDays){
            user.unlockDate = now + 3 days;
        }
        else if(lockType == LockType.Week){
            user.unlockDate = now + 1 weeks;
            pool.accTokensForTax = pool.accTokensForTax.add(_amount);

        }
        else if(lockType == LockType.Month){
            user.unlockDate = now + 30 days;
            pool.accTokensForLPTax = pool.accTokensForLPTax.add(_amount);
            pool.accTokensForTax = pool.accTokensForTax.add(_amount);
        }
        else if(lockType == LockType.Forever){
            user.unlockDate = now;
            pool.accTokensForLPTax = pool.accTokensForLPTax.add(_amount);
            pool.accTokensForTax = pool.accTokensForTax.add(_amount);
        }
        else {
            user.unlockDate = now;
        }
        userInfo[_pid][msg.sender].push(user);
        emit Deposit(msg.sender, _pid, _amount);
    }
}

// Deposit LP tokens to Bartender for PUB allocation.