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

    enum XDroidType {
        LEGENDARY100,
        MIXED9900
    }

    string public baseURI;

    string public baseExtension = ".json";
    string public notRevealedUri;

    uint256 public cost = 0.09 ether;
    uint256 public discountedCost = 0.07 ether;
    uint256 public maxSupply = 10000;
    uint256 public maxMintAmount = 5;
    uint256 public nftPerAddressLimit = 5;
    uint256 public offset = 100;
    uint256 public threshold = 1200;

    uint256 public legendaryCost = 0.25 ether;
    uint256 public legendaryMaxSupply = 100;
    uint256 public legendaryMaxMintAmount = 1;
    uint256 public legendaryNftPerAddressLimit = 1;
    uint256 public legendaryOffset = 0;

    bool public paused = true;
    bool public revealed;
    bool public onlyWhitelisted = true;

    address[] public whitelistedAddresses;
    address[] public legendaryWhitelistedAddresses;

    bool public legendaryOnlyWhitelisted = true;

    Counters.Counter public totalSupplyNormal;
    Counters.Counter public totalSupplyLegendary;

    mapping(address => Counters.Counter) public addressMintedBalance;
    mapping(address => Counters.Counter) public legendaryAddressMintedBalance;
    mapping(uint256 => XDroidType) public tokenIdType;

    constructor(
        string memory _name,
        string memory _symbol,
        string memory _initBaseURI,
        string memory _initNotRevealedUri
    ) ERC721(_name, _symbol) {
        setBaseURI(_initBaseURI);
        setNotRevealedURI(_initNotRevealedUri);
    }

    // modifier
    function checkMintCompliance(
        uint256 _mintAmount,
        uint256 _totalSupply,
        uint256 _maxSupply,
        uint256 _cost,
        uint256 _maxMintAmount,
        Counters.Counter storage _mintedCount,
        uint256 _nftPerAddressLimit,
        bool _onlyWhitelisted,
        bool _isWhitelisted
    ) internal {
        if (msg.sender != owner()) {
            require(!paused, "the contract is paused");
            require(_mintAmount > 0, "need to mint at least 1 NFT");
            require(
                _totalSupply + _mintAmount <= _maxSupply,
                "max NFT limit exceeded"
            );
            require(msg.value >= _cost * _mintAmount, "insufficient funds");
            require(
                _mintAmount <= _maxMintAmount,
                "max mint amount per session exceeded"
            );
            if (_onlyWhitelisted) {
                require(_isWhitelisted, "user is not whitelisted");
                require(
                    _mintedCount.current() + _mintAmount <= _nftPerAddressLimit,
                    "max NFT per address exceeded"
                );
            }
        }
    }

    // internal
    function dynamicCost() internal view returns (uint256 _cost) {
        if (Counters.current(totalSupplyNormal) < threshold) {
            return discountedCost;
        }

        if (Counters.current(totalSupplyNormal) >= threshold) {
            return cost;
        }
    }

    // public
    function mint(uint256 _mintAmount) public payable {
        checkMintCompliance(
            _mintAmount,
            Counters.current(totalSupplyNormal),
            maxSupply,
            dynamicCost(),
            maxMintAmount,
            addressMintedBalance[msg.sender],
            nftPerAddressLimit,
            onlyWhitelisted,
            isWhitelisted(msg.sender)
        );

        _mintLoop(
            _mintAmount,
            addressMintedBalance[msg.sender],
            totalSupplyNormal,
            XDroidType.MIXED9900,
            offset
        );
    }

    function _mintLoop(
        uint256 _mintAmount,
        Counters.Counter storage mintedBalance,
        Counters.Counter storage totalSupplyForType,
        XDroidType _type,
        uint256 _offset
    ) internal {
        uint256 supply = totalSupplyForType.current();

        for (uint256 i = 1; i <= _mintAmount; i++) {
            mintedBalance.increment();
            totalSupplyForType.increment();
            uint256 nextTokenId = _offset + supply + i;
            tokenIdType[nextTokenId] = _type;
            _safeMint(msg.sender, nextTokenId);
        }
    }

    function isWhitelisted(address _user) public view returns (bool) {
        for (uint256 i = 0; i < whitelistedAddresses.length; i++) {
            if (whitelistedAddresses[i] == _user) {
                return true;
            }
        }
        return false;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );

        string memory _currentBaseURI = baseURI;
        uint256 _totalSupplyType;
        uint256 _offset = offset;

        if (tokenIdType[tokenId] == XDroidType.LEGENDARY100) {
            _offset = legendaryOffset;
            _totalSupplyType = totalSupplyLegendary.current();
        } else {
            _totalSupplyType = totalSupplyNormal.current();
        }

        if (revealed == false) {
            return notRevealedUri;
        }

        return
            bytes(_currentBaseURI).length > 0
                ? string(
                    abi.encodePacked(
                        _currentBaseURI,
                        tokenId.toString(),
                        baseExtension
                    )
                )
                : "";
    }

    function totalSupply() public view returns (uint256 _totalSupply) {
        return totalSupplyNormal.current() + totalSupplyLegendary.current();
    }

    //only owner
    function reveal() public onlyOwner {
        revealed = true;
    }

    function setNftPerAddressLimit(uint256 _limit) public onlyOwner {
        nftPerAddressLimit = _limit;
    }

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

    function setDiscountedCost(uint256 _newCost) public onlyOwner {
        discountedCost = _newCost;
    }

    function setMaxMintAmount(uint256 _newmaxMintAmount) public onlyOwner {
        maxMintAmount = _newmaxMintAmount;
    }

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

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

    function pause(bool _state) public onlyOwner {
        paused = _state;
    }

    function setOnlyWhitelisted(bool _state) public onlyOwner {
        onlyWhitelisted = _state;
    }

    function whitelistUsers(address[] calldata _users) public onlyOwner {
        delete whitelistedAddresses;
        whitelistedAddresses = _users;
    }

    /**
    Legendary
     */

    function mintLegendary(uint256 _mintAmount) public payable {
        checkMintCompliance(
            _mintAmount,
            totalSupplyLegendary.current(),
            legendaryMaxSupply,
            legendaryCost,
            legendaryMaxMintAmount,
            legendaryAddressMintedBalance[msg.sender],
            legendaryNftPerAddressLimit,
            legendaryOnlyWhitelisted,
            isWhitelistedLegendary(msg.sender)
        );

        _mintLoop(
            _mintAmount,
            legendaryAddressMintedBalance[msg.sender],
            totalSupplyLegendary,
            XDroidType.LEGENDARY100,
            legendaryOffset
        );
    }

    function setLegendaryMaxMintAmount(uint256 _newmaxMintAmount)
        public
        onlyOwner
    {
        legendaryMaxMintAmount = _newmaxMintAmount;
    }

    function setLegendaryOffset(uint256 _newLegendaryOffset) public onlyOwner {
        legendaryOffset = _newLegendaryOffset;
    }

    function setLegendaryCost(uint256 _newCost) public onlyOwner {
        legendaryCost = _newCost;
    }

    function whitelistLegendaryUsers(address[] calldata _users)
        public
        onlyOwner
    {
        delete legendaryWhitelistedAddresses;
        legendaryWhitelistedAddresses = _users;
    }

    function setOnlyWhitelistedLegendary(bool _state) public onlyOwner {
        legendaryOnlyWhitelisted = _state;
    }

    function isWhitelistedLegendary(address _user) public view returns (bool) {
        for (uint256 i = 0; i < legendaryWhitelistedAddresses.length; i++) {
            if (legendaryWhitelistedAddresses[i] == _user) {
                return true;
            }
        }
        return false;
    }

    /**
    SHARED METHODS
    */
    function setBaseExtension(string memory _newBaseExtension)
        public
        onlyOwner
    {
        baseExtension = _newBaseExtension;
    }

    function withdraw() public payable onlyOwner {
        (bool os, ) = payable(owner()).call{value: address(this).balance}("");
        require(os);
    }
}
function checkMintCompliance(
    uint256 _mintAmount,
    uint256 _totalSupply,
    uint256 _maxSupply,
    uint256 _cost,
    uint256 _maxMintAmount,
    Counters.Counter storage _mintedCount,
    uint256 _nftPerAddressLimit,
    bool _onlyWhitelisted,
    bool _isWhitelisted
) internal {
    if (msg.sender != owner()) {
        require(!paused, "the contract is paused");
        require(_mintAmount > 0, "need to mint at least 1 NFT");
        require(
            _totalSupply + _mintAmount <= _maxSupply,
            "max NFT limit exceeded"
        );
        require(msg.value >= _cost * _mintAmount, "insufficient funds");
        require(
            _mintAmount <= _maxMintAmount,
            "max mint amount per session exceeded"
        );
        if (_onlyWhitelisted) {
            require(_isWhitelisted, "user is not whitelisted");
            require(
                _mintedCount.current() + _mintAmount <= _nftPerAddressLimit,
                "max NFT per address exceeded"
            );
        }
    }
}

// modifier