// Contract: DelegationManagementContract
// Target function: function revokeDelegationAddressUsingSubdelegation(address _delegatorAddress, address _collectionAddress, address _delegationAddress, uint256 _useCase) public
// SPDX-License-Identifier: MIT

//     _   ______________                                       
//    / | / / ____/_  __/                                       
//   /  |/ / /_    / /                                          
//  / /|  / __/   / /                                           
// /_/ |_/_/ ____/_/_    _______________  ______________  _   __
//    / __ \/ ____/ /   / ____/ ____/   |/_  __/  _/ __ \/ | / /
//   / / / / __/ / /   / __/ / / __/ /| | / /  / // / / /  |/ / 
//  / /_/ / /___/ /___/ /___/ /_/ / ___ |/ / _/ // /_/ / /|  /  
// /_____/_____/_____/_____/\____/_/  |_/_/ /___/\____/_/ |_/   
                                                             

/**
 *
 *  @title: NFTDelegation.com Management Contract
 *  @date: 20-Apr-2023 - 16:27
 *  @version: 5.20.15
 *  @notes: An advanced open-source trustless delegation and consolidation management contract.
 *  @author: 6529 team
 *  @contributors: https://github.com/6529-Collections/nftdelegation/graphs/contributors
 *
 */

pragma solidity ^0.8.18;

