// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

import "../extensions/UintLibrary.sol";
import "../extensions/HasSecondarySaleFees.sol";

import "./ERC20TransferProxy.sol";
import "./TransferProxy.sol";
import "./ExchangeOrdersHolder.sol";
import "./ExchangeDomain.sol";
import "./ExchangeState.sol";
import "./TransferProxyForDeprecated.sol";


contract NftExchange is Ownable, ExchangeDomain {
    using ECDSA for bytes32;
    using SafeMath for uint;
    using UintLibrary for uint;

    enum FeeSide {NONE, SELL, BUY}

    event Buy(
        address indexed sellToken, uint256 indexed sellTokenId, uint256 sellValue,
        address owner,
        address buyToken, uint256 buyTokenId, uint256 buyValue,
        address buyer,
        uint256 amount,
        uint256 salt
    );

    event Cancel(
        address indexed sellToken, uint256 indexed sellTokenId,
        address owner,
        address buyToken, uint256 buyTokenId,
        uint256 salt
    );

    bytes4 private constant _INTERFACE_ID_FEES = 0xb7799584;
    uint256 private constant UINT256_MAX = 2 ** 256 - 1;

    address payable public beneficiary;
    address public buyerFeeSigner;

    TransferProxy public transferProxy;
    TransferProxyForDeprecated public transferProxyForDeprecated;
    ERC20TransferProxy public erc20TransferProxy;
    ExchangeState public state;
    ExchangeOrdersHolder public ordersHolder;

    constructor(
        TransferProxy _transferProxy, TransferProxyForDeprecated _transferProxyForDeprecated, ERC20TransferProxy _erc20TransferProxy, ExchangeState _state,
        ExchangeOrdersHolder _ordersHolder, address payable _beneficiary, address _buyerFeeSigner
    ) {
        transferProxy = _transferProxy;
        transferProxyForDeprecated = _transferProxyForDeprecated;
        erc20TransferProxy = _erc20TransferProxy;
        state = _state;
        ordersHolder = _ordersHolder;
        beneficiary = _beneficiary;
        buyerFeeSigner = _buyerFeeSigner;
    }

    function setBeneficiary(address payable newBeneficiary) external onlyOwner {
        beneficiary = newBeneficiary;
    }

    function setBuyerFeeSigner(address newBuyerFeeSigner) external onlyOwner {
        buyerFeeSigner = newBuyerFeeSigner;
    }

    function exchange(
        Order calldata order,
        Sig calldata sig,
        uint buyerFee,
        Sig calldata buyerFeeSig,
        uint amount,
        address buyer
    ) payable external {
        validateOrderSig(order, sig);
        validateBuyerFeeSig(order, buyerFee, buyerFeeSig);
        require(order.expired > block.timestamp, "order expired");
        uint paying = order.buying.mul(amount).div(order.selling);
        verifyOpenAndModifyOrderState(order.key, order.selling, amount);
        require(order.key.sellAsset.assetType != AssetType.ETH, "ETH is not supported on sell side");
        if (order.key.buyAsset.assetType == AssetType.ETH) {
            validateEthTransfer(paying, buyerFee);
        }
        FeeSide feeSide = getFeeSide(order.key.sellAsset.assetType, order.key.buyAsset.assetType);
        if (buyer == address(0x0)) {
            buyer = msg.sender;
        }
        /* SELL = accept, BUY = buy */
        transferWithFeesPossibility(order.key.sellAsset, amount, order.key.owner, buyer, feeSide == FeeSide.SELL, buyerFee, order.sellerFee, order.key.buyAsset); 
        transferWithFeesPossibility(order.key.buyAsset, paying, msg.sender, order.key.owner, feeSide == FeeSide.BUY, order.sellerFee, buyerFee, order.key.sellAsset);
        emitBuy(order, amount, buyer);
    }

    function validateEthTransfer(uint value, uint buyerFee) internal view {
        uint256 buyerFeeValue = value.bp(buyerFee);
        require(msg.value == value + buyerFeeValue, "msg.value is incorrect");
    }

    function cancel(OrderKey calldata key) external {
        require(key.owner == msg.sender, "not an owner");
        state.setCompleted(key, UINT256_MAX);
        emit Cancel(key.sellAsset.token, key.sellAsset.tokenId, msg.sender, key.buyAsset.token, key.buyAsset.tokenId, key.salt);
    }

    function validateOrderSig(
        Order memory order,
        Sig memory sig
    ) internal view {
        if (sig.v == 0 && sig.r == bytes32(0x0) && sig.s == bytes32(0x0)) {
            require(ordersHolder.exists(order), "incorrect signature");
        } else {
            require(prepareMessage(order).toEthSignedMessageHash().recover(sig.v, sig.r, sig.s) == order.key.owner, "incorrect signature");
        }
    }

    function validateBuyerFeeSig(
        Order memory order,
        uint buyerFee,
        Sig memory sig
    ) internal view {
        require(prepareBuyerFeeMessage(order, buyerFee).toEthSignedMessageHash().recover(sig.v, sig.r, sig.s) == buyerFeeSigner, "incorrect buyer fee signature");
    }

    function prepareBuyerFeeMessage(Order memory order, uint fee) public pure returns (bytes32) {
        return keccak256(abi.encode(order, fee));
    }

    function prepareMessage(Order memory order) public pure returns (bytes32) {
        return keccak256(abi.encode(order));
    }

    function transferWithFeesPossibility(Asset memory firstType, uint value, address from, address to, bool hasFee, uint256 sellerFee, uint256 buyerFee, Asset memory secondType) internal {
        if (!hasFee) {
            transfer(firstType, value, from, to);
        } else {
            transferWithFees(firstType, value, from, to, sellerFee, buyerFee, secondType);
        }
    }

    function transfer(Asset memory asset, uint value, address from, address to) internal {
        if (asset.assetType == AssetType.ETH) {
            address payable toPayable = payable(to);
            toPayable.transfer(value);
        } else if (asset.assetType == AssetType.ERC20) {
            require(asset.tokenId == 0, "tokenId  be 0");
            erc20TransferProxy.erc20safeTransferFrom(IERC20(asset.token), from, to, value);
        } else if (asset.assetType == AssetType.ERC721) {
            require(value == 1, "value  be 1 for ERC-721");
            transferProxy.erc721safeTransferFrom(IERC721(asset.token), from, to, asset.tokenId);
        } else if (asset.assetType == AssetType.ERC721Deprecated) {
            require(value == 1, "value  be 1 for ERC-721");
            transferProxyForDeprecated.erc721TransferFrom(IERC721(asset.token), from, to, asset.tokenId);
        } else {
            transferProxy.erc1155safeTransferFrom(IERC1155(asset.token), from, to, asset.tokenId, value, "");
        }
    }

    function transferWithFees(Asset memory firstType, uint value, address from, address to, uint256 sellerFee, uint256 buyerFee, Asset memory secondType) internal {
        uint restValue = transferFeeToBeneficiary(firstType, from, value, sellerFee, buyerFee);
        if (
            secondType.assetType == AssetType.ERC1155 && IERC165(secondType.token).supportsInterface(_INTERFACE_ID_FEES) ||
            (secondType.assetType == AssetType.ERC721 || secondType.assetType == AssetType.ERC721Deprecated) && IERC165(secondType.token).supportsInterface(_INTERFACE_ID_FEES)
        ) {
            HasSecondarySaleFees withFees = HasSecondarySaleFees(secondType.token);
            address payable[] memory recipients = withFees.getFeeRecipients(secondType.tokenId);
            uint[] memory fees = withFees.getFeeBps(secondType.tokenId);
            require(fees.length == recipients.length);
            for (uint256 i = 0; i < fees.length; i++) {
                (uint newRestValue, uint current) = subFeeInBp(restValue, value, fees[i]);
                restValue = newRestValue;
                transfer(firstType, current, from, recipients[i]);
            }
        }
        address payable toPayable = payable(to);
        transfer(firstType, restValue, from, toPayable);
    }

    function transferFeeToBeneficiary(Asset memory asset, address from, uint total, uint sellerFee, uint buyerFee) internal returns (uint) {
        (uint restValue, uint sellerFeeValue) = subFeeInBp(total, total, sellerFee);
        uint buyerFeeValue = total.bp(buyerFee);
        uint beneficiaryFee = buyerFeeValue.add(sellerFeeValue);
        if (beneficiaryFee > 0) {
            transfer(asset, beneficiaryFee, from, beneficiary);
        }
        return restValue;
    }

    function emitBuy(Order memory order, uint amount, address buyer) internal {
        emit Buy(order.key.sellAsset.token, order.key.sellAsset.tokenId, order.selling,
            order.key.owner,
            order.key.buyAsset.token, order.key.buyAsset.tokenId, order.buying,
            buyer,
            amount,
            order.key.salt
        );
    }

    function subFeeInBp(uint value, uint total, uint feeInBp) internal pure returns (uint newValue, uint realFee) {
        return subFee(value, total.bp(feeInBp));
    }

    function subFee(uint value, uint fee) internal pure returns (uint newValue, uint realFee) {
        if (value > fee) {
            newValue = value - fee;
            realFee = fee;
        } else {
            newValue = 0;
            realFee = value;
        }
    }

    function verifyOpenAndModifyOrderState(OrderKey memory key, uint selling, uint amount) internal {
        uint completed = state.getCompleted(key);
        uint newCompleted = completed.add(amount);
        require(newCompleted <= selling, "not enough stock of order for buying");
        state.setCompleted(key, newCompleted);
    }

    function getFeeSide(AssetType sellType, AssetType buyType) internal pure returns (FeeSide) {
        if ((sellType == AssetType.ERC721 || sellType == AssetType.ERC721Deprecated) &&
            (buyType == AssetType.ERC721 || buyType == AssetType.ERC721Deprecated)) {
            return FeeSide.NONE;
        }
        if (uint(sellType) > uint(buyType)) {
            return FeeSide.BUY;
        }
        return FeeSide.SELL;
    }
}
[{"inputs":[{"internalType":"contract TransferProxy","name":"_transferProxy","type":"address"},{"internalType":"contract TransferProxyForDeprecated","name":"_transferProxyForDeprecated","type":"address"},{"internalType":"contract ERC20TransferProxy","name":"_erc20TransferProxy","type":"address"},{"internalType":"contract ExchangeState","name":"_state","type":"address"},{"internalType":"contract ExchangeOrdersHolder","name":"_ordersHolder","type":"address"},{"internalType":"address payable","name":"_beneficiary","type":"address"},{"internalType":"address","name":"_buyerFeeSigner","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sellToken","type":"address"},{"indexed":true,"internalType":"uint256","name":"sellTokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sellValue","type":"uint256"},{"indexed":false,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"address","name":"buyToken","type":"address"},{"indexed":false,"internalType":"uint256","name":"buyTokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"buyValue","type":"uint256"},{"indexed":false,"internalType":"address","name":"buyer","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"salt","type":"uint256"}],"name":"Buy","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sellToken","type":"address"},{"indexed":true,"internalType":"uint256","name":"sellTokenId","type":"uint256"},{"indexed":false,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"address","name":"buyToken","type":"address"},{"indexed":false,"internalType":"uint256","name":"buyTokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"salt","type":"uint256"}],"name":"Cancel","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[],"name":"beneficiary","outputs":[{"internalType":"address payable","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"buyerFeeSigner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"enum ExchangeDomain.AssetType","name":"assetType","type":"uint8"}],"internalType":"struct ExchangeDomain.Asset","name":"sellAsset","type":"tuple"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"enum ExchangeDomain.AssetType","name":"assetType","type":"uint8"}],"internalType":"struct ExchangeDomain.Asset","name":"buyAsset","type":"tuple"}],"internalType":"struct ExchangeDomain.OrderKey","name":"key","type":"tuple"}],"name":"cancel","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"erc20TransferProxy","outputs":[{"internalType":"contract ERC20TransferProxy","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"components":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"enum ExchangeDomain.AssetType","name":"assetType","type":"uint8"}],"internalType":"struct ExchangeDomain.Asset","name":"sellAsset","type":"tuple"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"enum ExchangeDomain.AssetType","name":"assetType","type":"uint8"}],"internalType":"struct ExchangeDomain.Asset","name":"buyAsset","type":"tuple"}],"internalType":"struct ExchangeDomain.OrderKey","name":"key","type":"tuple"},{"internalType":"uint256","name":"selling","type":"uint256"},{"internalType":"uint256","name":"buying","type":"uint256"},{"internalType":"uint256","name":"expired","type":"uint256"},{"internalType":"uint256","name":"sellerFee","type":"uint256"}],"internalType":"struct ExchangeDomain.Order","name":"order","type":"tuple"},{"components":[{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct ExchangeDomain.Sig","name":"sig","type":"tuple"},{"internalType":"uint256","name":"buyerFee","type":"uint256"},{"components":[{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct ExchangeDomain.Sig","name":"buyerFeeSig","type":"tuple"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"address","name":"buyer","type":"address"}],"name":"exchange","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"ordersHolder","outputs":[{"internalType":"contract ExchangeOrdersHolder","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"components":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"enum ExchangeDomain.AssetType","name":"assetType","type":"uint8"}],"internalType":"struct ExchangeDomain.Asset","name":"sellAsset","type":"tuple"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"enum ExchangeDomain.AssetType","name":"assetType","type":"uint8"}],"internalType":"struct ExchangeDomain.Asset","name":"buyAsset","type":"tuple"}],"internalType":"struct ExchangeDomain.OrderKey","name":"key","type":"tuple"},{"internalType":"uint256","name":"selling","type":"uint256"},{"internalType":"uint256","name":"buying","type":"uint256"},{"internalType":"uint256","name":"expired","type":"uint256"},{"internalType":"uint256","name":"sellerFee","type":"uint256"}],"internalType":"struct ExchangeDomain.Order","name":"order","type":"tuple"},{"internalType":"uint256","name":"fee","type":"uint256"}],"name":"prepareBuyerFeeMessage","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[{"components":[{"components":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"enum ExchangeDomain.AssetType","name":"assetType","type":"uint8"}],"internalType":"struct ExchangeDomain.Asset","name":"sellAsset","type":"tuple"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"enum ExchangeDomain.AssetType","name":"assetType","type":"uint8"}],"internalType":"struct ExchangeDomain.Asset","name":"buyAsset","type":"tuple"}],"internalType":"struct ExchangeDomain.OrderKey","name":"key","type":"tuple"},{"internalType":"uint256","name":"selling","type":"uint256"},{"internalType":"uint256","name":"buying","type":"uint256"},{"internalType":"uint256","name":"expired","type":"uint256"},{"internalType":"uint256","name":"sellerFee","type":"uint256"}],"internalType":"struct ExchangeDomain.Order","name":"order","type":"tuple"}],"name":"prepareMessage","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"newBeneficiary","type":"address"}],"name":"setBeneficiary","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newBuyerFeeSigner","type":"address"}],"name":"setBuyerFeeSigner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"state","outputs":[{"internalType":"contract ExchangeState","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"transferProxy","outputs":[{"internalType":"contract TransferProxy","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"transferProxyForDeprecated","outputs":[{"internalType":"contract TransferProxyForDeprecated","name":"","type":"address"}],"stateMutability":"view","type":"function"}]