contract Marketplace is ReentrancyGuard, Pausable, Ownable {
    using SafeMath for uint;

    IERC721 nft;  

    uint256 public platformFee = 1;
    uint256 public basisPoints = 100;
    uint256 public maxTokens = 7500;

    struct Offer {
        bool isForSale;
        uint index;
        address seller;
        uint minValue;          // in ether
        address onlySellTo;
    }

    struct Bid {
        bool hasBid;
        uint index;
        address bidder;
        uint value;
    }

    mapping (uint => Offer) public saleOffers;

    mapping (uint => Bid) public bids;

    // A record of pending ETH withdrawls by address
    mapping (address => uint) public pendingWithdrawals;

    event Offered(uint indexed index, uint minValue, address indexed toAddress);
    event BidEntered(uint indexed index, uint value, address indexed fromAddress);
    event BidWithdrawn(uint indexed index, uint value, address indexed fromAddress);
    event Bought(uint indexed index, uint value, address indexed fromAddress, address indexed toAddress);
    event RemovedFromSale(uint indexed index);

    /* Initializes contract with an instance of AfrikanNomads contract, and sets deployer as owner */
    constructor(address initialAddress) {
        IERC721(initialAddress).balanceOf(address(this));
        setContract(initialAddress);
    }

    function pause() public whenNotPaused onlyOwner {
        _pause();
    }

    function unpause() public whenPaused onlyOwner {
        _unpause();
    }

    /* Returns the contract address currently being used */
    function contractAddress() public view returns (address) {
      return address(nft);
    }


    function setContract(address newContract) public onlyOwner {
      nft = IERC721(newContract);
    }

    function setPlatformFee(uint256 newFee) public onlyOwner {
        platformFee = newFee;
    }

    function setBasisPoints(uint256 newBasisPoints) public onlyOwner {
        require(newBasisPoints > 0, "WTF");
        basisPoints = newBasisPoints;
    }

    function setMaxTokens(uint256 newMaxTokens) public onlyOwner {
        maxTokens = newMaxTokens;
    }

    function removeFromSale(uint index) public nonReentrant() {
        if (index >= maxTokens) revert('token index not valid');
        if (nft.ownerOf(index) != msg.sender) revert('you are not the owner of this token');
        saleOffers[index] = Offer(false, index, msg.sender, 0, address(0x0));
        emit RemovedFromSale(index);
    }

    function offerForSale(uint index, uint minSalePriceInWei) public whenNotPaused nonReentrant()  {
        if (index >= maxTokens) revert('token index not valid');
        if (minSalePriceInWei == 0) revert('Cannot set for price of 0');
        if (nft.ownerOf(index) != msg.sender) revert('you are not the owner of this token');
        saleOffers[index] = Offer(true, index, msg.sender, minSalePriceInWei, address(0x0));
        emit Offered(index, minSalePriceInWei, address(0x0));
    }

    function offerForSaleToAddress(uint index, uint minSalePriceInWei, address toAddress) public whenNotPaused nonReentrant() {
        if (index >= maxTokens) revert('token index not valid');
        if (minSalePriceInWei == 0) revert('Cannot set for price of 0');
        if (nft.ownerOf(index) != msg.sender) revert('you are not the owner of this token');
        saleOffers[index] = Offer(true, index, msg.sender, minSalePriceInWei, toAddress);

        emit Offered(index, minSalePriceInWei, toAddress);
    }
    

    function buy(uint index) payable public whenNotPaused nonReentrant() {
        if (index >= maxTokens) revert('token index not valid');
        Offer memory offer = saleOffers[index];
        if (!offer.isForSale) revert('NFT Not for sale'); 
        if (offer.onlySellTo != address(0x0) && offer.onlySellTo != msg.sender) revert();                
        if (msg.value != offer.minValue) revert('not enough ether');          // Didn't send enough ETH
        address seller = offer.seller;
        if (seller == msg.sender) revert('seller == msg.sender');
        if (seller != nft.ownerOf(index)) revert('seller no longer owner');


        saleOffers[index] = Offer(false, index, msg.sender, 0, address(0x0));
        nft.safeTransferFrom(seller, msg.sender, index);

        updateWithdrawals(seller, msg.value);

        emit Bought(index, msg.value, seller, msg.sender);

        // Check for the case where there is a bid from the new owner and refund it.
        // Any other bid can stay in place.
        Bid memory bid = bids[index];
        if (bid.bidder == msg.sender) {
            // Kill bid and refund value
            pendingWithdrawals[msg.sender] += bid.value;
            bids[index] = Bid(false, index, address(0x0), 0);
        }
    }

    /* Allows users to retrieve ETH from sales */
    function withdraw(address _user) public nonReentrant() {
        uint amount = pendingWithdrawals[_user];
        // Remember to zero the pending refund before
        // sending to prevent re-entrancy attacks
        require(amount > 0, "nothing to withdraw");
        pendingWithdrawals[_user] = 0;
        payable(_user).transfer(amount);
    }

    function enterBid(uint index) payable public whenNotPaused nonReentrant() {
        if (index >= maxTokens) revert('token index not valid');
        if (nft.ownerOf(index) == msg.sender) revert('you already own this');
        if (msg.value == 0) revert('cannot enter bid of zero');
        Bid memory existing = bids[index];
        if (msg.value <= existing.value) revert('your bid is too low');
        if (existing.value > 0) {
            // Refund the failing bid
            pendingWithdrawals[existing.bidder] += existing.value;
        }
        bids[index] = Bid(true, index, msg.sender, msg.value);
        emit BidEntered(index, msg.value, msg.sender);
    }

    function acceptBid(uint index, uint minPrice) public whenNotPaused nonReentrant() {
        if (index >= maxTokens) revert('token index not valid');
        if (nft.ownerOf(index) != msg.sender) revert('you do not own this token');
        address seller = msg.sender;
        Bid memory bid = bids[index];
        if (bid.value == 0) revert('cannot enter bid of zero');
        if (bid.value < minPrice) revert('your bid is too low');

        address bidder = bid.bidder;
        if (seller == bidder) revert('you already own this token');
        saleOffers[index] = Offer(false, index, bidder, 0, address(0x0));
        uint amount = bid.value;
        bids[index] = Bid(false, index, address(0x0), 0);
        nft.safeTransferFrom(msg.sender, bidder, index);

        updateWithdrawals(seller, amount);
        emit Bought(index, bid.value, seller, bidder);
    }

    function withdrawBid(uint index) public nonReentrant() {
        if (index >= maxTokens) revert('token index not valid');
        Bid memory bid = bids[index];
        if (bid.bidder != msg.sender) revert('the bidder is not message sender');
        emit BidWithdrawn(index, bid.value, msg.sender);
        uint amount = bid.value;
        bids[index] = Bid(false, index, address(0x0), 0);
        // Refund the bid money
        payable(msg.sender).transfer(amount);
    }

    function updateWithdrawals(address seller, uint amount) private {
        uint256 txPlatformFee = amount.div(basisPoints).mul(platformFee);
        uint256 remainder = amount - txPlatformFee;
        pendingWithdrawals[seller] += remainder;
        pendingWithdrawals[owner()] += txPlatformFee;
    }

}
function contractAddress() public view returns (address) {
  return address(nft);
}

/* Returns the contract address currently being used */