contract WarpControl is Ownable, Exponential {
    using SafeMath for uint256;

    SwapLPOracleI public oracle; //oracle contract 
    WarpVaultLPFactoryI public WVLPF;
    WarpVaultSCFactoryI public WVSCF;

    address public warpTeam;
    address public newWarpControl;
    uint256 public graceSpace;

    address[] public lpVaults;
    address[] public scVaults;

    mapping(address => address) public instanceLPTracker; //maps LP token address to the assets WarpVault
    mapping(address => address) public instanceSCTracker;
    mapping(address => address) public getAssetByVault;
    mapping(address => bool) public isVault;

    uint256 public borrowLimitPercentage = 66 ether;  // max percentage of collateral value allowed to borrow, with 18 decimals
    uint256 public liquidateLimitPercentage = 75 ether; // max percentage of collateral value before liquidation can occur
    uint256 public liquidationFee = 15 ether; // percentage of LP tokens to give to warp team upon liquidation

    event LogEvent2(uint256 lnum);
    event NewLPVault(address _newVault);
    event ImportedLPVault(address _vault);
    event NewSCVault(address _newVault, address _interestRateModel);
    event ImportedSCVault(address _vault);
    event NewBorrow(
        address _borrower,
        address _StableCoin,
        uint256 _amountBorrowed
    );
    event Liquidation(address _account, address liquidator);
    event NewInterestRateModelCreated(
      address _token,
      uint256 _baseRatePerYear,
      uint256 _multiplierPerYear,
      uint256 _jumpMultiplierPerYear,
      uint256 _optimal
    );

    /**
      @dev Throws if called by any account other than a warp vault
     */
    modifier onlyVault() {
        require(isVault[msg.sender] == true, "Only a vault may call this");
        _;
    }

    /**
    @notice the constructor function is fired during the contract deployment process. The constructor can only be fired once and
            is used to set up OracleFactory variables for the MoneyMarketFactory contract.
    @param _oracle is the address for the UniswapOracleFactorycontract
    @param _WVLPF is the address for the WarpVaultLPFactory used to produce LP Warp Vaults
    @param _WVSCF is the address for the WarpVaultSCFactory used to produce Stable Coin Warp Vaults
    @dev These factories are split into seperate contracts to avoid hitting the block gas limit
    **/
    constructor(
        address _oracle,
        address _WVLPF,
        address _WVSCF,
        address _warpTeam
    ) public {
        //instantiate the contracts
        oracle = SwapLPOracleI(_oracle);
        WVLPF = WarpVaultLPFactoryI(_WVLPF);
        WVSCF = WarpVaultSCFactoryI(_WVSCF);
        warpTeam = _warpTeam;
    }

    /**
    @notice viewNumLPVaults returns the number of lp vaults on the warp platform
    **/
    function viewNumLPVaults() external view returns (uint256) {
        return lpVaults.length;
    }

    /**
    @notice viewNumSCVaults returns the number of stablecoin vaults on the warp platform
    **/
    function viewNumSCVaults() external view returns (uint256) {
        return scVaults.length;
    }
     /**
    @notice createNewLPVault allows the contract owner to create a new WarpVaultLP contract for a specific LP token
    @param _timelock is a variable representing the number of seconds the timeWizard will prevent withdraws and borrows from a contracts(one week is 605800 seconds)
    @param _lp is the address for the LP token this Warp Vault will manage
    **/
    function createNewLPVault( 
        uint256 _timelock,
        address _lp
    ) public onlyOwner {
 
        require(instanceLPTracker[_lp] == address(0), "LP vault already exists");

        //create new Warp LP Vault
        address _WarpVault = WVLPF.createWarpVaultLP(_timelock, _lp);
        //track the warp vault lp instance by the address of the LP it represents
        instanceLPTracker[_lp] = _WarpVault;
        //add new LP Vault to the array of all LP vaults
        lpVaults.push(_WarpVault);
        //set Warp vault address as an approved vault
        isVault[_WarpVault] = true;
        //track vault to asset
        getAssetByVault[_WarpVault] = _lp;
        emit NewLPVault(_WarpVault);
    }

    function importLPVault(address _lpVault) public onlyOwner {
      require(isVault[_lpVault] == false);
        WarpVaultLPI _vault = WarpVaultLPI(_lpVault);
        address _lp = _vault.getAssetAdd();

        instanceLPTracker[_lp] = _lpVault;
        lpVaults.push(_lpVault);
        isVault[_lpVault] = true;
        getAssetByVault[_lpVault] = _lp;
        emit ImportedLPVault(_lpVault);
    }


    /**
    @notice createNewSCVault allows the contract owner to create a new WarpVaultLP contract for a specific LP token
    @param _timelock is a variable representing the number of seconds the timeWizard will prevent withdraws and borrows from a contracts(one week is 605800 seconds)
    @param _baseRatePerYear is the base rate per year(approx target base APR)
    @param _multiplierPerYear is the multiplier per year(rate of increase in interest w/ utilizastion)
    @param _jumpMultiplierPerYear is the Jump Multiplier Per Year(the multiplier per block after hitting a specific utilizastion point)
    @param _optimal is the this is the utilizastion point or "kink" at which the jump multiplier is applied
    @param _initialExchangeRate is the intitial exchange rate(the rate at which the initial exchange of asset/ART is set)
    @param _StableCoin is the address of the StableCoin this Warp Vault will manage
    **/
    function createNewSCVault(
        uint256 _timelock,
        uint256 _baseRatePerYear,
        uint256 _multiplierPerYear,
        uint256 _jumpMultiplierPerYear,
        uint256 _optimal,
        uint256 _initialExchangeRate,
        uint256 _reserveFactorMantissa,
        address _StableCoin
    ) public onlyOwner {
        //create the interest rate model for this stablecoin
        address IR = address(
            new JumpRateModelV2(
                _baseRatePerYear,
                _multiplierPerYear,
                _jumpMultiplierPerYear,
                _optimal,
                address(this)
            )
        );
        //create the SC Warp vault
        address _WarpVault = WVSCF.createNewWarpVaultSC(
            IR,
            _StableCoin,
            warpTeam,
            _initialExchangeRate,
            _timelock,
            _reserveFactorMantissa
        );
        //track the warp vault sc instance by the address of the stablecoin it represents
        instanceSCTracker[_StableCoin] = _WarpVault;
        //add new SC Vault to the array of all SC vaults
        scVaults.push(_WarpVault);
        //set Warp vault address as an approved vault
        isVault[_WarpVault] = true;
        //track vault to asset
        getAssetByVault[_WarpVault] = _StableCoin;
        emit NewSCVault(_WarpVault, IR);
    }

    function importSCVault(address _scVault) public onlyOwner {
      require(isVault[_scVault] == false);

        WarpVaultSCI _vault = WarpVaultSCI(_scVault);
        address _token = _vault.getSCAddress();

        // track token -> vault
        instanceSCTracker[_token] = _scVault;
        // vault list
        scVaults.push(_scVault);
        // register vault in mapping
        isVault[_scVault] = true;
        // track vault -> token
        getAssetByVault[_scVault] = _token;
        emit ImportedSCVault(_scVault);
    }

    function setBorrowThreshold(uint256 _borrowLimitPercentage) public onlyOwner {
        borrowLimitPercentage = _borrowLimitPercentage;
    }

    function setLiquidateThreshold(uint256 _liquidateimitPercentage) public onlyOwner{
        liquidateLimitPercentage = _liquidateimitPercentage;
    }

    function setLiquidationFee(uint256 _liquidationFee) public onlyOwner {
        liquidationFee = _liquidationFee;
    }

    /**
    @notice Figures out how much of a given LP token an account is allowed to withdraw
    @param account is the account being checked
    @param lpToken is the address of the lpToken the user wishes to withdraw
    @dev this function runs calculations to accrue interest for an up to date amount
     */
    function getMaxWithdrawAllowed(address account, address lpToken)
        public
        returns (uint256)
    {
        uint256 borrowedTotal = getTotalBorrowedValue(account);
        uint256 collateralValue = getTotalAvailableCollateralValue(account);
        uint256 requiredCollateral = calcCollateralRequired(borrowedTotal);
        if (collateralValue < requiredCollateral) {
            return 0;
        }
        uint256 leftoverCollateral = collateralValue.sub(requiredCollateral);
        uint256 lpValue = oracle.getUnderlyingPrice(lpToken);
        return leftoverCollateral.mul(1e18).div(lpValue);
    }


    /**
    @notice getTotalAvailableCollateralValue returns the total availible collaeral value for an account in USDC
    @param _account is the address whos collateral is being retreived
    @dev this function runs calculations to accrue interest for an up to date amount
    **/
    function getTotalAvailableCollateralValue(address _account)
        public
        returns (uint256)
    {
        //get the number of LP vaults the platform has
        uint256 numVaults = lpVaults.length;
        //initialize the totalCollateral variable to zero
        uint256 totalCollateral = 0;
        //loop through each lp wapr vault
        for (uint256 i = 0; i < numVaults; ++i) {
            //instantiate warp vault at that position
            WarpVaultLPI vault = WarpVaultLPI(lpVaults[i]);
            //retreive the address of its asset
            address asset = vault.getAssetAdd();
            //retrieve USD price of this asset
            uint256 assetPrice = oracle.getUnderlyingPrice(asset);

            uint256 accountCollateral = vault.collateralOfAccount(_account);
            //emit DebugValues(accountCollateral, assetPrice);

            //multiply the amount of collateral by the asset price and return it
            uint256 accountAssetsValue = accountCollateral.mul(assetPrice);
            //add value to total collateral
            totalCollateral = totalCollateral.add(accountAssetsValue);
        }
        //return total USDC value of all collateral
        return totalCollateral.div(1e18);
    }

    /**
    @notice getPriceOfCollateral returns the price of an lpToken
    @param lpToken is the address of the lp token
    @dev this function does not run calculations amd returns the previously calculated price
    **/
    function getPriceOfCollateral(address lpToken) public returns (uint256) {
        return oracle.getUnderlyingPrice(lpToken);
    }

    /**
    @notice viewPriceOfToken retrieves the price of a stablecoin
    @param token is the address of the stablecoin
    @param amount is the amount of stablecoin
    @dev this function does not run calculations amd returns the previously calculated price
    **/
    function getPriceOfToken(address token, uint256 amount)
        public
        returns (uint256)
    {
        return oracle.getPriceOfToken(token, amount);
    }

    /**
    @notice getTotalLentValue returns the total lent value for an account in USDC
    @param _account is the account whos lent value we are calculating
    **/
    function getTotalLentValue(address _account)
        public
        //view
        returns (uint256)
    {
        uint256 numSCVaults = scVaults.length;
        uint256 totalValue = 0;

        // Add up each stable coin vaults value
        for (uint256 i = 0; i < numSCVaults; ++i) {
            //instantiate each LP warp vault
            WarpVaultSCI WVSC = WarpVaultSCI(scVaults[i]);
            //retreive the amount user has borrowed from each stablecoin vault
            uint256 lentBalanceInStable = WVSC.viewAccountBalance(_account);
            if (lentBalanceInStable == 0) {
                continue;
            }
            uint256 usdcLentAmount = getPriceOfToken(
                WVSC.getSCAddress(),
                lentBalanceInStable
            );
            totalValue = totalValue.add(usdcLentAmount);
        }

        return totalValue;
    }

    /**
    @notice viewTotalBorrowedValue returns the total borrowed value for an account in USDC
    @param _account is the account whos borrowed value we are calculating
    @dev this function returns newly calculated values
    **/
    function getTotalBorrowedValue(address _account) public returns (uint256) {
        uint256 numSCVaults = scVaults.length;
        //initialize the totalBorrowedValue variable to zero
        uint256 totalBorrowedValue = 0;
        //loop through all stable coin vaults
        for (uint256 i = 0; i < numSCVaults; ++i) {
            //instantiate each LP warp vault
            WarpVaultSCI WVSC = WarpVaultSCI(scVaults[i]);
            //retreive the amount user has borrowed from each stablecoin vault
            uint256 borrowBalanceInStable = WVSC.borrowBalanceCurrent(_account);
            if (borrowBalanceInStable == 0) {
                continue;
            }
            uint256 usdcBorrowedAmount = getPriceOfToken(
                WVSC.getSCAddress(),
                borrowBalanceInStable
            );
            totalBorrowedValue = totalBorrowedValue.add(usdcBorrowedAmount);
        }
        //return total Borrowed Value
        return totalBorrowedValue;
    }

    function calcBorrowLimit(uint256 _collateralValue)
        public
        view
        returns (uint256)
    {
        return _collateralValue.mul(borrowLimitPercentage).div(100 ether);
    }

    function calcLiquidationLimit(uint256 _collateralValue)
        public
        view
        returns (uint256)
    {
        return _collateralValue.mul(liquidateLimitPercentage).div(100 ether);
    }

    /**
    @notice calcCollateralRequired returns the amount of collateral needed for an input borrow value
    liquidatteLimitPercentage
    @param _borrowAmount is the input borrow amount
    **/
    function calcCollateralRequired(uint256 _borrowAmount)
        public
        view
        returns (uint256)
    {
        return _borrowAmount.mul(100 ether).div(borrowLimitPercentage);
    }

    /**
    @notice getBorrowLimit returns the borrow limit for an account
    @param _account is the input account address
    @dev this calculation uses current values for calculations
    **/
    function getBorrowLimit(address _account) public returns (uint256) {
        uint256 availibleCollateralValue = getTotalAvailableCollateralValue(
            _account
        );

        return calcBorrowLimit(availibleCollateralValue);
    }

    function getLiquidationLimit(address _account) public returns (uint256) {
        uint256 availibleCollateralValue = getTotalAvailableCollateralValue(
            _account
        );

        return calcLiquidationLimit(availibleCollateralValue);
    }

    /**
    @notice borrowSC is the function an end user will call when they wish to borrow a stablecoin from the warp platform
    @param _StableCoin is the address of the stablecoin the user wishes to borrow
    @param _amount is the amount of that stablecoin the user wants to borrow
    **/
    function borrowSC(address _StableCoin, uint256 _amount) public {
        uint256 borrowedTotalInUSDC = getTotalBorrowedValue(msg.sender);
        uint256 borrowLimitInUSDC = getBorrowLimit(msg.sender);
        uint256 borrowAmountAllowedInUSDC = borrowLimitInUSDC.sub(
            borrowedTotalInUSDC
        );

        uint256 borrowAmountInUSDC = getPriceOfToken(_StableCoin, _amount);

        //require the amount being borrowed is less than or equal to the amount they are aloud to borrow
        require(
            borrowAmountAllowedInUSDC >= borrowAmountInUSDC,
            "Borrowing more than allowed"
        );

        //retreive stablecoin vault address being borrowed from and instantiate it
        WarpVaultSCI WV = WarpVaultSCI(instanceSCTracker[_StableCoin]);
        //call _borrow function on the stablecoin warp vault
        WV._borrow(_amount, msg.sender);
        emit NewBorrow(msg.sender, _StableCoin, _amount);
    }

    /**
    @notice liquidateAccount is used to liquidate a non-compliant loan after it has reached its 30 minute grace period
    @param _borrower is the address of the borrower whos loan is non-compliant
    **/
    function liquidateAccount(address _borrower) public {
        //require the liquidator is not also the borrower
        require(msg.sender != _borrower, "you cant liquidate yourself");
        //retreive the number of stablecoin vaults in the warp platform
        uint256 numSCVaults = scVaults.length;
        //retreive the number of LP vaults in the warp platform
        uint256 numLPVaults = lpVaults.length;
        // This is how much USDC worth of Stablecoin the user has borrowed
        uint256 borrowedAmount = 0;
        //initialize the stable coin balances array
        uint256[] memory scBalances = new uint256[](numSCVaults);
        // loop through and retreive the Borrowed Amount From All Vaults
        for (uint256 i = 0; i < numSCVaults; ++i) {
            //instantiate the vault at the current  position in the array
            WarpVaultSCI scVault = WarpVaultSCI(scVaults[i]);
            //retreive the borrowers borrow balance from this vault and add it to the scBalances array
            scBalances[i] = scVault.borrowBalanceCurrent(_borrower);
            uint256 borrowedAmountInUSDC = getPriceOfToken(
                getAssetByVault[address(scVault)],
                scBalances[i]
            );

            //add the borrowed amount to the total borrowed balance
            borrowedAmount = borrowedAmount.add(borrowedAmountInUSDC);
        }
        //retreve the USDC borrow limit for the borrower
        uint256 liquidationLimit = getLiquidationLimit(_borrower);
        
        //check if the borrow is less than the borrowed amount
        require(borrowedAmount > liquidationLimit, "Loan is still valid");

        // If it is Liquidate the account
        //loop through each SC vault so the  Liquidator can pay off Stable Coin loans
        for (uint256 i = 0; i < numSCVaults; ++i) {
            //instantiate the Warp SC Vault at the current position
            WarpVaultSCI scVault = WarpVaultSCI(scVaults[i]);
            //call repayLiquidatedLoan function to repay the loan
            scVault._repayLiquidatedLoan(
                _borrower,
                msg.sender,
                scBalances[i]
            );
        }
        //loop through each LP vault so the Liquidator gets the LP tokens the borrower had
        for (uint256 i = 0; i < numLPVaults; ++i) {
            //instantiate the Warp LP Vault at the current position
            WarpVaultLPI lpVault = WarpVaultLPI(lpVaults[i]);
            
            //call liquidateAccount function on that LP vault and transfer LP tokens to warp control
            lpVault._liquidateAccount(_borrower, address(this));

            IUniswapV2Pair lpToken = IUniswapV2Pair(lpVault.getAssetAdd());

            // transfer 15% of those tokens to the warp team
            lpToken.transfer(warpTeam, lpToken.balanceOf(address(this)) * liquidationFee / 100 ether);

            // transfer the remaining to liquidator
            lpToken.transfer(msg.sender, lpToken.balanceOf(address(this)));
        }
        emit Liquidation(_borrower, msg.sender);
    }

    /**
    @notice updateInterestRateModel allows the warp team to update the interest rate model for a stablecoin
    @param _token is the address of the stablecoin whos vault is having its interest rate updated
    @param _baseRatePerYear is the base rate per year(approx target base APR)
    @param _multiplierPerYear is the multiplier per year(rate of increase in interest w/ utilizastion)
    @param _jumpMultiplierPerYear is the Jump Multiplier Per Year(the multiplier per block after hitting a specific utilizastion point)
    @param _optimal is the this is the utilizastion point or "kink" at which the jump multiplier is applied
    **/
    function updateInterestRateModel(
        address _token,
        uint256 _baseRatePerYear,
        uint256 _multiplierPerYear,
        uint256 _jumpMultiplierPerYear,
        uint256 _optimal
    ) public onlyOwner {
        address IR = address(
            new JumpRateModelV2(
                _baseRatePerYear,
                _multiplierPerYear,
                _jumpMultiplierPerYear,
                _optimal,
                address(this)
            )
        );
        address vault = instanceSCTracker[_token];
        WarpVaultSCI WV = WarpVaultSCI(vault);
        WV.setNewInterestModel(IR);
        emit NewInterestRateModelCreated(
          _token,
           _baseRatePerYear,
          _multiplierPerYear,
          _jumpMultiplierPerYear,
          _optimal
        );
    }

    /**
    @notice startUpgradeTimer starts a two day timer signaling that this contract will soon be updated to a new version
    @param _newWarpControl is the address of the new Warp control contract being upgraded to
    **/
    function startUpgradeTimer(address _newWarpControl) public onlyOwner {
        newWarpControl = _newWarpControl;
        graceSpace = now.add(172800);
    }

    /**
    @notice upgradeWarp is used to upgrade the Warp platform to use a new version of the WarpControl contract
    **/
    function upgradeWarp() public onlyOwner {
        require(now >= graceSpace, "you cant ugrade yet, less than two days");
        require(newWarpControl != address(0), "no new warp control set");

        oracle.transferOwnership(newWarpControl);

        uint256 numVaults = lpVaults.length;
        uint256 numSCVaults = scVaults.length;

        for (uint256 i = 0; i < numVaults; ++i) {
            WarpVaultLPI vault = WarpVaultLPI(lpVaults[i]);
            vault.updateWarpControl(newWarpControl);
        }

        for (uint256 i = 0; i < numSCVaults; ++i) {
            WarpVaultSCI vault = WarpVaultSCI(scVaults[i]);
            vault.updateWarpControl(newWarpControl);
        }
    }

    /**
    @notice transferWarpTeam allows the wapr team address to be changed by the owner account
    @param _newWarp is the address of the new warp team
    **/
    function transferWarpTeam(address _newWarp) public onlyOwner {
        uint256 numSCVaults = scVaults.length;
        warpTeam = _newWarp;
        for (uint256 i = 0; i < numSCVaults; ++i) {
            WarpVaultSCI WVSC = WarpVaultSCI(scVaults[i]);
            WVSC.updateTeam(_newWarp);
        }
    }
}
/**
 WarpControl is designed to coordinate Warp Vaults
 This contract uses the OpenZeppelin contract Library to inherit functions from
 Ownable.sol
 **/
function getPriceOfCollateral(address lpToken) public returns (uint256) {
    return oracle.getUnderlyingPrice(lpToken);
}

/**
 @notice getPriceOfCollateral returns the price of an lpToken
 @param lpToken is the address of the lp token
 @dev this function does not run calculations amd returns the previously calculated price
 **/