// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/**
 * @title AereSwap — constant-product AMM
 * @notice Constant-product automated market maker for AERE Network
 * @dev Implements the x*y=k constant-product invariant, the canonical EVM
 *      pool design. Each pair is its own contract; this factory uses a
 *      simple single-pool design (token0/token1 paired against WAERE).
 *
 * Whitepaper alignment:
 *   - "Ultra-High Performance DEX Engine"
 *   - sub-second trade execution (matches AERE 100ms blocks)
 *   - near-zero slippage (governed by liquidity depth, same as any AMM)
 */

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

contract AereSwapPair {
    address public immutable token0;
    address public immutable token1;
    address public immutable factory;

    uint112 private reserve0;
    uint112 private reserve1;
    uint32  private blockTimestampLast;

    uint256 public constant MINIMUM_LIQUIDITY = 1000;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    event Mint(address indexed sender, uint256 amount0, uint256 amount1);
    event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
    event Swap(
        address indexed sender,
        uint256 amount0In,
        uint256 amount1In,
        uint256 amount0Out,
        uint256 amount1Out,
        address indexed to
    );
    event Sync(uint112 reserve0, uint112 reserve1);

    constructor(address _token0, address _token1) {
        token0 = _token0;
        token1 = _token1;
        factory = msg.sender;
    }

    function getReserves() public view returns (uint112 r0, uint112 r1, uint32 ts) {
        return (reserve0, reserve1, blockTimestampLast);
    }

    function _update(uint256 balance0, uint256 balance1) private {
        require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "AereSwap: overflow");
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = uint32(block.timestamp);
        emit Sync(reserve0, reserve1);
    }

    /**
     * @notice Mint LP tokens by depositing both tokens at the current ratio.
     * @dev Caller must have already transferred tokens to this contract.
     */
    function mint(address to) external returns (uint256 liquidity) {
        (uint112 _r0, uint112 _r1, ) = getReserves();
        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        uint256 amount0 = balance0 - _r0;
        uint256 amount1 = balance1 - _r1;

        uint256 _totalSupply = totalSupply;
        if (_totalSupply == 0) {
            liquidity = _sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
            balanceOf[address(0)] = MINIMUM_LIQUIDITY;
            totalSupply += MINIMUM_LIQUIDITY;
        } else {
            uint256 a = (amount0 * _totalSupply) / _r0;
            uint256 b = (amount1 * _totalSupply) / _r1;
            liquidity = a < b ? a : b;
        }
        require(liquidity > 0, "AereSwap: zero liquidity");
        balanceOf[to] += liquidity;
        totalSupply += liquidity;
        _update(balance0, balance1);
        emit Mint(msg.sender, amount0, amount1);
    }

    /**
     * @notice Burn LP tokens, withdraw both tokens.
     * @dev Caller must have transferred their LP tokens to this contract first.
     */
    function burn(address to) external returns (uint256 amount0, uint256 amount1) {
        uint256 liquidity = balanceOf[address(this)];
        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        uint256 _totalSupply = totalSupply;
        amount0 = (liquidity * balance0) / _totalSupply;
        amount1 = (liquidity * balance1) / _totalSupply;
        require(amount0 > 0 && amount1 > 0, "AereSwap: zero burn output");
        balanceOf[address(this)] = 0;
        totalSupply -= liquidity;
        IERC20(token0).transfer(to, amount0);
        IERC20(token1).transfer(to, amount1);
        balance0 = IERC20(token0).balanceOf(address(this));
        balance1 = IERC20(token1).balanceOf(address(this));
        _update(balance0, balance1);
        emit Burn(msg.sender, amount0, amount1, to);
    }

    /**
     * @notice Swap with constant-product invariant. 0.3% fee retained in pool (LP earns).
     * @dev Caller must have transferred input tokens to this contract first.
     */
    function swap(uint256 amount0Out, uint256 amount1Out, address to) external {
        require(amount0Out > 0 || amount1Out > 0, "AereSwap: zero output");
        (uint112 _r0, uint112 _r1, ) = getReserves();
        require(amount0Out < _r0 && amount1Out < _r1, "AereSwap: insufficient liquidity");

        if (amount0Out > 0) IERC20(token0).transfer(to, amount0Out);
        if (amount1Out > 0) IERC20(token1).transfer(to, amount1Out);

        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        uint256 amount0In = balance0 > _r0 - amount0Out ? balance0 - (_r0 - amount0Out) : 0;
        uint256 amount1In = balance1 > _r1 - amount1Out ? balance1 - (_r1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, "AereSwap: zero input");

        // Fee: 0.3% taken from input. Adjusted balances must satisfy x*y >= k.
        uint256 balance0Adj = balance0 * 1000 - amount0In * 3;
        uint256 balance1Adj = balance1 * 1000 - amount1In * 3;
        require(balance0Adj * balance1Adj >= uint256(_r0) * _r1 * 1_000_000, "AereSwap: K invariant");

        _update(balance0, balance1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }

    function _sqrt(uint256 x) private pure returns (uint256 r) {
        if (x == 0) return 0;
        uint256 z = (x + 1) / 2;
        r = x;
        while (z < r) { r = z; z = (x / z + z) / 2; }
    }
}

contract AereSwapFactory {
    address public immutable WAERE;
    address public feeTo;
    address public feeToSetter;

    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;

    event PairCreated(address indexed token0, address indexed token1, address pair, uint256 index);

    constructor(address _WAERE, address _feeToSetter) {
        WAERE = _WAERE;
        feeToSetter = _feeToSetter;
    }

    function allPairsLength() external view returns (uint256) {
        return allPairs.length;
    }

    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, "AereSwap: identical tokens");
        (address t0, address t1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(t0 != address(0), "AereSwap: zero address");
        require(getPair[t0][t1] == address(0), "AereSwap: pair exists");

        AereSwapPair p = new AereSwapPair(t0, t1);
        pair = address(p);
        getPair[t0][t1] = pair;
        getPair[t1][t0] = pair;
        allPairs.push(pair);
        emit PairCreated(t0, t1, pair, allPairs.length - 1);
    }

    function setFeeTo(address _feeTo) external {
        require(msg.sender == feeToSetter, "AereSwap: forbidden");
        feeTo = _feeTo;
    }
}
