// Contract: ClubVirtualMarketplaceERC721
// Target function: function _resetAuction(address _nftContractAddress, uint256 _tokenId)
(_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;
    }

    /// @notice Buyer will run this and buy the NFT from the proposal that he send and was accepted
    // by the seller.
    function buyFromProposal(address _nftContractAddress, uint256 _tokenId)
        external
        payable
        nonReentrant
    {
        address seller = nftContractSale[_nftContractAddress][_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;
    }