contract OwnixMarket is ERC1155Holder, Ownable, Pausable, IOwnixMarket, ReentrancyGuard {
  using Address for address;

  uint256 public constant ONE_MILLION = 1 * 10**6;

   // The ERC20 ownix token
  IOwnixERC20 immutable public ownixToken;

  // The ERC1155 ownix token
  IOwnixERC1155 immutable public ownixNFTToken;

   // The fee collector address
  address public feeCollector;
  
  // The inflation collector address
  address public inflationCollector;

  // Auctions by token token id => index => auction
  mapping(uint256 => mapping (uint256 => Auction)) private auctions;

  uint256 public inflationPerMillion;
  uint256 public bidFeePerMillion;
  uint256 public ownerSharePerMillion;
  uint256 public bidMinimumRaisePerMillion;
  uint256 public bidMinimumRaiseAmount;
  uint256 public royaltiesPerMillion;
  uint256 public minimumAuctionDuration;

  /**
   * @dev Constructor of the contract.
   * @param _ownixToken - address of the Ownix token
   * @param _ownixNFTToken - address of the Ownix NFT token
   * @param _owner - address of the owner for the contract
   * @param _feeCollector - address of the fee collector address
   * @param _inflationCollector - address of the inflation collector address
   */
  constructor(
    address _ownixToken,
    address _ownixNFTToken,
    address _owner,
    address _feeCollector,
    address _inflationCollector,
    uint256 _minimumAuctionDuration
  ) Ownable() Pausable() {
    require(_ownixToken != address(0), "Can't be zero address");
    require(_ownixNFTToken != address(0), "Can't not be zero address");
    require(_owner != address(0), "Can't be zero address");
    require(_feeCollector != address(0), "Can't be zero address");
    require(_inflationCollector != address(0), "Can't be zero address");
    require(_minimumAuctionDuration > 0, "Minimum auction duration can't be zero");

    ownixToken = IOwnixERC20(_ownixToken);
    ownixNFTToken = IOwnixERC1155(_ownixNFTToken);

    // Set owner
    transferOwnership(_owner);
    // Set fee collector address
    feeCollector = _feeCollector;
    // Set inflation fee collector address
    inflationCollector = _inflationCollector;
    // Set the minimum auction duration
    minimumAuctionDuration = _minimumAuctionDuration;
  }

  function listNFT(uint256 _tokenId, uint256 _amount, uint256 _reservePrice, uint32 _duration) override external nonReentrant() whenNotPaused() {
    require(_tokenId > 0, "token id can't be zero");
    require(_amount > 0, "amount can't be zero");
    require(_reservePrice > 0, "reservePrice id can't be zero");
    require(_duration >= minimumAuctionDuration, "duration must be bigger the minimum uction duration");

    for (uint32 i=0; i < _amount; ++i) {
      uint32 index = i;
      require(auctions[_tokenId][index].tokenId == 0, "auction with the same token id is already exists");
      
      auctions[_tokenId][index] = Auction({
        nftOwner: msg.sender,
        nft: ownixNFTToken,
        tokenId: _tokenId,
        amount: _amount,
        index: index,
        reservePrice: _reservePrice,
        duration: _duration,
        expiresAt: 0,
        bidder: address(0),
        bidPrice: 0,
        bidCreatedAt: 0
      });

      emit NFTListed(
        msg.sender,
        ownixNFTToken,
        _tokenId,
        _amount,
        index,
        _reservePrice,
        0
      );
     }
     

    ownixNFTToken.safeTransferFrom(
      msg.sender,
      address(this),
      _tokenId,
      _amount,
      ""
    );
  }

  function unlistNFT(uint256 _tokenId, uint32 _index) override external nonReentrant() whenNotPaused() {
    Auction memory auction = auctions[_tokenId][_index];
    require(auction.expiresAt == 0, "Can't unlist NFT after auction started");
    require(auction.nftOwner == msg.sender,  "Must be nft owner unlist NFT");
   
    emit NFTUnlisted(
     auction.nftOwner,
     ownixNFTToken, 
     _tokenId, 
     1,
     _index);

    ownixNFTToken.safeTransferFrom(
      address(this),
     auction.nftOwner,
      _tokenId,
      1,
      ""
    );

    delete auctions[_tokenId][_index];
  }

  function changeReservePrice(uint256 _tokenId, uint32 _index, uint256 _reservePrice) override external whenNotPaused() {
    Auction memory auction = auctions[_tokenId][_index];
    require(auction.expiresAt == 0, "Can't change reserve price after auction started");
    require(auction.nftOwner == msg.sender, "Must be nft owner to change reserve");

    auction.reservePrice = _reservePrice;

    auctions[_tokenId][_index] = auction;

    emit ReservePriceChanged(
      msg.sender,
      ownixNFTToken,
      _tokenId,
      1,
      _index,
      _reservePrice
    );
  }

  function placeBid(uint256 _tokenId, uint32 _index, uint256 _price) override external whenNotPaused() nonReentrant() { 
      _placeBid(msg.sender, _tokenId, _index, _price);
  }

  function placeBidWithPermit(
    address _bidder,
    uint256 _tokenId, 
    uint32 _index, 
    uint256 _price,
    uint256 _deadline,
    uint8 _v,
    bytes32 _r,
    bytes32 _s) override external whenNotPaused() nonReentrant() {
     
      IERC20Permit(address(ownixToken)).permit(_bidder, address(this), _price, _deadline, _v, _r, _s);
      _placeBid(_bidder, _tokenId, _index, _price);
  }

  function finishAuction(uint256 _tokenId, uint32 _index) override external nonReentrant() whenNotPaused() {
   
    Auction memory auction = auctions[_tokenId][_index];

    require(auction.bidder != address(0), "Can't finish Auction without any bids");
    require(auction.expiresAt < block.timestamp, "Can't finish Auction before it been ended");

    uint256 saleShareAmount;
    if (ownerSharePerMillion > 0) {
      // Calculate sale share
      saleShareAmount = auction.bidPrice * ownerSharePerMillion / ONE_MILLION;
      // Transfer share amount to the bid contract
      require(
        ownixToken.transfer(feeCollector, saleShareAmount),
        "Transferring the share failed"
      );
    }

    uint256 royaltiesAmount;
    if (royaltiesPerMillion > 0) {
       address firstTimeOwner = ownixNFTToken.getFirstTimeOwner(auction.tokenId);
       if(firstTimeOwner != auction.nftOwner) {
        // Calculate royalties
        royaltiesAmount = auction.bidPrice * royaltiesPerMillion / ONE_MILLION;
        // Transfer royalties Owner
        require(
          ownixToken.transfer(firstTimeOwner, royaltiesAmount),
          "Transferring the royalties failed"
        );
      }
    }

    // Transfer ownixToken from bidder to seller
    require(
      ownixToken.transfer(auction.nftOwner, auction.bidPrice - (saleShareAmount + royaltiesAmount)),
      "Transferring ownixToken to nft owner failed"
    );

    if (inflationPerMillion > 0) {
      // Calculate mint tokens
      uint256 mintShareAmount = auction.bidPrice * inflationPerMillion / ONE_MILLION;
      // Mint the new ownix tokens to the inflationCollector
      ownixToken.mint(inflationCollector, mintShareAmount);
    }

    delete auctions[_tokenId][_index];
    
    // Transfer token to bidder
    ownixNFTToken.safeTransferFrom(
      address(this),
      auction.bidder,
      _tokenId,
      1,
      "");

    emit BidAccepted(
      ownixNFTToken,
      _tokenId,
      1,
      auction.index,
      auction.bidder,
      auction.nftOwner,
      auction.bidPrice
    );
  }

  function setInflationPerMillion(uint256 _inflationPerMillion) override external onlyOwner {
    require(
      _inflationPerMillion < ONE_MILLION,
      "The inflation should be between 0 and 999,999"
    );

    inflationPerMillion = _inflationPerMillion;
    emit InflationPerMillionChanged(inflationPerMillion);
  }

  function setBidFeePerMillion(uint256 _bidFeePerMillion) override external onlyOwner {
    require(
      _bidFeePerMillion < ONE_MILLION,
      "The bid fee should be between 0 and 999,999"
    );

    bidFeePerMillion = _bidFeePerMillion;
    emit BidFeePerMillionChanged(bidFeePerMillion);
  }

  function setOwnerSharePerMillion(uint256 _ownerSharePerMillion) override external onlyOwner {
    require(
      _ownerSharePerMillion < ONE_MILLION,
      "The owner share should be between 0 and 999,999"
    );

    ownerSharePerMillion = _ownerSharePerMillion;
    emit OwnerSharePerMillionChanged(ownerSharePerMillion);
  }

  function setBidMinimumRaisePerMillion(uint256 _bidMinimumRaisePerMillion) override external onlyOwner {
    require(
      _bidMinimumRaisePerMillion < ONE_MILLION,
      "bid minimum raise should be between 0 and 999,999"
    );

    bidMinimumRaisePerMillion = _bidMinimumRaisePerMillion;
    emit BidMinimumRaisePerMillionChanged(bidMinimumRaisePerMillion);
  }

  function setBidMinimumRaiseAmount(uint256 _bidMinimumRaiseAmount) override external onlyOwner {
    require(
      _bidMinimumRaiseAmount > 0,
      "bid minimum raise should be bigger then 0"
    );

    bidMinimumRaiseAmount = _bidMinimumRaiseAmount;
    emit BidMinimumRaiseAmountChanged(_bidMinimumRaiseAmount);
  }

  function setFeeCollector(address _feeCollector) override external onlyOwner {
    require(_feeCollector != address(0), "address can't be the zero address");

    feeCollector = _feeCollector;
    emit FeeCollectorChanged(_feeCollector);
  }

  function setInflationCollector(address _inflationCollector) override external onlyOwner {
    require(
      _inflationCollector != address(0),
      "address can't be the zero address"
    );

    inflationCollector = _inflationCollector;
    emit InflationCollectorChanged(_inflationCollector);
  }

  function setRoyaltiesPerMillion(uint256 _royaltiesPerMillion) override external onlyOwner {
    require(
      _royaltiesPerMillion < ONE_MILLION,
      "bid minimum raise should be between 0 and 999,999"
    );

    royaltiesPerMillion = _royaltiesPerMillion;
    emit RoyaltiesPerMillionChanged(_royaltiesPerMillion);
  }

  function setMinimumAuctionDuration(uint256 _minimumAuctionDuration) override external onlyOwner {
    require(
      _minimumAuctionDuration > 0,
      "Minimum auction duration should be bigger then 0"
    );

    minimumAuctionDuration = _minimumAuctionDuration;
    emit MinimumAuctionDurationChanged(_minimumAuctionDuration);
  }

  function withdrawERC20(address _withdrawAddress, uint256 _amount) override external onlyOwner {
    require(
      _withdrawAddress != address(0),
      "address can't be the zero address"
    );

    require(
      ownixToken.transfer(_withdrawAddress, _amount),
      "Withdraw failed"
    );
  }

  function withdrawERC1155(
    address _tokenAddress,
    uint256 _tokenId,
    address _withdrawAddress,
    uint256 _amount) override external onlyOwner {
    require(
      _withdrawAddress != address(0),
      "address can't be the zero address"
    );

    IERC1155(_tokenAddress).safeTransferFrom(
      address(this),
      _withdrawAddress,
      _tokenId,
      _amount,
      ""
    );
  }

  function getAuction(uint256 _tokenId, uint32 _index) override external view returns (Auction memory)  {
    return auctions[_tokenId][_index];
  }

  function pause() override external onlyOwner {
    _pause();
  }

  function unpause() override external onlyOwner {
    _unpause();
  }


  /**
   * @dev Place a bid for an ERC1155 token.
   * @notice Tokens can have multiple bids by different users.
   * Users can have only one bid per token.
   * If the user places a bid and has an active bid for that token,
   * the older one will be replaced with the new one.
   * @param _bidder - address the bidder address
   * @param _tokenId - uint256 of the token id
   * @param _index The index of tokens being transferred
   * @param _price - of the price for the bid
   */
  function _placeBid(address _bidder, uint256 _tokenId, uint32 _index, uint256 _price) private whenNotPaused() {
    Auction memory auction = auctions[_tokenId][_index];

    require(auctions[_tokenId][_index].tokenId != 0, "auction must be listed first");
    require(auction.expiresAt == 0 || auction.expiresAt > block.timestamp,
      "List has been ended, Can't place bid"
    );

    require(
      (_price >= (auction.bidPrice + bidMinimumRaiseAmount) ||
        _price >= auction.bidPrice + (auction.bidPrice * bidMinimumRaisePerMillion / ONE_MILLION)) &&
        _price >= auction.reservePrice,
      "Price should be bigger than highest bid and reserve price"
    );

    
    if(auction.bidder == address(0)) {
      auction.expiresAt = block.timestamp + auction.duration;
      auctions[_tokenId][_index] = auction;

        emit AuctionStarted(
          auction.nftOwner,
          ownixNFTToken,
          _tokenId,
          auction.amount,
          auction.index,
          auction.reservePrice,
          auction.expiresAt);
    } else {
      require(ownixToken.transfer(auction.bidder, auction.bidPrice), "Refund failed");
    }

    // check if place bid in the last minimum auction duration
    if (auction.expiresAt - block.timestamp <= minimumAuctionDuration) {
      auction.expiresAt = block.timestamp + minimumAuctionDuration;

      emit AuctionExtended(
        auction.nftOwner,
        ownixNFTToken,
        _tokenId,
        auction.amount,
        _index,
        auction.reservePrice,
        auction.expiresAt
      );
    }

    // Transfer tokens to the market
    require(
      ownixToken.transferFrom(_bidder, address(this), _price),
      "Transferring the bid amount to the marketplace failed"
    );

    // Check if there's a bid fee and transfer the amount to marketplace owner
    if (bidFeePerMillion > 0) {
      // Calculate sale share
      uint256 feeAmount  = _price * bidFeePerMillion / ONE_MILLION;
      require(
        ownixToken.transferFrom(_bidder, feeCollector, feeAmount),
        "Transferring the bid fee to the marketplace owner failed"
      );
    }

    uint256 bidCreatedAt = block.timestamp;

    // Save Bid
    auction.bidder = _bidder;
    auction.bidPrice = _price;
    auction.bidCreatedAt = bidCreatedAt;

    auctions[_tokenId][_index] = auction;

    emit BidCreated(
      ownixNFTToken,
      _tokenId,
      1,
      _index,
      _bidder,
      _price,
      bidCreatedAt
    );
  }
}
function _placeBid(address _bidder, uint256 _tokenId, uint32 _index, uint256 _price) private whenNotPaused() {
  Auction memory auction = auctions[_tokenId][_index];

  require(auctions[_tokenId][_index].tokenId != 0, "auction must be listed first");
  require(auction.expiresAt == 0 || auction.expiresAt > block.timestamp,
    "List has been ended, Can't place bid"
  );

  require(
    (_price >= (auction.bidPrice + bidMinimumRaiseAmount) ||
      _price >= auction.bidPrice + (auction.bidPrice * bidMinimumRaisePerMillion / ONE_MILLION)) &&
      _price >= auction.reservePrice,
    "Price should be bigger than highest bid and reserve price"
  );

    
  if(auction.bidder == address(0)) {
    auction.expiresAt = block.timestamp + auction.duration;
    auctions[_tokenId][_index] = auction;

      emit AuctionStarted(
        auction.nftOwner,
        ownixNFTToken,
        _tokenId,
        auction.amount,
        auction.index,
        auction.reservePrice,
        auction.expiresAt);
  } else {
    require(ownixToken.transfer(auction.bidder, auction.bidPrice), "Refund failed");
  }

  // check if place bid in the last minimum auction duration
  if (auction.expiresAt - block.timestamp <= minimumAuctionDuration) {
    auction.expiresAt = block.timestamp + minimumAuctionDuration;

    emit AuctionExtended(
      auction.nftOwner,
      ownixNFTToken,
      _tokenId,
      auction.amount,
      _index,
      auction.reservePrice,
      auction.expiresAt
    );
  }

  // Transfer tokens to the market
  require(
    ownixToken.transferFrom(_bidder, address(this), _price),
    "Transferring the bid amount to the marketplace failed"
  );

  // Check if there's a bid fee and transfer the amount to marketplace owner
  if (bidFeePerMillion > 0) {
    // Calculate sale share
    uint256 feeAmount  = _price * bidFeePerMillion / ONE_MILLION;
    require(
      ownixToken.transferFrom(_bidder, feeCollector, feeAmount),
      "Transferring the bid fee to the marketplace owner failed"
    );
  }

  uint256 bidCreatedAt = block.timestamp;

  // Save Bid
  auction.bidder = _bidder;
  auction.bidPrice = _price;
  auction.bidCreatedAt = bidCreatedAt;

  auctions[_tokenId][_index] = auction;

  emit BidCreated(
    ownixNFTToken,
    _tokenId,
    1,
    _index,
    _bidder,
    _price,
    bidCreatedAt
  );
}

/**
 * @dev Place a bid for an ERC1155 token.
 * @notice Tokens can have multiple bids by different users.
 * Users can have only one bid per token.
 * If the user places a bid and has an active bid for that token,
 * the older one will be replaced with the new one.
 * @param _bidder - address the bidder address
 * @param _tokenId - uint256 of the token id
 * @param _index The index of tokens being transferred
 * @param _price - of the price for the bid
 */