contract LendingPair is ReentrancyGuard, TransferHelper {

  // Prevents division by zero and other undesirable behaviour
  uint public constant MIN_RESERVE = 1000;

  using Address for address;
  using Clones for address;

  mapping (address => mapping (address => uint)) public debtOf;
  mapping (address => mapping (address => uint)) public accountInterestSnapshot;
  mapping (address => uint) public cumulativeInterestRate; // 100e18 = 100%
  mapping (address => uint) public totalDebt;
  mapping (address => IERC20) public lpToken;

  IController public controller;
  address public tokenA;
  address public tokenB;
  uint public lastBlockAccrued;

  event Liquidation(
    address indexed account,
    uint supplyBurnA,
    uint supplyBurnB,
    uint borrowBurnA,
    uint borrowBurnB
  );

  event Deposit(address indexed token, uint amount);
  event Withdraw(address indexed token, uint amount);
  event Borrow(address indexed token, uint amount);
  event Repay(address indexed token, uint amount);

  receive() external payable {}

  function initialize(
    address _lpTokenMaster,
    address _controller,
    IERC20 _tokenA,
    IERC20 _tokenB
  ) public {
    require(address(tokenA) == address(0), "LendingPair: already initialized");
    require(address(_tokenA) != address(0) && address(_tokenB) != address(0), "LendingPair: cannot be ZERO address");

    controller = IController(_controller);
    tokenA = address(_tokenA);
    tokenB = address(_tokenB);
    lastBlockAccrued = block.number;

    lpToken[tokenA] = _createLpToken(_lpTokenMaster);
    lpToken[tokenB] = _createLpToken(_lpTokenMaster);
  }

  function depositRepay(address _token, uint _amount) public {
    _validateToken(_token);
    accrueAccount(msg.sender);

    _depositRepay(_token, _amount);
    _safeTransferFrom(_token, msg.sender, _amount);
  }

  function depositRepayETH() public payable {
    accrueAccount(msg.sender);

    _depositRepay(address(WETH), msg.value);
    _depositWeth();
  }

  function deposit(address _token, uint _amount) public {
    _validateToken(_token);
    accrueAccount(msg.sender);

    _deposit(_token, _amount);
    _safeTransferFrom(_token, msg.sender, _amount);
  }

  function withdrawBorrow(address _token, uint _amount) public {
    _validateToken(_token);
    accrueAccount(msg.sender);

    _withdrawBorrow(_token, _amount);
    _safeTransfer(IERC20(_token), msg.sender, _amount);
  }

  function withdrawBorrowETH(uint _amount) public {
    accrueAccount(msg.sender);

    _withdrawBorrow(address(WETH), _amount);
    _wethWithdrawTo(msg.sender, _amount);
    _checkMinReserve(address(WETH));
  }

  function withdraw(address _token, uint _amount) public {
    _validateToken(_token);
    accrueAccount(msg.sender);

    _withdraw(_token, _amount);
    _safeTransfer(IERC20(_token), msg.sender, _amount);
  }

  function withdrawAll(address _token) public {
    _validateToken(_token);
    accrueAccount(msg.sender);

    uint amount = lpToken[address(_token)].balanceOf(msg.sender);
    _withdraw(_token, amount);
    _safeTransfer(IERC20(_token), msg.sender, amount);
  }

  function borrow(address _token, uint _amount) public {
    _validateToken(_token);
    accrueAccount(msg.sender);

    _borrow(_token, _amount);
    _safeTransfer(IERC20(_token), msg.sender, _amount);
  }

  function repayAll(address _token) public {
    _validateToken(_token);
    accrueAccount(msg.sender);

    uint amount = debtOf[_token][msg.sender];
    _repay(_token, amount);
    _safeTransferFrom(_token, msg.sender, amount);
  }

  function repay(address _token, uint _amount) public {
    _validateToken(_token);
    accrueAccount(msg.sender);

    _repay(_token, _amount);
    _safeTransferFrom(_token, msg.sender, _amount);
  }

  function accrue() public {
    if (lastBlockAccrued < block.number) {
      _accrueInterest(tokenA);
      _accrueInterest(tokenB);
      lastBlockAccrued = block.number;
    }
  }

  function accrueAccount(address _account) public {
    accrue();
    _accrueAccountInterest(feeRecipient());
    _accrueAccountInterest(_account);
  }

  function accountHealth(address _account) public view returns(uint) {
    uint totalAccountSupply  = _supplyBalance(_account, tokenA, tokenA) + _supplyBalance(_account, tokenB, tokenA);
    uint totalAccountBorrrow = _borrowBalance(_account, tokenA, tokenA) + _borrowBalance(_account, tokenB, tokenA);

    if (totalAccountBorrrow == 0) {
      return controller.liqMinHealth();
    } else {
      return totalAccountSupply * 1e18 / totalAccountBorrrow;
    }
  }

  // Get borow balance converted to the units of _returnToken
  function borrowBalance(
    address _account,
    address _borrowedToken,
    address _returnToken
  ) public view returns(uint) {

    _validateToken(_borrowedToken);
    _validateToken(_returnToken);

    return _borrowBalance(_account, _borrowedToken, _returnToken);
  }

  function supplyBalance(
    address _account,
    address _suppliedToken,
    address _returnToken
  ) public view returns(uint) {

    _validateToken(_suppliedToken);
    _validateToken(_returnToken);

    return _supplyBalance(_account, _suppliedToken, _returnToken);
  }

  function supplyRatePerBlock(address _token) public view returns(uint) {
    _validateToken(_token);
    return controller.interestRateModel().supplyRatePerBlock(ILendingPair(address(this)), _token);
  }

  function borrowRatePerBlock(address _token) public view returns(uint) {
    _validateToken(_token);
    return _borrowRatePerBlock(_token);
  }

  // Sell collateral to reduce debt and increase accountHealth
  function liquidateAccount(address _account) public {

    _accrueAccountInterest(_account);
    _accrueAccountInterest(msg.sender);
    _accrueAccountInterest(feeRecipient());

    uint health = accountHealth(_account);
    require(health < controller.liqMinHealth(), "LendingPair: account health > liqMinHealth");

    (uint supplyBurnA, uint borrowBurnA) = _liquidateToken(_account, tokenA, tokenB);
    (uint supplyBurnB, uint borrowBurnB) = _liquidateToken(_account, tokenB, tokenA);

    emit Liquidation(_account, supplyBurnA, supplyBurnB, borrowBurnA, borrowBurnB);
  }

  function pendingSupplyInterest(address _token, address _account) public view returns(uint) {
    _validateToken(_token);
    uint newInterest = _newInterest(lpToken[_token].balanceOf(_account), _token, _account);
    return newInterest * _lpRate() / 100e18;
  }

  function pendingBorrowInterest(address _token, address _account) public view returns(uint) {
    _validateToken(_token);
    return _pendingBorrowInterest(_token, _account);
  }

  function feeRecipient() public view returns(address) {
    return controller.feeRecipient();
  }

  function checkAccountHealth(address _account) public view  {
    uint health = accountHealth(_account);
    require(health >= controller.liqMinHealth(), "LendingPair: insufficient accountHealth");
  }

  function convertTokenValues(
    address _fromToken,
    address _toToken,
    uint    _inputAmount
  ) public view returns(uint) {

    _validateToken(_fromToken);
    _validateToken(_toToken);

    return _convertTokenValues(_fromToken, _toToken, _inputAmount);
  }

  function _depositRepay(address _token, uint _amount) internal {

    uint debt = debtOf[_token][msg.sender];
    uint repayAmount = debt > _amount ? _amount : debt;

    if (repayAmount > 0) {
      _repay(_token, repayAmount);
    }

    uint depositAmount = _amount - repayAmount;

    if (depositAmount > 0) {
      _deposit(_token, depositAmount);
    }
  }

  function _withdrawBorrow(address _token, uint _amount) internal {

    uint supplyAmount = lpToken[_token].balanceOf(msg.sender);
    uint withdrawAmount = supplyAmount > _amount ? _amount : supplyAmount;

    if (withdrawAmount > 0) {
      _withdraw(_token, withdrawAmount);
    }

    uint borrowAmount = _amount - withdrawAmount;

    if (borrowAmount > 0) {
      _borrow(_token, borrowAmount);
    }
  }

  function _mintDebt(address _token, address _account, uint _amount) internal {
    debtOf[_token][_account] += _amount;
    totalDebt[_token] += _amount;
  }

  function _burnDebt(address _token, address _account, uint _amount) internal {
    debtOf[_token][_account] -= _amount;
    totalDebt[_token] -= _amount;
  }

  function _liquidateToken(
    address _account,
    address _supplyToken,
    address _borrowToken
  ) internal returns(uint, uint) {

    uint accountSupply  = lpToken[_supplyToken].balanceOf(_account);
    uint accountDebt    = debtOf[_borrowToken][_account];
    uint supplyDebt     = _convertTokenValues(_borrowToken, _supplyToken, accountDebt);
    uint supplyRequired = supplyDebt + supplyDebt * controller.liqFeesTotal() / 100e18;

    uint supplyBurn = supplyRequired > accountSupply ? accountSupply : supplyRequired;

    uint supplyBurnMinusFees = supplyBurn * 100e18 / (100e18 + controller.liqFeesTotal());
    uint systemFee = supplyBurnMinusFees * controller.liqFeeSystem() / 100e18;
    uint callerFee = supplyBurnMinusFees * controller.liqFeeCaller() / 100e18;

    lpToken[_supplyToken].burn(_account, supplyBurn);
    lpToken[_supplyToken].mint(msg.sender, callerFee);
    lpToken[_supplyToken].mint(feeRecipient(), systemFee);

    _burnDebt(_borrowToken, _account, accountDebt);

    return (supplyBurn, accountDebt);
  }

  function _accrueAccountInterest(address _account) internal {
    uint lpBalanceA = lpToken[tokenA].balanceOf(_account);
    uint lpBalanceB = lpToken[tokenB].balanceOf(_account);

    _accrueAccountSupply(tokenA, lpBalanceA, _account);
    _accrueAccountSupply(tokenB, lpBalanceB, _account);
    _accrueAccountDebt(tokenA, _account);
    _accrueAccountDebt(tokenB, _account);

    accountInterestSnapshot[tokenA][_account] = cumulativeInterestRate[tokenA];
    accountInterestSnapshot[tokenB][_account] = cumulativeInterestRate[tokenB];
  }

  function _accrueAccountSupply(address _token, uint _amount, address _account) internal {
    uint supplyInterest   = _newInterest(_amount, _token, _account);
    uint newSupplyAccount = supplyInterest * _lpRate() / 100e18;
    uint newSupplySystem  = supplyInterest * _systemRate() / 100e18;

    lpToken[_token].mint(_account, newSupplyAccount);
    lpToken[_token].mint(feeRecipient(), newSupplySystem);
  }

  function _accrueAccountDebt(address _token, address _account) internal {
    uint newDebt = _pendingBorrowInterest(_token, _account);
    _mintDebt(_token, _account, newDebt);
  }

  function _withdraw(address _token, uint _amount) internal {

    lpToken[address(_token)].burn(msg.sender, _amount);

    checkAccountHealth(msg.sender);

    emit Withdraw(_token, _amount);
  }

  function _borrow(address _token, uint _amount) internal {

    require(lpToken[address(_token)].balanceOf(msg.sender) == 0, "LendingPair: cannot borrow supplied token");

    _mintDebt(_token, msg.sender, _amount);

    checkAccountHealth(msg.sender);

    emit Borrow(_token, _amount);
  }

  function _repay(address _token, uint _amount) internal {
    _burnDebt(_token, msg.sender, _amount);

    emit Repay(_token, _amount);
  }

  function _deposit(address _token, uint _amount) internal {
    _checkDepositLimit(_token, _amount);

    require(debtOf[_token][msg.sender] == 0, "LendingPair: cannot deposit borrowed token");

    lpToken[address(_token)].mint(msg.sender, _amount);

    emit Deposit(_token, _amount);
  }

  function _accrueInterest(address _token) internal {
    uint blocksElapsed = block.number - lastBlockAccrued;
    uint newInterest = _borrowRatePerBlock(_token) * blocksElapsed;
    cumulativeInterestRate[_token] += newInterest;
  }

  function _createLpToken(address _lpTokenMaster) internal returns(IERC20) {
    IERC20 newLPToken = IERC20(_lpTokenMaster.clone());
    newLPToken.initialize();
    return newLPToken;
  }

  function _safeTransfer(IERC20 _token, address _recipient, uint _amount) internal {
    _token.transfer(_recipient, _amount);
    _checkMinReserve(address(_token));
  }

  function _wethWithdrawTo(address _to, uint _amount) internal override {
    TransferHelper._wethWithdrawTo(_to, _amount);
    _checkMinReserve(address(WETH));
  }

  function _borrowRatePerBlock(address _token) internal view returns(uint) {
    return controller.interestRateModel().borrowRatePerBlock(ILendingPair(address(this)), _token);
  }

  function _pendingBorrowInterest(address _token, address _account) internal view returns(uint) {
    return _newInterest(debtOf[_token][_account], _token, _account);
  }

  function _borrowBalance(
    address _account,
    address _borrowedToken,
    address _returnToken
  ) internal view returns(uint) {

    return _convertTokenValues(_borrowedToken, _returnToken, debtOf[_borrowedToken][_account]);
  }

  // Get supply balance converted to the units of _returnToken
  function _supplyBalance(
    address _account,
    address _suppliedToken,
    address _returnToken
  ) internal view returns(uint) {

    return _convertTokenValues(_suppliedToken, _returnToken, lpToken[_suppliedToken].balanceOf(_account));
  }

  function _convertTokenValues(
    address _fromToken,
    address _toToken,
    uint    _inputAmount
  ) internal view returns(uint) {

    uint priceFrom = controller.tokenPrice(_fromToken) * 1e18 / 10 ** IERC20(_fromToken).decimals();
    uint priceTo   = controller.tokenPrice(_toToken)   * 1e18 / 10 ** IERC20(_toToken).decimals();

    return _inputAmount * priceFrom / priceTo;
  }

  function _validateToken(address _token) internal view {
    require(_token == tokenA || _token == tokenB, "LendingPair: invalid token");
  }

  function _checkMinReserve(address _token) internal view {
    require(IERC20(_token).balanceOf(address(this)) >= MIN_RESERVE, "LendingPair: below MIN_RESERVE");
  }

  function _checkDepositLimit(address _token, uint _amount) internal view {
    uint depositLimit = controller.depositLimit(address(this), _token);

    if (depositLimit > 0) {
      require((lpToken[_token].totalSupply() + _amount) <= depositLimit, "LendingPair: deposit limit reached");
    }
  }

  function _systemRate() internal view returns(uint) {
    return controller.interestRateModel().systemRate(ILendingPair(address(this)));
  }

  function _lpRate() internal view returns(uint) {
    return 100e18 - _systemRate();
  }

  function _newInterest(uint _balance, address _token, address _account) internal view returns(uint) {
    return _balance * (cumulativeInterestRate[_token] - accountInterestSnapshot[_token][_account]) / 100e18;
  }
}
function _supplyBalance(
  address _account,
  address _suppliedToken,
  address _returnToken
) internal view returns(uint) {

  return _convertTokenValues(_suppliedToken, _returnToken, lpToken[_suppliedToken].balanceOf(_account));
}

// Get supply balance converted to the units of _returnToken