// Contract: ClubVirtualMarketplaceERC721
// Target function: function removeFromAuction(address _nftContractAddress, uint256 _tokenId)
Address][_tokenId]
            .nftSeller;
        require(
            msg.sender == buyProposal[_nftContractAddress][_tokenId].buyer,
            "Invalid Buyer"
        );
        require(msg.sender != seller, "Seller cannot buy own NFT");
        uint256 buyNowPrice = buyProposal[_nftContractAddress][_tokenId].price;
        uint256 platformFees = (buyNowPrice *
            (dataStorage.platformCommission())) / (10000);
        uint256 totalPayable = buyNowPrice + platformFees;
        uint royaltyAmount;

        require(msg.value >= totalPayable, "Must be greater than NFT cost");

        // 2. Cut ownerAmount from Seller
        IDataStorage.Affiliate[3] memory sellerCommission;
        sellerCommission = dataStorage.getSellerCommission(seller);
        for (uint8 i = 0; i < sellerCommission.length; i++) {
            if (sellerCommission[i].feePercent > 0) {
                uint256 ownerAmount = (buyNowPrice *
                    (sellerCommission[i].feePercent)) / (10000);
                buyNowPrice -= ownerAmount;
                (bool _success, ) = payable(sellerCommission[i].affiliateAddr)
                    .call{value: ownerAmount}("");
                require(_success, "TRANSFER_FAILED");
            }
        }
        // 3. Cut NFT Commission from Seller
        uint256 nftFee = (buyNowPrice * (dataStorage.nftCommission())) /
            (10000);

        // update total payable to seller
        totalPayable = totalPayable - (platformFees + buyNowPrice + nftFee);

        // Reset Sale Data
        _resetSale(_nftContractAddress, _tokenId);
        if (dataStorage.isActivated(_nftContractAddress, _tokenId) == true) {
            address royaltyOwner = dataStorage.getRoyaltyOwner(
                _nftContractAddress,
                _tokenId
            );
            uint _royaltyPercentage = dataStorage.getRoyaltyPercentage(
                _nftContractAddress,
                _tokenId
            );
            // 3. Cut Royaltypercentage
            royaltyAmount = (buyNowPrice * (_royaltyPercentage)) / (10000);
            totalPayable -= royaltyAmount;
            (bool _success, ) = payable(royaltyOwner).call{
                value: royaltyAmount
            }("");
            require(_success);
        } else {
            dataStorage.activateRoyalty(_nftContractAddress, _tokenId);
        }
        address owner = owner();
        uint256 adminFees = platformFees + nftFee;
        payable(owner).transfer(adminFees);
        (bool success, ) = payable(seller).call{value: totalPayable}("");
        if (!success) {
            revert();
        }
        IERC721(_nftContractAddress).safeTransferFrom(
            address(this),
            msg.sender,
            _tokenId
        );
    }

    function _resetSale(address _nftContractAddress, uint256 _tokenId)
        internal
    {
        delete nftContractSale[_nftContractAddress][_tokenId];
    }

    /**
        @dev Gives the next minimum bidding amount by calculating how much amount bidder 
        already has in the contract and how much more needs to be added based on bid increase percentage
        @return Gives the next min extra bid.
     */
    function getNextMinBid(address _nftContractAddress, uint256 _tokenId)
        public
        view
        returns (uint)
    {
        uint256 currentHighestBid = nftContractAuctions[_nftContractAddress][
            _tokenId
        ].nftHighestBid;
        uint256 bidIncreasePercentage = nftContractAuctions[
            _nftContractAddress
        ][_tokenId].bidIncreasePercentage;

        // Min Valid Bid Amount
        uint256 minBidAmount = (currentHighestBid *
            (100 + bidIncreasePercentage)) / 100;

        return minBidAmount;
    }

    /// @dev make bid and update the data
    function _makeBid(
        address _nftContractAddress,
        uint256 _tokenId,
        address _erc20Token,
        uint256 _tokenAmount
    )
        internal
        notNftSeller(_nftContractAddress, _tokenId)
        paymentAccepted(
            _nftContractAddress,
            _tokenId,
            _erc20Token,
            _tokenAmount
        )
    {
        // Total Bid Amount
        uint256 bidAmount = _tokenAmount +
            auctionBids[_nftContractAddress][_tokenId][msg.sender].bidAmount;

        // Min Valid Bid Amount
        uint256 minBidAmount = getNextMinBid(_nftContractAddress, _tokenId);
        require(bidAmount >= minBidAmount, "BID_NOT_ENOUGH");

        // final amount = extra bid amount + platform commission
        uint256 finalAmt = _tokenAmount +
            ((dataStorage.platformCommission() * _tokenAmount) / 10000);

        // Take platform commission from the bidder
        if (!_isERC20Auction(_erc20Token)) {
            require(msg.value == finalAmt, "PLATFORM_COMMISSION_ESCAPE");
        } else {
            IERC20(_erc20Token).transferFrom(
                msg.sender,
                address(this),
                finalAmt
            );
        }
        _reversePreviousBidAndUpdateHighestBid(
            _nftContractAddress,
            _tokenId,
            bidAmount
        );
        auctionBids[_nftContractAddress][_tokenId][msg.sender]
            .depositedAmt += finalAmt;
    }

    /**
        @notice Make a bid of _tokenAmount for the auction
        @dev Actual amount to transfer should be +platform fee greater than the _tokenAmount
        E.g. Bid Amount= 100 ETH, Actual Amt to deposit = 110 (if platform fee= 10%)
        Each next bid is added to the previous bid amount
        @param _nftContractAddress Contract address of NFT Token
        @param _tokenId TokenID of the NFT
        @param _tokenAmount Bidding Amount
     */
    function makeBid(
        address _nftContractAddress,
        uint256 _tokenId,
        uint256 _tokenAmount
    )
        external
        payable
        nonReentrant
        auctionOngoing(_nftContractAddress, _tokenId)
    {
        require(
            nftContractAuctions[_nftContractAddress][_tokenId]
                .nftHighestBidder != msg.sender,
            "Cannot bid again"
        );
        require(
            nftContractAuctions[_nftContractAddress][_tokenId]
                .auctionStartTime < block.timestamp,
            "Auction hasn't begun yet"
        );
        require(
            nftContractAuctions[_nftContractAddress][_tokenId].auctionEnd != 0,
            "Auction Invalid"
        );
        uint256 minPrice = nftContractAuctions[_nftContractAddress][_tokenId]
            .minPrice;
        address _erc20Token = nftContractAuctions[_nftContractAddress][_tokenId]
            .ERC20Token;
        // bid more than minimum price
        if (
            nftContractAuctions[_nftContractAddress][_tokenId].nftHighestBid ==
            0
        ) {
            require(
                (_tokenAmount >= minPrice) || (msg.value >= minPrice),
                "Must be greater than minimum amount"
            );
        }

        _makeBid(_nftContractAddress, _tokenId, _erc20Token, _tokenAmount);
        // Save Bidding Data
        auctionBids[_nftContractAddress][_tokenId][msg.sender]
            .ERC20Token = _erc20Token;
    }

    /*
     * Reset all auction related parameters for an NFT.
     * This effectively removes an EFT as an item up for auction
     */
    function _resetAuction(address _nftContractAddress, uint256 _tokenId)
        internal
    {
        delete nftContractAuctions[_nftContractAddress][_tokenId];
    }

    function _payout(
        address _nftContractAddress,
        uint256 _tokenId,
        address _recipient,
        uint256 _amount
    ) internal {
        address auctionERC20Token = nftContractAuctions[_nftContractAddress][
            _tokenId
        ].ERC20Token;
        if (_isERC20Auction(auctionERC20Token)) {
            IERC20(auctionERC20Token).transfer(_recipient, _amount);
        } else {
            // attempt to send the funds to the recipient
            (bool success, ) = payable(_recipient).call{value: _amount}("");
            if (!success) {
                revert();
            }
        }
    }

    /**
        @notice Sends NFT and Amount to Buyer and Seller respectively
        @dev The admin fees is cut extra platform fee is cut from buyer, rest from seller
     */
    function _purchaseAndTransfer(
        address _nftContractAddress,
        uint256 _tokenId,
        address _nftSeller,
        uint256 _highestBid
    ) internal {
        address erc20Token = nftContractAuctions[_nftContractAddress][_tokenId]
            .ERC20Token;
        address _nftRecipient = nftContractAuctions[_nftContractAddress][
            _tokenId
        ].nftHighestBidder;
        uint totalAmount = _highestBid;
        // 1. Cut platform fees from the buyer
        uint256 platformFees = (_highestBid *
            (dataStorage.platformCommission())) / (10000);

        // 3. Cut NFT Commission from Seller
        uint256 nftFee = (_highestBid * (dataStorage.nftCommission())) /
            (10000);

        // Reset Sale Data and Bidding data of the Winner
        _resetAuction(_nftContractAddress, _tokenId);
        delete auctionBids[_nftContractAddress][_tokenId][_nftRecipient];

        if (dataStorage.isActivated(_nftContractAddress, _tokenId)) {
            address royaltyOwner = dataStorage.getRoyaltyOwner(
                _nftContractAddress,
                _tokenId
            );
            uint _royaltyPercentage = dataStorage.getRoyaltyPercentage(
                _nftContractAddress,
                _tokenId
            );
            // 3. Cut Royaltypercentage
            uint royaltyAmount = (_highestBid * (_royaltyPercentage)) / (10000);
            totalAmount -= royaltyAmount;
            if (_isERC20Auction(erc20Token)) {
                IERC20(erc20Token).transfer(royaltyOwner, royaltyAmount);
            } else {
                (bool success, ) = payable(royaltyOwner).call{
                    value: royaltyAmount
                }("");
                require(success);
            }
        } else {
            dataStorage.activateRoyalty(_nftContractAddress, _tokenId);
        }

        uint256 adminFees = platformFees + nftFee;
        totalAmount -= nftFee;

        IDataStorage.Affiliate[3] memory sellerCommission;
        sellerCommission = dataStorage.getSellerCommission(_nftSeller);
        if (_isERC20Auction(erc20Token)) {
            // Seller Commission to Affiliates
            for (uint8 i = 0; i < sellerCommission.length; i++) {
                if (sellerCommission[i].feePercent > 0) {
                    uint256 ownerAmount = (_highestBid *
                        (sellerCommission[i].feePercent)) / (10000);
                    totalAmount -= ownerAmount;
                    IERC20(erc20Token).transfer(
                        sellerCommission[i].affiliateAddr,
                        ownerAmount
                    );
                }
            }
            // Admin's Fee
            if (adminFees > 0) {
                IERC20(erc20Token).transfer(owner(), adminFees);
            }
            // Seller's Amount
            IERC20(erc20Token).transfer(_nftSeller, totalAmount);
        } else {
            for (uint8 i = 0; i < sellerCommission.length; i++) {
                if (sellerCommission[i].feePercent > 0) {
                    uint256 ownerAmount = (_highestBid *
                        (sellerCommission[i].feePercent)) / (10000);
                    totalAmount -= ownerAmount;
                    (bool _success, ) = payable(
                        sellerCommission[i].affiliateAddr
                    ).call{value: ownerAmount}("");
                    require(_success, "TRANSFER_FAILED");
                }
            }

            // Admin Fees
            if (adminFees > 0) {
                payable(owner()).transfer(adminFees);
            }
            // Seller's Amount
            (bool success, ) = payable(_nftSeller).call{value: totalAmount}("");
            if (!success) {
                failedCredits[_nftSeller] += totalAmount;
            }
        }
        IERC721(_nftContractAddress).safeTransferFrom(
            address(this),
            _nftRecipient,
            _tokenId
        );
        emit SettleAuction(
            _nftContractAddress,
            _tokenId,
            _nftSeller,
            _nftRecipient,
            _highestBid
        );
    }

    /// @notice Settles the auction and pays the seller
    function settleAuction(address _nftContractAddress, uint256 _tokenId)
        external
        // payable
        nonReentrant
        isAuctionOver(_nftContractAddress, _tokenId)
    {
        uint256 _nftHighestBid = nftContractAuctions[_nftContractAddress][
            _tokenId
        ].nftHighestBid;
        uint256 minPrice = nftContractAuctions[_nftContractAddress][_tokenId]
            .minPrice;
        address _nftSeller = nftContractAuctions[_nftContractAddress][_tokenId]
            .nftSeller;
        require(_nftHighestBid >= minPrice, "No bid has been made");
        _purchaseAndTransfer(
            _nftContractAddress,
            _tokenId,
            _nftSeller,
            _nftHighestBid
        );
    }

    /**
        @notice switch auction to sale
        @param _nftContractAddress Address of NFT token
        @param _erc20Token ERC20 token used in purchase
        @param _buyNowPrice Buy now price of NFT
        @param _royaltyPercentage Royalty percent
     */
    function switchAuctionToSale(
        address _nftContractAddress,
        uint256 _tokenId,
        address _erc20Token,
        uint256 _buyNowPrice,
        uint32 _royaltyPercentage
    )
        public
        minimumBidNotMade(_nftContractAddress, _tokenId)
        auctionOngoing(_nftContractAddress, _tokenId)
        onlyNftSeller(_nftContractAddress, _tokenId)
    {
        // reset auction
        _resetAuction(_nftContractAddress, _tokenId);
        // setupResale
        _setupSale(
            _nftContractAddress,
            _tokenId,
            _erc20Token,
            _buyNowPrice,
            _royaltyPercentage
        );
    }

    /**
        @notice Switch Sale to Auction
        @param _minPrice Minimum bidding price
        @param _royaltyPercentage royalty percentage for NFT
        @param _auctionBidPeriod Auction duration
        @param _bidIncreasePercentage Percentage that next bid should increase
        @param _auctionStartTime Seconds after which auction will start
     */
    function switchSaleToAuction(
        address _nftContractAddress,
        uint256 _tokenId,
        address _erc20Token,
        uint256 _minPrice,
        uint32 _royaltyPercentage,
        uint256 _auctionBidPeriod,
        uint32 _bidIncreasePercentage,
        uint256 _auctionStartTime
    ) public {
        address nftSeller = nftContractSale[_nftContractAddress][_tokenId]
            .nftSeller;
        require(msg.sender == nftSeller, "Unauthorized seller");
        // Reset Sale
        _resetSale(_nftContractAddress, _tokenId);

        nftContractAuctions[_nftContractAddress][_tokenId]
            .bidIncreasePercentage = _bidIncreasePercentage;

        if (
            dataStorage.getRoyaltyOwner(_nftContractAddress, _tokenId) ==
            address(0)
        ) {
            dataStorage.setRoyaltyData(
                _nftContractAddress,
                _tokenId,
                _royaltyPercentage,
                msg.sender
            );
        }
        _setupAuction(
            _nftContractAddress,
            _tokenId,
            _erc20Token,
            _minPrice,
            _auctionStartTime,
            _auctionBidPeriod
        );
    }

    /**
        @notice Seller can withdraw his NFT sale
     */
    function withdrawSale(address _nftContractAddress, uint256 _tokenId)
        external
        nonReentrant
    {
        address nftSeller = nftContractSale[_nftContractAddress][_tokenId]
            .nftSeller;
        require(
            nftSeller == msg.sender,
            "Only the owner can call this function"
        );
        // reset sale
        _resetSale(_nftContractAddress, _tokenId);
        // transfer the NFT back to the Seller
        IERC721(_nftContractAddress).safeTransferFrom(
            address(this),
            nftSeller,
            _tokenId
        );
    }

    /**
        @notice Admin can remove the specified NFT from sale
     */
    function removeFromSale(address _nftContractAddress, uint256 _tokenId)
        external
        nonReentrant
        onlyOwner
    {
        address nftSeller = nftContractSale[_nftContractAddress][_tokenId]
            .nftSeller;
        // reset sale
        _resetSale(_nftContractAddress, _tokenId);
        // transfer the NFT back to the Seller
        IERC721(_nftContractAddress).safeTransferFrom(
            address(this),
            nftSeller,
            _tokenId
        );
    }

    /**
        @notice Seller can withdraw his NFT auction anytime, any bidder will get back the bid amt
    */
    function withdrawAuction(address _nftContractAddress, uint256 _tokenId)
        external
        nonReentrant
        onlyNftSeller(_nftContractAddress, _tokenId)
    {
        address _nftRecipient = nftContractAuctions[_nftContractAddress][
            _tokenId
        ].nftSeller;

        // Reset values of this Auction
        _resetAuction(_nftContractAddress, _tokenId);
        IERC721(_nftContractAddress).safeTransferFrom(
            address(this),
            _nftRecipient,
            _tokenId
        );
    }