contract OptimizerZap {
    IRouter constant router = IRouter(0x11111112542D85B3EF69AE05771c2dCCff4fAa26);
    address constant eth = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
    address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public immutable DAO;
    
    struct Cache {
        uint256 amount0;
        uint256 amount1;
        uint256 return0Amount;
        uint256 return1Amount;
    }

    struct TokenData {
        bool IsUno;
        IAggregationExecutor caller;
        IRouter.SwapDescription desc;
        bytes data;
        bytes32[] pools;
    }

    constructor(address _DAO) {
        DAO = _DAO;
    }

    function DepositInEth(address optimizer, address to, TokenData calldata tokenData) external payable {
        uint value = msg.value;
       
        address token0 = IPopsicleV3Optimizer(optimizer).token0();
        address token1 = IPopsicleV3Optimizer(optimizer).token1();
        IWETH9(weth).deposit{value: value}();
        _approveToken(weth, optimizer, value);
        require(token0 == weth || token1 == weth, "BO");
        if (token0 == weth) {
            require(token1 == address(tokenData.desc.srcToken), "TNA");
            
            TransferHelper.safeTransferFrom(token1, msg.sender, address(this), tokenData.desc.amount);
            _approveToken(token1, optimizer, tokenData.desc.amount);
            (, uint256 amount0,uint256 amount1) = IPopsicleV3Optimizer(optimizer).deposit(value, tokenData.desc.amount, to);

            uint256 res0 = value-amount0;
            uint256 res1 = tokenData.desc.amount-amount1;
            if (res0 > 0 ) TransferHelper.safeTransfer(token0, to, res0);
            if (res1 > 0 ) TransferHelper.safeTransfer(token1, to, res1);
        } else {
            require(token0 == address(tokenData.desc.srcToken), "TNA");
            TransferHelper.safeTransferFrom(token0, msg.sender, address(this), tokenData.desc.amount);
            _approveToken(token0, optimizer, tokenData.desc.amount);
            (, uint256 amount0,uint256 amount1) =IPopsicleV3Optimizer(optimizer).deposit(tokenData.desc.amount, value,  to);

            uint256 res0 = tokenData.desc.amount-amount0;
            uint256 res1 = value-amount1;
            if (res0 > 0 ) TransferHelper.safeTransfer(token0, to, res0);
            if (res1 > 0 ) TransferHelper.safeTransfer(token1, to, res1);
        }
    }

    function ZapIn(address tokenIn, uint amount, address optimizer, address to, TokenData calldata token0Data, TokenData calldata token1Data) external payable {
        require(optimizer != address(0));
        require(to != address(0));
        address token0 = IPopsicleV3Optimizer(optimizer).token0();
        address token1 = IPopsicleV3Optimizer(optimizer).token1();
        require(tokenIn == address(token0Data.desc.srcToken), "NAT0");
        require(tokenIn == address(token1Data.desc.srcToken), "NAT1");
        require(token0 == address(token0Data.desc.dstToken), "IT0");
        require(token1 == address(token1Data.desc.dstToken), "IT1");

        require(token0Data.desc.amount + token1Data.desc.amount <= amount, "IA");
        Cache memory cache;
        if (tokenIn == eth || tokenIn == address(0)) {
            require(amount <= msg.value, "BA");

            if (token0 == weth) {
                IWETH9(weth).deposit{value: token0Data.desc.amount}();
                _approveToken(weth, optimizer, token0Data.desc.amount);
                if (token1Data.IsUno)
                {
                    cache.return1Amount = router.unoswap{value: token1Data.desc.amount}(IERC20(tokenIn), token1Data.desc.amount, token1Data.desc.minReturnAmount, token1Data.pools);
                } else {
                    (cache.return1Amount, ) = router.swap{value: token1Data.desc.amount}(token1Data.caller, token1Data.desc, token1Data.data);
                }
                _approveToken(token1, optimizer, cache.return1Amount);
                (, cache.amount0, cache.amount1) = IPopsicleV3Optimizer(optimizer).deposit(token0Data.desc.amount, cache.return1Amount, to);
                cache.return0Amount = token0Data.desc.amount-cache.amount0;
                cache.return1Amount = cache.return1Amount-cache.amount1;
                if (cache.return0Amount > 0 ) TransferHelper.safeTransfer(token0, to, cache.return0Amount);
                if (cache.return1Amount > 0 ) TransferHelper.safeTransfer(token1, to, cache.return1Amount);
            } else if (token1 == weth) {
                IWETH9(weth).deposit{value: token1Data.desc.amount}();
                _approveToken(weth, optimizer, token1Data.desc.amount);
                if (token0Data.IsUno)
                {
                    cache.return0Amount = router.unoswap{value: token0Data.desc.amount}(IERC20(tokenIn), token0Data.desc.amount, token0Data.desc.minReturnAmount, token0Data.pools);
                } else {
                    (cache.return0Amount, ) = router.swap{value: token0Data.desc.amount}(token0Data.caller, token0Data.desc, token0Data.data);
                }
                _approveToken(token0, optimizer, cache.return0Amount);
                (, cache.amount0, cache.amount1) = IPopsicleV3Optimizer(optimizer).deposit(cache.return0Amount, token1Data.desc.amount, to);
                cache.return0Amount = cache.return0Amount-cache.amount0;
                cache.return1Amount = token1Data.desc.amount-cache.amount1;
                if (cache.return0Amount > 0 ) TransferHelper.safeTransfer(token0, to, cache.return0Amount);
                if (cache.return1Amount > 0 ) TransferHelper.safeTransfer(token1, to, cache.return1Amount);
            } else {
                if (token0Data.IsUno)
                {
                    cache.return0Amount = router.unoswap{value: token0Data.desc.amount}(IERC20(tokenIn), token0Data.desc.amount, token0Data.desc.minReturnAmount, token0Data.pools);
                } else {
                    (cache.return0Amount, ) = router.swap{value: token0Data.desc.amount}(token0Data.caller, token0Data.desc, token0Data.data);
                }
                if (token1Data.IsUno)
                {
                    cache.return1Amount = router.unoswap{value: token1Data.desc.amount}(IERC20(tokenIn), token1Data.desc.amount, token1Data.desc.minReturnAmount, token1Data.pools);
                } else {
                    (cache.return1Amount, ) = router.swap{value: token1Data.desc.amount}(token1Data.caller, token1Data.desc, token1Data.data);
                }
                _approveToken(token0, optimizer, cache.return0Amount);
                _approveToken(token1, optimizer, cache.return1Amount);
                (, cache.amount0, cache.amount1) = IPopsicleV3Optimizer(optimizer).deposit(cache.return0Amount, cache.return1Amount, to);
                cache.return0Amount = cache.return0Amount-cache.amount0;
                cache.return1Amount = cache.return1Amount-cache.amount1;
                if (cache.return0Amount > 0 ) TransferHelper.safeTransfer(token0, to, cache.return0Amount);
                if (cache.return1Amount > 0 ) TransferHelper.safeTransfer(token1, to, cache.return1Amount);
            }
            return;
        } else {
            
            TransferHelper.safeTransferFrom(tokenIn, msg.sender, address(this), amount);
            _approveToken(tokenIn, address(router), amount);
            if (tokenIn == token0) {
                cache.return0Amount = token0Data.desc.amount;
            } else {

                if (token0Data.IsUno)
                {
                    cache.return0Amount = router.unoswap(IERC20(tokenIn), token0Data.desc.amount, token0Data.desc.minReturnAmount, token0Data.pools);
                } else {
                    (cache.return0Amount, ) = router.swap(token0Data.caller, token0Data.desc, token0Data.data);
                }
            }
            if (tokenIn == token1) {
                cache.return1Amount = token1Data.desc.amount;
            } else {
                
                if (token1Data.IsUno)
                {
                    cache.return1Amount = router.unoswap(IERC20(tokenIn), token1Data.desc.amount, token1Data.desc.minReturnAmount, token1Data.pools);
                } else {
                    (cache.return1Amount, ) = router.swap(token1Data.caller, token1Data.desc, token1Data.data);
                }
            }
            _approveToken(token0, optimizer, cache.return0Amount);
            _approveToken(token1, optimizer, cache.return1Amount);
            (, cache.amount0, cache.amount1) = IPopsicleV3Optimizer(optimizer).deposit(cache.return0Amount, cache.return1Amount, to);
            cache.return0Amount = cache.return0Amount-cache.amount0;
            cache.return1Amount = cache.return1Amount-cache.amount1;
            if (cache.return0Amount > 0 ) TransferHelper.safeTransfer(token0, to, cache.return0Amount);
            if (cache.return1Amount > 0 ) TransferHelper.safeTransfer(token1, to, cache.return1Amount);
            return;
        }
    }

    function _approveToken(
        address token,
        address spender,
        uint256 amount
    ) internal {
        if (IERC20(token).allowance(address(this), spender) > 0) IERC20(token).approve(spender, 0);
        IERC20(token).approve(spender, amount);
    }
    
    

    /* ======= AUXILLIARY ======= */

    /**
     *  @notice allow anyone to send lost tokens to the DAO
     *  @return bool
     */
    function recoverLostToken( address _token ) external returns ( bool ) {
        TransferHelper.safeTransfer(_token, DAO, IERC20( _token ).balanceOf( address(this)));
        return true;
    }

    function refundETH() external returns ( bool ) {
        if (address(this).balance > 0) TransferHelper.safeTransferETH(DAO, address(this).balance);
        return true;
    }
}
function recoverLostToken( address _token ) external returns ( bool ) {
    TransferHelper.safeTransfer(_token, DAO, IERC20( _token ).balanceOf( address(this)));
    return true;
}

/**
 *  @notice allow anyone to send lost tokens to the DAO
 *  @return bool
 */