contract Dreamlands is ERC721Enumerable, Ownable, Pausable {
    using Strings for uint256;

    uint256 public constant MAX_SUPPLY = 10000;
    uint256 public constant GIVEAWAY = 110;
    uint256 public constant DEV_AMOUNT = 4 ether;
    address public constant DEV_ADDRESS = 0x1C78a76c0B4a4C2f99ad8D0aBB7a1556Fa55Df59;

    bool public freeStarted = false;
    bool public saleStarted = false;
    bool public whitelistStarted = false;
    bool public lock = false;
    uint256 public devWithdrawnAmount = 0;
    uint256 public currentWave = 0;
    uint256 public maxPublicMint = 3;
    address private _signerAddress;

    string public prefixURI;
    string public commonURI;

    mapping(bytes32 => bool) private _usedHashes;
    mapping(uint256 => string) public tokenURIs;

    // Wave Config
    mapping(uint256 => uint256) public publicPrices;
    mapping(uint256 => uint256) public whitelistPrices;

    mapping(uint256 => uint256) public publicMaxSupply;
    mapping(uint256 => uint256) public whitelistMaxSupply;
    mapping(uint256 => uint256) public freeMaxSupply;


    constructor() ERC721("Dreamlands", "DREAM") {
        for (uint256 i = 1; i <= GIVEAWAY; i++) {
            _safeMint(msg.sender, totalSupply() + 1);
        }

        publicPrices[1] = 0.055 ether;
        publicPrices[2] = 0.069 ether;
        publicPrices[3] = 0.079 ether;
        publicPrices[4] = 0.089 ether;
        publicPrices[5] = 0.1 ether;

        whitelistPrices[1] = 0.05 ether;
        whitelistPrices[2] = 0.065 ether;
        whitelistPrices[3] = 0.075 ether;
        whitelistPrices[4] = 0.085 ether;
        whitelistPrices[5] = 0.095 ether;

        // Wave 1
        freeMaxSupply[1] = 160;
        whitelistMaxSupply[1] = 760;
        publicMaxSupply[1] = 1100;

        // Wave 2
        freeMaxSupply[2] = 1210;
        whitelistMaxSupply[2] = 1560;
        publicMaxSupply[2] = 2110;

        /// Wave 3
        freeMaxSupply[3] = 2160;
        whitelistMaxSupply[3] = 3160;
        publicMaxSupply[3] = 4110;

        // Wave 4
        freeMaxSupply[4] = 4160;
        whitelistMaxSupply[4] = 5660;
        publicMaxSupply[4] = 7110;

        // Wave 5
        freeMaxSupply[5] = 7160;
        whitelistMaxSupply[5] = 8550;
        publicMaxSupply[5] = 10000;
    }

    modifier onlyOwnerOrDev {
        require(msg.sender == owner() || msg.sender == DEV_ADDRESS, "Ownable: caller is not the owner");
        _;
    }

    function mintWhitelist(
        uint256 _count,
        uint256 _maxCount,
        uint256 _wave,
        bytes memory _sig)
        external
        payable
    {
        require(currentWave > 0 && _wave == currentWave, "WAVE_INCORRECT");
        require(whitelistStarted || freeStarted, "MINT_NOT_STARTED");
        require(_count > 0 && _count <= _maxCount, "COUNT_INVALID");
        require(totalSupply() + _count <= MAX_SUPPLY, "MAX_SUPPLY_REACHED");

        if (whitelistStarted) {
            require(totalSupply() + _count <= whitelistMaxSupply[currentWave], "MAX_SUPPLY_REACHED");
            require(msg.value == (_count * whitelistPrices[currentWave]), "INVALID_ETH_SENT");
        } else {
            require(totalSupply() + _count <= freeMaxSupply[currentWave], "MAX_SUPPLY_REACHED");
            require(msg.value == 0, "MINT_PRICE_SHOULD_BE_FREE");
        }

        bytes32 hash = keccak256(abi.encode(_msgSender(), _maxCount, _wave));
        require(!_usedHashes[hash], "HASH_ALREADY_USED");
        require(matchSigner(hash, _sig), "INVALID_SIGNER");

        _usedHashes[hash] = true;
        for (uint256 i = 1; i <= _count; i++) {
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    function mintPublic(
        uint256 _count
    )
        external
        payable
    {
        require(currentWave > 0, "WAVE_INCORRECT");
        require(saleStarted, "MINT_NOT_STARTED");
        require(_count > 0 && _count <= maxPublicMint, "COUNT_INVALID");
        require(totalSupply() + _count <= MAX_SUPPLY, "MAX_SUPPLY_REACHED");
        require(totalSupply() + _count <= publicMaxSupply[currentWave], "MAX_SUPPLY_REACHED");
        require(msg.value == (_count * publicPrices[currentWave]), "INVALID_ETH_SENT");

        for (uint256 i = 1; i <= _count; i++) {
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    function matchSigner(bytes32 _hash, bytes memory _signature) private view returns(bool) {
        return _signerAddress == ECDSA.recover(ECDSA.toEthSignedMessageHash(_hash), _signature);
    }

    function isWhiteList(
        address _sender,
        uint256 _maxCount,
        uint256 _wave,
        bytes calldata _sig
    ) public view returns(bool) {
        bytes32 _hash = keccak256(abi.encode(_sender, _maxCount, _wave));
        if (!matchSigner(_hash, _sig)) {
            return false;
        }
        if (_usedHashes[_hash]) {
            return false;
        }
        return true;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        if (bytes(tokenURIs[tokenId]).length != 0) {
            return tokenURIs[tokenId];
        }
        if (bytes(commonURI).length != 0) {
            return commonURI;
        }
        return string(abi.encodePacked(prefixURI, tokenId.toString()));
    }

    // ** Admin Fuctions ** //
    function setSaleStarted(bool _hasStarted) external onlyOwnerOrDev {
        saleStarted = _hasStarted;
    }

    function setWhitelistStarted(bool _hasStarted) external onlyOwnerOrDev {
        whitelistStarted = _hasStarted;
    }

    function setFreeStarted(bool _hasStarted) external onlyOwnerOrDev {
        freeStarted = _hasStarted;
    }

    function setSignerAddress(address _signer) external onlyOwnerOrDev {
        require(_signer != address(0), "SIGNER_ADDRESS_ZERO");
        _signerAddress = _signer;
    }

    function lockBaseURI() external onlyOwner {
        require(!lock, "ALREADY_LOCKED");
        lock = true;
    }

    function setPrefixURI(string calldata _uri) external onlyOwnerOrDev {
        require(!lock, "ALREADY_LOCKED");
        prefixURI = _uri;
        commonURI = '';
    }

    function setCommonURI(string calldata _uri) external onlyOwnerOrDev {
        require(!lock, "ALREADY_LOCKED");
        commonURI = _uri;
        prefixURI = '';
    }

    function setTokenURI(string calldata _uri, uint256 _tokenId) external onlyOwnerOrDev {
        require(!lock, "ALREADY_LOCKED");
        tokenURIs[_tokenId] = _uri;
    }

    function setPublicPrices(uint256 _wave, uint256 _price) external onlyOwnerOrDev {
        publicPrices[_wave] = _price;
    }

    function setWhitelistPrices(uint256 _wave, uint256 _price) external onlyOwnerOrDev {
        whitelistPrices[_wave] = _price;
    }

    function setPublicMaxSupply(uint256 _wave, uint256 _supply) external onlyOwnerOrDev {
        publicMaxSupply[_wave] = _supply;
    }

    function setWhitelistMaxSupply(uint256 _wave, uint256 _supply) external onlyOwnerOrDev {
        whitelistMaxSupply[_wave] = _supply;
    }

    function setFreeMaxSupply(uint256 _wave, uint256 _supply) external onlyOwnerOrDev {
        freeMaxSupply[_wave] = _supply;
    }

    function setWave(uint256 _wave) external onlyOwnerOrDev {
        require(_wave >= 1 && _wave <= 5, "WAVE_OUT_OF_BOUNDS");
        currentWave = _wave;
    }

    function setMaxPublicMint(uint256 _maxMint) external onlyOwnerOrDev {
        maxPublicMint = _maxMint;
    }

    function pause() external onlyOwner {
        require(!paused(), "ALREADY_PAUSED");
        _pause();
    }

    function unpause() external onlyOwner {
        require(paused(), "ALREADY_UNPAUSED");
        _unpause();
    }

    function withdraw() external onlyOwner {
        require(address(this).balance > 0, "EMPTY_BALANCE");
        require(devWithdrawnAmount >= DEV_AMOUNT, "DEV_WITHDRAWN_FIRST");
        payable(msg.sender).transfer(address(this).balance);
    }

    function withdrawDev() external {
        require(msg.sender == DEV_ADDRESS, "NOT_DEV_ADDRESS");
        require(devWithdrawnAmount < DEV_AMOUNT, "DEV_WITHDRAWN_AMOUNT_EXCEED");
        uint256 amount = DEV_AMOUNT - devWithdrawnAmount;
        require(amount > 0, "AMOUNT_ZERO");
        amount = Math.min(amount, address(this).balance);
        require(amount > 0, "AMOUNT_ZERO");
        devWithdrawnAmount += amount;
        payable(msg.sender).transfer(amount);
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) override internal virtual {
        require(!paused(), "TRANSFER_PAUSED");
        super._beforeTokenTransfer(from, to, tokenId);
    }
}
function setSaleStarted(bool _hasStarted) external onlyOwnerOrDev {
    saleStarted = _hasStarted;
}

// ** Admin Fuctions ** //