contract DOS is ERC721, Ownable, ReentrancyGuard {
    string private _collectionURI;
    string public baseURI;

    uint256 public constant TEAM_ALLOC = 396;
    uint256 public constant MAX_SUPPLY = 3996;

    uint256 public cost = 0.06 ether;
    uint256 public mintsPerTx = 5;

    bool private _isMainSaleActive;

    mapping(uint256 => string) internal tokenUris;
    mapping(address => uint256) internal walletCap;

    address editor;

    using Counters for Counters.Counter;
    Counters.Counter private _tokenSupply;

    constructor() ERC721("Dreams of Synesthesia", "DOS") {
        _isMainSaleActive = false;
    }

    modifier onlyEditor() {
        require(msg.sender == editor);
        _;
    }

    modifier isCorrectPayment(uint256 price, uint256 numberOfTokens) {
        require(
            price * numberOfTokens == msg.value,
            "Incorrect ETH value sent"
        );
        _;
    }

    // ============ PUBLIC FUNCTIONS FOR MINTING ============ 

    /**
    * @dev mints specified # of tokens to sender address
    * 3600 public mints
    */
    function mint(
      uint256 numberOfTokens
    )
        public
        payable
        isCorrectPayment(cost, numberOfTokens)
        nonReentrant
    {
        require(_isMainSaleActive, "Main sale must be active to mint.");
        require(numberOfTokens > 0, "Must mint at least 1 token.");
        require(numberOfTokens <= mintsPerTx, "Exceeded max tokens minted at a time");
        require(_tokenSupply.current() + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max number of whitelist tokens");

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _tokenSupply.increment();
            _mint(msg.sender, _tokenSupply.current());
        }
        walletCap[msg.sender] += numberOfTokens;
    }

    // ============ PUBLIC READ-ONLY FUNCTIONS ============
    function tokenURI(uint256 tokenId)
      public
      view
      virtual
      override
      returns (string memory)
    {
      require(_exists(tokenId), "ERC721Metadata: query for nonexistent token");
      
      // Custom tokenURI exists
      if (bytes(tokenUris[tokenId]).length != 0) {
        return tokenUris[tokenId];
      }
      else {
        return string(abi.encodePacked(baseURI, Strings.toString(tokenId)));
      }
    }

    function totalSupply() public view returns (uint256) {
        return _tokenSupply.current();
    }

    /**
    * @dev collection URI for marketplace display
    */
    function contractURI() public view returns (string memory) {
        return _collectionURI;
    }

    function isMainSaleActive() external view returns (bool) {
        return _isMainSaleActive;
    }

    function numMintedForAddress(address addr) external view returns (uint256) {
        return walletCap[addr];
    }

    // ============ OWNER-ONLY ADMIN FUNCTIONS ============
    // @dev Private mint function reserved for company.
    // @param _to The user receiving the tokens
    // @param _mintAmount The number of tokens to distribute
    function mintToAddress(address _to, uint256 _mintAmount) external onlyOwner {
        require(_mintAmount > 0, "You can only mint more than 0 tokens");
        require(_tokenSupply.current() + _mintAmount <= MAX_SUPPLY, "Can't mint more than max supply");
        for (uint256 i = 0; i < _mintAmount; i++) {
            _tokenSupply.increment();
            _mint(_to, _tokenSupply.current());
        }
    }

    // @dev Private mint function reserved for company.
    // @param _to The user receiving the tokens
    function airdropMint(address[] memory _to) external onlyOwner {
        uint32 l = uint32(_to.length);
        require(l <= TEAM_ALLOC, "Total 396 NFTs eligible for airdrop");
        for (uint32 i = 0; i < l; i++) {
            _tokenSupply.increment();
            _mint(_to[i], _tokenSupply.current());
        }
    }

    function setBaseURI(string memory _baseURI) external onlyOwner {
        baseURI = _baseURI;
    }

    function setCollectionURI(string memory collectionURI) external virtual onlyOwner {
        _collectionURI = collectionURI;
    }

    function flipMainSaleState() external onlyOwner {
        _isMainSaleActive = !_isMainSaleActive;
    }

    function setTokenURI(uint256 _tokenId, string memory _uri) external onlyEditor {
        tokenUris[_tokenId] = _uri;
    }

    function setCost(uint256 _newCost) external onlyOwner {
        cost = _newCost;
    }

    function setMintsPerTx(uint256 numMints) external onlyOwner {
        mintsPerTx = numMints;
    }

    function setEditor(address ed) external onlyOwner {
        editor = ed;
    }

    /**
     * @dev withdraw funds for to specified account
     */
    function withdraw() public onlyOwner {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }
}
function airdropMint(address[] memory _to) external onlyOwner {
    uint32 l = uint32(_to.length);
    require(l <= TEAM_ALLOC, "Total 396 NFTs eligible for airdrop");
    for (uint32 i = 0; i < l; i++) {
        _tokenSupply.increment();
        _mint(_to[i], _tokenSupply.current());
    }
}

// @dev Private mint function reserved for company.
// @param _to The user receiving the tokens