// Contract: ClubVirtualMarketplaceERC721
// Target function: function switchSaleToAuction(
FT 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;
    }

    /*
     * 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
        );
    }