// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity ^0.8.27;

import {
    IERC20
} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {
    SafeERC20
} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {ISwapAdapter} from "../interfaces/ISwapAdapter.sol";
import {Funding} from "./Funding.sol";
import {IPartyInfo} from "./IPartyInfo.sol";
import {IPartyPlanner} from "./IPartyPlanner.sol";
import {IPartyPool} from "./IPartyPool.sol";
import {console2} from "../../lib/forge-std/src/console2.sol";

contract LiquidityPartySwapAdapter is ISwapAdapter {
    using SafeERC20 for IERC20;

    // Forge lint wants immutables to be all caps. Slither wants them to be
    // mixed case. Why do we care about pedantic linters? The Solidity style
    // guide mentions "constants" but never "immutables." Faced with an
    // irresolvable linter conflict, I chose to disable the slither linter,
    // since its detection of immutables as constants seems to be broken.
    // slither-disable-next-line naming-convention
    IPartyPlanner public immutable PLANNER;
    // slither-disable-next-line naming-convention
    IPartyInfo public immutable INFO;

    constructor(IPartyPlanner planner, IPartyInfo info) {
        PLANNER = planner;
        INFO = info;
    }

    function price(
        bytes32 poolId,
        address sellToken,
        address buyToken,
        uint256[] memory specifiedAmounts
    ) external view override returns (Fraction[] memory prices) {
        IPartyPool pool = _poolFromId(poolId);
        (uint256 indexIn, uint256 indexOut) =
            _tokenIndexes(pool, sellToken, buyToken);
        prices = new Fraction[](specifiedAmounts.length);
        for (uint256 i = 0; i < specifiedAmounts.length; i++) {
            uint256 amount = specifiedAmounts[i];
            if (amount == 0) {
                // Marginal price support
                prices[i] = _marginalPrice(pool, indexIn, indexOut);
            } else {
                // Regular slippage calculation
                // slither-disable-next-line unused-return calls-loop
                (
                    uint256 amountIn,
                    uint256 amountOut, /*uint256 inFee*/
                ) = pool.swapAmounts(indexIn, indexOut, amount, 0);
                prices[i].numerator = amountOut;
                prices[i].denominator = amountIn;
            }
        }
    }

    function swap(
        bytes32 poolId,
        address sellToken,
        address buyToken,
        OrderSide,
        /*side*/
        uint256 specifiedAmount
    ) external returns (Trade memory trade) {
        // Setup
        address swapper = msg.sender;
        IPartyPool pool = _poolFromId(poolId);
        // This require should never trigger if the substreams module correctly
        // removes components that were killed.
        if (!INFO.working(pool)) {
            revert Unavailable("LiqP pool not working");
        }
        (uint256 indexIn, uint256 indexOut) =
            _tokenIndexes(pool, sellToken, buyToken);

        // Transfer and Swap
        uint256 startingGas = gasleft();
        IERC20(sellToken)
            .safeTransferFrom(swapper, address(pool), specifiedAmount);
        // slither-disable-next-line unused-return
        try pool.swap(
            address(0),
            Funding.PREFUNDING,
            swapper,
            indexIn,
            indexOut,
            specifiedAmount,
            0,
            0,
            false,
            ""
        ) returns (
            uint256 amountIn, uint256 amountOut, uint256 inFee
        ) {
            uint256 endingGas = gasleft();
            uint256 gasUsed = startingGas - endingGas;
            Fraction memory poolPrice = _marginalPrice(pool, indexIn, indexOut);
            console2.log("Successfully swapped", amountOut);
            // forge-lint: disable-next-line(named-struct-fields)
            return Trade(amountOut, gasUsed, poolPrice);
        } catch (bytes memory reason) {
            bytes32 hash = keccak256(reason);
            if (hash == keccak256("swap: input too small after fee")) {
                revert TooSmall(0);
            } else if (
                hash == keccak256("swap: transfer exceeds max")
                    || hash
                        == keccak256("LMSR: a/b too large (would overflow exp)")
                    || hash == keccak256("swap: transfer exceeds max")
            ) {
                revert LimitExceeded(0); // todo size
            } else if (hash == keccak256("killed")) {
                revert Unavailable("pool has been permanently killed");
            } else if (hash == keccak256("LMSR: size metric zero")) {
                revert Unavailable("pool currently has no LP assets");
            } else if (hash == keccak256("LMSR: limitPrice <= current price")) {
                revert InvalidOrder("limit price is below current price");
            } else if (
                hash == keccak256("LMSR: ratio<=0") // invalid limit price
            ) {
                revert InvalidOrder("limit price cannot be negative");
            } else {
                console2.log("Unhandled error", string(reason));
                // re-raise
                assembly {
                    revert(add(reason, 0x20), mload(reason))
                }
            }
        }
    }

    function getLimits(bytes32 poolId, address sellToken, address buyToken)
        external
        view
        returns (uint256[] memory limits)
    {
        // We arbitrarily limit the amounts like Uniswap V2 does, to make the
        // test cases work. There is no theoretical limit on the input amount.
        // forge-lint: disable-next-line(unsafe-typecast)
        address pool = address(bytes20(poolId));
        limits = new uint256[](2);

        // input token limit: Theoretically unlimited, but artificially limited
        // here to appease Tycho's test cases. Instead of estimating actual
        // input limits based on a maximum target slippage, we merely return the
        // current
        // inventory of input token. Even for large stablecoin pools with a
        // kappa near 1, this input amount should result in an "unreasonably"
        // high slippage:
        // Pool Size => Slippage for inputAmount=reserveBalance and kappa=1
        //  2 => 33.7%
        // 10 => 9.2%
        // 25 => 3.9%
        // 50 => 2.1%
        // See the commented-out method below for an exact computation of the
        // maximum input amount for a given pool configuration and target
        // slippage.
        limits[0] = IERC20(sellToken).balanceOf(pool);

        // output token limit: the pool's current balance (an overestimate)
        limits[1] = IERC20(buyToken).balanceOf(pool);
    }

    function getCapabilities(
        bytes32,
        /*poolId*/
        address,
        /*sellToken*/
        address /*buyToken*/
    )
        external
        pure
        returns (Capability[] memory capabilities)
    {
        capabilities = new Capability[](3);
        capabilities[0] = Capability.SellOrder;
        capabilities[1] = Capability.PriceFunction;
        capabilities[2] = Capability.MarginalPrice;
        return capabilities;
    }

    function getTokens(bytes32 poolId)
        external
        view
        returns (address[] memory tokens)
    {
        IPartyPool pool = _poolFromId(poolId);
        return pool.allTokens();
    }

    function getPoolIds(uint256 offset, uint256 limit)
        external
        view
        returns (bytes32[] memory ids)
    {
        IPartyPool[] memory pools = PLANNER.getAllPools(offset, limit);
        ids = new bytes32[](pools.length);
        for (uint256 i = 0; i < pools.length; i++) {
            ids[i] = bytes32(uint256(uint160(address(pools[i]))));
        }
    }

    //
    // Internal Helpers
    //

    uint256 private constant NONE = type(uint256).max;

    /// @dev Liquidity Party pools identify tokens by index rather than address,
    /// saving 5200 gas per swap.
    function _tokenIndexes(IPartyPool pool, address sellToken, address buyToken)
        internal
        view
        returns (uint256 indexIn, uint256 indexOut)
    {
        indexIn = NONE;
        indexOut = NONE;
        address[] memory tokens = pool.allTokens();
        uint256 numTokens = pool.numTokens();
        for (uint256 i = 0; i < numTokens; i++) {
            if (tokens[i] == sellToken) {
                indexIn = i;
            } else if (tokens[i] == buyToken) {
                indexOut = i;
            }
        }
        // This should never happen if the token metadata was correctly loaded
        // by substreams
        require(indexIn != NONE && indexOut != NONE, "tokens not in pool");
    }

    function _marginalPrice(IPartyPool pool, uint256 indexIn, uint256 indexOut)
        internal
        view
        returns (Fraction memory poolPrice)
    {
        // Liquidity Party prices are Q128.128 fixed point format
        // slither-disable-next-line calls-loop
        uint256 price128x128 = INFO.price(pool, indexIn, indexOut);
        uint256 feePpm = pool.fee(indexIn, indexOut);
        //        price128x128 *= 1_000_000 - feePpm;
        //        price128x128 /= 1_000_000;
        // forge-lint: disable-next-line(unsafe-typecast,named-struct-fields)
        return Fraction(price128x128, 1 << 128);
    }

    function _poolFromId(bytes32 poolId) internal pure returns (IPartyPool) {
        // forge-lint: disable-next-line(unsafe-typecast)
        return IPartyPool(address(bytes20(poolId)));
    }
}

