contract Quik_Marketplace is Ownable {
    using SafeMath for uint256;

    event SaleSuccess(address nft, uint256 indexed tokenId, uint256 sellingPrice, address buyer, address seller);
    event AuctionCreated(address nft, uint256 indexed tokenId, uint256 startingPrice);
    event BidCreated(
        address nft,
        uint256 indexed tokenId,
        address indexed bidder,
        uint256 bidAmount
    );
    event AuctionCancelled(address nft, uint256 indexed tokenId);

    // address nftContract; //contract address for NFT
    address payable public marketPlaceOwner; //address of marketPlace owner, gets commissions on each sales
    uint256 public minimunBidPer10000; // minimum bid amount to be set
    //Token Contract 
    address public tradeToken;
    //nft contract 
    mapping (address => bool) public verifiedNFT;

    //fees
    uint256 ethTradeSellerFee = 500;
    uint256 quikTradeSellerFee = 250;

    //Auction details
    struct Auction {
      address nftAddress;
      address seller;
      uint256 startingPrice;
      uint256 expiresAt; //auction expiry date.
      uint256 currentBidPrice; //Current max bidding price
      address currentBidder; //Current max bidder
      bool onAuction;
      bool quikTrade;
    }

    mapping (address => mapping(uint256 => Auction)) public TokenAuctions; // nftaddress -> tokenId -> Auction
    mapping (uint256 => bool) usedNonces;// checks if the nonces are there or not. 
    ///@dev sets deployer as marketplace owner and init minimum bid percent to 2.5%
    constructor( address _token) {
        tradeToken = _token;
        marketPlaceOwner = payable (msg.sender);
        minimunBidPer10000 = 250; // 2.5% minimunBid at initial
    }

    ///@notice check if caller is tokenOwner of given tokenID
    ///@param _nft nft address
    ///@param _tokenId tokenId of NFT
    modifier IsTokenOwner(address _nft,uint256 _tokenId) {
        require(
            IERC721(_nft).ownerOf(_tokenId) == msg.sender,
            "MarketPlace: Caller is not the owner of this token."
        );
        _;
    }


    ///@dev checks if amount is greater that 10000
    modifier checkPrice(uint256 _amount) {
        require(
            _amount > 10000,
            "MarketPlace: Amount should be minimum 10000 wei."
        );
        _;
    }

    ///@dev checks if the nft is verified or not
    modifier checkNFT(address _nft) {
      require(
        verifiedNFT[_nft],
        "Marketplace: NFT not verified for trade in the platform."
      );
      _;
    }

    ///@dev sets given account as marketplace owner
    ///@param _account address of marketplace owner to  be set
    function setMarketPlaceOwner(address _account) external onlyOwner{
        marketPlaceOwner = payable(_account);
    }


    ///@notice Set NFT for Auction
    ///@dev allows token owner to place their token to Auction
    ///@param _nft token nft
    ///@param _tokenId TokenId of NFT
    ///@param _startingPrice starting price of token
    ///@param _expiresAt Expiry timestamp of auction
    ///@param _isQuikTrade quik token trade or not
    function setNFTDomainToAuction(
        address _nft, 
        uint256 _tokenId,
        uint256 _startingPrice,
        uint256 _expiresAt,
        bool _isQuikTrade 
    ) public 
    IsTokenOwner(_nft, _tokenId) 
    checkPrice(_startingPrice)
    checkNFT(_nft)
     {
        require(
            IERC721(_nft).isApprovedForAll(msg.sender, address(this)),
            "MarketPlace: Need to approve marketplace as its token operator"
        );
        require(
            _expiresAt > block.timestamp,
            "MarketPlace: Invalid token Expiry date"
        );

        require(
            TokenAuctions[_nft][_tokenId].onAuction == false,
            "MarketPlace: Token already on Auction"
        );
        require(
            IERC721(_nft).isApprovedForAll(msg.sender, address(this)),
            "MarketPlace: Minter need to approve marketplace as its token operator"
        );

        TokenAuctions[_nft][_tokenId] = Auction({
            nftAddress: _nft, 
            seller: msg.sender,
            startingPrice: _startingPrice,
            expiresAt: _expiresAt,
            currentBidPrice: 0,
            currentBidder: address(0),
            onAuction: true, 
            quikTrade: _isQuikTrade
        });

        emit AuctionCreated(_nft, _tokenId, _startingPrice);
    }

    ///@notice bid an amount to given tokenId
    ///@dev holds bid amount of the bidder and set it as current bidder if bid amount is higher than previous bidder
    ///@param _nft NFT contract address 
    ///@param _tokenId TokenId of NFT
    ///@param _tokenAmount Total token to auction for NOTE: Buyer commission is also added here in the token amount 
    function bidForAuction(address _nft, uint256 _tokenId, uint256 _tokenAmount) public payable{
        Auction storage act = TokenAuctions[_nft][_tokenId];
        require(
            act.onAuction == true && act.expiresAt > block.timestamp,
            "MarketPlace: Token expired from auction"
        );
        if(act.quikTrade){
          require(
                _tokenAmount >= act.startingPrice,
                "MarketPlace: Bid amount is lower than startingPrice"
          );

          if (act.currentBidPrice != 0) {
              require(
                  _tokenAmount >=
                      (act.currentBidPrice +
                          cutPer10000(minimunBidPer10000, act.currentBidPrice)),
                  "MarketPlace: Bid Amount is less than minimunBid required"
              );
          }
        }
        else{
          require(
                  msg.value >= act.startingPrice,
                  "MarketPlace: Bid amount is lower than startingPrice"
                );

          if (act.currentBidPrice != 0) {
              require(
                  msg.value >=
                      (act.currentBidPrice +
                          cutPer10000(minimunBidPer10000, act.currentBidPrice)),
                  "MarketPlace: Bid Amount is less than minimunBid required"
              );
            }
        }
      
        address currentBidder = act.currentBidder;
        uint256 prevBidPrice = act.currentBidPrice;
        act.currentBidPrice = 0;
        if(act.quikTrade){
          //add the current bid amount 
          IERC20(tradeToken).transferFrom(msg.sender, address(this), _tokenAmount);
          
          //return the previous tokens to the bidder
          if(currentBidder != address(0))
              IERC20(tradeToken).transfer(currentBidder, prevBidPrice);
        }
        else{
          //return the previous eth to the bidder
          if(currentBidder != address(0))
              withdrawETH(currentBidder, prevBidPrice);
        }

        //update
        if(act.quikTrade){
          act.currentBidPrice = _tokenAmount;
        }else{
          act.currentBidPrice = msg.value;
        }

        act.currentBidder = msg.sender;
        emit BidCreated(_nft, _tokenId, msg.sender, _tokenAmount);
    }



    ///@notice cancel ongoing auction
    ///@dev stops the ongoing auction and if bidder exists refund the bidders amount
    ///@param _nft nft contract address
    ///@param _tokenId TokenId of NFT
    function cancelAuction(address _nft, uint256 _tokenId)
        public
        IsTokenOwner(_nft, _tokenId)
    {
        Auction storage act = TokenAuctions[_nft][_tokenId];

        act.onAuction = false;
        if (act.currentBidder != address(0)) {
            address currentBidder = act.currentBidder;
            uint256 bidPrice = act.currentBidPrice;
            act.currentBidPrice = 0;
            if(act.quikTrade){
              IERC20(tradeToken).transfer(currentBidder, bidPrice);
            }
            else{
              withdrawETH(currentBidder, bidPrice);
            }
        }

        emit AuctionCancelled(_nft, _tokenId);
    }



    ///@notice claim the NFT once the bidder wins an auction
    ///@dev tranfer nft token to highest bidder and divide the commission to token minter and marketplace owner
    ///@param _nft NFT contract address
    ///@param _tokenId TokenId of NFT
    function ClaimNFTDomain(address _nft, uint256 _tokenId) public payable{
        Auction storage act = TokenAuctions[_nft][_tokenId];
        require(
            msg.sender == act.currentBidder,
            "MarketPlace: Caller is not a highest bidder"
        );
        require(
            act.expiresAt < block.timestamp,
            "MarketPlace: Token still on auction"
        );

        address tokenOwner = act.seller;
        (address minter, uint256 royaltyFee) = ERC721Royalty(_nft).royaltyInfo(_tokenId, act.currentBidPrice);
        uint256 sellerFee = calculateSellerFee(act.currentBidPrice, act.quikTrade);
  
        //transfer commissions
        act.currentBidPrice = 0;
        IERC721(_nft).transferFrom(tokenOwner, msg.sender, _tokenId);
        if(act.quikTrade){
          //transfer marketplace owner commissions
          IERC20(tradeToken).transferFrom(msg.sender, marketPlaceOwner, sellerFee);
          //transfer remainings to token owner
          IERC20(tradeToken).transferFrom(msg.sender, tokenOwner, act.currentBidPrice.sub(sellerFee).sub(royaltyFee));
          if(royaltyFee!=0){
              //transfer minter commission
              IERC20(tradeToken).transferFrom(msg.sender, minter, royaltyFee);
          }
        }else{
          //transfer marketplace owner commissions
          withdrawETH(marketPlaceOwner, sellerFee);
          //transfer remainings to token owner
          withdrawETH(tokenOwner, act.currentBidPrice.sub(sellerFee).sub(royaltyFee));
          if(royaltyFee!=0){
              //transfer minter commission
              withdrawETH(minter, royaltyFee);
          }
        }

    
    }

    ///@notice change the minimum bid percent
    ///@param _minBidPer10000 minimun bid amount per 10000
    function setMinimumBidPercent(uint256 _minBidPer10000) public onlyOwner {
        require(
            _minBidPer10000 < 10000,
            "MarketPlace: minimun Bid cannot be more that 100%"
        );
        minimunBidPer10000 = _minBidPer10000;
    }

    function withdrawETH(address _reciever, uint256 _amount) internal{
      (bool os, ) = payable(_reciever).call{value: _amount}("");
      require(os);
    }

    function rescueETH() external payable onlyOwner {
      (bool os, ) = payable(owner()).call{value: address(this).balance}("");
      require(os);
    }


    function calculateSellerFee(uint256 _amount, bool quikTrade) public view returns(uint256){
      if(quikTrade){
        return cutPer10000(quikTradeSellerFee, _amount);
      }else{
        return cutPer10000(ethTradeSellerFee, _amount);
      }
    }

    ///@notice calculate percent amount for given percent and total
    ///@dev calculates the cut per 10000 fo the given total
    ///@param _cut cut to be caculated per 10000, i.e percentAmount * 100
    ///@param _total total amount from which cut is to be calculated
    function cutPer10000(uint256 _cut, uint256 _total)
        internal
        pure
        returns (uint256)
    {
        uint256 cutAmount = _total.mul(_cut).div(10000);
        return cutAmount;
    }

    ///@dev update eth fees 
    ///@param fee fee to set seller and buyer
    function updateEthTradeFees(uint256 fee) external onlyOwner{
      ethTradeSellerFee = fee;
    } 

    ///@dev update quik fees 
    ///@param fee fee to set 
    function updateQuikTradeFees(uint256 fee) external onlyOwner{
      quikTradeSellerFee = fee;
    }

    ///@dev update trade token  
    ///@param token token to trade on 
    function updateTradeToken(address token) external onlyOwner{
      tradeToken = token;
    }

    ///@dev add verified nft for sales
    ///@param nft nft contract address to add
    function AddVerifiedNFTDomain(address[] memory nft) external onlyOwner{
      for(uint i=0; i< nft.length; i++){
        verifiedNFT[nft[i]] = true;
      }
    }

    ///@dev remove verified nft
    ///@param nft nft contract address to remove
    function removeVerifiedNFTDomain(address[] memory nft) external onlyOwner{
      for(uint i=0; i< nft.length; i++){
        verifiedNFT[nft[i]] = false;
      }
    }

    ///@dev call to buy the nft
    ///@param tokenAmount: amount of token or eth to pay
    ///@param nonce: nonce for the transactions
    ///@param nft: nft address
    ///@param tokenId: token id to trade
    ///@param seller: seller address
    ///@param sellingPrice: selling price of the token
    ///@param quikTrade: is quik trade or not
    ///@param sig: signature to check for
    function buyNFTDomain(
        uint256 tokenAmount, 
        uint256 nonce, 
        address nft, 
        uint256 tokenId, 
        address seller, 
        uint256 sellingPrice, 
        bool quikTrade,
        bytes memory sig
    ) 
    public 
    payable
    checkNFT(nft)
    {
        require(!usedNonces[nonce], "Marketplace: Nonce already used!");
        usedNonces[nonce] = true;

        uint256 sellerFee;
        address minter; 
        uint256 royaltyFee;

        require(returnSigner(  
            tokenAmount, 
            nonce, 
            nft, 
            tokenId, 
            seller, 
            sellingPrice, 
            quikTrade,
            sig
        ) == seller, "Marketplace: Signer not seller or wrong data");
      

        if(quikTrade){
            require(
            tokenAmount >= sellingPrice,
            "MarketPlace: Not Enough value to buy token!"
            );
        }
        else{
            require(
            msg.value >= sellingPrice,
            "MarketPlace: Not Enough value to buy token!"
            );
        }

        ( minter, royaltyFee) = ERC721Royalty(nft).royaltyInfo(tokenId, sellingPrice);
        sellerFee = calculateSellerFee(sellingPrice, quikTrade);
        
    	IERC721(nft).transferFrom(seller,  msg.sender, tokenId);
        {
            if(quikTrade){
                //transfer marketplace owner commissions
                IERC20(tradeToken).transferFrom(msg.sender, marketPlaceOwner, sellerFee);
                //transfer remainings to token owner
                IERC20(tradeToken).transferFrom(msg.sender, seller, sellingPrice.sub(sellerFee).sub(royaltyFee));
                if(royaltyFee!=0){
                    //transfer minter commission
                    IERC20(tradeToken).transferFrom(msg.sender, minter, royaltyFee);
                }
            }else{
                //transfer marketplace owner commissions
                withdrawETH(marketPlaceOwner, sellerFee);
                //transfer remainings to token owner
                withdrawETH(seller, sellingPrice.sub(sellerFee).sub(royaltyFee));
                if(royaltyFee!=0){
                    //transfer minter commission
                    withdrawETH(minter, royaltyFee);
                }
            }
        }

      emit SaleSuccess(nft, tokenId, tokenAmount, msg.sender, seller);
    }
    

    function returnSigner(
        uint256 tokenAmount, 
        uint256 nonce, 
        address nft, 
        uint256 tokenId, 
        address seller, 
        uint256 sellingPrice, 
        bool quikTrade,
        bytes memory sig
    ) internal view returns(address){
        //verifying signature 
        uint chainId;
        assembly {
          chainId := chainid()
        }
        
        bytes32 eip712DomainHash;
        bytes32 hashStruct;
        bytes32 hash;
        eip712DomainHash = keccak256(
            abi.encode(
                keccak256(
                    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
                ),
                keccak256(bytes("Quik_Marketplace")),
                keccak256(bytes("1")),
                chainId,
                address(this)
            )
        );  

        hashStruct = keccak256(
            abi.encode(
                keccak256("Sell(uint256 tokenAmount,uint256 nonce,address nft,uint256 tokenId,address seller,uint256 sellingPrice,bool quikTrade)"),
                tokenAmount, 
                nonce, 
                nft, 
                tokenId, 
                seller, 
                sellingPrice, 
                quikTrade
            )
        );

        hash = keccak256(abi.encodePacked("\x19\x01", eip712DomainHash, hashStruct));

        // This recreates the message that was signed on the client.
        return recoverSigner(hash, sig);
    
    }

    // Signature methods
    function splitSignature(bytes memory sig)
        internal
        pure
        returns (uint8, bytes32, bytes32)
    {
        require(sig.length == 65);

        bytes32 r;
        bytes32 s;
        uint8 v;

        assembly {
            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        uint8 v;
        bytes32 r;
        bytes32 s;

        (v, r, s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

}
/// @title MarketPlace Contract that regulates NFT contract/token
/// @notice This Contract is used to set NFT tokens to Aution and Sale
/// @dev Uses safeMath library for all calulations and holds crypto/tokens using auction
function cancelAuction(address _nft, uint256 _tokenId)
    public
    IsTokenOwner(_nft, _tokenId)
{
    Auction storage act = TokenAuctions[_nft][_tokenId];

    act.onAuction = false;
    if (act.currentBidder != address(0)) {
        address currentBidder = act.currentBidder;
        uint256 bidPrice = act.currentBidPrice;
        act.currentBidPrice = 0;
        if(act.quikTrade){
          IERC20(tradeToken).transfer(currentBidder, bidPrice);
        }
        else{
          withdrawETH(currentBidder, bidPrice);
        }
    }

    emit AuctionCancelled(_nft, _tokenId);
}

///@notice cancel ongoing auction
///@dev stops the ongoing auction and if bidder exists refund the bidders amount
///@param _nft nft contract address
///@param _tokenId TokenId of NFT