contract WACNFT is ERC721, Ownable {
  using Counters for Counters.Counter;
  using Strings for uint256;

  string  private baseURI;
  string  private notRevealedUri;
  uint256 public PRESALE_COST = 0.08 ether;
  uint256 public PUBLIC_COST = 0.1 ether;
  uint256 public PRESALE_MINT_AMOUNT = 3;
  uint256 public PUBLICT_MINT_AMOUNT = 5;  
  uint256 public MAX_SUPPLY = 10000;
  uint256 public MAX_PRESALE = 1500;
  uint256 public RESERVE_SUPPLY = 200;
  uint256 public UNIC_SUPPLY = 7; 
  uint256 public PRESALE_MINT_DATE = 1639778400;
  uint256 public PUBLICSALE_MINT_DATE = 1640037600;
  mapping(address => bool) public WLIST;
  mapping(address => uint256) public addressMintedBalance;
  address[] public addressRaffle; 
  Counters.Counter public tokenIdCounter;
  Counters.Counter public reserveTokenIdCounter;
  Counters.Counter public unicTokenIdCounter;

  constructor(
    string memory _name,
    string memory _symbol,
    string memory _initBaseURI,
    string memory _initNotRevealedUri,
    address[] memory _whitelisted
   ) 
    ERC721(_name, _symbol)
   {
    setBaseURI(_initBaseURI);
    setNotRevealedURI(_initNotRevealedUri);
    tokenIdCounter._value = 7;
    for (uint256 i = 0; i < _whitelisted.length; i++) {
      if (!WLIST[_whitelisted[i]]) {
        WLIST[_whitelisted[i]] = true;
      }
    }
  }

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

  function random() internal view returns (uint) {
      return uint(keccak256(abi.encodePacked(block.difficulty, block.timestamp, addressRaffle)));
  }  

  // public
  function mint(uint256 _mintAmount) public payable {
    require(tokenIdCounter.current() + _mintAmount <= MAX_SUPPLY - RESERVE_SUPPLY + reserveTokenIdCounter.current(), "Max NFT limit exceeded");
    uint256 mintCost = PUBLIC_COST;
    uint256 maxMintAmount = PUBLICT_MINT_AMOUNT;
    if(block.timestamp >= PRESALE_MINT_DATE &&  block.timestamp < PUBLICSALE_MINT_DATE) {
          require(WLIST[msg.sender] == true, "User is not whitelisted");
          require(tokenIdCounter.current() + _mintAmount <= MAX_PRESALE - RESERVE_SUPPLY + reserveTokenIdCounter.current(), "Max pre-sale NFT limit exceeded");
          maxMintAmount = PRESALE_MINT_AMOUNT;
          mintCost = PRESALE_COST;
    } 

    if(addressMintedBalance[msg.sender]+_mintAmount >= maxMintAmount && !(addressMintedBalance[msg.sender] >= maxMintAmount)) {
      addressRaffle.push(msg.sender);
    }
    require(msg.value >= mintCost * _mintAmount, "Insufficient funds");
    for (uint256 i = 1; i <= _mintAmount; i++) {
      tokenIdCounter.increment();
      _safeMint(msg.sender, tokenIdCounter.current());
    }
    addressMintedBalance[msg.sender] += _mintAmount;
  }

  function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    require(_exists(tokenId),"ERC721Metadata: URI query for nonexistent token");
    
    if(block.timestamp < PRESALE_MINT_DATE) {
        return notRevealedUri;
    }

    string memory currentBaseURI = _baseURI();
    return bytes(currentBaseURI).length > 0
        ? string(abi.encodePacked(currentBaseURI, tokenId.toString(), ".json"))
        : "";
  } 

  //only owner
  function reserveMint(uint256 _mintAmount) public onlyOwner {
    require(reserveTokenIdCounter.current() + _mintAmount <= RESERVE_SUPPLY, "reserve NFT limit exceeded");
    for (uint256 i = 1; i <= _mintAmount; i++) {
      reserveTokenIdCounter.increment();
      tokenIdCounter.increment();
      _safeMint(msg.sender, tokenIdCounter.current());
    }
  }

  function raffle() public onlyOwner {
    address winner = addressRaffle[random()%addressRaffle.length];
    unicTokenIdCounter.increment();
    require(unicTokenIdCounter.current() <= UNIC_SUPPLY, "Unic NFT limit exceeded");
    _safeMint(winner, unicTokenIdCounter.current());
  }

  function setBaseURI(string memory _newBaseURI) public onlyOwner {
    baseURI = _newBaseURI;
  }
  
  function setNotRevealedURI(string memory _notRevealedURI) public onlyOwner {
    notRevealedUri = _notRevealedURI;
  }

  function addToWhiteList(address[] calldata addresses) external onlyOwner {
    for (uint256 i = 0; i < addresses.length; i++) {
      if (!WLIST[addresses[i]]) {
        WLIST[addresses[i]] = true;
      }
    }
  }
    
  function withdraw() public payable onlyOwner {
    (bool os, ) = payable(owner()).call{value: address(this).balance}("");
    require(os);
  }
}
//------------------------------------------------------------------------------
// Author: orion (@OrionDevStar)
//------------------------------------------------------------------------------
function _baseURI() internal view virtual override returns (string memory) {
  return baseURI;
}

// internal