// Contract: Recstartoken
// Target function: function withdrawTokens(IERC20 wsToken, uint256 _amount, address _to) external onlyOwner
 of the token to be added to the tokens list of the given address
     */
    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
        uint256 length = ERC721.balanceOf(to);
        _ownedTokens[to][length] = tokenId;
        _ownedTokensIndex[tokenId] = length;
    }

    /**
     * @dev Private function to add a token to this extension's token tracking data structures.
     * @param tokenId uint256 ID of the token to be added to the tokens list
     */
    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokensIndex[tokenId] = _allTokens.length;
        _allTokens.push(tokenId);
    }

    /**
     * @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
     * while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
     * gas optimizations e.g. when performing a transfer operation (avoiding double writes).
     * This has O(1) time complexity, but alters the order of the _ownedTokens array.
     * @param from address representing the previous owner of the given token ID
     * @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
     */
    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId)
        private
    {
        // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
        uint256 tokenIndex = _ownedTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary
        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
        }

        // This also deletes the contents at the last position of the array
        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    /**
     * @dev Private function to remove a token from this extension's token tracking data structures.
     * This has O(1) time complexity, but alters the order of the _allTokens array.
     * @param tokenId uint256 ID of the token to be removed from the tokens list
     */
    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
        // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
        // an 'if' statement (like in _removeTokenFromOwnerEnumeration)
        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

        // This also deletes the contents at the last position of the array
        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }
}

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Returns the amount of tokens in existence.
     */
    function erc20totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function erc20balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);

    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event ERC20Transfer(
        address indexed from,
        address indexed to,
        uint256 value
    );

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event ERC20Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
}

/**
 * @title SafeERC20
 * @dev Wrappers around ERC20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    using Address for address;

    function safeTransfer(
        IERC20 token,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(token.transfer.selector, to, value)
        );
    }

    function safeTransferFrom(
        IERC20 token,
        address from,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(token.transferFrom.selector, from, to, value)
        );
    }

    /**
     * @dev Deprecated. This function has issues similar to the ones found in
     * {IERC20-approve}, and its usage is discouraged.
     *
     * Whenever possible, use {safeIncreaseAllowance} and
     * {safeDecreaseAllowance} instead.
     */
    function safeApprove(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(token.approve.selector, spender, value)
        );
    }

    function safeIncreaseAllowance(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        uint256 newAllowance = token.allowance(address(this), spender) + value;
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(
                token.approve.selector,
                spender,
                newAllowance
            )
        );
    }

    function erc20safeDecreaseAllowance(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        unchecked {
            uint256 oldAllowance = token.allowance(address(this), spender);
            require(
                oldAllowance >= value,
                "SafeERC20: decreased allowance below zero"
            );
            uint256 newAllowance = oldAllowance - value;
            _callOptionalReturn(
                token,
                abi.encodeWithSelector(
                    token.approve.selector,
                    spender,
                    newAllowance
                )
            );
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        bytes memory returndata = address(token).functionCall(
            data,
            "SafeERC20: low-level call failed"
        );
        if (returndata.length > 0) {
            // Return data is optional
            require(
                abi.decode(returndata, (bool)),
                "SafeERC20: ERC20 operation did not succeed"
            );
        }
    }
}

