contract DystoApez is ERC721, Ownable, Pausable {
    using Strings for uint256;
    uint256 public constant MAX_SUPPLY = 4444;
    uint256 public constant MINT_PRICE = 0.06 ether;
    uint256 public constant MINT_PRICE_PUBLIC = 0.07 ether;
    uint256 public constant MAX_MINT_PER_TX = 5;
    uint256 constant public LEGENDARY_SUPPLY = 10;

    mapping(bytes32 => bool) private _usedHashes;

    bool public saleStarted = false;
    bool public preSaleStarted = false;
    bool public lock = false;
    address private _signerAddress;
    uint256 public totalSupply = 0;
    IScrapToken public scrapToken;

    string public prefixURI;
    string public commonURI;

    mapping(uint256 => string) public tokenURIs;

    constructor() ERC721("DystoApez", "DYSTO") { }

    function mintLegendary() external onlyOwner {
        require(totalSupply == 0, "MINT_ALREADY_STARTED");
        for (uint256 i = 1; i <= LEGENDARY_SUPPLY; i++) {
            _safeMint(msg.sender, totalSupply + i);
        }
        totalSupply += LEGENDARY_SUPPLY;
    }

    function mintAdmin(uint256 _count, address _account) external onlyOwner {
        require(totalSupply + _count <= MAX_SUPPLY, "MAX_SUPPLY_REACHED");
        require(totalSupply >= LEGENDARY_SUPPLY, "LEGENDARY_NOT_MINTED");
        for (uint256 i = 1; i <= _count; i++) {
            _safeMint(_account, totalSupply + i);
        }
        totalSupply += _count;
    }

    function mintWhitelist(
        uint256 _count,
        uint256 _maxCount,
        bytes memory _sig)
        external
        payable
    {
        require(preSaleStarted, "MINT_NOT_STARTED");
        require(_count > 0 && _count <= _maxCount, "COUNT_INVALID");
        require(totalSupply + _count <= MAX_SUPPLY, "MAX_SUPPLY_REACHED");
        require(totalSupply >= LEGENDARY_SUPPLY, "LEGENDARY_NOT_MINTED");
        require(msg.value == (_count * MINT_PRICE), "INVALID_ETH_SENT");

        bytes32 hash = keccak256(abi.encode(_msgSender(), _maxCount));
        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 + i);
        }
        totalSupply += _count;
    }

    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,
        bytes calldata _sig
    ) public view returns(bool) {
        bytes32 _hash = keccak256(abi.encode(_sender, _maxCount));
        if (!_matchSigner(_hash, _sig)) {
            return false;
        }
        if (_usedHashes[_hash]) {
            return false;
        }
        return true;
    }


    // Public Mint
    function mint(uint256 _count) public payable {
        require(saleStarted, "MINT_NOT_STARTED");
        require(_count > 0 && _count <= MAX_MINT_PER_TX, "COUNT_INVALID");
        require(totalSupply >= LEGENDARY_SUPPLY, "LEGENDARY_NOT_MINTED");
        require(totalSupply + _count <= MAX_SUPPLY, "MAX_SUPPLY_REACHED");
        require(msg.value == (_count * MINT_PRICE_PUBLIC), "INVALID_ETH_SENT");

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

    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 withdraw(address payable _to) external onlyOwner {
        require(_to != address(0), "WITHDRAW_ADDRESS_ZERO");
        require(address(this).balance > 0, "EMPTY_BALANCE");
        _to.transfer(address(this).balance);
    }

    function setSaleStarted(bool _hasStarted) external onlyOwner {
        require(saleStarted != _hasStarted, "SALE_STARTED_ALREADY_SET");
        saleStarted = _hasStarted;
    }

    function setPreSaleStarted(bool _hasStarted) external onlyOwner {
        require(preSaleStarted != _hasStarted, "SALE_STARTED_ALREADY_SET");
        preSaleStarted = _hasStarted;
    }

    function setSignerAddress(address _signer) external onlyOwner {
        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 onlyOwner {
        require(!lock, "ALREADY_LOCKED");
        prefixURI = _uri;
        commonURI = '';
    }

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

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

    function setScrapToken(address _token) external onlyOwner {
        require(_token != address(scrapToken), "TOKEN_ALREADY_SET");
        require(_token != address(0), "TOKEN_ADDRESS_ZERO");
        scrapToken = IScrapToken(_token);
    }


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

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

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) override internal virtual {
        require(!paused(), "TRANSFER_PAUSED");
        scrapToken.updateReward(from, to, tokenId);
        super._beforeTokenTransfer(from, to, tokenId);
    }
}
function withdraw(address payable _to) external onlyOwner {
    require(_to != address(0), "WITHDRAW_ADDRESS_ZERO");
    require(address(this).balance > 0, "EMPTY_BALANCE");
    _to.transfer(address(this).balance);
}

// ** Admin Fuctions ** //