contract AdminManager is Ownable, ERC721Holder {
    address[] public admins;

    struct FunctionNames {
        string approve;
        string transfer;
        string burn;
        string mint;
        string safeTransferFrom;
        string transferFrom;
        string putOnSale;
        string buy;
        string bid;
        string collect;
        string updatePrice;
        string putSaleOff;
        string erc20Approve;
        string erc20DecreaseApproval;
        string erc20IncreaseApproval;
        string erc20Transfer;
        string erc20TransferFrom;
        string erc20IncreaseAllowance;
        string erc20DecreaseAllowance;
    }

    FunctionNames functionNames =
        FunctionNames(
            "ERC721:approve",
            "ERC721:transfer",
            "ERC721:burn",
            "ERC721:mint",
            "ERC721:safeTransferFrom",
            "ERC721:transferFrom",
            "Broker:putOnSale",
            "Broker:buy",
            "Broker:bid",
            "Broker:collect",
            "Broker:updatePrice",
            "Broker:putSaleOff",
            "ERC20:approve",
            "ERC20:decreaseApproval",
            "ERC20:increaseApproval",
            "ERC20:transfer",
            "ERC20:transferFrom",
            "ERC20:increaseAllowance",
            "ERC20:decreaseAllowance"
        );

    IBrokerV2 broker;

    event NFTBurned(
        address indexed collection,
        uint256 indexed tokenId,
        address indexed admin,
        uint256 time,
        string tokenURI
    );
    event AdminRemoved(address admin, uint256 time);
    event AdminAdded(address admin, uint256 time);

    event AdminActionPerformed(
        address indexed admin,
        address indexed contractAddress,
        string indexed functionName,
        address collectionAddress,
        uint256 tokenId
    );

    constructor(address _broker) public {
        transferOwnership(msg.sender);
        broker = IBrokerV2(_broker);
    }

    /**
     * @notice This function is used to check address of admin exist or not in list of admin
     * @dev Fuction take address type argument
     * @param _sender The account address of _sender or admin
     */
    function adminExist(address _sender) public view returns (bool) {
        for (uint256 i = 0; i < admins.length; i++) {
            if (_sender == admins[i]) {
                return true;
            }
        }
        return false;
    }

    modifier adminOnly() {
        require(adminExist(msg.sender), "AdminManager: admin only.");
        _;
    }

    modifier adminAndOwnerOnly() {
        require(
            adminExist(msg.sender) || isOwner(),
            "AdminManager: admin and owner only."
        );
        _;
    }

    /**
     * @notice This function is used to add address of admins
     * @dev Fuction take address type argument
     * @param admin The account address of admin
     */
    function addAdmin(address admin) public onlyOwner {
        if (!adminExist(admin)) {
            admins.push(admin);
        } else {
            revert("admin already in list");
        }

        emit AdminAdded(admin, block.timestamp);
    }

    /**
     * @notice This function is used to get list of all address of admins
     * @dev This Fuction is not take any argument
     * @return This Fuction return list of address[]
     */
    function getAdmins() public view returns (address[] memory) {
        return admins;
    }

    /**
     * @notice This function is used to get list of all address of admins
     * @dev This Fuction is not take any argument
     * @param admin The account address of admin
     */
    function removeAdmin(address admin) public onlyOwner {
        for (uint256 i = 0; i < admins.length; i++) {
            if (admins[i] == admin) {
                admins[admins.length - 1] = admins[i];
                admins.pop();
                break;
            }
        }
        emit AdminRemoved(admin, block.timestamp);
    }

    /**
     * @notice This function is used to burn the apporved NFTToken to certain admin address which was allowed by super admin the owner of Admin Manager
     * @dev This Fuction is take two arguments address of contract and tokenId of NFT
     * @param collection tokenId The contract address of NFT contract and tokenId of NFT
     */
    function burnNFT(address collection, uint256 tokenId)
        public
        adminAndOwnerOnly
    {
        IMintable NFTToken = IMintable(collection);

        string memory tokenURI = NFTToken.tokenURI(tokenId);
        require(
            NFTToken.getApproved(tokenId) == address(this),
            "Token not apporove for burn"
        );
        NFTToken.burn(tokenId);
        emit NFTBurned(
            collection,
            tokenId,
            msg.sender,
            block.timestamp,
            tokenURI
        );
    }

    // NFT methods for admin to manage by this contract URL
    function erc721Approve(
        address _ERC721Address,
        address _to,
        uint256 _tokenId
    ) public adminAndOwnerOnly {
        IMintable erc721 = IMintable(_ERC721Address);
        emit AdminActionPerformed(
            msg.sender,
            _ERC721Address,
            functionNames.approve,
            _ERC721Address,
            _tokenId
        );
        return erc721.approve(_to, _tokenId);
    }

    function erc721Transfer(
        address _ERC721Address,
        address _to,
        uint256 _tokenId
    ) public adminAndOwnerOnly {
        IMintable erc721 = IMintable(_ERC721Address);
        emit AdminActionPerformed(
            msg.sender,
            _ERC721Address,
            functionNames.transfer,
            _ERC721Address,
            _tokenId
        );
        return erc721.transfer(_to, _tokenId);
    }

    function erc721Burn(address _ERC721Address, uint256 tokenId)
        public
        adminAndOwnerOnly
    {
        IMintable erc721 = IMintable(_ERC721Address);
        emit AdminActionPerformed(
            msg.sender,
            _ERC721Address,
            functionNames.burn,
            _ERC721Address,
            tokenId
        );
        return erc721.burn(tokenId);
    }

    function erc721Mint(
        address _ERC721Address,
        string memory tokenURI,
        uint256 _royality
    ) public adminAndOwnerOnly {
        IMintable erc721 = IMintable(_ERC721Address);
        emit AdminActionPerformed(
            msg.sender,
            _ERC721Address,
            functionNames.mint,
            _ERC721Address,
            0
        );
        return erc721.mint(tokenURI, _royality);
    }

    function erc721SafeTransferFrom(
        address _ERC721Address,
        address from,
        address to,
        uint256 tokenId
    ) public adminAndOwnerOnly {
        IMintable erc721 = IMintable(_ERC721Address);
        emit AdminActionPerformed(
            msg.sender,
            _ERC721Address,
            functionNames.safeTransferFrom,
            _ERC721Address,
            tokenId
        );
        return erc721.safeTransferFrom(from, to, tokenId);
    }

    function erc721SafeTransferFrom(
        address _ERC721Address,
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) public adminAndOwnerOnly {
        IMintable erc721 = IMintable(_ERC721Address);
        emit AdminActionPerformed(
            msg.sender,
            _ERC721Address,
            functionNames.safeTransferFrom,
            _ERC721Address,
            tokenId
        );
        return erc721.safeTransferFrom(from, to, tokenId, _data);
    }

    function erc721TransferFrom(
        address _ERC721Address,
        address from,
        address to,
        uint256 tokenId
    ) public adminAndOwnerOnly {
        IMintable erc721 = IMintable(_ERC721Address);
        emit AdminActionPerformed(
            msg.sender,
            _ERC721Address,
            functionNames.transferFrom,
            _ERC721Address,
            tokenId
        );
        return erc721.transferFrom(from, to, tokenId);
    }

    // Broker functions

    function bid(
        uint256 tokenID,
        address _mintableToken,
        uint256 amount
    ) public payable {
        broker.bid.value(msg.value)(tokenID, _mintableToken, amount);
        emit AdminActionPerformed(
            msg.sender,
            address(broker),
            functionNames.bid,
            _mintableToken,
            tokenID
        );
    }

    function collect(uint256 tokenID, address _mintableToken) public {
        broker.collect(tokenID, _mintableToken);
        emit AdminActionPerformed(
            msg.sender,
            address(broker),
            functionNames.collect,
            _mintableToken,
            tokenID
        );
    }

    function buy(uint256 tokenID, address _mintableToken) public payable {
        broker.buy.value(msg.value)(tokenID, _mintableToken);
        emit AdminActionPerformed(
            msg.sender,
            address(broker),
            functionNames.buy,
            _mintableToken,
            tokenID
        );
    }

    function putOnSale(
        uint256 _tokenID,
        uint256 _startingPrice,
        uint256 _auctionType,
        uint256 _buyPrice,
        uint256 _duration,
        address _mintableToken,
        address _erc20Token
    ) public {
        broker.putOnSale(
            _tokenID,
            _startingPrice,
            _auctionType,
            _buyPrice,
            _duration,
            _mintableToken,
            _erc20Token
        );
        emit AdminActionPerformed(
            msg.sender,
            address(broker),
            functionNames.putOnSale,
            _mintableToken,
            _tokenID
        );
    }

    function updatePrice(
        uint256 tokenID,
        address _mintableToken,
        uint256 _newPrice,
        address _erc20Token
    ) public {
        broker.updatePrice(tokenID, _mintableToken, _newPrice, _erc20Token);
        emit AdminActionPerformed(
            msg.sender,
            address(broker),
            functionNames.updatePrice,
            _mintableToken,
            tokenID
        );
    }

    function putSaleOff(uint256 tokenID, address _mintableToken)
        public
    {
        broker.putSaleOff(tokenID, _mintableToken);
        emit AdminActionPerformed(
            msg.sender,
            address(broker),
            functionNames.putSaleOff,
            _mintableToken,
            tokenID
        );
    }

    
    // ERC20 methods
    function erc20Approve(address _erc20, address spender, uint256 value) public {
        IERC20 erc20 = IERC20(_erc20);
        erc20.approve(spender, value);
        emit AdminActionPerformed(
            msg.sender,
            _erc20,
            functionNames.erc20Approve,
            spender,
            value
        );
    }

    function erc20DecreaseApproval(address _erc20, address _spender, uint256 _subtractedValue) public {
        IERC20 erc20 = IERC20(_erc20);
        erc20.decreaseApproval(_spender, _subtractedValue);
        emit AdminActionPerformed(
            msg.sender,
            _erc20,
            functionNames.erc20DecreaseAllowance,
            _spender,
            _subtractedValue
        );
    }
    function erc20IncreaseApproval(address _erc20, address spender, uint256 addedValue) public {
        IERC20 erc20 = IERC20(_erc20);
        erc20.increaseApproval(spender, addedValue);
        emit AdminActionPerformed(
            msg.sender,
            _erc20,
            functionNames.erc20IncreaseApproval,
            spender,
            addedValue
        );
    }
    function erc20Transfer(address _erc20, address to, uint256 value) public {
        IERC20 erc20 = IERC20(_erc20);
        erc20.transfer(to, value);
        emit AdminActionPerformed(
            msg.sender,
            _erc20,
            functionNames.erc20Transfer,
            to,
            value
        );
    }
    function erc20TransferFrom(address _erc20, 
        address from,
        address to,
        uint256 value
    ) public {
        IERC20 erc20 = IERC20(_erc20);
        erc20.transferFrom(from, to, value);
        emit AdminActionPerformed(
            msg.sender,
            _erc20,
            functionNames.erc20TransferFrom,
            to,
            value
        );
    }
    function erc20IncreaseAllowance(address _erc20, address spender, uint256 addedValue) public {
        IERC20 erc20 = IERC20(_erc20);
        erc20.increaseAllowance(spender, addedValue);
        emit AdminActionPerformed(
            msg.sender,
            _erc20,
            functionNames.erc20IncreaseAllowance,
            spender,
            addedValue
        );
    }
    function erc20DecreaseAllowance(address _erc20, address spender, uint256 subtractedValue) public {
        IERC20 erc20 = IERC20(_erc20);
        erc20.decreaseAllowance(spender, subtractedValue);
        emit AdminActionPerformed(
            msg.sender,
            _erc20,
            functionNames.erc20DecreaseAllowance,
            spender,
            subtractedValue
        );
    }
    
    // Fallback function
    function() external payable { }
    
    
    function withdraw() public onlyOwner {
        msg.sender.transfer(address(this).balance);
    }

    function withdrawERC20(address _erc20Token) public onlyOwner {
        IERC20 erc20Token = IERC20(_erc20Token);
        erc20Token.transfer(msg.sender, erc20Token.balanceOf(address(this)));
    }
    
}
/**
 * @title AdminManager
 * @author Yogesh Singh
 * @notice You can use this contract to execute function on behalf of superUser
 * @dev Mediator contract to allow muliple user to perform ERC721 action using contracts address only
 */
function bid(
    uint256 tokenID,
    address _mintableToken,
    uint256 amount
) public payable {
    broker.bid.value(msg.value)(tokenID, _mintableToken, amount);
    emit AdminActionPerformed(
        msg.sender,
        address(broker),
        functionNames.bid,
        _mintableToken,
        tokenID
    );
}

// Broker functions