// Contract: ClubVirtualMarketplaceERC721
// Target function: function buyFromProposal(address _nftContractAddress, uint256 _tokenId)
 be 0");

        if (_nftContractAddress == address(0) && _tokenId == 0) {
            IERC721(platformNormalNFT).mint(address(this), 1, _uri); // mint to this contract
            uint[] memory tokens = IERC721(platformNormalNFT).tokensOwned(
                address(this)
            );
            uint tokenId = tokens[tokens.length - 1];

            Auction storage auction = nftContractAuctions[platformNormalNFT][
                tokenId
            ];

            auction.bidIncreasePercentage = _bidIncreasePercentage;
            // set royalty data
            dataStorage.setRoyaltyData(
                platformNormalNFT,
                tokenId,
                _royaltyPercentage,
                msg.sender
            );

            _setupAuction(
                platformNormalNFT,
                tokenId,
                _erc20Token,
                _minPrice,
                _auctionStartTime,
                _auctionBidPeriod
            );
            emit CreateAuction(
                tokenId,
                platformNormalNFT,
                msg.sender,
                _minPrice
            );
        } else {
            nftContractAuctions[_nftContractAddress][_tokenId]
                .bidIncreasePercentage = _bidIncreasePercentage;
            if (
                dataStorage.getRoyaltyOwner(_nftContractAddress, _tokenId) ==
                address(0)
            ) {
                dataStorage.setRoyaltyData(
                    _nftContractAddress,
                    _tokenId,
                    _royaltyPercentage,
                    msg.sender
                );
            }
            _createNewNftAuction(
                _nftContractAddress,
                _tokenId,
                _erc20Token,
                _minPrice,
                _auctionStartTime,
                _auctionBidPeriod
            );
            emit CreateAuction(
                _tokenId,
                _nftContractAddress,
                msg.sender,
                _minPrice
            );
        }
    }

    function _setupAuction(
        address _nftContractAddress,
        uint256 _tokenId,
        address _erc20Token,
        uint256 _minPrice,
        uint256 _auctionStartTime,
        uint256 _auctionBidPeriod
    ) internal {
        if (_erc20Token != address(0)) {
            nftContractAuctions[_nftContractAddress][_tokenId]
                .ERC20Token = _erc20Token;
        }
        nftContractAuctions[_nftContractAddress][_tokenId].minPrice = _minPrice;
        nftContractAuctions[_nftContractAddress][_tokenId].nftSeller = msg
            .sender;
        nftContractAuctions[_nftContractAddress][_tokenId].auctionEnd =
            _auctionBidPeriod +
            (block.timestamp);
        nftContractAuctions[_nftContractAddress][_tokenId].auctionStartTime =
            _auctionStartTime +
            block.timestamp;
    }

    function _createNewNftAuction(
        address _nftContractAddress,
        uint256 _tokenId,
        address _erc20Token,
        uint256 _minPrice,
        uint256 _auctionStartTime,
        uint256 _auctionBidPeriod
    ) internal {
        if (
            _nftContractAddress == platformNormalNFT ||
            _nftContractAddress == platformLazyNFT
        ) {
            // Sending the NFT to this contract
            IERC721(_nftContractAddress).contractSafeTransferFrom(
                msg.sender,
                address(this),
                _tokenId
            );
        }
        // If NFT not one of ours then it needs approval
        else {
            IERC721(_nftContractAddress).safeTransferFrom(
                msg.sender,
                address(this),
                _tokenId
            );
        }
        _setupAuction(
            _nftContractAddress,
            _tokenId,
            _erc20Token,
            _minPrice,
            _auctionStartTime,
            _auctionBidPeriod
        );
    }

    function _updateHighestBid(
        address _nftContractAddress,
        uint256 _tokenId,
        uint256 _tokenAmount
    ) internal {
        nftContractAuctions[_nftContractAddress][_tokenId]
            .nftHighestBidder = msg.sender;

        nftContractAuctions[_nftContractAddress][_tokenId]
            .nftHighestBid = _tokenAmount;
    }

    function _reversePreviousBidAndUpdateHighestBid(
        address _nftContractAddress,
        uint256 _tokenId,
        uint256 _tokenAmount
    ) internal {
        uint256 prevNftHighestBid = nftContractAuctions[_nftContractAddress][
            _tokenId
        ].nftHighestBid;
        auctionBids[_nftContractAddress][_tokenId][msg.sender]
            .bidAmount = _tokenAmount;

        _updateHighestBid(_nftContractAddress, _tokenId, _tokenAmount);
        // Let bidder remove his previous bid
        if (prevNftHighestBid > 0) {}
    }

    function withdrawFailedBids(address _nftContractAddress, uint256 _tokenId)
        public
    {
        require(
            nftContractAuctions[_nftContractAddress][_tokenId].auctionEnd <
                block.timestamp
        );
        require(
            msg.sender !=
                nftContractAuctions[_nftContractAddress][_tokenId]
                    .nftHighestBidder,
            "HIGHEST_BIDDER_NOT_ALLOWED"
        );
        uint256 withdrawalAmt = auctionBids[_nftContractAddress][_tokenId][
            msg.sender
        ].depositedAmt;

        require(withdrawalAmt > 0, "NOT_ENOUGH_WITHDRAWAL");
        // Add the platform commission to the bid amount to withdraw

        address erc20Token = auctionBids[_nftContractAddress][_tokenId][
            msg.sender
        ].ERC20Token;

        delete auctionBids[_nftContractAddress][_tokenId][msg.sender];

        // Transfer
        if (_isERC20Auction(erc20Token)) {
            IERC20(erc20Token).transfer(msg.sender, withdrawalAmt);
        } else {
            (bool success, ) = payable(msg.sender).call{value: withdrawalAmt}(
                ""
            );
            if (!success) {
                revert();
            }
        }
    }

    function withdrawFailedCredits() public {
        require(failedCredits[msg.sender] > 0, "ZERO_CREDITS");
        (bool success, ) = payable(msg.sender).call{
            value: failedCredits[msg.sender]
        }("");
        if (!success) {
            revert();
        }
    }

    function _isMinimumBidMade(address _nftContractAddress, uint256 _tokenId)
        internal
        view
        returns (bool)
    {
        uint256 minPrice = nftContractAuctions[_nftContractAddress][_tokenId]
            .minPrice;
        return
            minPrice > 0 &&
            (nftContractAuctions[_nftContractAddress][_tokenId].nftHighestBid >=
                minPrice);
    }

    function _setupSale(
        address _nftContractAddress,
        uint256 _tokenId,
        address _erc20Token,
        uint256 _buyNowPrice,
        uint32 _royaltyPercentage
    ) internal {
        if (_erc20Token != address(0)) {
            nftContractSale[_nftContractAddress][_tokenId]
                .ERC20Token = _erc20Token;
        }
        nftContractSale[_nftContractAddress][_tokenId]
            .buyNowPrice = _buyNowPrice;
        nftContractSale[_nftContractAddress][_tokenId].nftSeller = msg.sender;

        if (
            dataStorage.getRoyaltyOwner(_nftContractAddress, _tokenId) ==
            address(0)
        ) {
            dataStorage.setRoyaltyData(
                _nftContractAddress,
                _tokenId,
                _royaltyPercentage,
                msg.sender
            );
        }
    }

    function createResale(
        address _nftContractAddress,
        uint256 _tokenId,
        address _erc20Token,
        uint256 _buyNowPrice,
        uint32 _royaltyPercentage
    ) external priceGreaterThanZero(_buyNowPrice) {
        // If it's our own platform NFT
        if (
            _nftContractAddress == platformNormalNFT ||
            _nftContractAddress == platformLazyNFT
        ) {
            IERC721(_nftContractAddress).contractSafeTransferFrom(
                msg.sender,
                address(this),
                _tokenId
            );
            _setupSale(
                _nftContractAddress,
                _tokenId,
                _erc20Token,
                _buyNowPrice,
                _royaltyPercentage
            );
        }
        // if it's not our own platform NFT
        else {
            IERC721(_nftContractAddress).safeTransferFrom(
                msg.sender,
                address(this),
                _tokenId
            );
            _setupSale(
                _nftContractAddress,
                _tokenId,
                _erc20Token,
                _buyNowPrice,
                _royaltyPercentage
            );
        }
    }

    function createBatchResale(
        address _nftContractAddress,
        address _erc20Token,
        uint256[] memory _tokenId,
        uint256[] memory _buyNowPrice,
        uint32 _royaltyPercentage
    ) external batchWithinLimits(_tokenId.length) {
        require(_tokenId.length == _buyNowPrice.length);
        for (uint i = 0; i < _tokenId.length; i++) {
            IERC721(_nftContractAddress).contractSafeTransferFrom(
                msg.sender,
                address(this),
                _tokenId[i]
            );
            _setupSale(
                _nftContractAddress,
                _tokenId[i],
                _erc20Token,
                _buyNowPrice[i],
                _royaltyPercentage
            );
        }
    }

    function createSale(
        address _erc20Token,
        uint256 _buyNowPrice,
        uint32 _royaltyPercentage,
        string memory _uri
    ) external priceGreaterThanZero(_buyNowPrice) returns (uint256) {
        IERC721(platformNormalNFT).mint(address(this), 1, _uri); // mint to this contract
        // last token minted
        uint[] memory tokens = IERC721(platformNormalNFT).tokensOwned(
            address(this)
        );
        uint _tokenId = tokens[tokens.length - 1];

        _setupSale(
            address(platformNormalNFT),
            _tokenId,
            _erc20Token,
            _buyNowPrice,
            _royaltyPercentage
        );

        emit SaleCreated(_tokenId, platformNormalNFT, msg.sender, _buyNowPrice);
        return _tokenId;
    }

    function createBatchSale(
        uint256[] memory _batchTokenPrice,
        address _erc20Token,
        uint32 _royaltyPercentage,
        string[] memory _uri
    ) external batchWithinLimits(_batchTokenPrice.length) {
        for (uint i = 0; i < _batchTokenPrice.length; i++) {
            require(_batchTokenPrice[i] > 0, "price invalid");
            IERC721(platformNormalNFT).mint(address(this), 1, _uri[i]); // mint to this contract
            // last token minted
            uint[] memory tokens = IERC721(platformNormalNFT).tokensOwned(
                address(this)
            );
            uint _tokenId = tokens[tokens.length - 1];

            _setupSale(
                address(platformNormalNFT),
                _tokenId,
                _erc20Token,
                _batchTokenPrice[i],
                _royaltyPercentage
            );
        }
    }

    function buyNFT(address _nftContractAddress, uint256 _tokenId)
        public
        payable
        nonReentrant
    {
        Sale storage sale = nftContractSale[_nftContractAddress][_tokenId];
        address seller = sale.nftSeller;
        require(msg.sender != seller, "Seller cannot buy own NFT");
        uint256 buyNowPrice = sale.buyNowPrice;
        uint256 platformFees = (buyNowPrice *
            (dataStorage.platformCommission())) / (10000);
        uint256 totalPayable = buyNowPrice + platformFees;

        address erc20Token = sale.ERC20Token;
        if (_isERC20Auction(erc20Token)) {
            _buyNFTWithERC20(
                _nftContractAddress,
                _tokenId,
                seller,
                erc20Token,
                buyNowPrice
            );
            IERC721(_nftContractAddress).safeTransferFrom(
                address(this),
                msg.sender,
                _tokenId
            );
            return;
        } else {
            require(msg.value >= totalPayable, "Must be greater than NFT cost");
        }
        _buyNFTWithEth(_nftContractAddress, _tokenId);

        IERC721(_nftContractAddress).safeTransferFrom(
            address(this),
            msg.sender,
            _tokenId
        );
    }

    function _buyNFTWithERC20(
        address _nftContractAddress,
        uint256 _tokenId,
        address _seller,
        address _erc20Token,
        uint256 _buyNowPrice
    ) internal {
        uint totalAmount = _buyNowPrice;

        // 1. Cut platform fees from the buyer
        uint256 platformFees = (_buyNowPrice *
            (dataStorage.platformCommission())) / (10000);
        // 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);
                totalAmount -= ownerAmount;
                IERC20(_erc20Token).transferFrom(
                    msg.sender,
                    sellerCommission[i].affiliateAddr,
                    ownerAmount
                );
            }
        }
        // 3. Cut NFT Commission from Seller
        uint256 nftFee = (_buyNowPrice * (dataStorage.nftCommission())) /
            (10000);

        // Reset Sale Data
        _resetSale(_nftContractAddress, _tokenId);
        if (dataStorage.isActivated(_nftContractAddress, _tokenId)) {
            address royaltyOwner = dataStorage.getRoyaltyOwner(
                _nftContractAddress,
                _tokenId
            );
            uint _royaltyPercentage = dataStorage.getRoyaltyPercentage(
                _nftContractAddress,
                _tokenId
            );
            // 3. Cut Royaltypercentage
            uint royaltyAmount = (_buyNowPrice * (_royaltyPercentage)) /
                (10000);
            totalAmount -= royaltyAmount;
            IERC20(_erc20Token).transferFrom(
                msg.sender,
                royaltyOwner,
                royaltyAmount
            );
        } else {
            dataStorage.activateRoyalty(_nftContractAddress, _tokenId);
        }
        totalAmount -= nftFee;
        address owner = owner();
        uint256 adminFees = platformFees + nftFee;
        IERC20(_erc20Token).transferFrom(msg.sender, owner, adminFees);
        IERC20(_erc20Token).transferFrom(msg.sender, _seller, totalAmount);
    }

    /// @notice Buy the NFT with ETH/BNB/MATIC
    function _buyNFTWithEth(address _nftContractAddress, uint256 _tokenId)
        internal
    {
        Sale storage sale = nftContractSale[_nftContractAddress][_tokenId];
        address seller = sale.nftSeller;
        uint totalAmount = msg.value;
        uint buyNowPrice = sale.buyNowPrice;
        // 1. Cut platform fees from the buyer
        uint256 platformFees = (buyNowPrice *
            (dataStorage.platformCommission())) / (10000);
        totalAmount -= platformFees;
        // 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);
                totalAmount -= 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);
        totalAmount -= nftFee;
        // Reset Sale Data
        _resetSale(_nftContractAddress, _tokenId);
        // Check Royalty Applicable
        if (dataStorage.isActivated(_nftContractAddress, _tokenId)) {
            address royaltyOwner = dataStorage.getRoyaltyOwner(
                _nftContractAddress,
                _tokenId
            );
            uint _royaltyPercentage = dataStorage.getRoyaltyPercentage(
                _nftContractAddress,
                _tokenId
            );
            // 3. Cut Royalty Percentage
            uint royaltyAmount = (buyNowPrice * (_royaltyPercentage)) / (10000);
            totalAmount -= 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: totalAmount}("");
        if (!success) {
            revert();
        }
    }

    /// @notice NFT Seller can accept the proposal
    function acceptBuyProposal(
        address _nftContractAddress,
        uint256 _tokenId,
        uint256 _proposedPrice,
        address _proposingBuyer
    ) public {
        require(
            (msg.sender ==
                nftContractSale[_nftContractAddress][_tokenId].nftSeller),
            "Only Seller function"
        );
        buyProposal[_nftContractAddress][_tokenId].buyer = _proposingBuyer;
        buyProposal[_nftContractAddress][_tokenId].price = _proposedPrice;
    }