// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;

import "@interfaces/IExecutor.sol";
import {ICallback} from "@interfaces/ICallback.sol";
import {TokenTransfer} from "./TokenTransfer.sol";
import {
    IERC20,
    SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {
    Currency, CurrencyLibrary
} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
import {IUnlockCallback} from
    "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {TransientStateLibrary} from
    "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";

error UniswapV4Executor__InvalidDataLength();
error UniswapV4Executor__NotPoolManager();
error UniswapV4Executor__UnknownCallback(bytes4 selector);
error UniswapV4Executor__DeltaNotPositive(Currency currency);
error UniswapV4Executor__DeltaNotNegative(Currency currency);
error UniswapV4Executor__V4TooMuchRequested(
    uint256 maxAmountInRequested, uint256 amountRequested
);

contract UniswapV4Executor is
    IExecutor,
    IUnlockCallback,
    ICallback,
    TokenTransfer
{
    using SafeERC20 for IERC20;
    using CurrencyLibrary for Currency;
    using SafeCast for *;
    using TransientStateLibrary for IPoolManager;

    IPoolManager public immutable poolManager;
    address private immutable _self;

    bytes4 constant SWAP_EXACT_INPUT_SINGLE_SELECTOR = 0x8bc6d0d7;
    bytes4 constant SWAP_EXACT_INPUT_SELECTOR = 0xaf90aeb1;

    struct UniswapV4Pool {
        address intermediaryToken;
        uint24 fee;
        int24 tickSpacing;
    }

    constructor(IPoolManager _poolManager, address _permit2)
        TokenTransfer(_permit2)
    {
        poolManager = _poolManager;
        _self = address(this);
    }

    /**
     * @dev Modifier to restrict access to only the pool manager.
     */
    modifier poolManagerOnly() virtual {
        if (msg.sender != address(poolManager)) {
            revert UniswapV4Executor__NotPoolManager();
        }
        _;
    }

    function swap(uint256 amountIn, bytes calldata data)
        external
        payable
        returns (uint256 calculatedAmount)
    {
        (
            address tokenIn,
            address tokenOut,
            bool zeroForOne,
            TransferType transferType,
            address receiver,
            UniswapV4Executor.UniswapV4Pool[] memory pools
        ) = _decodeData(data);
        bytes memory swapData;
        if (pools.length == 1) {
            PoolKey memory key = PoolKey({
                currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut),
                currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
                fee: pools[0].fee,
                tickSpacing: pools[0].tickSpacing,
                hooks: IHooks(address(0))
            });
            swapData = abi.encodeWithSelector(
                this.swapExactInputSingle.selector,
                key,
                zeroForOne,
                amountIn,
                msg.sender,
                transferType,
                receiver,
                bytes("")
            );
        } else {
            PathKey[] memory path = new PathKey[](pools.length);
            for (uint256 i = 0; i < pools.length; i++) {
                path[i] = PathKey({
                    intermediateCurrency: Currency.wrap(pools[i].intermediaryToken),
                    fee: pools[i].fee,
                    tickSpacing: pools[i].tickSpacing,
                    hooks: IHooks(address(0)),
                    hookData: bytes("")
                });
            }

            Currency currencyIn = Currency.wrap(tokenIn);
            swapData = abi.encodeWithSelector(
                this.swapExactInput.selector,
                currencyIn,
                path,
                amountIn,
                msg.sender,
                transferType,
                receiver
            );
        }

        bytes memory result = poolManager.unlock(swapData);
        uint128 amountOut = abi.decode(result, (uint128));

        return amountOut;
    }

    function _decodeData(bytes calldata data)
        internal
        pure
        returns (
            address tokenIn,
            address tokenOut,
            bool zeroForOne,
            TransferType transferType,
            address receiver,
            UniswapV4Pool[] memory pools
        )
    {
        if (data.length < 88) {
            revert UniswapV4Executor__InvalidDataLength();
        }

        tokenIn = address(bytes20(data[0:20]));
        tokenOut = address(bytes20(data[20:40]));
        zeroForOne = (data[40] != 0);
        transferType = TransferType(uint8(data[41]));
        receiver = address(bytes20(data[42:62]));

        uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object
        pools = new UniswapV4Pool[](poolsLength);
        bytes memory poolsData = data[62:];
        uint256 offset = 0;
        for (uint256 i = 0; i < poolsLength; i++) {
            address intermediaryToken;
            uint24 fee;
            int24 tickSpacing;

            // slither-disable-next-line assembly
            assembly {
                intermediaryToken := mload(add(poolsData, add(offset, 20)))
                fee := shr(232, mload(add(poolsData, add(offset, 52))))
                tickSpacing := shr(232, mload(add(poolsData, add(offset, 55))))
            }
            pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing);
            offset += 26;
        }
    }

    /**
     * @notice Handles the callback from the pool manager. This is used for callbacks from the router.
     */
    function handleCallback(bytes calldata data)
        external
        returns (bytes memory)
    {
        verifyCallback(data);
        return _unlockCallback(data);
    }

    function verifyCallback(bytes calldata) public view poolManagerOnly {}

    /**
     * @notice Handles the unlock callback from the pool manager. This is used for swaps against the executor directly (bypassing the router).
     */
    function unlockCallback(bytes calldata data)
        external
        poolManagerOnly
        returns (bytes memory)
    {
        return _unlockCallback(data);
    }

    /**
     * @dev Internal function to handle the unlock callback.
     */
    function _unlockCallback(bytes calldata data)
        internal
        returns (bytes memory)
    {
        bytes4 selector = bytes4(data[:4]);
        if (
            selector != SWAP_EXACT_INPUT_SELECTOR
                && selector != SWAP_EXACT_INPUT_SINGLE_SELECTOR
        ) {
            revert UniswapV4Executor__UnknownCallback(selector);
        }

        // here we expect to call either `swapExactInputSingle` or `swapExactInput`. See `swap` to see how we encode the selector and the calldata
        // slither-disable-next-line low-level-calls
        (bool success, bytes memory returnData) = _self.delegatecall(data);
        if (!success) {
            revert(
                string(
                    returnData.length > 0
                        ? returnData
                        : abi.encodePacked("Uniswap v4 Callback failed")
                )
            );
        }
        return returnData;
    }

    /**
     * @notice Performs an exact input single swap. It settles and takes the tokens after the swap.
     * @param poolKey The key of the pool to swap in.
     * @param zeroForOne Whether the swap is from token0 to token1 (true) or vice versa (false).
     * @param amountIn The amount of tokens to swap in.
     * @param sender The address of the sender.
     * @param transferType The type of transfer in to use.
     * @param receiver The address of the receiver.
     * @param hookData Additional data for hook contracts.
     */
    function swapExactInputSingle(
        PoolKey memory poolKey,
        bool zeroForOne,
        uint128 amountIn,
        address sender,
        TransferType transferType,
        address receiver,
        bytes calldata hookData
    ) external returns (uint128) {
        uint128 amountOut = _swap(
            poolKey, zeroForOne, -int256(uint256(amountIn)), hookData
        ).toUint128();

        Currency currencyIn = zeroForOne ? poolKey.currency0 : poolKey.currency1;
        uint256 amount = _getFullDebt(currencyIn);
        if (amount > amountIn) {
            revert UniswapV4Executor__V4TooMuchRequested(amountIn, amount);
        }
        _settle(currencyIn, amount, sender, transferType);

        Currency currencyOut =
            zeroForOne ? poolKey.currency1 : poolKey.currency0;
        _take(currencyOut, receiver, _mapTakeAmount(amountOut, currencyOut));
        return amountOut;
    }

    /**
     * @notice Performs an exact input swap along a path. It settles and takes the tokens after the swap.
     * @param currencyIn The currency of the input token.
     * @param path The path to swap along.
     * @param amountIn The amount of tokens to swap in.
     * @param sender The address of the sender.
     * @param transferType The type of transfer in to use.
     * @param receiver The address of the receiver.
     */
    function swapExactInput(
        Currency currencyIn,
        PathKey[] calldata path,
        uint128 amountIn,
        address sender,
        TransferType transferType,
        address receiver
    ) external returns (uint128) {
        uint128 amountOut = 0;
        Currency swapCurrencyIn = currencyIn;
        uint256 swapAmountIn = amountIn;
        unchecked {
            uint256 pathLength = path.length;
            PathKey calldata pathKey;

            for (uint256 i = 0; i < pathLength; i++) {
                pathKey = path[i];
                (PoolKey memory poolKey, bool zeroForOne) =
                    pathKey.getPoolAndSwapDirection(swapCurrencyIn);
                // The output delta will always be positive, except for when interacting with certain hook pools
                amountOut = _swap(
                    poolKey,
                    zeroForOne,
                    -int256(uint256(swapAmountIn)),
                    pathKey.hookData
                ).toUint128();

                swapAmountIn = amountOut;
                swapCurrencyIn = pathKey.intermediateCurrency;
            }
        }

        uint256 amount = _getFullDebt(currencyIn);
        if (amount > amountIn) {
            revert UniswapV4Executor__V4TooMuchRequested(amountIn, amount);
        }
        _settle(currencyIn, amount, sender, transferType);

        _take(
            swapCurrencyIn, // at the end of the loop this is actually currency out
            receiver,
            _mapTakeAmount(amountOut, swapCurrencyIn)
        );
        return amountOut;
    }

    function _swap(
        PoolKey memory poolKey,
        bool zeroForOne,
        int256 amountSpecified,
        bytes calldata hookData
    ) private returns (int128 reciprocalAmount) {
        unchecked {
            // slither-disable-next-line calls-loop
            BalanceDelta delta = poolManager.swap(
                poolKey,
                IPoolManager.SwapParams(
                    zeroForOne,
                    amountSpecified,
                    zeroForOne
                        ? TickMath.MIN_SQRT_PRICE + 1
                        : TickMath.MAX_SQRT_PRICE - 1
                ),
                hookData
            );

            reciprocalAmount = (zeroForOne == amountSpecified < 0)
                ? delta.amount1()
                : delta.amount0();
        }
    }

    /**
     * @notice Obtains the full amount owed by this contract (negative delta).
     * @param currency The currency to get the delta for.
     * @return amount The amount owed by this contract.
     */
    function _getFullCredit(Currency currency)
        internal
        view
        returns (uint256 amount)
    {
        int256 _amount = poolManager.currencyDelta(address(this), currency);
        // If the amount is negative, it should be settled not taken.
        if (_amount < 0) revert UniswapV4Executor__DeltaNotPositive(currency);
        amount = uint256(_amount);
    }

    /// @notice Obtain the full amount owed by this contract (negative delta)
    /// @param currency Currency to get the delta for
    /// @return amount The amount owed by this contract as a uint256
    function _getFullDebt(Currency currency)
        internal
        view
        returns (uint256 amount)
    {
        int256 _amount = poolManager.currencyDelta(address(this), currency);
        // If the amount is positive, it should be taken not settled.
        if (_amount > 0) revert UniswapV4Executor__DeltaNotNegative(currency);
        // Casting is safe due to limits on the total supply of a pool
        amount = uint256(-_amount);
    }

    /**
     * @notice Pays and settles a currency to the pool manager.
     * @dev The implementing contract must ensure that the `payer` is a secure address.
     * @param currency The currency to settle.
     * @param amount The amount to send.
     * @param sender The address of the payer.
     * @param transferType The type of transfer to use.
     * @dev Returns early if the amount is 0.
     */
    function _settle(
        Currency currency,
        uint256 amount,
        address sender,
        TransferType transferType
    ) internal {
        if (amount == 0) return;
        poolManager.sync(currency);
        if (currency.isAddressZero()) {
            // slither-disable-next-line unused-return
            poolManager.settle{value: amount}();
        } else {
            _transfer(
                Currency.unwrap(currency),
                sender,
                address(poolManager),
                amount,
                transferType
            );
            // slither-disable-next-line unused-return
            poolManager.settle();
        }
    }

    /**
     * @notice Takes an amount of currency out of the pool manager.
     * @param currency The currency to take.
     * @param recipient The address to receive the currency.
     * @param amount The amount to take.
     * @dev Returns early if the amount is 0.
     */
    function _take(Currency currency, address recipient, uint256 amount)
        internal
    {
        if (amount == 0) return;
        poolManager.take(currency, recipient, amount);
    }

    function _mapTakeAmount(uint256 amount, Currency currency)
        internal
        view
        returns (uint256)
    {
        if (amount == 0) {
            return _getFullCredit(currency);
        } else {
            return amount;
        }
    }
}
