contract NftAuctionETH is Ownable, ReentrancyGuard, BidETHWrapper, IERC1155Receiver {
  using SafeMath for uint256;

  address public nftsAddress;
  address public runner;

  // info about a particular auction
  struct AuctionInfo {
    address beneficiary;
    uint256 fee;
    uint256 auctionStart;
    uint256 auctionEnd;
    uint256 originalAuctionEnd;
    uint256 endedAt;
    uint256 extension;
    uint256 nft;
    address highestBidder;
    uint256 highestBid;
    uint256 startBidWei;
    uint256 bidStepWei;
    uint256 buyNowPriceWei;
    bool auctionEnded;
  }

  mapping(uint256 => AuctionInfo) public auctionsById;
  uint256[] public auctions;

  // Events that will be fired on changes.
  event BidPlaced(address indexed user, uint256 indexed id, uint256 amount);
  event Withdrawn(address indexed user, uint256 indexed id, uint256 amount);
  event Ended(address indexed user, uint256 indexed id, uint256 amount);
  event RunnerFeeChanged(uint256 indexed id, uint256 fee);

  constructor(address _runner, address _nftsAddress) public {
    runner = _runner;
    nftsAddress = _nftsAddress;
  }

  function auctionStart(uint256 id) public view returns (uint256) {
    return auctionsById[id].auctionStart;
  }

  function beneficiary(uint256 id) public view returns (address) {
    return auctionsById[id].beneficiary;
  }

  function auctionEnd(uint256 id) public view returns (uint256) {
    return auctionsById[id].auctionEnd;
  }

  function nftTokenId(uint256 id) public view returns (uint256) {
    return auctionsById[id].nft;
  }

  function highestBidder(uint256 id) public view returns (address) {
    return auctionsById[id].highestBidder;
  }

  function highestBid(uint256 id) public view returns (uint256) {
    return auctionsById[id].highestBid;
  }

  function isEndedByTime(uint256 id) public view returns (bool) {
    return now >= auctionsById[id].auctionEnd;
  }

  function ended(uint256 id) public view returns (bool) {
    return auctionsById[id].auctionEnded;
  }

  function bidStepWei(uint256 id) public view returns (uint256) {
    return auctionsById[id].bidStepWei;
  }

  function startBidWei(uint256 id) public view returns (uint256) {
    return auctionsById[id].startBidWei;
  }

  function buyNowPriceWei(uint256 id) public view returns (uint256) {
    return auctionsById[id].buyNowPriceWei;
  }

  function endedAt(uint256 id) public view returns (uint256) {
    return auctionsById[id].endedAt;
  }

  function runnerFee(uint256 id) public view returns (uint256) {
    return auctionsById[id].fee;
  }

  function setRunnerAddress(address account) public onlyOwner {
    runner = account;
  }

  function create(
    uint256 id,
    address beneficiaryAddress,
    uint256 fee,
    uint256 start,
    uint256 duration,
    uint256 extension, // in minutes
    uint256 _startBidWei,
    uint256 _bidStepWei,
    uint256 _buyNowPriceWei,
    address _tokenBeneficiary,
    uint256 _tokenResidualsFee,
    bool _tokenResidualsRequired
  ) public onlyOwner {
    AuctionInfo storage auction = auctionsById[id];
    require(auction.beneficiary == address(0), "NftAuction::create: auction already created");
    require(_bidStepWei > 0, "NftAuction::create: bid step = 0");
    require(_buyNowPriceWei > _bidStepWei, "NftAuction::create: buy now price too small");

    auction.beneficiary = beneficiaryAddress;
    auction.fee = fee;
    auction.auctionStart = start;
    auction.auctionEnd = start.add(duration * 1 days);
    auction.originalAuctionEnd = start.add(duration * 1 days);
    auction.extension = extension * 60;
    auction.startBidWei = _startBidWei;
    auction.bidStepWei = _bidStepWei;
    auction.buyNowPriceWei = _buyNowPriceWei;

    auctions.push(id);

    uint256 tokenId =
      IERC1155Tradable(nftsAddress).create(
        1,
        1,
        "",
        "",
        _tokenBeneficiary,
        _tokenResidualsFee,
        _tokenResidualsRequired
      );
    require(tokenId > 0, "NftAuction::create: ERC1155 create did not succeed");
    auction.nft = tokenId;
  }

  function bid(uint256 id) public payable override nonReentrant {
    uint256 amount = msg.value;
    AuctionInfo storage auction = auctionsById[id];
    require(auction.beneficiary != address(0), "NftAuction::bid: auction does not exist");
    require(now >= auction.auctionStart, "NftAuction::bid: auction has not started");
    require(now <= auction.auctionEnd, "NftAuction::bid: auction has ended");
    require(!auction.auctionEnded, "NftAuction::bid: auction has ended");

    uint256 newAmount = amount.add(balanceOf(msg.sender, id));
    if (auction.highestBid == 0) {
      require(newAmount >= auction.startBidWei, "NftAuction::bid: start bid too small");
    }
    require(newAmount >= auction.highestBid.add(auction.bidStepWei), "NftAuction::bid: bid too small");

    auction.highestBidder = msg.sender;
    auction.highestBid = newAmount;

    if (auction.extension > 0 && auction.auctionEnd.sub(now) <= auction.extension) {
      auction.auctionEnd = now.add(auction.extension);
    }

    super.bid(id);
    emit BidPlaced(msg.sender, id, amount);

    if (auction.highestBid >= auction.buyNowPriceWei) {
      end(id);
    }
  }

  function withdraw(uint256 id) public override nonReentrant {
    AuctionInfo storage auction = auctionsById[id];
    uint256 amount = balanceOf(msg.sender, id);
    require(auction.beneficiary != address(0), "NftAuction::withdraw: auction does not exist");
    require(amount > 0, "NftAuction::withdraw: cannot withdraw 0");

    require(
      auction.highestBidder != msg.sender,
      "NftAuction::withdraw: you are the highest bidder and cannot withdraw"
    );

    super.withdraw(id);
    emit Withdrawn(msg.sender, id, amount);
  }

  function emergencyWithdraw(uint256 id) public onlyOwner {
    AuctionInfo storage auction = auctionsById[id];
    require(auction.beneficiary != address(0), "NftAuction::emergencyWithdraw: auction does not exist");
    require(now >= auction.auctionEnd, "NftAuction::emergencyWithdraw: the auction has not ended");
    require(!auction.auctionEnded, "NftAuction::emergencyWithdraw: auction ended and item sent");

    _emergencyWithdraw(auction.highestBidder, id);
    emit Withdrawn(auction.highestBidder, id, auction.highestBid);
  }

  function emergencyCancel(uint256 id, address bidder) public onlyOwner {
    AuctionInfo storage auction = auctionsById[id];
    require(auction.beneficiary != address(0), "NftAuction::emergencyCancel: auction does not exist");
    require(auction.highestBidder != bidder, "NftAuction::emergencyCancel: address is highest bidder");

    uint256 _amount = balanceOf(bidder, id);
    require(_amount > 0, "NftAuction::emergencyCancel: cannot withdraw 0");
    _emergencyWithdraw(bidder, id);
    emit Withdrawn(bidder, id, _amount);
  }

  function end(uint256 id) internal {
    AuctionInfo storage auction = auctionsById[id];
    auction.auctionEnded = true;
    auction.endedAt = now;
    _end(id, auction.highestBidder, auction.beneficiary, runner, auction.fee, auction.highestBid);
    IERC1155(nftsAddress).safeTransferFrom(address(this), auction.highestBidder, auction.nft, 1, "");
    emit Ended(auction.highestBidder, id, auction.highestBid);
  }

  function close(uint256 id) public nonReentrant {
    AuctionInfo storage auction = auctionsById[id];
    require(auction.beneficiary != address(0), "NftAuction::end: auction does not exist");
    require(now >= auction.auctionEnd, "NftAuction::end: the auction has not ended");
    require(!auction.auctionEnded, "NftAuction::end: auction already ended");
    end(id);
  }

  function setEndTime(uint256 id, uint256 endTimestamp) external onlyOwner {
    AuctionInfo storage auction = auctionsById[id];
    require(auction.beneficiary != address(0), "NftAuction::setEndTime: auction does not exist");
    require(now <= auction.auctionEnd, "NftAuction::setEndTime: auction has ended");
    require(!auction.auctionEnded, "NftAuction::setEndTime: auction closed");
    require(auction.auctionStart < endTimestamp, "NftAuction::setEndTime: before start");

    auction.auctionEnd = endTimestamp;
    auction.originalAuctionEnd = endTimestamp;
  }

  function setRunnerFee(uint256 id, uint256 _fee) external onlyOwner {
    AuctionInfo storage auction = auctionsById[id];
    require(auction.beneficiary != address(0), "NftAuction::setRunnerFee: auction does not exist");
    require(now <= auction.auctionEnd, "NftAuction::setRunnerFee: auction has ended");
    require(!auction.auctionEnded, "NftAuction::setRunnerFee: auction closed");
    require(_fee <= MAX_FEE, "NftAuction::setRunnerFee: fee too high");

    auction.fee = _fee;
    emit RunnerFeeChanged(id, _fee);
  }

  function setStartBid(uint256 id, uint256 _startBidWei) external onlyOwner {
    AuctionInfo storage auction = auctionsById[id];
    require(auction.beneficiary != address(0), "NftAuction::setStartBid: auction does not exist");
    require(now <= auction.auctionStart, "NftAuction::setStartBid: auction started");
    require(_startBidWei <= auction.buyNowPriceWei, "NftAuction::setStartBid: price less than startBid");
    require(_startBidWei > 0, "NftAuction::setStartBid: startBid == 0");

    auction.startBidWei = _startBidWei;
  }

  function onERC1155Received(
    address _operator,
    address, // _from
    uint256, // _id
    uint256, // _amount
    bytes memory // _data
  ) public override returns (bytes4) {
    require(msg.sender == address(nftsAddress), "NftAuction::onERC1155Received:: invalid token address");
    require(_operator == address(this), "NftAuction::onERC1155Received:: operator must be auction contract");

    // Return success
    return this.onERC1155Received.selector;
  }

  function onERC1155BatchReceived(
    address _operator,
    address, // _from,
    uint256[] memory, // _ids,
    uint256[] memory, // _amounts,
    bytes memory // _data
  ) public override returns (bytes4) {
    require(msg.sender == address(nftsAddress), "NftAuction::onERC1155BatchReceived:: invalid token address");
    require(_operator == address(this), "NftAuction::onERC1155BatchReceived:: operator must be auction contract");

    // Return success
    return this.onERC1155BatchReceived.selector;
  }

  /**
   * INTERFACE_SIGNATURE_ERC165 = bytes4(keccak256("supportsInterface(bytes4)"));
   *
   * INTERFACE_SIGNATURE_ERC1155TokenReceiver =
   * bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))
   * ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"));
   */
  function supportsInterface(bytes4 interfaceID) external view override returns (bool) {
    return
      interfaceID == 0x01ffc9a7 || // ERC-165 support
      interfaceID == 0x4e2312e0; // ERC-1155 `ERC1155TokenReceiver` support
  }
}
function supportsInterface(bytes4 interfaceID) external view override returns (bool) {
  return
    interfaceID == 0x01ffc9a7 || // ERC-165 support
    interfaceID == 0x4e2312e0; // ERC-1155 `ERC1155TokenReceiver` support
}

/**
 * INTERFACE_SIGNATURE_ERC165 = bytes4(keccak256("supportsInterface(bytes4)"));
 *
 * INTERFACE_SIGNATURE_ERC1155TokenReceiver =
 * bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))
 * ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"));
 */