pragma solidity ^0.8.9;

contract EscrowSell is IEscrowSell, Ownable, ReentrancyGuard {
    using SafeERC20 for IRODO;
    using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet;
    enum SellStatus {
        Pending,
        Executed,
        Claimed,
        Executeable,
        Claimeable
    }

    struct SellInfo {
        uint256 id;
        uint256 amount;
        uint256 feeAmount;
        uint256 startBlock;
        uint256 endBlock;
        address user;
        SellStatus status;
    }

    // price of token in eth.
    uint256 public price;

    // rodo token address
    IRODO public rodo;

    // fee that will be received on top of price. 5000 = 5%
    uint256 public feePercentage;

    address public admin;

    address public escrowManager;

    address public burnAddress;

    uint256 public escrowBlocks;

    uint256 public totalAdminTokens;

    uint256 public totalLpTokens;

    uint256 public totalEth;

    uint256 public totalSellQueueTokens;

    uint256 public totalBurned;

    SellInfo[] private _sellInfos;

    mapping(address => EnumerableSetUpgradeable.UintSet) private _userSellIds;

    fallback() external payable {}

    receive() external payable {}

    modifier onlyAdmin() {
        require(admin == _msgSender(), "Escrow: caller is not admin");
        _;
    }

    modifier onlyOwnerOrEscrowManager() {
        require(
            escrowManager == _msgSender() || owner() == _msgSender(),
            "Escrow: caller is not escrowManager or owner"
        );
        _;
    }

    constructor(
        IRODO _rodo,
        uint256 _price,
        uint256 _feePercentage,
        uint256 _escrowBlocks,
        address _admin,
        address _escrowManager,
        address _burnAddress
    ) {
        Helpers.requireNonZeroAddress(address(_rodo));
        Helpers.requireNonZeroAddress(_admin);
        Helpers.requireNonZeroAddress(_escrowManager);
        Helpers.requireNonZeroAddress(_burnAddress);
        price = _price;
        rodo = _rodo;
        feePercentage = _feePercentage;
        escrowBlocks = _escrowBlocks;
        admin = _admin;
        escrowManager = _escrowManager;
        burnAddress = _burnAddress;
    }

    function allSells() public view returns (SellInfo[] memory) {
        uint256 length = _sellInfos.length;
        SellInfo[] memory userSells = new SellInfo[](length);
        for (uint256 i = 0; i <= length - 1; i++) {
            userSells[i] = _sellInfos[i];
            userSells[i].status = status(i);
        }
        return userSells;
    }

    function getTotalSellCount() public view returns (uint256) {
        return _sellInfos.length;
    }

    function getSell(uint256 index) public view returns (SellInfo memory) {
        return _sellInfos[index];
    }

    function paginateSells(
        uint256 start,
        uint256 end
    ) public view returns (SellInfo[] memory) {
        if (end >= _sellInfos.length) {
            end = _sellInfos.length - 1;
        }
        uint256 length = end - start + 1;
        SellInfo[] memory userSells = new SellInfo[](length);
        uint256 currentIndex = 0;
        for (uint256 i = start; i <= end; i++) {
            userSells[currentIndex] = _sellInfos[i];
            userSells[currentIndex].status = status(i);
            currentIndex++;
        }
        return userSells;
    }

    function getUserSells(
        address _account
    ) public view returns (SellInfo[] memory) {
        uint256 length = _userSellIds[_account].length();
        SellInfo[] memory userSells = new SellInfo[](length);
        for (uint256 i = 0; i < length; i++) {
            userSells[i] = _sellInfos[_userSellIds[_account].at(i)];
        }
        return userSells;
    }

    function getRequires(
        uint256 _amount,
        uint256 _price
    )
        internal
        pure
        returns (uint256 adminRequired, uint256 lpRequired, uint256 ethRequired)
    {
        adminRequired = (_amount * 20) / 100;
        lpRequired = (_amount * 80) / 100;
        ethRequired = (_amount * _price) / 1e2;
    }

    function status(uint256 _pid) public view returns (SellStatus) {
        SellInfo memory userSellInfo = _sellInfos[_pid];

        if (userSellInfo.status == SellStatus.Executed) {
            return SellStatus.Executed;
        }

        if (userSellInfo.status == SellStatus.Claimed) {
            return SellStatus.Claimed;
        }

        if (userSellInfo.status == SellStatus.Executeable) {
            return SellStatus.Executeable;
        } else if (block.number > userSellInfo.endBlock) {
            return SellStatus.Claimeable;
        } else return SellStatus.Pending;
    }

    function sell(uint256 _amount) public {
        address account = msg.sender;
        require(_amount > 0, "Escrow: Amount cannot be 0");
        uint256 fee = (_amount * feePercentage) / 1e5;
        uint256 amount = _amount + fee;
        uint256 id = _sellInfos.length;
        _sellInfos.push(
            SellInfo({
                id: id,
                user: account,
                amount: _amount,
                feeAmount: fee,
                startBlock: block.number,
                endBlock: block.number + escrowBlocks,
                status: SellStatus.Pending
            })
        );
        _userSellIds[account].add(id);
        totalSellQueueTokens += _amount;
        rodo.transferFrom(account, address(this), amount);
    }

    function approveSells(
        uint256[] memory pids
    ) public override onlyOwnerOrEscrowManager nonReentrant {
        uint256 totalAmount;
        uint256 sellPrice = price; // for saving gas
        for (uint256 i = 0; i < pids.length; i++) {
            SellInfo storage userSellInfo = _sellInfos[pids[i]];
            require(
                userSellInfo.startBlock < block.number &&
                    userSellInfo.endBlock >= block.number,
                "Escrow: invalid block"
            );
            require(
                userSellInfo.status == SellStatus.Pending,
                "Escrow: invalid status"
            );
            totalAmount += userSellInfo.amount;

            userSellInfo.status = SellStatus.Executeable;
        }
        uint256 currentIncomingLp = rodo.balanceOf(address(this)) -
            totalLpTokens -
            totalSellQueueTokens -
            totalAdminTokens;
        uint256 currentIncomingEth = address(this).balance - totalEth;
        (
            uint256 adminRequired,
            uint256 lpRequired,
            uint256 ethRequired
        ) = getRequires(totalAmount, sellPrice);
        require(currentIncomingLp >= lpRequired, "Escrow: low incoming tokens");
        require(currentIncomingEth >= ethRequired, "Escrow: low incoming eth");
        require(totalAdminTokens >= adminRequired, "Escrow: low admin tokens");
        totalLpTokens += lpRequired;
        totalEth += ethRequired;
        totalAdminTokens -= adminRequired;
        totalSellQueueTokens -= totalAmount;
    }

    function execute(uint256 _pid) public {
        address account = msg.sender;
        require(
            _userSellIds[account].contains(_pid),
            "Escrow: not owner of sell request"
        );
        SellInfo storage userSellInfo = _sellInfos[_pid];
        require(
            userSellInfo.status == SellStatus.Executeable,
            "Escrow: request is not executeable"
        );
        (, uint256 lpRequired, uint256 ethRequired) = getRequires(
            userSellInfo.amount,
            price
        );
        totalLpTokens -= lpRequired;
        totalEth -= ethRequired;
        totalBurned += userSellInfo.amount;
        userSellInfo.status = SellStatus.Executed;
        payable(account).transfer(ethRequired);
        rodo.transfer(admin, userSellInfo.feeAmount);
        rodo.transfer(burnAddress, userSellInfo.amount);
    }

    function claim(uint256 _pid) public nonReentrant {
        SellInfo storage userSellInfo = _sellInfos[_pid];
        address account = userSellInfo.user;
        require(userSellInfo.endBlock < block.number, "Escrow: invalid block");
        require(
            userSellInfo.status == SellStatus.Pending,
            "Escrow: invalid status"
        );
        userSellInfo.status = SellStatus.Claimed;
        totalSellQueueTokens -= userSellInfo.amount;
        rodo.transfer(account, userSellInfo.amount + userSellInfo.feeAmount);
    }

    function getTotalRequiredAdminTokens() public view returns (uint256) {
        return (totalSellQueueTokens * 20) / 100;
    }

    function getTotalRequiredLpAdminTokens()
        public
        view
        returns (uint256 amount, uint256 ethRequired)
    {
        amount = (totalSellQueueTokens * 80) / 100;
        (, , ethRequired) = getRequires(totalSellQueueTokens, price);
    }

    function addAdminTokens(uint256 _amount) public onlyAdmin {
        address account = msg.sender;
        totalAdminTokens += _amount;
        rodo.transferFrom(account, address(this), _amount);
    }

    function updatePrice(uint256 _price) public onlyOwner {
        price = _price;
    }

    function updateAdmin(address _admin) public onlyOwner {
        Helpers.requireNonZeroAddress(_admin);
        admin = _admin;
    }

    function updateEscrowManager(address _escrowManager) public onlyOwner {
        Helpers.requireNonZeroAddress(_escrowManager);
        escrowManager = _escrowManager;
    }

    function updateBurnAddress(address _burnAddress) public onlyOwner {
        Helpers.requireNonZeroAddress(_burnAddress);
        burnAddress = _burnAddress;
    }

    function updateFeePercentage(uint256 _feePercentage) public onlyOwner {
        require(_feePercentage <= 1e5, "Escrow: fee percentage exceeding.");
        feePercentage = _feePercentage;
    }

    function updateRodo(IRODO _rodo) public onlyOwner {
        Helpers.requireNonZeroAddress(address(_rodo));
        rodo = _rodo;
    }

    function updateEscrowBlocks(uint256 _escrowBlocks) public onlyOwner {
        escrowBlocks = _escrowBlocks;
    }

    function withdrawToken(IRODO _token, uint256 _amount) public onlyOwner {
        _token.transfer(owner(), _amount);
    }

    function withdrawEth(uint256 _amount) public onlyOwner {
        payable(owner()).transfer(_amount);
    }
}
[{"inputs":[{"internalType":"contract IRODO","name":"_rodo","type":"address"},{"internalType":"uint256","name":"_price","type":"uint256"},{"internalType":"uint256","name":"_feePercentage","type":"uint256"},{"internalType":"uint256","name":"_escrowBlocks","type":"uint256"},{"internalType":"address","name":"_admin","type":"address"},{"internalType":"address","name":"_escrowManager","type":"address"},{"internalType":"address","name":"_burnAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"addAdminTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"allSells","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"enum EscrowSell.SellStatus","name":"status","type":"uint8"}],"internalType":"struct EscrowSell.SellInfo[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"pids","type":"uint256[]"}],"name":"approveSells","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"burnAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_pid","type":"uint256"}],"name":"claim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"escrowBlocks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"escrowManager","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_pid","type":"uint256"}],"name":"execute","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"feePercentage","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getSell","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"enum EscrowSell.SellStatus","name":"status","type":"uint8"}],"internalType":"struct EscrowSell.SellInfo","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTotalRequiredAdminTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTotalRequiredLpAdminTokens","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"ethRequired","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTotalSellCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"getUserSells","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"enum EscrowSell.SellStatus","name":"status","type":"uint8"}],"internalType":"struct EscrowSell.SellInfo[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"start","type":"uint256"},{"internalType":"uint256","name":"end","type":"uint256"}],"name":"paginateSells","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"uint256","name":"startBlock","type":"uint256"},{"internalType":"uint256","name":"endBlock","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"enum EscrowSell.SellStatus","name":"status","type":"uint8"}],"internalType":"struct EscrowSell.SellInfo[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"price","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"rodo","outputs":[{"internalType":"contract IRODO","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"sell","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_pid","type":"uint256"}],"name":"status","outputs":[{"internalType":"enum EscrowSell.SellStatus","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalAdminTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalBurned","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalEth","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalLpTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSellQueueTokens","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_admin","type":"address"}],"name":"updateAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_burnAddress","type":"address"}],"name":"updateBurnAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_escrowBlocks","type":"uint256"}],"name":"updateEscrowBlocks","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_escrowManager","type":"address"}],"name":"updateEscrowManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_feePercentage","type":"uint256"}],"name":"updateFeePercentage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_price","type":"uint256"}],"name":"updatePrice","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IRODO","name":"_rodo","type":"address"}],"name":"updateRodo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withdrawEth","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IRODO","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withdrawToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]