contract SimpleMarketplace is Ownable, ISimpleMarketplace {

    struct Bundle {
        address contractAddress;
        address creatorAddress;
        uint256 price;
        uint256[] tokenIds;
    }

    address public registryAddress;

    /**
     * Setting a min price so that users cannot accidentally
     * sell their tokens at a price that is off by a conversion rate.
     * The min WEI price below is equivalent to ETH 0.000001
     *
     * WEI has a conversion rate of:
     * 1 ETH = 1_000_000_000 GWEI
     * 1 GWEI = 1_000_000_000 WEI
     */
    uint256 public MIN_WEI_PRICE = 1_000_000_000_000;

    uint256 public MAX_BUNDLE_SIZE = 25;

    uint256 private nextBundleId = 1;

    mapping(uint256 => Bundle) private _bundleIdToBundle;
    mapping(address => mapping(uint256 => uint256)) private _tokenToBundleId;

    /**
     * @dev Constructor function
     */
    constructor(address registryAddress_) { 
        registryAddress = registryAddress_;
    }

    /**
     * @dev Verify that the seller is either the contractAddress or the creator.
     * Also ensure that this contract has approval for all tokens for that creator.
     */
    function _verifySeller(address contractAddress, address creatorAddress) internal view {
        address msgSender = _msgSender();

        bool isValidSender = msgSender == contractAddress || msgSender == creatorAddress;
        require(isValidSender, "Seller must be the creator or the contract");

        IERC721 nonFungibleContract = IERC721(contractAddress);
        require(
            nonFungibleContract.isApprovedForAll(creatorAddress, address(this)), 
            "Seller must approve this contract for all tokens"
        );
    }

    /**
     * @dev Valid bundles start with id 1, so tokens with bundleId 0 are not bundled
     */
    function _isBundled(address contractAddress, uint256 tokenId) internal view returns (bool) {
        return _tokenToBundleId[contractAddress][tokenId] != 0;
    }

    /**
     * @dev Valid bundles have a price above minimum and are reset when deleted. So
     * we can use price as a proxy to see if the bundle exists
     */
    function _bundleExists(uint256 bundleId) internal view returns (bool) {
        return _bundleIdToBundle[bundleId].price >= MIN_WEI_PRICE;
    }

    /**
     * @dev See {ISimpleMarketplace-isBuyable}.
     */
    function _isBuyable(uint256 bundleId) internal view returns (bool) {
        // Cannot sell the bundle if it does not exist
        if(!_bundleExists(bundleId)) {
            return false;
        }

        Bundle memory bundle = _bundleIdToBundle[bundleId];
        IERC721 nonFungibleContract = IERC721(bundle.contractAddress);

        // Cannot sell the bundle if we are not approved to transfer all
        if(!nonFungibleContract.isApprovedForAll(bundle.creatorAddress, address(this))) {
            return false; 
        }

        uint256[] memory tokenIds = bundle.tokenIds;
        uint256 numTokens = tokenIds.length;

        // Cannot sell the bundle if the owner no longer owns all the tokens
        for (uint256 i = 0; i < numTokens; i++) {
            if(nonFungibleContract.ownerOf(tokenIds[i]) != bundle.creatorAddress) {
                return false;
            }
        }

        return true;
    }

    /**
     * @dev Returns all pertinent information related to a bundleId
     */
    function getBundle(uint256 bundleId) external view returns (Bundle memory) {
        require(_isBuyable(bundleId), "This bundle is not buyable");
        return _bundleIdToBundle[bundleId];
    }

    /**
     * @dev See {ISimpleMarketplace-isBuyable}.
     */
    function isBuyable(uint256 bundleId) external view override returns (bool) {
        return _isBuyable(bundleId);
    }

    /**
     * @dev See {ISimpleMarketplace-getBundleId}.
     */
    function getBundleId(address contractAddress, uint256 tokenId) external view override returns (uint256) {
        uint256 bundleId = _tokenToBundleId[contractAddress][tokenId];
        require(bundleId != 0, "This token is not part of a bundle");
        
        return bundleId;
    }

    /**
     * @dev After every transaction the marketplace holds a cut. A portion of this
     * accumulated amount can be withdrawn by the owner at anytime.
     *
     * Requirements:
     * - Only Marketplace owner can withdraw balance.
     */
    function withdrawBalance(uint256 amount) external onlyOwner {
        require(amount <= address(this).balance, "Contract balance too low");
        payable(owner()).transfer(amount);
    }

    /**
     * @dev See {ISimpleMarketplace-sellBundle}.
     */
    function sellBundle(
        address contractAddress, 
        address creatorAddress, 
        uint256[] memory tokenIds, 
        uint256 price
    ) external override {
        require(tokenIds.length < MAX_BUNDLE_SIZE, "Too many tokens given");
        require(price >= MIN_WEI_PRICE, "Price given is below minimum");
        require(IERC165(contractAddress).supportsInterface(type(IERC721).interfaceId), "Contract must support IERC721");
        _verifySeller(contractAddress, creatorAddress);

        Bundle memory newBundle;
        newBundle.contractAddress = contractAddress;
        newBundle.creatorAddress = creatorAddress;
        newBundle.tokenIds = tokenIds;
        newBundle.price = price;

        _bundleIdToBundle[nextBundleId] = newBundle;

        uint256 numTokens = tokenIds.length;
        for (uint256 i = 0; i < numTokens; i++) {
            uint256 tokenId = tokenIds[i];
            require(!_isBundled(contractAddress, tokenId), "Token already in bundle");
            _tokenToBundleId[contractAddress][tokenId] = nextBundleId;
        }

        emit BundleCreated(nextBundleId, contractAddress, creatorAddress, tokenIds, price);

        nextBundleId = nextBundleId + 1;
    }

    /**
     * @dev See {ISimpleMarketplace-withdrawBundle}.
     */
    function withdrawBundle(uint256 bundleId) external override {
        require(_bundleExists(bundleId), "Bundle does not exist");

        address msgSender = _msgSender();
        Bundle memory bundle = _bundleIdToBundle[bundleId];
        address contractAddress = bundle.contractAddress;

        bool isValidWithdrawer = msgSender == contractAddress || msgSender == bundle.creatorAddress;
        require(isValidWithdrawer, "Only the contract or bundle creator can withdraw");
        
        uint256[] memory tokenIds = bundle.tokenIds;
        uint256 numTokens = tokenIds.length;

        for (uint256 i = 0; i < numTokens; i++) {
            _tokenToBundleId[contractAddress][tokenIds[i]] = 0;
        }

        delete _bundleIdToBundle[bundleId];

        emit BundleWithdrawn(bundleId);
    }

    /**
     * @dev See {ISimpleMarketplace-buyBundle}.
     */
    function buyBundle(uint256 bundleId) external payable override {
        require(_bundleExists(bundleId), "Bundle does not exist");

        Bundle memory bundle = _bundleIdToBundle[bundleId];

        uint256 msgValue = msg.value;
        address senderAddress = _msgSender();
        address creatorAddress = bundle.creatorAddress;

        require(msgValue >= bundle.price, "Msg value too small");
        
        uint256 creatorPayout = 
            IEaselyRegistry(registryAddress).calculateCreatorPayout(creatorAddress, bundle.price); 

        IERC721 nonFungibleContract = IERC721(bundle.contractAddress);
        uint256[] memory tokenIds = bundle.tokenIds;

        for (uint256 i = 0; i < tokenIds.length; i++) {
            uint256 tokenId = bundle.tokenIds[i];
            
            _tokenToBundleId[bundle.contractAddress][tokenId] = 0;
            nonFungibleContract.safeTransferFrom(creatorAddress, senderAddress, tokenId);
        } 

        // Send back any value greater than the listed price
        payable(creatorAddress).transfer(creatorPayout);
        payable(senderAddress).transfer(msgValue - bundle.price);

        delete _bundleIdToBundle[bundleId];

        emit BundleBought(bundleId);
    }
}
function buyBundle(uint256 bundleId) external payable override {
    require(_bundleExists(bundleId), "Bundle does not exist");

    Bundle memory bundle = _bundleIdToBundle[bundleId];

    uint256 msgValue = msg.value;
    address senderAddress = _msgSender();
    address creatorAddress = bundle.creatorAddress;

    require(msgValue >= bundle.price, "Msg value too small");
        
    uint256 creatorPayout = 
        IEaselyRegistry(registryAddress).calculateCreatorPayout(creatorAddress, bundle.price); 

    IERC721 nonFungibleContract = IERC721(bundle.contractAddress);
    uint256[] memory tokenIds = bundle.tokenIds;

    for (uint256 i = 0; i < tokenIds.length; i++) {
        uint256 tokenId = bundle.tokenIds[i];
            
        _tokenToBundleId[bundle.contractAddress][tokenId] = 0;
        nonFungibleContract.safeTransferFrom(creatorAddress, senderAddress, tokenId);
    } 

    // Send back any value greater than the listed price
    payable(creatorAddress).transfer(creatorPayout);
    payable(senderAddress).transfer(msgValue - bundle.price);

    delete _bundleIdToBundle[bundleId];

    emit BundleBought(bundleId);
}

/**
 * @dev See {ISimpleMarketplace-buyBundle}.
 */