contract LiquidLong is Ownable, Claimable, Pausable {
	using SafeMath for uint256;
	using SafeMathFixedPoint for uint256;

	uint256 public providerFeePerEth;

	MatchingMarket public matchingMarket;
	Maker public maker;
	Dai public dai;
	Weth public weth;
	Peth public peth;
	Mkr public mkr;

	ProxyRegistry public proxyRegistry;

	struct CDP {
		uint256 id;
		uint256 debtInAttodai;
		uint256 lockedAttoeth;
		address owner;
		bool userOwned;
	}

	event NewCup(address user, uint256 cup);
	event CloseCup(address user, uint256 cup);

	constructor(MatchingMarket _matchingMarket, Maker _maker, ProxyRegistry _proxyRegistry) public payable {
		providerFeePerEth = 0.01 ether;

		matchingMarket = _matchingMarket;
		maker = _maker;
		dai = maker.sai();
		weth = maker.gem();
		peth = maker.skr();
		mkr = maker.gov();

		// MatchingMarket buy/sell
		dai.approve(address(_matchingMarket), uint256(-1));
		weth.approve(address(_matchingMarket), uint256(-1));
		// Wipe
		dai.approve(address(_maker), uint256(-1));
		mkr.approve(address(_maker), uint256(-1));
		// Join
		weth.approve(address(_maker), uint256(-1));
		// Lock
		peth.approve(address(_maker), uint256(-1));

		proxyRegistry = _proxyRegistry;

		if (msg.value > 0) {
			weth.deposit.value(msg.value)();
		}
	}

	// Receive ETH from WETH withdraw
	function () external payable {
	}

	function wethDeposit() public payable {
		weth.deposit.value(msg.value)();
	}

	function wethWithdraw(uint256 _amount) public onlyOwner {
		weth.withdraw(_amount);
		owner.transfer(_amount);
	}

	function attowethBalance() public view returns (uint256 _attoweth) {
		return weth.balanceOf(address(this));
	}

	function ethWithdraw() public onlyOwner {
		uint256 _amount = address(this).balance;
		owner.transfer(_amount);
	}

	function transferTokens(ERC20 _token) public onlyOwner {
		_token.transfer(owner, _token.balanceOf(this));
	}

	function ethPriceInUsd() public view returns (uint256 _attousd) {
		return uint256(maker.pip().read());
	}

	function estimateDaiSaleProceeds(uint256 _attodaiToSell) public view returns (uint256 _daiPaid, uint256 _wethBought) {
		return getPayPriceAndAmount(dai, weth, _attodaiToSell);
	}

	// buy/pay are from the perspective of the taker/caller (MatchingMarket contracts use buy/pay terminology from perspective of the maker)
	function getPayPriceAndAmount(ERC20 _payGem, ERC20 _buyGem, uint256 _payDesiredAmount) public view returns (uint256 _paidAmount, uint256 _boughtAmount) {
		uint256 _offerId = matchingMarket.getBestOffer(_buyGem, _payGem);
		while (_offerId != 0) {
			uint256 _payRemaining = _payDesiredAmount.sub(_paidAmount);
			(uint256 _buyAvailableInOffer,  , uint256 _payAvailableInOffer,) = matchingMarket.getOffer(_offerId);
			if (_payRemaining <= _payAvailableInOffer) {
				uint256 _buyRemaining = _payRemaining.mul(_buyAvailableInOffer).div(_payAvailableInOffer);
				_paidAmount = _paidAmount.add(_payRemaining);
				_boughtAmount = _boughtAmount.add(_buyRemaining);
				break;
			}
			_paidAmount = _paidAmount.add(_payAvailableInOffer);
			_boughtAmount = _boughtAmount.add(_buyAvailableInOffer);
			_offerId = matchingMarket.getWorseOffer(_offerId);
		}
		return (_paidAmount, _boughtAmount);
	}

	function estimateDaiPurchaseCosts(uint256 _attodaiToBuy) public view returns (uint256 _wethPaid, uint256 _daiBought) {
		return getBuyPriceAndAmount(weth, dai, _attodaiToBuy);
	}

	// buy/pay are from the perspective of the taker/caller (MatchingMarket contracts use buy/pay terminology from perspective of the maker)
	function getBuyPriceAndAmount(ERC20 _payGem, ERC20 _buyGem, uint256 _buyDesiredAmount) public view returns (uint256 _paidAmount, uint256 _boughtAmount) {
		uint256 _offerId = matchingMarket.getBestOffer(_buyGem, _payGem);
		while (_offerId != 0) {
			uint256 _buyRemaining = _buyDesiredAmount.sub(_boughtAmount);
			(uint256 _buyAvailableInOffer, , uint256 _payAvailableInOffer,) = matchingMarket.getOffer(_offerId);
			if (_buyRemaining <= _buyAvailableInOffer) {
				// TODO: verify this logic is correct
				uint256 _payRemaining = _buyRemaining.mul(_payAvailableInOffer).div(_buyAvailableInOffer);
				_paidAmount = _paidAmount.add(_payRemaining);
				_boughtAmount = _boughtAmount.add(_buyRemaining);
				break;
			}
			_paidAmount = _paidAmount.add(_payAvailableInOffer);
			_boughtAmount = _boughtAmount.add(_buyAvailableInOffer);
			_offerId = matchingMarket.getWorseOffer(_offerId);
		}
		return (_paidAmount, _boughtAmount);
	}

	modifier wethBalanceIncreased() {
		uint256 _startingAttowethBalance = weth.balanceOf(this);
		_;
		require(weth.balanceOf(this) > _startingAttowethBalance);
	}

	// TODO: change affiliate fee to be 50% of service fee, no parameter needed
	function openCdp(uint256 _leverage, uint256 _leverageSizeInAttoeth, uint256 _allowedFeeInAttoeth, address _affiliateAddress) public payable wethBalanceIncreased returns (bytes32 _cdpId) {
		require(_leverage >= 100 && _leverage <= 300);
		uint256 _lockedInCdpInAttoeth = _leverageSizeInAttoeth.mul(_leverage).div(100);
		uint256 _loanInAttoeth = _lockedInCdpInAttoeth.sub(_leverageSizeInAttoeth);
		uint256 _feeInAttoeth = _loanInAttoeth.mul18(providerFeePerEth);
		require(_feeInAttoeth <= _allowedFeeInAttoeth);
		uint256 _drawInAttodai = _loanInAttoeth.mul18(uint256(maker.pip().read()));
		uint256 _attopethLockedInCdp = _lockedInCdpInAttoeth.div27(maker.per());

		// Convert all incoming eth to weth (we will pay back later if too much)
		weth.deposit.value(msg.value)();
		// Open CDP
		_cdpId = maker.open();
		// Convert WETH into PETH
		maker.join(_attopethLockedInCdp);
		// Store PETH in CDP
		maker.lock(_cdpId, _attopethLockedInCdp);

		// This could be 0 if the user has requested a leverage of exactly 100 (1x).
		// There's no reason to draw 0 dai, try to sell it, or to pay out affiliate
		// Drawn dai is used to satisfy the _loanInAttoeth. 0 DAI means 0 loan.
		if (_drawInAttodai > 0) {
			// Withdraw DAI from CDP
			maker.draw(_cdpId, _drawInAttodai);
			// Sell DAI for WETH
			sellDai(_drawInAttodai, _lockedInCdpInAttoeth, _feeInAttoeth);
			// Pay provider fee
			if (_affiliateAddress != address(0)) {
				// Fee charged is constant. If affiliate provided, split fee with affiliate
				// Don't bother sending eth to owner, the owner has all weth anyway
				weth.transfer(_affiliateAddress, _feeInAttoeth.div(2));
			}
		}
		emit NewCup(msg.sender, uint256(_cdpId));

		giveCdpToProxy(msg.sender, _cdpId);
	}

	function giveCdpToProxy(address _ownerOfProxy, bytes32 _cdpId) private {
		DSProxy _proxy = proxyRegistry.proxies(_ownerOfProxy);
		if (_proxy == DSProxy(0) || _proxy.owner() != _ownerOfProxy) {
			_proxy = proxyRegistry.build(_ownerOfProxy);
		}
		// Send the CDP to the owner's proxy instead of directly to owner
		maker.give(_cdpId, _proxy);
	}

	// extracted function to mitigate stack depth issues
	function sellDai(uint256 _drawInAttodai, uint256 _lockedInCdpInAttoeth, uint256 _feeInAttoeth) private {
		uint256 _wethBoughtInAttoweth = matchingMarket.sellAllAmount(dai, _drawInAttodai, weth, 0);
		// SafeMath failure below catches not enough eth provided
		uint256 _refundDue = msg.value.add(_wethBoughtInAttoweth).sub(_lockedInCdpInAttoeth).sub(_feeInAttoeth);
		if (_refundDue > 0) {
			weth.withdraw(_refundDue);
			require(msg.sender.call.value(_refundDue)());
		}
	}

	// closeCdp is intended to be a delegate call that executes as a user's DSProxy
	function closeCdp(LiquidLong _liquidLong, uint256 _cdpId, uint256 _minimumValueInAttoeth, address _affiliateAddress) external returns (uint256 _payoutOwnerInAttoeth) {
		address _owner = DSProxy(this).owner();
		uint256 _startingAttoethBalance = _owner.balance;

		// This is delegated, we cannot use storage
		Maker _maker = _liquidLong.maker();

		// if the CDP is already empty, early return (this allows this method to be called off-chain to check estimated payout and not fail for empty CDPs)
		uint256 _lockedPethInAttopeth = _maker.ink(bytes32(_cdpId));
		if (_lockedPethInAttopeth == 0) return 0;

		_maker.give(bytes32(_cdpId), _liquidLong);
		_payoutOwnerInAttoeth = _liquidLong.closeGiftedCdp(bytes32(_cdpId), _minimumValueInAttoeth, _owner, _affiliateAddress);

		require(_maker.lad(bytes32(_cdpId)) == address(this));
		require(_owner.balance > _startingAttoethBalance);
		return _payoutOwnerInAttoeth;
	}

	// Close cdp that was just received as part of the same transaction
	function closeGiftedCdp(bytes32 _cdpId, uint256 _minimumValueInAttoeth, address _recipient, address _affiliateAddress) external wethBalanceIncreased returns (uint256 _payoutOwnerInAttoeth) {
		require(_recipient != address(0));
		uint256 _lockedPethInAttopeth = maker.ink(_cdpId);
		uint256 _debtInAttodai = maker.tab(_cdpId);

		// Calculate what we need to claim out of the CDP in Weth
		uint256 _lockedWethInAttoweth = _lockedPethInAttopeth.div27(maker.per());

		// Buy DAI and wipe the entire CDP
		// Pass in _lockedWethInAttoweth as "max fill amount". If buying DAI costs more in eth than the entire CDP has locked up, revert (we will fail later anyway)
		uint256 _wethSoldInAttoweth = matchingMarket.buyAllAmount(dai, _debtInAttodai, weth, _lockedWethInAttoweth);
		uint256 _providerFeeInAttoeth = _wethSoldInAttoweth.mul18(providerFeePerEth);

		// Calculating governance fee is difficult and gas-intense. Just look up how wiping impacts balance
		// Then convert that difference into weth, the only asset we charge in. This will require loading up
		// mkr periodically
		uint256 _mkrBalanceBeforeInAttomkr = mkr.balanceOf(this);
		maker.wipe(_cdpId, _debtInAttodai);
		uint256 _mkrBurnedInAttomkr = _mkrBalanceBeforeInAttomkr.sub(mkr.balanceOf(this));
		uint256 _ethValueOfBurnedMkrInAttoeth = _mkrBurnedInAttomkr.mul(uint256(maker.pep().read())) // converts Mkr to DAI
			.div(uint256(maker.pip().read())); // converts DAI to ETH

		// Relying on safe-math to revert a situation where LiquidLong would lose weth
		_payoutOwnerInAttoeth = _lockedWethInAttoweth.sub(_wethSoldInAttoweth).sub(_providerFeeInAttoeth).sub(_ethValueOfBurnedMkrInAttoeth);

		// Ensure remaining peth in CDP is greater than the value they requested as minimum value
		require(_payoutOwnerInAttoeth >= _minimumValueInAttoeth);

		// Pull that value from the CDP, convert it back to WETH for next time
		// We rely on "free" reverting the transaction if there is not enough peth to profitably close CDP
		maker.free(_cdpId, _lockedPethInAttopeth);
		maker.exit(_lockedPethInAttopeth);

		// DSProxy (or other proxy?) will have issued this request, send it back to the proxy contract. CDP is empty and valueless
		maker.give(_cdpId, msg.sender);

		weth.withdraw(_payoutOwnerInAttoeth);
		if (_affiliateAddress != address(0)) {
			// Fee charged is constant. If affiliate provided, split fee with affiliate
			// Don't bother sending eth to owner, the owner has all weth anyway
			weth.transfer(_affiliateAddress, _providerFeeInAttoeth.div(2));
		}
		require(_recipient.call.value(_payoutOwnerInAttoeth)());
		emit CloseCup(msg.sender, uint256(_cdpId));
	}

	// Retrieve CDPs by EFFECTIVE owner, which address owns the DSProxy which owns the CDPs
	function getCdps(address _owner, uint32 _offset, uint32 _pageSize) public returns (CDP[] _cdps) {
		// resolve a owner to a proxy, then query by that proxy
		DSProxy _cdpProxy = proxyRegistry.proxies(_owner);
		require(_cdpProxy != address(0));
		return getCdpsByAddresses(_owner, _cdpProxy, _offset, _pageSize);
	}

	// Retrieve CDPs by TRUE owner, as registered in Maker
	function getCdpsByAddresses(address _owner, address _proxy, uint32 _offset, uint32 _pageSize) public returns (CDP[] _cdps) {
		_cdps = new CDP[](getCdpCountByOwnerAndProxy(_owner, _proxy, _offset, _pageSize));
		uint256 _cdpCount = cdpCount();
		uint32 _matchCount = 0;
		for (uint32 _i = _offset; _i <= _cdpCount && _i < _offset + _pageSize; ++_i) {
			address _cdpOwner = maker.lad(bytes32(_i));
			if (_cdpOwner != _owner && _cdpOwner != _proxy) continue;
			_cdps[_matchCount] = getCdpDetailsById(_i, _owner);
			++_matchCount;
		}
		return _cdps;
	}

	function cdpCount() public view returns (uint32 _cdpCount) {
		uint256 count = maker.cupi();
		require(count < 2**32);
		return uint32(count);
	}

	function getCdpCountByOwnerAndProxy(address _owner, address _proxy, uint32 _offset, uint32 _pageSize) private view returns (uint32 _count) {
		uint256 _cdpCount = cdpCount();
		_count = 0;
		for (uint32 _i = _offset; _i <= _cdpCount && _i < _offset + _pageSize; ++_i) {
			address _cdpOwner = maker.lad(bytes32(_i));
			if (_cdpOwner != _owner && _cdpOwner != _proxy) continue;
			++_count;
		}
		return _count;
	}

	function getCdpDetailsById(uint32 _cdpId, address _owner) private returns (CDP _cdp) {
		(address _cdpOwner, uint256 _collateral,,) = maker.cups(bytes32(_cdpId));
		// this one line makes this function not `view`. tab calls chi, which calls drip which mutates state and we can't directly access _chi to bypass this
		uint256 _debtInAttodai = maker.tab(bytes32(_cdpId));
		// Adjust locked attoeth to factor in peth/weth ratio
		uint256 _lockedAttoeth = (_collateral + 1).mul27(maker.gap().mul18(maker.per()));
		_cdp = CDP({
			id: _cdpId,
			debtInAttodai: _debtInAttodai,
			lockedAttoeth: _lockedAttoeth,
			owner: _cdpOwner,
			userOwned: _cdpOwner == _owner
		});
		return _cdp;
	}
}
function openCdp(uint256 _leverage, uint256 _leverageSizeInAttoeth, uint256 _allowedFeeInAttoeth, address _affiliateAddress) public payable wethBalanceIncreased returns (bytes32 _cdpId) {
	require(_leverage >= 100 && _leverage <= 300);
	uint256 _lockedInCdpInAttoeth = _leverageSizeInAttoeth.mul(_leverage).div(100);
	uint256 _loanInAttoeth = _lockedInCdpInAttoeth.sub(_leverageSizeInAttoeth);
	uint256 _feeInAttoeth = _loanInAttoeth.mul18(providerFeePerEth);
	require(_feeInAttoeth <= _allowedFeeInAttoeth);
	uint256 _drawInAttodai = _loanInAttoeth.mul18(uint256(maker.pip().read()));
	uint256 _attopethLockedInCdp = _lockedInCdpInAttoeth.div27(maker.per());

	// Convert all incoming eth to weth (we will pay back later if too much)
	weth.deposit.value(msg.value)();
	// Open CDP
	_cdpId = maker.open();
	// Convert WETH into PETH
	maker.join(_attopethLockedInCdp);
	// Store PETH in CDP
	maker.lock(_cdpId, _attopethLockedInCdp);

	// This could be 0 if the user has requested a leverage of exactly 100 (1x).
	// There's no reason to draw 0 dai, try to sell it, or to pay out affiliate
	// Drawn dai is used to satisfy the _loanInAttoeth. 0 DAI means 0 loan.
	if (_drawInAttodai > 0) {
		// Withdraw DAI from CDP
		maker.draw(_cdpId, _drawInAttodai);
		// Sell DAI for WETH
		sellDai(_drawInAttodai, _lockedInCdpInAttoeth, _feeInAttoeth);
		// Pay provider fee
		if (_affiliateAddress != address(0)) {
			// Fee charged is constant. If affiliate provided, split fee with affiliate
			// Don't bother sending eth to owner, the owner has all weth anyway
			weth.transfer(_affiliateAddress, _feeInAttoeth.div(2));
		}
	}
	emit NewCup(msg.sender, uint256(_cdpId));

	giveCdpToProxy(msg.sender, _cdpId);
}

// TODO: change affiliate fee to be 50% of service fee, no parameter needed