contract UniMexMargin is Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    
    address private WETH_ADDRESS;
    IERC20 public WETH;
    uint256 public constant MAG = 1e18;
    uint256 public constant LIQUIDATION_MARGIN = 11*1e17; //10%
    uint256 public liquidationBonus = 9 * 1e16;
    uint256 public borrowInterestPercentScaled = 100; //10%
    uint256 public constant YEAR = 31536000;
    uint256 public positionNonce = 0;
    
    struct Position {
        bytes32 id;
        address token;
        address owner;
        uint256 owed;
        uint256 input;
        uint256 commitment;
        uint256 leverage;
        uint256 startTimestamp;
        bool isClosed;
        bool isShort;
        uint256 borrowInterest;
    }
    
    mapping(bytes32 => Position) public positionInfo;
    mapping(address => uint256) public balanceOf;
    mapping(address => uint256) public escrow;
    
    uint256 public delay;

    IUniMexStaking public staking;
    IUniMexFactory public unimex_factory;
    IUniswapV2Factory public uniswap_factory;
    IUniswapV2Router02 public uniswap_router;
    
    event OnOpenPosition(
        address indexed sender,
        bytes32 positionId,
        bool isShort,
        address indexed token
    );
    
    event OnClosePosition(
        address indexed sender,
        bytes32 positionId,
        bool isShort,
        address indexed token
    );
    
    //to prevent flashloans
    modifier isHuman() {
        require(msg.sender == tx.origin);
        _;
    }

    constructor(
        address _staking,
        address _factory,
        address _weth,
        address _uniswap_factory,
        address _uniswap_router
    ) public {
        staking = IUniMexStaking(_staking);
        unimex_factory = IUniMexFactory(_factory);
        WETH_ADDRESS = _weth;
        WETH = IERC20(_weth);
        uniswap_factory = IUniswapV2Factory(_uniswap_factory);
        uniswap_router = IUniswapV2Router02(_uniswap_router);
    }

    function setDelay(uint256 _delay) external onlyOwner {
        delay = _delay;
    }

    function setStaking(address _staking) external onlyOwner {
        require(_staking != address(0));
        staking = IUniMexStaking(_staking);
    }

    function setLiquidationBonus(uint256 _liquidationBonus) external onlyOwner {
        require(_liquidationBonus > 0, "ZERO_LIQUIDATION_BONUS");
        liquidationBonus = _liquidationBonus;
    }

    function setBorrowPercent(uint256 _newPercentScaled) external onlyOwner {
        borrowInterestPercentScaled = _newPercentScaled;
    }

    function deposit(uint256 _amount) public {
        WETH.safeTransferFrom(msg.sender, address(this), _amount);
        balanceOf[msg.sender] = balanceOf[msg.sender].add(_amount);
    }

    function withdraw(uint256 _amount) public {
        require(balanceOf[msg.sender] >= _amount);
        balanceOf[msg.sender] = balanceOf[msg.sender].sub(_amount);
        WETH.safeTransfer(msg.sender, _amount);
    }

    function transferUserToEscrow(address from, address to, uint256 amount) private {
        require(balanceOf[from] >= amount);
        balanceOf[from] = balanceOf[from].sub(amount);
        escrow[to] = escrow[to].add(amount);
    }

    function transferEscrowToUser(address from, address to, uint256 amount) private {
        require(escrow[from] >= amount);
        escrow[from] = escrow[from].sub(amount);
        balanceOf[to] = balanceOf[to].add(amount);
    }

    function transferToUser(address to, uint256 amount) private {
        balanceOf[to] = balanceOf[to].add(amount);
    }

    function getPositionId(
        address maker,
        address token,
        uint256 amount,
        uint256 leverage,
        uint256 nonce
    ) private pure returns (bytes32 positionId) {
        //date acts as a nonce
        positionId = keccak256(
            abi.encodePacked(maker, token, amount, leverage, nonce)
        );
    }

    function calculateConvertedValue(address baseToken, address quoteToken, uint256 amount) private view returns (uint256) {
        address token0;
        address token1;
        (token0, token1) = UniswapV2Library.sortTokens(baseToken, quoteToken);
        IUniswapV2Pair pair = IUniswapV2Pair(uniswap_factory.getPair(token0, token1));
        (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();
        uint256 value;
        if (token1 == baseToken) {
            value = UniswapV2Library.getAmountOut(amount, reserve1, reserve0);
        } else {
            value = UniswapV2Library.getAmountOut(amount, reserve0, reserve1);
        }
        return value;
    }

    function swapTokens(address baseToken, address quoteToken, uint256 input, uint256 slippage) private returns (uint256 swap) {
        IERC20(baseToken).approve(address(uniswap_router), input);
        address[] memory path = new address[](2);
        path[0] = baseToken;
        path[1] = quoteToken;
        uint256 deadline = block.timestamp.add(delay);
        uint256 output = calculateConvertedValue(baseToken, quoteToken, input);
        uint256 outputWithSlippage = (output.sub(((output.mul(slippage)).div(MAG))));
        uint256 balanceBefore = IERC20(quoteToken).balanceOf(address(this));

        IUniswapV2Router02(uniswap_router)
            .swapExactTokensForTokensSupportingFeeOnTransferTokens(
                input,
                outputWithSlippage,
                path,
                address(this),
                deadline
            );
            
        uint256 balanceAfter = IERC20(quoteToken).balanceOf(address(this));
        swap = balanceAfter.sub(balanceBefore);

        require(swap > 0, "FAILED_SWAP");
    }

    function getCommitment(uint256 _amount, uint scaledLeverage) private pure returns (uint256 commitment) {
        commitment = (_amount.mul(MAG)).div(scaledLeverage);
    }

    function transferFees(uint256 fees, address pool) private {
        uint256 halfFees = fees.div(2);

        // Pool fees
        WETH.approve(pool, halfFees);	
        IUniMexPool(pool).distribute(halfFees);	

        // Staking Fees
        WETH.approve(address(staking), fees.sub(halfFees));
        staking.distribute(fees.sub(halfFees));
    }

    function transferToPool(address pool, address token, uint256 amount) private {
        IERC20(token).approve(pool, amount);
        IUniMexPool(pool).repay(amount);
    }

    function calculateBorrowInterest(bytes32 positionId) public view returns (uint256) {
        Position storage position = positionInfo[positionId];
        uint256 loanTime = block.timestamp.sub(position.startTimestamp);
        return position.owed.mul(loanTime).mul(position.borrowInterest).div(1000).div(YEAR);
    }

    function _openPosition(address token, uint256 amount, uint256 scaledLeverage, uint256 scaledSlippage, bool isShort) private {
        require(amount > 0, "AMOUNT_ZERO");
        address pool = unimex_factory.getPool(address(isShort ? IERC20(token) : WETH));

        require(pool != address(0), "POOL_DOES_NOT_EXIST");
        require(scaledLeverage <= unimex_factory.getMaxLeverage(token).mul(MAG), "LEVERAGE_EXCEEDS_MAX");
        require(scaledLeverage >= MAG, "LEVERAGE_BELOW_1");

        uint amountInWeth = isShort ? calculateConvertedValue(token, WETH_ADDRESS, amount) : amount;
        uint256 commitment = getCommitment(amountInWeth, scaledLeverage);
        require(balanceOf[msg.sender] >= commitment, "NO_BALANCE");
        
        IUniMexPool(pool).borrow(amount);

        uint256 swap;
        
        {
            (address baseToken, address quoteToken) = isShort ? (token, WETH_ADDRESS) : (WETH_ADDRESS, token);
            swap = swapTokens(baseToken, quoteToken, amount, scaledSlippage);
        }

        uint256 fees = (swap.mul(8)).div(1000);	

        swap = swap.sub(fees); // swap minus fees

        if(!isShort) {
            fees = swapTokens(token, WETH_ADDRESS, fees, scaledSlippage); // convert fees to ETH
        }

        transferFees(fees, pool);

        transferUserToEscrow(msg.sender, msg.sender, commitment.add(liquidationBonus));

        positionNonce = positionNonce + 1; //possible overflow is ok
        bytes32 positionId = getPositionId(
            msg.sender,
            token,
            amount,
            scaledLeverage,
            positionNonce
        );

        Position memory position = Position({
            owed: amount,
            input: swap,
            commitment: commitment,	
            owner: msg.sender,	
            startTimestamp: block.timestamp,
            isShort: isShort,
            isClosed: false,	
            leverage: scaledLeverage,
            token: token,
            id: positionId,
            borrowInterest: borrowInterestPercentScaled
        });
        
        positionInfo[position.id] = position;

        emit OnOpenPosition(msg.sender, position.id, isShort, token);
    }

    //slippage is a percentage x 1e18
    function openShortPosition(address token, uint256 amount, uint256 leverage, uint256 slippage) public isHuman {
        _openPosition(token, amount, leverage, slippage, true);
    }

    function openLongPosition(address token, uint256 amount, uint256 leverage, uint256 slippage) public isHuman {
        _openPosition(token, amount, leverage, slippage, false);
    }

    function _closeShort(Position storage position, uint256 slippage) private {

        uint256 input = position.input;
        uint256 owed = position.owed;
        uint256 commitment = position.commitment;

        address pool = unimex_factory.getPool(position.token);

        uint256 poolInterestInTokens = calculateBorrowInterest(position.id);
        uint256 swap = swapTokens(WETH_ADDRESS, position.token, input, slippage);
        require(swap >= owed.add(poolInterestInTokens).mul(input).div(input.add(commitment)), "LIQUIDATE_ONLY");

        bool isProfit = owed < swap;
        uint256 amount;

        if(isProfit) {
            uint256 profitInTokens = swap.sub(owed);
            amount = swapTokens(position.token, WETH_ADDRESS, profitInTokens, slippage); //profit in eth
        } else {
            uint256 commitmentInTokens = swapTokens(WETH_ADDRESS, position.token, commitment, slippage);
            amount = swapTokens(position.token, WETH_ADDRESS, commitmentInTokens.sub(owed.sub(swap)), slippage); //return to user's balance
        }

        uint256 poolInterestInWeth = poolInterestInTokens > 0 ? calculateConvertedValue(position.token, address(WETH), poolInterestInTokens) : 0;

        uint256 fees = amount.mul(8e15)
                             .div(1e18)
                             .add(poolInterestInWeth);

        transferToPool(pool, position.token, owed);

        transferFees(fees, pool);

        transferEscrowToUser(position.owner, isProfit ? position.owner : address(0x0), commitment);
        transferEscrowToUser(position.owner, position.owner, liquidationBonus);
        transferToUser(position.owner, amount.sub(fees));
        
        position.isClosed = true;
        emit OnClosePosition(msg.sender, position.id, true, position.token);
    }

    function _closeLong(Position storage position, uint256 slippage) private {
        uint256 input = position.input;
        uint256 owed = position.owed;
        address pool = unimex_factory.getPool(WETH_ADDRESS);

        uint256 poolInterestValue = calculateBorrowInterest(position.id);
        uint256 swap = swapTokens(position.token, WETH_ADDRESS, input, slippage);
        require(swap >= owed.sub(position.commitment).add(poolInterestValue), "LIQUIDATE_ONLY");

        uint256 commitment = position.commitment;

        bool isProfit = swap >= owed;

        uint256 amount = isProfit ? swap.sub(owed) : commitment.sub(owed.sub(swap));

        uint256 fees = amount.mul(8e15)
                             .div(1e18)
                             .add(poolInterestValue);

        transferToPool(pool, WETH_ADDRESS, owed);

        transferFees(fees, pool);

        transferEscrowToUser(position.owner, isProfit ? position.owner : address(0x0), commitment);
        transferEscrowToUser(position.owner, position.owner, liquidationBonus);

        transferToUser(position.owner, amount.sub(fees));

        position.isClosed = true;
        emit OnClosePosition(msg.sender, position.id, false, position.token);
    }

    function closePosition(bytes32 positionId, uint256 slippage) external isHuman {
        Position storage position = positionInfo[positionId];
        require(position.isClosed == false, "CLOSED_POSITION");
        require(msg.sender == position.owner, "BORROWER_ONLY");
        if(position.isShort) {
            _closeShort(position, slippage);
        }else{
            _closeLong(position, slippage);
        }
    }

    function canLiquidate(bytes32 positionId) public view returns(bool) {
        Position storage position = positionInfo[positionId];
        if(position.isShort) {
            uint256 value = calculateConvertedValue(WETH_ADDRESS, position.token, position.input);
            uint256 poolInterestInTokens = calculateBorrowInterest(position.id);

            return value < position.owed.add(poolInterestInTokens)
                                        .mul(position.input)
                                        .div(position.input.add(position.commitment))
                                        .mul(LIQUIDATION_MARGIN)
                                        .div(MAG);
        } else {
            uint256 value = calculateConvertedValue(position.token, WETH_ADDRESS, position.input);
            uint256 poolInterestValue = calculateBorrowInterest(position.id);
            return value.add(position.commitment) < position.owed.add(poolInterestValue)
                                                                 .mul(LIQUIDATION_MARGIN)
                                                                 .div(MAG);
        }

    }

    function liquidatePosition(bytes32 positionId, uint256 slippage) external isHuman {
        Position storage position = positionInfo[positionId];
        require(position.isClosed == false, "CLOSED_POSITION");
        bool isShort = position.isShort;
        (address baseToken, address quoteToken) = isShort ? (position.token, WETH_ADDRESS) : (WETH_ADDRESS, position.token);

        uint256 input = position.input;

        require(canLiquidate(positionId), "CANNOT_LIQUIDATE");

        uint256 swap = swapTokens(quoteToken, baseToken, input, slippage);

        uint256 commitment = isShort ? swapTokens(WETH_ADDRESS, position.token, position.commitment, slippage) : position.commitment;

        uint256 fees = calculateBorrowInterest(position.id)
                            .add(position.owed.mul(8e15).div(1e18));
        uint256 canReturn = swap.add(commitment);
        liquidate(position, canReturn, fees, slippage);
        transferEscrowToUser(position.owner, address(0x0), position.commitment.add(liquidationBonus));

        position.isClosed = true;
        
        emit OnClosePosition(msg.sender, position.id, isShort, position.token);
        WETH.safeTransfer(msg.sender, liquidationBonus);
    }

    function liquidate(Position memory position, uint256 canReturn, uint fees, uint slippage) private {
        address baseToken = position.isShort ? position.token : WETH_ADDRESS;
        address pool = unimex_factory.getPool(baseToken);
        if(canReturn > position.owed) {
            transferToPool(pool, baseToken, position.owed);
            uint256 remainder = canReturn.sub(position.owed);
            if(remainder > fees) { //can pay fees completely
                uint256 userReturnAmount = remainder.sub(fees);
                if(position.isShort) {
                    fees = swapTokens(position.token, WETH_ADDRESS, fees, slippage); //convert fees to weth
                }
                transferFees(fees, pool);
                if(position.isShort) {
                    userReturnAmount = swapTokens(position.token, WETH_ADDRESS, remainder.sub(fees), slippage);
                }
                transferToUser(position.owner, userReturnAmount);
            } else { //all is left is for fees
                if(position.isShort) {
                    //convert remainder to weth
                    remainder = swapTokens(position.token, WETH_ADDRESS, 
                        canReturn.sub(position.owed), slippage);
                }
                transferFees(remainder, pool);
            }
        } else {
            //return to pool all that's left
            transferToPool(pool, baseToken, canReturn);
        }
    }


}
function openShortPosition(address token, uint256 amount, uint256 leverage, uint256 slippage) public isHuman {
    _openPosition(token, amount, leverage, slippage, true);
}

//slippage is a percentage x 1e18