contract Recstartoken is ERC721Enumerable, Ownable {
    using SafeERC20 for IERC20;
    using SafeMath for uint256;

    enum currency {
        Ether,
        Token
    }

    struct Collection {
        currency purchaseCurrency;
        uint256 floorPrice;
        address owner;
    }

    mapping(string => Collection) public collectionBuyInfo;

    struct NFT {
        uint256 price;
        address ownerAddress;
        address creatorAddress;
        currency sellCurrency;
        bool sell;
        string collectionID;
    }

    mapping(uint256 => NFT) private internalNFTInfo;

    IERC20 public purchaseToken;
    string private _baseTokenURI;

    bool public isSaleStart;
    uint256 public floorPrice;
    uint256 public floorPriceToken;

    constructor() ERC721("Recstartoken", "Rec") {
        purchaseToken = IERC20(0x09985613331A269bDC4B54f8b34fC8B7262f666b);
        _baseTokenURI = "https://recstartoken.com/metadata/";

        isSaleStart = true;
        floorPrice = 0.002 * 1e18;
        floorPriceToken = 0.08 * 1e18;
    }

    function changeTokenProperties(
        uint256 _price,
        uint256 _tokenId,
        bool _sell,
        currency _sellCurrency
    ) external {
        require(
            msg.sender == ownerOf(_tokenId),
            "You're not the owner of this asset!"
        );
        internalNFTInfo[_tokenId].price = _price;
        internalNFTInfo[_tokenId].sellCurrency = _sellCurrency;
        internalNFTInfo[_tokenId].sell = _sell;
    }

    function NFTINFO(uint256 _tokenId) external view returns(NFT memory) {
        NFT memory tempNFTInfo = internalNFTInfo[_tokenId];
        tempNFTInfo.ownerAddress = ownerOf(_tokenId);
        return tempNFTInfo;
    }

    function mint(currency _currency) external payable {
        require(isSaleStart, "Sale isn't running!");

        // Handle payment
        if(_currency == currency.Ether) {
            require(
                msg.value >= floorPrice,
                "Not enough Ether sent with the transaction!"
            );

            payable(owner()).transfer(msg.value);
        } else {
            require(
                purchaseToken.allowance(msg.sender, address(this)) >=
                    floorPriceToken,
                "Please allow fund first!"
            );

            purchaseToken.safeTransferFrom(msg.sender, owner(), floorPriceToken);
        }

        // Handle Mint
        uint256 totalSupply = totalSupply();
        uint256 currentTokenID = totalSupply + 1;

        _safeMint(msg.sender, currentTokenID);
        internalNFTInfo[currentTokenID].price = _currency == currency.Ether
            ? floorPrice : floorPriceToken;
        internalNFTInfo[currentTokenID].sellCurrency = _currency;
        internalNFTInfo[currentTokenID].creatorAddress = msg.sender;
        internalNFTInfo[currentTokenID].sell = false;
    }

    function collectionBuy(string memory _collectionID) external payable returns(uint256) {
        require(isSaleStart, "Sale isn't running!");
        require(collectionBuyInfo[_collectionID].owner != address(0), "This collection does not exist");

        uint256 _floorPrice = collectionBuyInfo[_collectionID].floorPrice;
        address _collectionOwner = collectionBuyInfo[_collectionID].owner;

        // Handle payment
        if(collectionBuyInfo[_collectionID].purchaseCurrency == currency.Ether) {
            require(
                msg.value >= _floorPrice,
                "Insufficient value sent with the transaction!"
            );
            payable(_collectionOwner).transfer(msg.value);
        } else {
            require(
                purchaseToken.allowance(msg.sender, address(this)) >=
                    _floorPrice,
                "Insufficient allowance!"
            );
            purchaseToken.safeTransferFrom(
                msg.sender,
                _collectionOwner,
                _floorPrice
            );
        }

        // Handle mint
        uint256 totalSupply = totalSupply();
        uint256 currentTokenID = totalSupply + 1;

        _safeMint(msg.sender, currentTokenID);
        internalNFTInfo[currentTokenID].price = collectionBuyInfo[_collectionID].floorPrice;
        internalNFTInfo[currentTokenID].sellCurrency = collectionBuyInfo[_collectionID].purchaseCurrency;
        internalNFTInfo[currentTokenID].creatorAddress = collectionBuyInfo[_collectionID].owner;
        internalNFTInfo[currentTokenID].sell = false;
        internalNFTInfo[currentTokenID].collectionID = _collectionID;

        return currentTokenID;
    }

    function buy(uint256 _tokenId) external payable {
        require(internalNFTInfo[_tokenId].sell, "Item is not available for sale!");

        address currentOwner = ownerOf(_tokenId);

        if(internalNFTInfo[_tokenId].sellCurrency == currency.Ether) {
            require(
                msg.value >= internalNFTInfo[_tokenId].price,
                "Insufficient value sent with the transaction!"
            );
            payable(currentOwner).transfer(internalNFTInfo[_tokenId].price);
        } else {
            require(
                purchaseToken.allowance(msg.sender, address(this)) >=
                    internalNFTInfo[_tokenId].price,
                "Insufficient allowance!"
            );
            purchaseToken.safeTransferFrom(
                msg.sender,
                currentOwner,
                internalNFTInfo[_tokenId].price
            );
        }

        handleTokenTransferOnBuy(_tokenId);
    }

    // ======================== Admin ====================================
    function createCollection(
        string memory _collectionID,
        currency _purchaseCurrency,
        uint256 _floorPrice,
        address _collectionOwner
    ) external onlyOwner {
        require(collectionBuyInfo[_collectionID].owner == address(0), "Collection already exists!");

        collectionBuyInfo[_collectionID].purchaseCurrency = _purchaseCurrency;
        collectionBuyInfo[_collectionID].floorPrice = _floorPrice;
        collectionBuyInfo[_collectionID].owner = _collectionOwner;
    }

    function changeFloorPriceCollection(string memory _collectionID, uint256 _newFloorPrice) external onlyOwner {
        require(collectionBuyInfo[_collectionID].owner != address(0), "Collection doesn't exist!");

        collectionBuyInfo[_collectionID].floorPrice = _newFloorPrice;
    }

    function deleteCollection(string memory _collectionID) external onlyOwner {
        require(collectionBuyInfo[_collectionID].owner != address(0), "Collection doesn't exist!");

        collectionBuyInfo[_collectionID].purchaseCurrency = currency.Ether;
        collectionBuyInfo[_collectionID].floorPrice = 0;
        collectionBuyInfo[_collectionID].owner = address(0);
    }

    function setFloorPrice(uint256 _floorPriceToken, uint256 _floorPrice)
        external
        onlyOwner
    {
        floorPriceToken = _floorPriceToken;
        floorPrice = _floorPrice;
    }

    function setPurchaseToken(IERC20 _newToken) external onlyOwner {
        purchaseToken = _newToken;
    }

    function setSaleStatus(bool _newStatus) external onlyOwner {
        isSaleStart = _newStatus;
    }

    function setBaseURI(string memory baseURI) external onlyOwner {
        _baseTokenURI = baseURI;
    }

    // To withdraw the wrongly received Ethers in the contract
    function withdrawEthers(uint256 _amount, address _to) external onlyOwner {
        payable(_to).transfer(_amount);
    }