contract DelegationManagementContract {
    // Constant declarations
    address constant ALL_COLLECTIONS = 0x8888888888888888888888888888888888888888;
    uint256 constant USE_CASE_SUB_DELEGATION = 998;
    uint256 constant USE_CASE_CONSOLIDATION = 999;

    // Variable declarations
    uint256 public useCaseCounter;

    // Mapping declarations
    mapping(bytes32 => address[]) public delegatorHashes;
    mapping(bytes32 => address[]) public delegationAddressHashes;

    struct GlobalData {
        address delegatorAddress;
        address delegationAddress;
        uint256 registeredDate;
        uint256 expiryDate;
        bool allTokens;
        uint256 tokens;
    }

    // Mapping of GlobalData struct declaration
    mapping(bytes32 => GlobalData[]) public globalDelegationHashes;

    // Events declaration
    event RegisterDelegation(address indexed from, address indexed collectionAddress, address indexed delegationAddress, uint256 useCase, bool allTokens, uint256 _tokenId);
    event RegisterDelegationUsingSubDelegation(address indexed delegator, address from, address indexed collectionAddress, address indexed delegationAddress, uint256 useCase, bool allTokens, uint256 _tokenId);
    event RevokeDelegation(address indexed from, address indexed collectionAddress, address indexed delegationAddress, uint256 useCase);
    event RevokeDelegationUsingSubDelegation(address indexed delegator, address from, address indexed collectionAddress, address indexed delegationAddress, uint256 useCase);
    event UpdateDelegation(address indexed from, address indexed collectionAddress, address olddelegationAddress, address indexed newdelegationAddress, uint256 useCase, bool allTokens, uint256 _tokenId);

    // Locks declarations
    mapping(address => bool) public globalLock;
    mapping(bytes32 => bool) public collectionLock;
    mapping(bytes32 => bool) public collectionUsecaseLock;

    // Constructor
    constructor() {
        useCaseCounter = 999;
    }

    /**
     * @notice Delegator assigns a delegation address for a specific use case on a specific NFT collection for a certain duration
     * @notice _collectionAddress --> ALL_COLLECTIONS = Applies to all collections
     * @notice For all Tokens-- > _allTokens needs to be true, _tokenId does not matter
     */

    function registerDelegationAddress(address _collectionAddress, address _delegationAddress, uint256 _expiryDate, uint256 _useCase, bool _allTokens, uint256 _tokenId) public {
        require((_useCase > 0 && _useCase <= useCaseCounter));
        bytes32 delegatorHash;
        bytes32 delegationAddressHash;
        bytes32 globalHash;
        bytes32 collectionLockHash;
        bytes32 collectionUsecaseLockHash;
        bytes32 collectionUsecaseLockHashAll;
        // Locks
        collectionLockHash = keccak256(abi.encodePacked(_collectionAddress, _delegationAddress));
        collectionUsecaseLockHash = keccak256(abi.encodePacked(_collectionAddress, _delegationAddress, _useCase));
        collectionUsecaseLockHashAll = keccak256(abi.encodePacked(ALL_COLLECTIONS, _delegationAddress, _useCase));
        require(globalLock[_delegationAddress] == false);
        require(collectionLock[collectionLockHash] == false);
        require(collectionUsecaseLock[collectionUsecaseLockHash] == false);
        require(collectionUsecaseLock[collectionUsecaseLockHashAll] == false);
        // Push data to mappings
        globalHash = keccak256(abi.encodePacked(msg.sender, _collectionAddress, _delegationAddress, _useCase));
        delegatorHash = keccak256(abi.encodePacked(msg.sender, _collectionAddress, _useCase));
        // Stores delegation addresses on a delegator hash
        delegationAddressHash = keccak256(abi.encodePacked(_delegationAddress, _collectionAddress, _useCase));
        delegatorHashes[delegatorHash].push(_delegationAddress);
        // Stores delegators addresses on a delegation address hash
        delegationAddressHashes[delegationAddressHash].push(msg.sender);
        // Push additional data to the globalDelegationHashes mapping
        if (_allTokens == true) {
            GlobalData memory newdelegationGlobalData = GlobalData(msg.sender, _delegationAddress, block.timestamp, _expiryDate, true, 0);
            globalDelegationHashes[globalHash].push(newdelegationGlobalData);
        } else {
            GlobalData memory newdelegationGlobalData = GlobalData(msg.sender, _delegationAddress, block.timestamp, _expiryDate, false, _tokenId);
            globalDelegationHashes[globalHash].push(newdelegationGlobalData);
        }
        emit RegisterDelegation(msg.sender, _collectionAddress, _delegationAddress, _useCase, _allTokens, _tokenId);
    }

    /**
     * @notice Function to support subDelegation rights
     * @notice A delegation Address that has subDelegation rights given by a Delegator can register Delegations on behalf of Delegator
     */

    function registerDelegationAddressUsingSubDelegation(address _delegatorAddress, address _collectionAddress, address _delegationAddress, uint256 _expiryDate, uint256 _useCase, bool _allTokens, uint256 _tokenId) public {
        // Check subdelegation rights for the specific collection
        {
            bool subdelegationRightsCol;
            address[] memory allDelegators = retrieveDelegators(msg.sender, _collectionAddress, USE_CASE_SUB_DELEGATION);
            if (allDelegators.length > 0) {
                for (uint i = 0; i < allDelegators.length; ) {
                    if (_delegatorAddress == allDelegators[i]) {
                        subdelegationRightsCol = true;
                        break;
                    }

                    unchecked {
                        ++i;
                    }
                }
            }
            // Check subdelegation rights for All collections
            allDelegators = retrieveDelegators(msg.sender, ALL_COLLECTIONS, USE_CASE_SUB_DELEGATION);
            if (allDelegators.length > 0) {
                if (subdelegationRightsCol != true) {
                    for (uint i = 0; i < allDelegators.length; ) {
                        if (_delegatorAddress == allDelegators[i]) {
                            subdelegationRightsCol = true;
                            break;
                        }

                        unchecked {
                            ++i;
                        }
                    }
                }
            }
            // Allow to register
            require((subdelegationRightsCol == true));
        }
        // If check passed then register delegation address for Delegator
        require((_useCase > 0 && _useCase <= useCaseCounter));
        bytes32 delegatorHash;
        bytes32 delegationAddressHash;
        bytes32 globalHash;
        bytes32 collectionLockHash;
        bytes32 collectionUsecaseLockHash;
        bytes32 collectionUsecaseLockHashAll;
        // Locks
        collectionLockHash = keccak256(abi.encodePacked(_collectionAddress, _delegationAddress));
        collectionUsecaseLockHash = keccak256(abi.encodePacked(_collectionAddress, _delegationAddress, _useCase));
        collectionUsecaseLockHashAll = keccak256(abi.encodePacked(ALL_COLLECTIONS, _delegationAddress, _useCase));
        require(globalLock[_delegationAddress] == false);
        require(collectionLock[collectionLockHash] == false);
        require(collectionUsecaseLock[collectionUsecaseLockHash] == false);
        require(collectionUsecaseLock[collectionUsecaseLockHashAll] == false);
        // Push data to mappings
        globalHash = keccak256(abi.encodePacked(_delegatorAddress, _collectionAddress, _delegationAddress, _useCase));
        delegatorHash = keccak256(abi.encodePacked(_delegatorAddress, _collectionAddress, _useCase));
        // Stores delegation addresses on a delegator hash
        delegationAddressHash = keccak256(abi.encodePacked(_delegationAddress, _collectionAddress, _useCase));
        delegatorHashes[delegatorHash].push(_delegationAddress);
        // Stores delegators addresses on a delegation address hash
        delegationAddressHashes[delegationAddressHash].push(_delegatorAddress);
        // Push additional data to the globalDelegationHashes mapping
        if (_allTokens == true) {
            GlobalData memory newdelegationGlobalData = GlobalData(_delegatorAddress, _delegationAddress, block.timestamp, _expiryDate, true, 0);
            globalDelegationHashes[globalHash].push(newdelegationGlobalData);
        } else {
            GlobalData memory newdelegationGlobalData = GlobalData(_delegatorAddress, _delegationAddress, block.timestamp, _expiryDate, false, _tokenId);
            globalDelegationHashes[globalHash].push(newdelegationGlobalData);
        }
        emit RegisterDelegationUsingSubDelegation(_delegatorAddress, msg.sender, _collectionAddress, _delegationAddress, _useCase, _allTokens, _tokenId);
    }

    /**
     * @notice Delegator revokes delegation rights given to a delegation address on a specific use case on a specific NFT collection
     * @notice This function does not remove the delegation from the collectionsRegistered or useCaseRegistered as we want to track delegations history
     */

    function revokeDelegationAddress(address _collectionAddress, address _delegationAddress, uint256 _useCase) public {
        bytes32 delegatorHash;
        bytes32 delegationAddressHash;
        bytes32 globalHash;
        uint256 count;
        globalHash = keccak256(abi.encodePacked(msg.sender, _collectionAddress, _delegationAddress, _useCase));
        delegatorHash = keccak256(abi.encodePacked(msg.sender, _collectionAddress, _useCase));
        delegationAddressHash = keccak256(abi.encodePacked(_delegationAddress, _collectionAddress, _useCase));
        // Revoke delegation Address from the delegatorHashes mapping
        count = 0;
        if (delegatorHashes[delegatorHash].length > 0) {
            for (uint256 i = 0; i < delegatorHashes[delegatorHash].length; ) {
                if (_delegationAddress == delegatorHashes[delegatorHash][i]) {
                    count = count + 1;
                }

                unchecked {
                    ++i;
                }
            }
            uint256[] memory delegationsPerUser = new uint256[](count);
            uint256 count1 = 0;
            for (uint256 i = 0; i < delegatorHashes[delegatorHash].length; ) {
                if (_delegationAddress == delegatorHashes[delegatorHash][i]) {
                    delegationsPerUser[count1] = i;
                    count1 = count1 + 1;
                }

                unchecked {
                    ++i;
                }
            }

            if (count1 > 0) {
                for (uint256 j = 0; j < delegationsPerUser.length; ) {
                    uint256 temp1;
                    uint256 temp2;
                    temp1 = delegationsPerUser[delegationsPerUser.length - 1 - j];
                    temp2 = delegatorHashes[delegatorHash].length - 1;
                    delegatorHashes[delegatorHash][temp1] = delegatorHashes[delegatorHash][temp2];
                    delegatorHashes[delegatorHash].pop();

                    unchecked {
                        ++j;
                    }
                }                
            }
            // Revoke delegator Address from the delegationAddressHashes mapping
            uint256 countDA = 0;
            for (uint256 i = 0; i < delegationAddressHashes[delegationAddressHash].length; ) {
                if (msg.sender == delegationAddressHashes[delegationAddressHash][i]) {
                    countDA = countDA + 1;
                }

                unchecked {
                    ++i;
                }
            }
            uint256[] memory delegatorsPerUser = new uint256[](countDA);
            uint256 countDA1 = 0;
            for (uint256 i = 0; i < delegationAddressHashes[delegationAddressHash].length; ) {
                if (msg.sender == delegationAddressHashes[delegationAddressHash][i]) {
                    delegatorsPerUser[countDA1] = i;
                    countDA1 = countDA1 + 1;
                }

                unchecked {
                    ++i;
                }
            }
            if (countDA1 > 0) {
                for (uint256 j = 0; j < delegatorsPerUser.length; ) {
                    uint256 temp1;
                    uint256 temp2;
                    temp1 = delegatorsPerUser[delegatorsPerUser.length - 1 - j];
                    temp2 = delegationAddressHashes[delegationAddressHash].length - 1;
                    delegationAddressHashes[delegationAddressHash][temp1] = delegationAddressHashes[delegationAddressHash][temp2];
                    delegationAddressHashes[delegationAddressHash].pop();

                    unchecked {
                        ++j;
                    }
                }
            }
            // Delete global delegation data and emit event
            delete globalDelegationHashes[globalHash];
            emit RevokeDelegation(msg.sender, _collectionAddress, _delegationAddress, _useCase);
        }
    }