contract SwapContract is Ownable {
    using SafeMath for uint256;

    IERC20 public tokenOutput;

    struct TokenInputInfo {
        address addr;
        uint256 rateInput;
        uint256 rateOutput;
    }
    mapping (uint256 => TokenInputInfo) public tokenInput;

    struct VipVesting {
        uint256 totalBalance;
        uint256 totalClaimed;
        uint256 start;
        uint256 end;
        uint256 releasePercentWhenStart;
        uint256 releasePercentEachMonth;
        uint256 claimedCheckPoint;

        // To calculate Reward
        uint256 rewardTokenDebt;
        uint256 rewardEthDebt;
    }
    mapping (address => VipVesting) public vestingList;
    mapping (address => bool) public isBlacklistWallet;

    uint256 public totalTokenForSwapping;
    uint256 public totalTokenForSeed;
    uint256 public totalTokenForPublic;

    uint256 public soldAmountSeed        = 0;
    uint256 public soldAmountPublic      = 0;
    uint256 public soldTotal             = 0;

    uint256 public TYPE_SEED = 1;
    uint256 public TYPE_PUBLIC = 2;

    uint256 public MONTH = 30 days;

    bool public swapEnabled;

    constructor() {}    
    
    function startSwap(address outputToken) public onlyOwner{
        require(swapEnabled == false, "Swap already started");
        tokenOutput = IERC20(outputToken);
        swapEnabled = true;
    }

    function stopSwap() public onlyOwner {
        swapEnabled = false;
    }

    function addInputTokenForSwap(uint256 _id, address _inputToken, uint256 _inputRate, uint256 _outputRate)public onlyOwner{
        require(_id < 3);
        tokenInput[_id].addr = _inputToken;
        tokenInput[_id].rateInput = _inputRate;
        tokenInput[_id].rateOutput = _outputRate;
    }

    receive() external payable {
    }

    function setBlacklistWallet(address account, bool blacklisted) external onlyOwner {
        isBlacklistWallet[account] = blacklisted;
    }

    function addOutputTokenForSwap(uint256 amount) public onlyOwner{    
        tokenOutput.transferFrom(msg.sender, address(this), amount);
        totalTokenForSwapping = totalTokenForSwapping.add(amount);
    }

    function ownerWithdrawToken(address tokenAddress, uint256 amount) public onlyOwner{    
        if(tokenAddress == address(tokenOutput)){
            require(amount < totalTokenForSwapping.sub(soldTotal), "You're trying withdraw an amount that exceed availabe balance");
            totalTokenForSwapping = totalTokenForSwapping.sub(amount);
        }
        IERC20(tokenAddress).transfer(msg.sender, amount);
    }

    function getClaimableInVesting(address account) public view returns (uint256){
        VipVesting memory vestPlan = vestingList[account];

        //Already withdraw all
        if(vestPlan.totalClaimed == vestPlan.totalBalance){
            return 0;
        }

        //No infor
        if(vestPlan.start == 0 || vestPlan.end == 0 || vestPlan.totalBalance == 0){
            return 0;
        }
        
        uint256 currentTime = block.timestamp;
        if(currentTime >= vestPlan.end){
            return vestPlan.totalBalance.sub(vestPlan.totalClaimed);
        }else {
            uint256 currentCheckPoint = (currentTime - vestPlan.start) / MONTH;
            if(currentCheckPoint > vestPlan.claimedCheckPoint){
                uint256 claimable =  ((currentCheckPoint - vestPlan.claimedCheckPoint)* vestPlan.releasePercentEachMonth * vestPlan.totalBalance) / 10000;
                return claimable;
            }else
                return 0;
        }
    }

    function balanceRemainingInVesting(address account) public view returns(uint256){
        VipVesting memory vestPlan = vestingList[account];
        return vestPlan.totalBalance -  vestPlan.totalClaimed;
    }

    function withDrawFromVesting() public {
        VipVesting storage vestPlan = vestingList[msg.sender];

        uint256 claimableAmount = getClaimableInVesting(msg.sender);
        require(claimableAmount > 0, "There isn't token in vesting that claimable at the moment");

        uint256 currentTime = block.timestamp;
        if(currentTime > vestPlan.end){
            currentTime = vestPlan.end;
        }
        
        vestPlan.claimedCheckPoint = (currentTime - vestPlan.start) / MONTH;
        vestPlan.totalClaimed = vestPlan.totalClaimed.add(claimableAmount);

        tokenOutput.transfer(msg.sender, claimableAmount);
    }

    function deposite(uint256 inputTokenId, uint256 inputAmount, uint256 buyType) public payable {
        require(inputTokenId < 3, "Invalid input token ID");
        require(isBlacklistWallet[msg.sender] == false, "You're in blacklist");
        require(swapEnabled, "Swap is not available");

        IERC20 inputToken = IERC20(tokenInput[inputTokenId].addr);

        uint256 numOutputToken = inputAmount.mul(tokenInput[inputTokenId].rateOutput).mul(10**tokenOutput.decimals()).div(tokenInput[inputTokenId].rateInput);
        if(buyType == TYPE_SEED)
            numOutputToken = numOutputToken.mul(3);
     
        require(numOutputToken < totalTokenForSwapping.sub(soldTotal), "Exceed avaialble token");

        inputToken.transferFrom(msg.sender, address(this), inputAmount.mul(10**inputToken.decimals()));
        soldTotal = soldTotal.add(numOutputToken);
        addingVestToken(msg.sender, numOutputToken, buyType);
    }

    function addingVestToken(address account, uint256 amount, uint256 vType) private {
        VipVesting storage vestPlan = vestingList[account];
        if(vType == TYPE_SEED){
            soldAmountSeed = soldAmountSeed.add(amount);
            vestPlan.releasePercentWhenStart = 833; // 8.33%
            vestPlan.releasePercentEachMonth = 833; // 8.33%
        }else{
            soldAmountPublic = soldAmountPublic.add(amount);
            tokenOutput.transfer(account, amount);
            return;
        }

        vestPlan.totalBalance = vestPlan.totalBalance.add(amount);
        vestPlan.start = vestPlan.start == 0 ? block.timestamp : vestPlan.start;
        vestPlan.end = vestPlan.end == 0 ? block.timestamp + ((10000 - vestPlan.releasePercentWhenStart)/vestPlan.releasePercentEachMonth) * MONTH : vestPlan.end;

        uint256 claimNow = (amount * vestPlan.releasePercentWhenStart)/10000;
        vestPlan.totalClaimed = vestPlan.totalClaimed.add(claimNow);

        tokenOutput.transfer(account, claimNow);
    }

    /*REWARD FOR VESTING*/
    address public rewardToken;
    uint256 public amountTokenForReward;
    uint256 public amountEthForReward;

    uint256 public totalRewardEthDistributed;
    uint256 public totalRewardTokenDistributed;

    uint256 public rewardTokenPerSecond;
    uint256 public rewardEthPerSecond;

    // Accrued token per share
    uint256 public accTokenPerShare;    
    // Accrued EHT per share
    uint256 public accEthPerShare;
    // The block number of the last pool update
    uint256 public lastRewardTime;
    // The precision factor
    uint256 public PRECISION_FACTOR = 10**12;

    bool public enableRewardSystem;

    function startRewardSystem(address _rewardToken) public onlyOwner{
        enableRewardSystem = true;
        rewardToken = _rewardToken;
        rewardTokenPerSecond = 0.05 ether;   // 0.65 token/block
        rewardEthPerSecond = 0.000004 ether; // 10 eth/month
        
        lastRewardTime = block.timestamp;
    }

    function setNewRewardToken(address _rewardToken)public onlyOwner{
        rewardToken = _rewardToken;
    }

    function setRewardTokenPerSecond(uint256 _rewardTokenPerSecond) public onlyOwner{
        rewardTokenPerSecond = _rewardTokenPerSecond;
    }

    function setRewardEthPerSecond(uint256 _rewardEthPerSecond) public onlyOwner{
        rewardEthPerSecond = _rewardEthPerSecond;
    }

    function addTokenForReward(uint256 amount) public {
        IERC20(rewardToken).transferFrom(msg.sender, address(this), amount);
        amountTokenForReward = amountTokenForReward.add(amount);
    }

    function addEthForReward() payable public {
        amountEthForReward = amountEthForReward.add(msg.value);
    }

     
    /*
     * Harvest reward
     */
    function harvest() public {
        _updatePool();
        VipVesting storage user = vestingList[msg.sender];

        if (user.totalBalance > 0) {
            uint256 pendingToken = user.totalBalance.mul(accTokenPerShare).div(PRECISION_FACTOR).sub(user.rewardTokenDebt);
            uint256 pendingEth = user.totalBalance.mul(accEthPerShare).div(PRECISION_FACTOR).sub(user.rewardEthDebt);
            if (pendingToken > 0) {
                IERC20(rewardToken).transfer( address(msg.sender), pendingToken);
            }
            if (pendingEth > 0) {
                payable(msg.sender).transfer(pendingEth);
            }
        }
        user.rewardTokenDebt = user.totalBalance.mul(accTokenPerShare).div(PRECISION_FACTOR);
        user.rewardEthDebt = user.totalBalance.mul(accEthPerShare).div(PRECISION_FACTOR);
    }

    /*
     * @notice View function to see pending reward on frontend.
     * @param _user: user address
     * @return Pending reward for a given user
     */
    function pendingReward(address _user) external view returns (uint256, uint256) {
        VipVesting storage user = vestingList[_user];
        uint256 pendingToken;
        uint256 pendingEth;
        
        if(enableRewardSystem ==  false){
            return (0, 0);
        }

        if (block.timestamp > lastRewardTime && soldAmountSeed != 0) {
            uint256 multiplier = _getMultiplier(lastRewardTime, block.timestamp);

            uint256 tokenReward = multiplier.mul(rewardTokenPerSecond);
            if(tokenReward > amountTokenForReward){
                tokenReward = amountTokenForReward;
            }
            uint256 adjustedTokenPerShare = accTokenPerShare.add(tokenReward.mul(PRECISION_FACTOR).div(soldAmountSeed));

            uint256 ethReward = multiplier.mul(rewardEthPerSecond);
            if(ethReward > amountEthForReward){
                ethReward = amountEthForReward;
            }
            uint256 adjustedEthPerShare = accEthPerShare.add(ethReward.mul(PRECISION_FACTOR).div(soldAmountSeed));

            pendingToken =  user.totalBalance.mul(adjustedTokenPerShare).div(PRECISION_FACTOR).sub(user.rewardTokenDebt);
            pendingEth =  user.totalBalance.mul(adjustedEthPerShare).div(PRECISION_FACTOR).sub(user.rewardEthDebt);
        } else {
            pendingToken = user.totalBalance.mul(accTokenPerShare).div(PRECISION_FACTOR).sub(user.rewardTokenDebt);
            pendingEth = user.totalBalance.mul(accEthPerShare).div(PRECISION_FACTOR).sub(user.rewardEthDebt);
        }

        return (pendingToken, pendingEth);
    }


    /*
     * @notice Update reward variables of the given pool to be up-to-date.
     */
    function _updatePool() internal {
        if (block.timestamp <= lastRewardTime) {
            return;
        }

        if (enableRewardSystem == false || soldAmountSeed == 0) {
            lastRewardTime = block.timestamp;
            return;
        }

        uint256 multiplier = _getMultiplier(lastRewardTime, block.timestamp);

        uint256 tokenReward = multiplier.mul(rewardTokenPerSecond);
        if(tokenReward > amountTokenForReward){
            tokenReward = amountTokenForReward;
        }
        accTokenPerShare = accTokenPerShare.add(tokenReward.mul(PRECISION_FACTOR).div(soldAmountSeed));
        amountTokenForReward = amountTokenForReward.sub(tokenReward);
        totalRewardTokenDistributed = totalRewardTokenDistributed.add(tokenReward);


        uint256 ethReward = multiplier.mul(rewardEthPerSecond);
        if(ethReward > amountEthForReward){
            ethReward = amountEthForReward;
        }
        accEthPerShare = accEthPerShare.add(ethReward.mul(PRECISION_FACTOR).div(soldAmountSeed));
        amountEthForReward = amountEthForReward.sub(ethReward);
        totalRewardEthDistributed = totalRewardEthDistributed.add(ethReward);

        lastRewardTime = block.timestamp;
    }

    /*
     * @notice Return reward multiplier over the given _from to _to block.
     * @param _from: block to start
     * @param _to: block to finish
     */
    function _getMultiplier(uint256 _from, uint256 _to) internal pure returns (uint256) {
            return _to.sub(_from);
    }


}
function _updatePool() internal {
    if (block.timestamp <= lastRewardTime) {
        return;
    }

    if (enableRewardSystem == false || soldAmountSeed == 0) {
        lastRewardTime = block.timestamp;
        return;
    }

    uint256 multiplier = _getMultiplier(lastRewardTime, block.timestamp);

    uint256 tokenReward = multiplier.mul(rewardTokenPerSecond);
    if(tokenReward > amountTokenForReward){
        tokenReward = amountTokenForReward;
    }
    accTokenPerShare = accTokenPerShare.add(tokenReward.mul(PRECISION_FACTOR).div(soldAmountSeed));
    amountTokenForReward = amountTokenForReward.sub(tokenReward);
    totalRewardTokenDistributed = totalRewardTokenDistributed.add(tokenReward);


    uint256 ethReward = multiplier.mul(rewardEthPerSecond);
    if(ethReward > amountEthForReward){
        ethReward = amountEthForReward;
    }
    accEthPerShare = accEthPerShare.add(ethReward.mul(PRECISION_FACTOR).div(soldAmountSeed));
    amountEthForReward = amountEthForReward.sub(ethReward);
    totalRewardEthDistributed = totalRewardEthDistributed.add(ethReward);

    lastRewardTime = block.timestamp;
}

/*
 * @notice Update reward variables of the given pool to be up-to-date.
 */