contract SoulZ is Ownable, ERC721A, ReentrancyGuard {
  uint256 public immutable maxPerAddressDuringMint;
  uint256 private immutable amountForDevs;
  uint256 private immutable amountForAuctionAndDev;
  uint256 private immutable maxPerWhitelist;
  uint256 private immutable maxPerCollected;

  struct SaleConfig {
    uint32 auctionSaleStartTime;
    uint32 whitelistSalesTime;
    uint32 publicSaleStartTime;
    uint64 whitelistPrice;
    uint64 collectedlistPrice;
    uint64 publicPrice;
    uint32 publicSaleKey;
  }

  SaleConfig public saleConfig;

  mapping(address => uint256) public whitelist;
  mapping(address => uint256) public collectedlist;

  constructor(
    uint256 maxBatchSize_,
    uint256 collectionSize_,
    uint256 collectedlistBatchSize_,
    uint256 whitelistBatchSize_,
    uint256 amountForAuctionAndDev_,
    uint256 amountForDevs_

  ) ERC721A("SoulZ Monogatari", "SLZM", maxBatchSize_, collectionSize_ ) {
    maxPerAddressDuringMint = maxBatchSize_;
    maxPerWhitelist = whitelistBatchSize_;
    maxPerCollected = collectedlistBatchSize_;
    amountForAuctionAndDev = amountForAuctionAndDev_;
    amountForDevs = amountForDevs_;
    require(
      amountForAuctionAndDev_ <= collectionSize_,
      "larger collection size needed"
    );
  }

  modifier callerIsUser() {
    require(tx.origin == msg.sender, "The caller is another contract");
    _;
  }

  function auctionMint(uint256 quantity) external payable callerIsUser {
    uint256 _saleStartTime = uint256(saleConfig.auctionSaleStartTime);
    require(
      _saleStartTime != 0 && block.timestamp >= _saleStartTime,
      "sale has not started yet"
    );
    require(
      totalSupply() + quantity <= amountForAuctionAndDev,
      "not enough remaining reserved for auction to support desired mint amount"
    );
    require(
      numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint,
      "can not mint this many"
    );
    uint256 totalCost = getAuctionPrice(_saleStartTime) * quantity;
    _safeMint(msg.sender, quantity);
    refundIfOver(totalCost);
  }

    function whitelistMint(uint256 quantity) external payable callerIsUser {
    uint256 price = uint256(saleConfig.whitelistPrice);
    require(price != 0, "whitelist sale has not begun yet");
    require(whitelist[msg.sender] > 0, "not eligible for whitelist mint");
    require(totalSupply() + 1 <= collectionSize, "reached max supply");
    require(numberMinted(msg.sender) + quantity <= maxPerWhitelist,"Ascended can not mint this many");
    whitelist[msg.sender]--;
    _safeMint(msg.sender, 1);
    refundIfOver(price);
  }

    function collectedlistMint(uint256 quantity) external payable callerIsUser {
    uint256 price = uint256(saleConfig.collectedlistPrice);
    require(price != 0, "Collected sale has not begun yet");
    require(collectedlist[msg.sender] > 0, "not eligible for collected mint");
    require(totalSupply() + 1 <= collectionSize, "reached max supply");
    require(numberMinted(msg.sender) + quantity <= maxPerCollected,"Collected can not mint this many");
    collectedlist[msg.sender]--;
    _safeMint(msg.sender, 1);
    refundIfOver(price);
  }

  function publicSaleMint(uint256 quantity, uint256 callerPublicSaleKey)
    external
    payable
    callerIsUser
  {
    SaleConfig memory config = saleConfig;
    uint256 publicSaleKey = uint256(config.publicSaleKey);
    uint256 publicPrice = uint256(config.publicPrice);
    uint256 publicSaleStartTime = uint256(config.publicSaleStartTime);
    require(
      publicSaleKey == callerPublicSaleKey,
      "called with incorrect public sale key"
    );

    require(
      isPublicSaleOn(publicPrice, publicSaleKey, publicSaleStartTime),
      "public sale has not begun yet"
    );
    require(totalSupply() + quantity <= collectionSize, "reached max supply");
    require(
      numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint,
      "can not mint this many"
    );
    _safeMint(msg.sender, quantity);
    refundIfOver(publicPrice * quantity);
  }

  function refundIfOver(uint256 price) private {
    require(msg.value >= price, "Need to send more ETH.");
    if (msg.value > price) {
      payable(msg.sender).transfer(msg.value - price);
    }
  }

  function isPublicSaleOn(
    uint256 publicPriceWei,
    uint256 publicSaleKey,
    uint256 publicSaleStartTime
  ) public view returns (bool) {
    return
      publicPriceWei != 0 &&
      publicSaleKey != 0 &&
      block.timestamp >= publicSaleStartTime;
  }

  uint256 public constant AUCTION_START_PRICE = 0.75 ether;
  uint256 public constant AUCTION_END_PRICE = 0.2 ether;
  uint256 public constant AUCTION_PRICE_CURVE_LENGTH = 720 minutes;
  uint256 public constant AUCTION_DROP_INTERVAL = 30 minutes;
  uint256 public constant AUCTION_DROP_PER_STEP = 0.05 ether;
  
  function getAuctionPrice(uint256 _saleStartTime)
    public
    view
    returns (uint256)
  {
    if (block.timestamp < _saleStartTime) {
      return AUCTION_START_PRICE;
    }
    if (block.timestamp - _saleStartTime >= AUCTION_PRICE_CURVE_LENGTH) {
      return AUCTION_END_PRICE;
    } else {
      uint256 steps = (block.timestamp - _saleStartTime) /
        AUCTION_DROP_INTERVAL;
      return AUCTION_START_PRICE - (steps * AUCTION_DROP_PER_STEP);
    }
  }

  function endAuctionAndSetupNonAuctionSaleInfo(
    uint64 whitelistPriceWei,
    uint64 publicPriceWei,
    uint32 whitelistSaleStartTime,
    uint32 collectedlistSaleStartTime,
    uint32 publicSaleStartTime
  ) external onlyOwner {
    saleConfig = SaleConfig(
      0,
      publicSaleStartTime,
      whitelistSaleStartTime,
      collectedlistSaleStartTime,
      whitelistPriceWei,
      publicPriceWei,
      saleConfig.publicSaleKey
    );
  }

  function setAuctionSaleStartTime(uint32 timestamp) external onlyOwner {
    saleConfig.auctionSaleStartTime = timestamp;
  }

  function setPublicSaleKey(uint32 key) external onlyOwner {
    saleConfig.publicSaleKey = key;
  }

  function seedWhitelist(address[] memory addresses, uint256[] memory numSlots)
    external
    onlyOwner
  {
    require(
      addresses.length == numSlots.length,
      "addresses does not match numSlots length"
    );
    for (uint256 i = 0; i < addresses.length; i++) {
      whitelist[addresses[i]] = numSlots[i];
    }
  }

  function seedCollectedlist(address[] memory addresses, uint256[] memory numSlots)
    external
    onlyOwner
  {
    require(
      addresses.length == numSlots.length,
      "addresses does not match numSlots length"
    );
    for (uint256 i = 0; i < addresses.length; i++) {
      collectedlist[addresses[i]] = numSlots[i];
    }
  }

  // For marketing etc.
  function devMint(uint256 quantity) external onlyOwner {
    require(
      totalSupply() + quantity <= amountForDevs,
      "too many already minted before dev mint"
    );
    require(
      quantity % maxBatchSize == 0,
      "can only mint a multiple of the maxBatchSize"
    );
    uint256 numChunks = quantity / maxBatchSize;
    for (uint256 i = 0; i < numChunks; i++) {
      _safeMint(msg.sender, maxBatchSize);
    }
  }

  // // metadata URI
  string private _baseTokenURI;
  bool public revealed = false;

  function _baseURI() internal view virtual override returns (string memory) {
    return _baseTokenURI;
  }

  function setBaseURI(string calldata baseURI) external onlyOwner {
    _baseTokenURI = baseURI;
  }

  function changeRevealed(bool _revealed) public onlyOwner {
    revealed = _revealed;
  }
  
  function tokenURI(uint256 tokenid) public view override returns (string memory) {
    require(_exists(tokenid), "ERC721Metadata: URI query for nonexistant token");

    if (revealed) {
      string memory baseURI = _baseURI();
    return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenid)) : "";
    } else {
      return string(abi.encodePacked(_baseURI(), "Hidden.json"));
    }
  }

  
    function withdrawToSender() external onlyOwner {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }

    function withdraw() external onlyOwner {
        address[8] memory addresses = [
            0x2bC46E31a324AB9a208B3B0Fb91958E390DC0797,
            0x897C456868d4888c258528f8660b932804Cb6948,
            0xc0524078b6ABC601158bFc328c9A2B64Ee376e23,
            0x8FE4A152939Ece65f1fC651e57b8aA84cFc137C2,
            0x29d54F704a4253B5c3a8aE6CBDFDb01472119713,
            0x896baBEE76dBdF3F6d3b7470ad1e47e8c2016BDB,
            0xE892C48B5CdD20F50dbFdF4A949c649Aee9F24Da,
            0x24e21ae83ccB58EbAE990Cf1e014e062F6bb7B19
        ];

        uint256[8] memory shares = [
            uint256(2),
            uint256(2),
            uint256(3),
            uint256(3),
            uint256(10),
            uint256(60),
            uint256(60),
            uint256(60)
        ];

        uint256 balance = address(this).balance;

        for (uint256 i = 0; i < addresses.length; i++) {
            uint256 amount = i == addresses.length - 1 ? address(this).balance : balance * shares[i] / 200;
            payable(addresses[i]).transfer(amount);
        }
    }

  function setOwnersExplicit(uint256 quantity) external onlyOwner nonReentrant {
    _setOwnersExplicit(quantity);
  }

  function numberMinted(address owner) public view returns (uint256) {
    return _numberMinted(owner);
  }

  function getOwnershipData(uint256 tokenId)
    external
    view
    returns (TokenOwnership memory)
  {
    return ownershipOf(tokenId);
  }
}
function devMint(uint256 quantity) external onlyOwner {
  require(
    totalSupply() + quantity <= amountForDevs,
    "too many already minted before dev mint"
  );
  require(
    quantity % maxBatchSize == 0,
    "can only mint a multiple of the maxBatchSize"
  );
  uint256 numChunks = quantity / maxBatchSize;
  for (uint256 i = 0; i < numChunks; i++) {
    _safeMint(msg.sender, maxBatchSize);
  }
}

// For marketing etc.