// SPDX-License-Identifier: MIT

pragma solidity 0.6.12;
pragma experimental ABIEncoderV2;

import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol";
import "@boringcrypto/boring-solidity/contracts/BoringBatchable.sol";
import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol";
import "./libraries/SignedSafeMath.sol";
import "./interfaces/IRewarder.sol";
import "./interfaces/IMasterDeveloper.sol";

interface IMigratorDeveloper {
    // Take the current LP token address and return the new LP token address.
    // Migrator should have full access to the caller's LP token.
    function migrate(IERC20 token) external returns (IERC20);
}

/// @notice The (older) MasterDeveloper contract gives out a constant number of INNAI tokens per block.
/// It is the only address with minting rights for INNAI.
/// The idea for this MasterDeveloper V2 (MDV2) contract is therefore to be the owner of a dummy token
/// that is deposited into the MasterDeveloper V1 (MDV1) contract.
/// The allocation point for this pool on MDV1 is the total allocation point for all pools that receive double incentives.
contract MasterDeveloperV2 is BoringOwnable, BoringBatchable {
    using BoringMath for uint256;
    using BoringMath128 for uint128;
    using BoringERC20 for IERC20;
    using SignedSafeMath for int256;

    /// @notice Info of each MDV2 user.
    /// `amount` LP token amount the user has provided.
    /// `rewardDebt` The amount of INNAI entitled to the user.
    struct UserInfo {
        uint256 amount;
        int256 rewardDebt;
    }

    /// @notice Info of each MDV2 pool.
    /// `allocPoint` The amount of allocation points assigned to the pool.
    /// Also known as the amount of INNAI to distribute per block.
    struct PoolInfo {
        uint128 accInnovaiPerShare;
        uint64 lastRewardBlock;
        uint64 allocPoint;
    }

    /// @notice Address of MDV1 contract.
    IMasterDeveloper public immutable MASTER_DEVELOPER;
    /// @notice Address of INNAI contract.
    IERC20 public immutable INNAI;
    /// @notice The index of MDV2 master pool in MDV1.
    uint256 public immutable MASTER_PID;
    // @notice The migrator contract. It has a lot of power. Can only be set through governance (owner).
    IMigratorDeveloper public migrator;

    /// @notice Info of each MDV2 pool.
    PoolInfo[] public poolInfo;
    /// @notice Address of the LP token for each MDV2 pool.
    IERC20[] public lpToken;
    /// @notice Address of each `IRewarder` contract in MDV2.
    IRewarder[] public rewarder;

    /// @notice Info of each user that stakes LP tokens.
    mapping (uint256 => mapping (address => UserInfo)) public userInfo;
    /// @dev Total allocation points. Must be the sum of all allocation points in all pools.
    uint256 public totalAllocPoint;

    uint256 private constant MASTERDEVELOPER_INNAI_PER_BLOCK = 1e20;
    uint256 private constant ACC_INNAI_PRECISION = 1e12;

    event Deposit(address indexed user, uint256 indexed pid, uint256 amount, address indexed to);
    event Withdraw(address indexed user, uint256 indexed pid, uint256 amount, address indexed to);
    event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount, address indexed to);
    event Harvest(address indexed user, uint256 indexed pid, uint256 amount);
    event LogPoolAddition(uint256 indexed pid, uint256 allocPoint, IERC20 indexed lpToken, IRewarder indexed rewarder);
    event LogSetPool(uint256 indexed pid, uint256 allocPoint, IRewarder indexed rewarder, bool overwrite);
    event LogUpdatePool(uint256 indexed pid, uint64 lastRewardBlock, uint256 lpSupply, uint256 accInnovaiPerShare);
    event LogInit();

    /// @param _MASTER_DEVELOPER The InnoSwap MDV1 contract address.
    /// @param _innovai The INNAI token contract address.
    /// @param _MASTER_PID The pool ID of the dummy token on the base MDV1 contract.
    constructor(IMasterDeveloper _MASTER_DEVELOPER, IERC20 _innovai, uint256 _MASTER_PID) public {
        MASTER_DEVELOPER = _MASTER_DEVELOPER;
        INNAI = _innovai;
        MASTER_PID = _MASTER_PID;
    }

    /// @notice Deposits a dummy token to `MASTER_DEVELOPER` MDV1. This is required because MDV1 holds the minting rights for INNAI.
    /// Any balance of transaction sender in `dummyToken` is transferred.
    /// The allocation point for the pool on MDV1 is the total allocation point for all pools that receive double incentives.
    /// @param dummyToken The address of the ERC-20 token to deposit into MDV1.
    function init(IERC20 dummyToken) external {
        uint256 balance = dummyToken.balanceOf(msg.sender);
        require(balance != 0, "MasterDeveloperV2: Balance must exceed 0");
        dummyToken.safeTransferFrom(msg.sender, address(this), balance);
        dummyToken.approve(address(MASTER_DEVELOPER), balance);
        MASTER_DEVELOPER.deposit(MASTER_PID, balance);
        emit LogInit();
    }

    /// @notice Returns the number of MDV2 pools.
    function poolLength() public view returns (uint256 pools) {
        pools = poolInfo.length;
    }

    /// @notice Add a new LP to the pool. Can only be called by the owner.
    /// DO NOT add the same LP token more than once. Rewards will be messed up if you do.
    /// @param allocPoint AP of the new pool.
    /// @param _lpToken Address of the LP ERC-20 token.
    /// @param _rewarder Address of the rewarder delegate.
    function add(uint256 allocPoint, IERC20 _lpToken, IRewarder _rewarder) public onlyOwner {
        uint256 lastRewardBlock = block.number;
        totalAllocPoint = totalAllocPoint.add(allocPoint);
        lpToken.push(_lpToken);
        rewarder.push(_rewarder);

        poolInfo.push(PoolInfo({
            allocPoint: allocPoint.to64(),
            lastRewardBlock: lastRewardBlock.to64(),
            accInnovaiPerShare: 0
        }));
        emit LogPoolAddition(lpToken.length.sub(1), allocPoint, _lpToken, _rewarder);
    }

    /// @notice Update the given pool's INNAI allocation point and `IRewarder` contract. Can only be called by the owner.
    /// @param _pid The index of the pool. See `poolInfo`.
    /// @param _allocPoint New AP of the pool.
    /// @param _rewarder Address of the rewarder delegate.
    /// @param overwrite True if _rewarder should be `set`. Otherwise `_rewarder` is ignored.
    function set(uint256 _pid, uint256 _allocPoint, IRewarder _rewarder, bool overwrite) public onlyOwner {
        totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add(_allocPoint);
        poolInfo[_pid].allocPoint = _allocPoint.to64();
        if (overwrite) { rewarder[_pid] = _rewarder; }
        emit LogSetPool(_pid, _allocPoint, overwrite ? _rewarder : rewarder[_pid], overwrite);
    }

    /// @notice Set the `migrator` contract. Can only be called by the owner.
    /// @param _migrator The contract address to set.
    function setMigrator(IMigratorDeveloper _migrator) public onlyOwner {
        migrator = _migrator;
    }

    /// @notice Migrate LP token to another LP contract through the `migrator` contract.
    /// @param _pid The index of the pool. See `poolInfo`.
    function migrate(uint256 _pid) public {
        require(address(migrator) != address(0), "MasterDeveloperV2: no migrator set");
        IERC20 _lpToken = lpToken[_pid];
        uint256 bal = _lpToken.balanceOf(address(this));
        _lpToken.approve(address(migrator), bal);
        IERC20 newLpToken = migrator.migrate(_lpToken);
        require(bal == newLpToken.balanceOf(address(this)), "MasterDeveloperV2: migrated balance must match");
        lpToken[_pid] = newLpToken;
    }

    /// @notice View function to see pending INNAI on frontend.
    /// @param _pid The index of the pool. See `poolInfo`.
    /// @param _user Address of user.
    /// @return pending INNAI reward for a given user.
    function pendingInnovai(uint256 _pid, address _user) external view returns (uint256 pending) {
        PoolInfo memory pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][_user];
        uint256 accInnovaiPerShare = pool.accInnovaiPerShare;
        uint256 lpSupply = lpToken[_pid].balanceOf(address(this));
        if (block.number > pool.lastRewardBlock && lpSupply != 0) {
            uint256 blocks = block.number.sub(pool.lastRewardBlock);
            uint256 innovaiReward = blocks.mul(innovaiPerBlock()).mul(pool.allocPoint) / totalAllocPoint;
            accInnovaiPerShare = accInnovaiPerShare.add(innovaiReward.mul(ACC_INNAI_PRECISION) / lpSupply);
        }
        pending = int256(user.amount.mul(accInnovaiPerShare) / ACC_INNAI_PRECISION).sub(user.rewardDebt).toUInt256();
    }

    /// @notice Update reward variables for all pools. Be careful of gas spending!
    /// @param pids Pool IDs of all to be updated. Make sure to update all active pools.
    function massUpdatePools(uint256[] calldata pids) external {
        uint256 len = pids.length;
        for (uint256 i = 0; i < len; ++i) {
            updatePool(pids[i]);
        }
    }

    /// @notice Calculates and returns the `amount` of INNAI per block.
    function innovaiPerBlock() public view returns (uint256 amount) {
        amount = uint256(MASTERDEVELOPER_INNAI_PER_BLOCK)
            .mul(MASTER_DEVELOPER.poolInfo(MASTER_PID).allocPoint) / MASTER_DEVELOPER.totalAllocPoint();
    }

    /// @notice Update reward variables of the given pool.
    /// @param pid The index of the pool. See `poolInfo`.
    /// @return pool Returns the pool that was updated.
    function updatePool(uint256 pid) public returns (PoolInfo memory pool) {
        pool = poolInfo[pid];
        if (block.number > pool.lastRewardBlock) {
            uint256 lpSupply = lpToken[pid].balanceOf(address(this));
            if (lpSupply > 0) {
                uint256 blocks = block.number.sub(pool.lastRewardBlock);
                uint256 innovaiReward = blocks.mul(innovaiPerBlock()).mul(pool.allocPoint) / totalAllocPoint;
                pool.accInnovaiPerShare = pool.accInnovaiPerShare.add((innovaiReward.mul(ACC_INNAI_PRECISION) / lpSupply).to128());
            }
            pool.lastRewardBlock = block.number.to64();
            poolInfo[pid] = pool;
            emit LogUpdatePool(pid, pool.lastRewardBlock, lpSupply, pool.accInnovaiPerShare);
        }
    }

    /// @notice Deposit LP tokens to MDV2 for INNAI allocation.
    /// @param pid The index of the pool. See `poolInfo`.
    /// @param amount LP token amount to deposit.
    /// @param to The receiver of `amount` deposit benefit.
    function deposit(uint256 pid, uint256 amount, address to) public {
        PoolInfo memory pool = updatePool(pid);
        UserInfo storage user = userInfo[pid][to];

        // Effects
        user.amount = user.amount.add(amount);
        user.rewardDebt = user.rewardDebt.add(int256(amount.mul(pool.accInnovaiPerShare) / ACC_INNAI_PRECISION));

        // Interactions
        IRewarder _rewarder = rewarder[pid];
        if (address(_rewarder) != address(0)) {
            _rewarder.onInnovaiReward(pid, to, to, 0, user.amount);
        }

        lpToken[pid].safeTransferFrom(msg.sender, address(this), amount);

        emit Deposit(msg.sender, pid, amount, to);
    }

    /// @notice Withdraw LP tokens from MDV2.
    /// @param pid The index of the pool. See `poolInfo`.
    /// @param amount LP token amount to withdraw.
    /// @param to Receiver of the LP tokens.
    function withdraw(uint256 pid, uint256 amount, address to) public {
        PoolInfo memory pool = updatePool(pid);
        UserInfo storage user = userInfo[pid][msg.sender];

        // Effects
        user.rewardDebt = user.rewardDebt.sub(int256(amount.mul(pool.accInnovaiPerShare) / ACC_INNAI_PRECISION));
        user.amount = user.amount.sub(amount);

        // Interactions
        IRewarder _rewarder = rewarder[pid];
        if (address(_rewarder) != address(0)) {
            _rewarder.onInnovaiReward(pid, msg.sender, to, 0, user.amount);
        }
        
        lpToken[pid].safeTransfer(to, amount);

        emit Withdraw(msg.sender, pid, amount, to);
    }

    /// @notice Harvest proceeds for transaction sender to `to`.
    /// @param pid The index of the pool. See `poolInfo`.
    /// @param to Receiver of INNAI rewards.
    function harvest(uint256 pid, address to) public {
        PoolInfo memory pool = updatePool(pid);
        UserInfo storage user = userInfo[pid][msg.sender];
        int256 accumulatedInnovai = int256(user.amount.mul(pool.accInnovaiPerShare) / ACC_INNAI_PRECISION);
        uint256 _pendingInnovai = accumulatedInnovai.sub(user.rewardDebt).toUInt256();

        // Effects
        user.rewardDebt = accumulatedInnovai;

        // Interactions
        if (_pendingInnovai != 0) {
            INNAI.safeTransfer(to, _pendingInnovai);
        }
        
        IRewarder _rewarder = rewarder[pid];
        if (address(_rewarder) != address(0)) {
            _rewarder.onInnovaiReward( pid, msg.sender, to, _pendingInnovai, user.amount);
        }

        emit Harvest(msg.sender, pid, _pendingInnovai);
    }
    
    /// @notice Withdraw LP tokens from MDV2 and harvest proceeds for transaction sender to `to`.
    /// @param pid The index of the pool. See `poolInfo`.
    /// @param amount LP token amount to withdraw.
    /// @param to Receiver of the LP tokens and INNAI rewards.
    function withdrawAndHarvest(uint256 pid, uint256 amount, address to) public {
        PoolInfo memory pool = updatePool(pid);
        UserInfo storage user = userInfo[pid][msg.sender];
        int256 accumulatedInnovai = int256(user.amount.mul(pool.accInnovaiPerShare) / ACC_INNAI_PRECISION);
        uint256 _pendingInnovai = accumulatedInnovai.sub(user.rewardDebt).toUInt256();

        // Effects
        user.rewardDebt = accumulatedInnovai.sub(int256(amount.mul(pool.accInnovaiPerShare) / ACC_INNAI_PRECISION));
        user.amount = user.amount.sub(amount);
        
        // Interactions
        INNAI.safeTransfer(to, _pendingInnovai);

        IRewarder _rewarder = rewarder[pid];
        if (address(_rewarder) != address(0)) {
            _rewarder.onInnovaiReward(pid, msg.sender, to, _pendingInnovai, user.amount);
        }

        lpToken[pid].safeTransfer(to, amount);

        emit Withdraw(msg.sender, pid, amount, to);
        emit Harvest(msg.sender, pid, _pendingInnovai);
    }

    /// @notice Harvests INNAI from `MASTER_DEVELOPER` MDV1 and pool `MASTER_PID` to this MDV2 contract.
    function harvestFromMasterDeveloper() public {
        MASTER_DEVELOPER.deposit(MASTER_PID, 0);
    }

    /// @notice Withdraw without caring about rewards. EMERGENCY ONLY.
    /// @param pid The index of the pool. See `poolInfo`.
    /// @param to Receiver of the LP tokens.
    function emergencyWithdraw(uint256 pid, address to) public {
        UserInfo storage user = userInfo[pid][msg.sender];
        uint256 amount = user.amount;
        user.amount = 0;
        user.rewardDebt = 0;

        IRewarder _rewarder = rewarder[pid];
        if (address(_rewarder) != address(0)) {
            _rewarder.onInnovaiReward(pid, msg.sender, to, 0, 0);
        }

        // Note: transfer can fail or succeed if `amount` is zero.
        lpToken[pid].safeTransfer(to, amount);
        emit EmergencyWithdraw(msg.sender, pid, amount, to);
    }
}
