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

///         ______         __
/// .-----.|__    |.-----.|__|.--.--.
/// |  _  ||    __||  _  ||  ||_   _|
/// |   __||______||   __||__||__.__|
/// |__|           |__|
///

import { OwnerSettings, ERC20, SafeTransferLib } from "contracts/core/OwnerSettings.sol";
import { BaseUtils } from "contracts/core/BaseUtils.sol";
import { DataTypes as DT } from "contracts/core/DataTypes.sol";


contract P2PIX is BaseUtils {
    // solhint-disable use-forbidden-name
    // solhint-disable no-inline-assembly
    // solhint-disable no-empty-blocks

    using DT for DT.Lock;
    using DT for DT.LockStatus;

    uint256 public lockCounter;

    /// @dev List of Locks.
    mapping(uint256 => DT.Lock) public mapLocks;
    /// @dev Stores an relayer's last computed credit.
    mapping(uint256 => uint256) public userRecord;

    constructor(
        uint256 defaultBlocks,
        address[] memory validSigners,
        address _reputation,
        ERC20[] memory tokens,
        bool[] memory tokenStates
    ) 
        OwnerSettings(
            defaultBlocks, 
            validSigners, 
            _reputation, 
            tokens, 
            tokenStates
        ) 
        payable {/*  */}

    /// @notice Creates a deposit order based on a seller's
    /// offer of an amount of ERC20 tokens.
    /// @notice Seller needs to send his tokens to the P2PIX smart contract.
    /// @param pixTarget Pix key destination provided by the offer's seller.
    /// @param allowlistRoot Optional allow list merkleRoot update `bytes32` value.
    /// as the deposit identifier.
    /// @dev Function sighash: 0x5e918943
    function deposit(
        string calldata pixTarget,
        bytes32 allowlistRoot,
        ERC20 token,
        uint96 amount,
        bool valid
    ) public nonReentrant {

        if (bytes(pixTarget).length == 0) revert EmptyPixTarget();
        if (!allowedERC20s(token)) revert TokenDenied();
        uint256 _sellerBalance = __sellerBalance(msg.sender, token);

        uint256 currBal = _sellerBalance & BITMASK_SB_ENTRY;
        uint256 _newBal = uint256(currBal + amount); 
        if (_newBal > MAXBALANCE_UPPERBOUND)
            revert MaxBalExceeded();

        if (allowlistRoot != 0) {
            setRoot(msg.sender, allowlistRoot);
        }

        bytes32 pixTargetCasted = getStr(pixTarget);
        uint256 validCasted = _castBool(valid);

        _setSellerBalance(
            msg.sender, 
            token, 
            (_newBal | (validCasted << BITPOS_VALID)),
            pixTargetCasted
        );

        SafeTransferLib.safeTransferFrom(
            token,
            msg.sender,
            address(this),
            amount
        );

        emit DepositAdded(msg.sender, token, amount);
    }

    /// @notice Enables seller to invalidate future
    /// locks made to his/her token offering order.
    /// @notice This function does not affect any ongoing active locks.
    /// @dev Function sighash: 0x6d82d9e0
    function setValidState(ERC20 token, bool state) public {
        uint256 _sellerBalance = __sellerBalance(msg.sender, token);

        if (_sellerBalance != 0) {
            uint256 _valid = _castBool(state);

            _sellerBalance =
                (_sellerBalance & BITMASK_SB_ENTRY) |
                (_valid << BITPOS_VALID);

            _setValidState(msg.sender, token, _sellerBalance);

            emit ValidSet(msg.sender, token, state);
        } else revert NotInitialized();
    }

    /// @notice Public method designed to lock an remaining amount of
    /// the deposit order of a seller.
    /// @notice Transaction forwarding must leave `merkleProof` empty;
    /// otherwise, the trustedForwarder must be previously added 
    /// to a seller whitelist.
    /// @notice This method can be performed either by:
    /// - An user allowed via the seller's allowlist;
    /// - An user with enough userRecord to lock the wished amount;
    /// @notice There can only exist a lock per each `amount` partitioned
    /// from the total `remaining` value.
    /// @notice Locks can only be performed in valid orders.
    /// @param amount The deposit's remaining amount wished to be locked.
    /// @param merkleProof Provided as a pass if the `msg.sender` is in the
    /// seller's allowlist; Left empty otherwise;
    /// @param expiredLocks An array of identifiers to be
    /// provided so to unexpire locks using this transaction gas push.
    /// @return lockID The lock identifier.
    /// @dev Function sighash: 0xdc43221c
    function lock(
        address seller,
        ERC20 token,
        uint80 amount,
        bytes32[] calldata merkleProof,
        uint256[] calldata expiredLocks,
        bool bond
    ) public nonReentrant returns (uint256 lockID) {
        unlockExpired(expiredLocks);

        if (!getValid(seller, token)) revert InvalidDeposit();

        uint256 bal = getBalance(seller, token);
        if (bal < amount) revert NotEnoughTokens();

        unchecked { 
            lockID = ++lockCounter;
        }

        if (
            mapLocks[lockID].expirationBlock >= block.number
        ) revert NotExpired();

        bytes32 _pixTarget = getPixTarget(seller, token);

        if (bond){
          SafeTransferLib.safeTransferFrom(
              token,
              _msgSender(),
              address(this),
              amount >> BOND_DIVISOR
          );
        } else if (merkleProof.length != 0) {
            // transaction forwarding must leave `merkleProof` empty;
            // otherwise, the trustedForwarder must be previously added 
            // to a seller whitelist.
            _merkleVerify( merkleProof, sellerAllowList(seller), _msgSender());

        } else if ( amount > REPUTATION_LOWERBOUND && msg.sender == _msgSender() ) {
            
            uint256 spendLimit; uint256 userCredit = 
            userRecord[_castAddrToKey(_msgSender())];
            (spendLimit) = _limiter(userCredit / WAD);
            if ( 
                amount > (spendLimit * WAD) || 
                amount > LOCKAMOUNT_UPPERBOUND 
            ) revert AmountNotAllowed();
        }

        DT.Lock memory l = DT.Lock(
            lockID,
            (block.number + defaultLockBlocks),
            _pixTarget,
            amount,
            token,
            _msgSender(),
            seller,
            bond
        );

        _addLock(bal, l);
    }

    /// @notice Lock release method that liquidate lock
    /// orders and distributes relayer fees.
    /// @notice This method can be called by any public actor
    /// as long the signature provided is valid.
    /// @notice `relayerPremium` gets splitted equaly
    /// if relayer addresses differ.
    /// @notice If the `msg.sender` of this method and `l.relayerAddress` are the same,
    /// `msg.sender` accrues both l.amount and l.relayerPremium as userRecord credit.
    ///  In case of they differing:
    /// - `lock` caller gets accrued with `l.amount` as userRecord credit;
    /// - `release` caller gets accrued with `l.relayerPremium` as userRecord credit;
    /// @dev Function sighash: 0x11fc7f9a
    function release(
        uint256 lockID,
        bytes32 pixTimestamp,
        bytes calldata signature
    ) public nonReentrant {
        DT.Lock storage l = mapLocks[lockID];

        if (l.amount == 0) revert AlreadyReleased();
        if (l.expirationBlock < block.number)
            revert LockExpired();

        bytes32 message = keccak256(
            abi.encodePacked(
                l.pixTarget,
                l.amount,
                pixTimestamp
            )
        );

        _signerCheck(message, signature);

        ERC20 t = ERC20(l.token);

        // We cache lockAmount value before zeroing it out.
        uint256 lockAmount = l.amount;

        l.amount = 0;
        l.expirationBlock = 0;
        _setUsedTransactions(message);
        
        if (_msgSender() == msg.sender) { 
        if (msg.sender != l.buyerAddress) {
            userRecord[_castAddrToKey(msg.sender)] += (lockAmount >> 1);
            userRecord[_castAddrToKey(l.buyerAddress)] += (lockAmount >> 1);
        } else {
            userRecord[_castAddrToKey(msg.sender)] += lockAmount;
        }}

        SafeTransferLib.safeTransfer(
            t,
            l.buyerAddress,
            l.bond ? lockAmount + lockAmount >> BOND_DIVISOR : lockAmount
        );

        emit LockReleased(l.buyerAddress, lockID, lockAmount);
    }

    /// @notice Unlocks expired locks.
    /// @notice Triggered in the callgraph by both `lock` and `withdraw` functions.
    /// @notice This method can also have any public actor as its `tx.origin`.
    /// @notice For each successfull unexpired lock recovered,
    /// `userRecord[_castAddrToKey(l.relayerAddress)]` is decreased by half of its value.
    /// @dev Function sighash: 0xb0983d39
    function unlockExpired(uint256[] calldata lockIDs)
        public
    {
        uint256 i;
        uint256 locksSize = lockIDs.length;

        for (i; i < locksSize; ) {
            DT.Lock storage l = mapLocks[lockIDs[i]];

            _notExpired(l);

            uint256 _sellerBalance = 
            __sellerBalance(l.seller, l.token) & BITMASK_SB_ENTRY;

            if ((_sellerBalance + l.amount) > MAXBALANCE_UPPERBOUND)
                revert MaxBalExceeded();

            _addSellerBalance(l.seller, l.token, l.bond ? l.amount + l.amount >> BOND_DIVISOR : l.amount);

            l.amount = 0;

            uint256 userKey = _castAddrToKey(
                l.buyerAddress
            );
            uint256 _newUserRecord = (userRecord[userKey] >>
                1);

            if (_newUserRecord <= REPUTATION_LOWERBOUND) {
                userRecord[userKey] = REPUTATION_LOWERBOUND;
            } else {
                userRecord[userKey] = _newUserRecord;
            }
            emit LockReturned(l.buyerAddress, lockIDs[i]);

            unchecked {
                ++i;
            }
        }

        assembly ("memory-safe") {
            if lt(i, locksSize) {
                // LoopOverflow()
                mstore(0x00, 0xdfb035c9)
                revert(0x1c, 0x04)
            }
        }
    }

    /// @notice Seller's expired deposit fund sweeper.
    /// @notice A seller may use this method to recover
    /// tokens from expired deposits.
    /// @dev Function sighash: 0xfb8c5ef0
    function withdraw(
        ERC20 token,
        uint256 amount,
        uint256[] calldata expiredLocks
    ) public nonReentrant {
        unlockExpired(expiredLocks);

        if (getValid(msg.sender, token))
            setValidState(token, false);

        _decBal(
            (__sellerBalance(msg.sender, token) & BITMASK_SB_ENTRY),
            amount,
            token,
            msg.sender
        );

        // safeTransfer tokens to seller
        SafeTransferLib.safeTransfer(
            token,
            msg.sender,
            amount
        );

        emit DepositWithdrawn(
            msg.sender,
            token,
            amount
        );
    }

    function setRoot(address addr, bytes32 merkleroot)
        public
    {
        assembly ("memory-safe") {
            // if (addr != msg.sender)  
            if iszero(eq(addr, caller())) {
                // revert OnlySeller()
                mstore(0x00, 0x85d1f726)
                revert(0x1c, 0x04)
            }
            // sets root to SellerAllowlist's storage slot
            mstore(0x0c, _SELLER_ALLOWLIST_SLOT_SEED)
            mstore(0x00, addr)
            sstore(keccak256(0x00, 0x20), merkleroot)

            // emit RootUpdated(addr, merkleroot);
            log3(
                0, 
                0, 
                _ROOT_UPDATED_EVENT_SIGNATURE,
                addr,
                merkleroot
            )
        }
    }

    // solhint-disable-next-line no-empty-blocks
    receive() external payable {}

    /// @notice Private view auxiliar logic that reverts
    /// on a not expired lock passed as argument of the function.
    /// @dev Called exclusively by the `unlockExpired` method.
    function _notExpired(DT.Lock storage _l) private view {
        if (_l.expirationBlock > block.number)
            revert NotExpired();
        if (_l.amount == 0) revert AlreadyReleased();
    }

    function _addLock(
        uint256 _bal,
        DT.Lock memory _l
    ) internal {
        mapLocks[_l.counter] = _l;

        _decBal(_bal, _l.amount, ERC20(_l.token), _l.seller);

        emit LockAdded(
            _l.buyerAddress,
            _l.counter,
            _l.seller,
            _l.amount
        );
    }

    function _decBal(
        uint256 _bal,
        uint256 _amount,
        ERC20 _t,
        address _k
    ) private {
        assembly ("memory-safe") {
            if iszero(
                iszero(
                    or(
                        iszero(_bal),
                        gt(sub(_bal, _amount), _bal)
                    )
                )
            ) {
                // DecOverflow()
                mstore(0x00, 0xce3a3d37)
                revert(0x1c, 0x04)
            }
        }

        // we can directly dec from packed uint entry value
        _decSellerBalance(_k,_t, _amount);
    }

    function getBalance(address seller, ERC20 token)
        public
        view
        returns (uint256 bal)
    {
        assembly ("memory-safe") {
            for {
                /*  */
            } iszero(returndatasize()) {
                /*  */
            } {
                mstore(0x20, token)
                mstore(0x0c, _SELLER_BALANCE_SLOT_SEED)
                mstore(0x00, seller)
                bal := and(
                    BITMASK_SB_ENTRY,
                    sload(add(keccak256(0x0c, 0x34), 0x01))
                )
                break
            }
        }
    }

    function getValid(address seller, ERC20 token)
        public
        view
        returns (bool valid)
    {
        assembly ("memory-safe") {
            for {
                /*  */
            } iszero(returndatasize()) {
                /*  */
            } {
                mstore(0x20, token)
                mstore(0x0c, _SELLER_BALANCE_SLOT_SEED)
                mstore(0x00, seller)
                valid := and(
                    BITMASK_SB_ENTRY,
                    shr(
                        BITPOS_VALID,
                        sload(add(keccak256(0x0c, 0x34), 0x01))
                    )
                )
                break
            }
        }
    }

    function getPixTarget(address seller, ERC20 token)
        public
        view
        returns (bytes32 pixTarget)
    {
        assembly ("memory-safe") {
            for {
                /*  */
            } iszero(returndatasize()) {
                /*  */
            } {
                mstore(0x20, token)
                mstore(0x0c, _SELLER_BALANCE_SLOT_SEED)
                mstore(0x00, seller)
                pixTarget := sload(keccak256(0x0c, 0x34))
                break
            }
        }
    }

    function getPixTargetString(address seller, ERC20 token) public view returns (string memory pixTarget) {
        bytes32 _pixEnc = getPixTarget(seller, token);
        pixTarget =  string(abi.encodePacked(_pixEnc));
    }

    function getBalances(
        address[] memory sellers,
        ERC20 token
    ) external view returns (uint256[] memory) {
        uint256 j;
        uint256 len = sellers.length;
        uint256[] memory balances = new uint256[](len);
        while (j < len) {
            uint256 bal = getBalance(sellers[j], token);
            balances[j] = bal;
            unchecked {
                ++j;
            }
        }

        return balances;
    }

    /// @notice External getter that returns the status of a lockIDs array.
    /// @notice Call will not revert if provided with an empty array as parameter.
    /// @dev Function sighash: 0x49ef8448
    function getLocksStatus(uint256[] memory ids)
        external
        view
        returns (uint256[] memory, DT.LockStatus[] memory)
    {
        if (ids.length == 0) {
            uint256[] memory null1 = new uint256[](0);
            DT.LockStatus[]
                memory null2 = new DT.LockStatus[](0);
            return (null1, null2);
        }

        uint256 c;
        uint256 len = ids.length;

        uint256[] memory sortedIDs = new uint256[](len);
        DT.LockStatus[] memory status = new DT.LockStatus[](
            len
        );
        unchecked {
            for (c; c < len; ) {
                if (mapLocks[ids[c]].seller == address(0)) {
                    sortedIDs[c] = ids[c];
                    status[c] = type(DT.LockStatus).min;
                    ++c;
                } else if (mapLocks[ids[c]].amount == 0x0) {
                    sortedIDs[c] = ids[c];
                    status[c] = type(DT.LockStatus).max;
                    ++c;
                } else if (
                    mapLocks[ids[c]].expirationBlock <
                    block.number
                ) {
                    sortedIDs[c] = ids[c];
                    status[c] = DT.LockStatus.Expired;
                    ++c;
                } else {
                    sortedIDs[c] = ids[c];
                    status[c] = DT.LockStatus.Active;
                    ++c;
                }
            }
        }
        return (sortedIDs, status);
    }
}
