/*

    Copyright 2020 DODO ZOO.
    SPDX-License-Identifier: Apache-2.0

*/

pragma solidity 0.6.9;
pragma experimental ABIEncoderV2;

import {IQuota} from "../../DODOFee/UserQuota.sol";
import {SafeMath} from "../../lib/SafeMath.sol";
import {DecimalMath} from "../../lib/DecimalMath.sol";
import {IERC20} from "../../intf/IERC20.sol";
import {SafeERC20} from "../../lib/SafeERC20.sol";
import {Vesting} from "./Vesting.sol";


contract InstantFunding is Vesting {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    // ============ Instant Commit Mode ============
    uint256 public _START_PRICE_;
    uint256 public _END_PRICE_;

    mapping(address => uint256) _FUNDS_USED_;
    mapping(address => uint256) _TOKEN_ALLOCATION_;
    uint256 public _TOTAL_ALLOCATED_TOKEN_;

    // ============ Events ============
    event DepositFund(address indexed account, uint256 fundAmount, uint256 allocationAmount);
    event ClaimToken(address indexed caller, address indexed to, uint256 tokenAmount);

    event WithdrawUnallocatedToken(address indexed to, uint256 tokenAmount);
    event InitializeLiquidity(address pool, uint256 tokenAmount);
    event ClaimFund(address indexed to, uint256 fundAmount);

    // ============ Init ============
    function init(
        address[] calldata addressList,
        uint256[] calldata timeLine,
        uint256[] calldata valueList
    ) external {
        /*
        Address List
        0. owner
        1. sellToken
        2. fundToken
        3. quotaManager
        4. poolFactory
      */

        require(addressList.length == 5, "ADDR_LENGTH_WRONG");

        initOwner(addressList[0]);
        _TOKEN_ADDRESS_ = addressList[1];
        _FUNDS_ADDRESS_ = addressList[2];
        _QUOTA_ = addressList[3];
        _POOL_FACTORY_ = addressList[4];

        /*
        Time Line
        0. starttime
        1. bid duration
        2. token vesting starttime
        3. token vesting duration
        4. fund vesting starttime
        5. fund vesting duration
        6. lp vesting starttime
        7. lp vesting duration
        */

        require(timeLine.length == 8, "TIME_LENGTH_WRONG");

        _START_TIME_ = timeLine[0];
        _BIDDING_DURATION_ = timeLine[1];

        _TOKEN_VESTING_START_ = timeLine[2];
        _TOKEN_VESTING_DURATION_ = timeLine[3];

        _FUNDS_VESTING_START_ = timeLine[4];
        _FUNDS_VESTING_DURATION_ = timeLine[5];

        _LP_VESTING_START_ = timeLine[6];
        _LP_VESTING_DURATION_ = timeLine[7];

        require(block.timestamp <= _START_TIME_, "START_TIME_WRONG");
        require(_START_TIME_.add(_BIDDING_DURATION_) <= _TOKEN_VESTING_START_, "TOKEN_VESTING_TIME_WRONG");
        require(_START_TIME_.add(_BIDDING_DURATION_) <= _FUNDS_VESTING_START_, "FUND_VESTING_TIME_WRONG");
        require(_START_TIME_.add(_BIDDING_DURATION_) <= _LP_VESTING_START_, "LP_VESTING_TIME_WRONG");

        /*
        Value List
        0. start price
        1. end price
        2. token cliffRate
        3. fund cliffRate
        4. lp cliffRate
        5. initial liquidity
        */

        require(valueList.length == 6, "VALUE_LENGTH_WRONG");

        _START_PRICE_ = valueList[0];
        _END_PRICE_ = valueList[1];

        _TOKEN_CLIFF_RATE_ = valueList[2];
        _FUNDS_CLIFF_RATE_ = valueList[3];
        _LP_CLIFF_RATE_ = valueList[4];

        _INITIAL_FUND_LIQUIDITY_ = valueList[5];

        require(_START_PRICE_ > 0, "START_PRICE_INVALID");
        require(_END_PRICE_ > 0, "END_PRICE_INVALID");
        require(_TOKEN_CLIFF_RATE_ <= 1e18, "TOKEN_CLIFF_RATE_WRONG");
        require(_FUNDS_CLIFF_RATE_ <= 1e18, "FUND_CLIFF_RATE_WRONG");
        require(_LP_CLIFF_RATE_ <= 1e18, "LP_CLIFF_RATE_WRONG");

        _TOTAL_TOKEN_AMOUNT_ = IERC20(_TOKEN_ADDRESS_).balanceOf(address(this));

        require(_TOTAL_TOKEN_AMOUNT_ > 0, "NO_TOKEN_TRANSFERED");
    }

    // ============ View Functions ============
    function getCurrentPrice() public view returns (uint256 price) {
        if (block.timestamp <= _START_TIME_) {
            price = _START_PRICE_;
        } else if (block.timestamp >= _START_TIME_.add(_BIDDING_DURATION_)) {
            price = _END_PRICE_;
        } else {
            uint256 timePast = block.timestamp.sub(_START_TIME_);
            price = _START_PRICE_.mul(_BIDDING_DURATION_.sub(timePast)).div(_BIDDING_DURATION_).add(
                _END_PRICE_.mul(timePast).div(_BIDDING_DURATION_)
            );
        }
    }

    function getUserTokenAllocation(address user) public view returns (uint256) {
        return _TOKEN_ALLOCATION_[user];
    }

    function getUserFundsUsed(address user) public view returns (uint256) {
        return _FUNDS_USED_[user];
    }

    // ============ Funding Functions ============

    function depositFunds(address to)
        external
        preventReentrant
        isNotForceStop
        returns (uint256 newTokenAllocation)
    {
        require(isDepositOpen(), "DEPOSIT_NOT_OPEN");
        // input fund check
        uint256 inputFund = IERC20(_FUNDS_ADDRESS_).balanceOf(address(this)).sub(_FUNDS_RESERVE_);

        if (_QUOTA_ != address(0)) {
            require(
                inputFund.add(_FUNDS_USED_[to]) <= uint256(IQuota(_QUOTA_).getUserQuota(to)),
                "QUOTA_EXCEED"
            );
        }

        // allocation calculation
        uint256 currentPrice = getCurrentPrice();
        newTokenAllocation = DecimalMath.divFloor(inputFund, currentPrice);

        uint256 depositFundAmount = inputFund;
        if (newTokenAllocation.add(_TOTAL_ALLOCATED_TOKEN_) > _TOTAL_TOKEN_AMOUNT_) {
            newTokenAllocation = _TOTAL_TOKEN_AMOUNT_.sub(_TOTAL_ALLOCATED_TOKEN_);
            uint256 fundUsed = DecimalMath.mulFloor(newTokenAllocation, currentPrice);
            _FUNDS_USED_[to] = _FUNDS_USED_[to].add(fundUsed);
            _TOTAL_RAISED_FUNDS_ = _TOTAL_RAISED_FUNDS_.add(fundUsed);
            _FUNDS_RESERVE_ = _FUNDS_RESERVE_.add(fundUsed);
            depositFundAmount = fundUsed;

            IERC20(_FUNDS_ADDRESS_).safeTransfer(to, inputFund.sub(fundUsed));
        } else {
            _FUNDS_USED_[to] = _FUNDS_USED_[to].add(inputFund);
            _TOTAL_RAISED_FUNDS_ = _TOTAL_RAISED_FUNDS_.add(inputFund);
            _FUNDS_RESERVE_ = _FUNDS_RESERVE_.add(inputFund);
        }

        _TOKEN_ALLOCATION_[to] = _TOKEN_ALLOCATION_[to].add(newTokenAllocation);
        _TOTAL_ALLOCATED_TOKEN_ = _TOTAL_ALLOCATED_TOKEN_.add(newTokenAllocation);

        emit DepositFund(to, depositFundAmount, newTokenAllocation);
    }

    function claimToken(address to) external preventReentrant {
        uint256 totalAllocation = getUserTokenAllocation(msg.sender);
        uint256 claimableTokenAmount = _claimToken(to, totalAllocation);

        emit ClaimToken(msg.sender, to, claimableTokenAmount);
    }

    // ============ Ownable Functions ============

    function withdrawUnallocatedToken(address to) external preventReentrant onlyOwner {
        require(isFundingEnd(), "CAN_NOT_WITHDRAW");
        uint256 unallocatedAmount = _TOTAL_TOKEN_AMOUNT_.sub(_TOTAL_ALLOCATED_TOKEN_);
        IERC20(_TOKEN_ADDRESS_).safeTransfer(to, unallocatedAmount);
        _TOTAL_TOKEN_AMOUNT_ = _TOTAL_ALLOCATED_TOKEN_;

        emit WithdrawUnallocatedToken(to, unallocatedAmount);
    }

    function initializeLiquidity(uint256 initialTokenAmount, uint256 lpFeeRate, bool isOpenTWAP) external preventReentrant onlyOwner {
        require(isFundingEnd(),"FUNDING_NOT_FINISHED");
        _initializeLiquidity(initialTokenAmount, _TOTAL_RAISED_FUNDS_, lpFeeRate, isOpenTWAP);

        emit InitializeLiquidity(_INITIAL_POOL_, initialTokenAmount);
    }

    function claimFund(address to) external preventReentrant onlyOwner {
        uint256 claimableFund = _claimFunds(to,_TOTAL_RAISED_FUNDS_);

        emit ClaimFund(to, claimableFund);
    }

    // ============ Timeline Control Functions ============

    function isDepositOpen() public view returns (bool) {
        return
            block.timestamp >= _START_TIME_ &&
            block.timestamp < _START_TIME_.add(_BIDDING_DURATION_);
    }

    function isFundingEnd() public view returns (bool) {
        return block.timestamp > _START_TIME_.add(_BIDDING_DURATION_);
    }


    // ============ Version Control ============

    function version() virtual public pure returns (string memory) {
        return "InstantFunding 1.0.0";
    }

    // ============ View Helper  ==============
    function getCurrentFundingInfo(address user) external view returns(
        uint256 raiseFundAmount,
        uint256 userFundAmount,
        uint256 currentPrice,
        uint256 soldTokenAmount,
        uint256 claimableTokenAmount,
        bool isHaveCap,
        uint256 userQuota,
        uint256 userCurrentQuota
    ) {
        raiseFundAmount =_TOTAL_RAISED_FUNDS_;
        userFundAmount =  _FUNDS_USED_[user];
        currentPrice = getCurrentPrice();
        soldTokenAmount = _TOTAL_ALLOCATED_TOKEN_;

        if(block.timestamp > _TOKEN_VESTING_START_) {
            uint256 totalAllocation = getUserTokenAllocation(user);
            uint256 remainingToken = DecimalMath.mulFloor(
                getRemainingRatio(block.timestamp,0),
                totalAllocation
            );
            claimableTokenAmount = totalAllocation.sub(remainingToken).sub(_CLAIMED_TOKEN_[user]);
        }else {
            claimableTokenAmount = 0;
        }

        if(_QUOTA_ == address(0)) {
            isHaveCap = false;
            userQuota = uint256(-1);
            userCurrentQuota = uint256(-1);
        } else {
            isHaveCap = true;
            userQuota = uint256(IQuota(_QUOTA_).getUserQuota(user));
            if(userQuota > userFundAmount) {
                userCurrentQuota = userQuota - userFundAmount;
            } else {
                userCurrentQuota = 0;
            }
        }
    }


    function getBaseFundInfo() external view returns(
        address tokenAddress,
        address fundAddress,
        uint256 totalTokenAmount,
        uint256 price0, //_START_PRICE_
        uint256 price1, //_END_PRICE_
        string memory versionType,
        uint256 startTime,
        uint256 bidDuration,
        uint256 tokenVestingStart,
        uint256 tokenVestingDuration
    ) {
        tokenAddress = _TOKEN_ADDRESS_;
        fundAddress = _FUNDS_ADDRESS_;
        totalTokenAmount = _TOTAL_TOKEN_AMOUNT_;
        price0 = _START_PRICE_;
        price1 = _END_PRICE_;
        
        versionType = version();

        startTime = _START_TIME_;
        bidDuration = _BIDDING_DURATION_;
        tokenVestingStart = _TOKEN_VESTING_START_;
        tokenVestingDuration = _TOKEN_VESTING_DURATION_;
    }
}
