contract tlMinter is ERC721, Ownable {
    // Minting Price
    uint256 public ethPrice = 0.125 ether;

    // Supply variables
    uint256 public totalSupply;
    uint256 public maxSupply;
    uint256 public maxPerTxn;
    uint256 public maxPerWallet;

    // Sale state control variables
    bool public burningEnabled;
    bool public whitelistMintingEnabled;
    bool public publicMintingEnabled;
    uint256 public startSaleTimestamp;

    // Wallet to withdraw to
    address payable public payableWallet;

    // Metadata variables
    string public _baseURI_;

    // External Contracts
    iWhitelist tlWhitelist;
    address public evolutionAddress;

    // Track mints
    mapping(address => uint256) walletMints;

    // Add one eth on deploy
    constructor() payable ERC721("the littles NFT", "thelittlesnft") {
        totalSupply = 0; // Start at Token ID 1
        maxSupply = 10000;
        maxPerTxn = 2;
        maxPerWallet = 2; // @todo This will update to 4 during public minting
        startSaleTimestamp = 1637978400; // @todo set minting date

        // placeholder for testing
        _baseURI_ = "https://s3.amazonaws.com/nft-meta.thelittles.io/";

        // @todo Add wil's wallet address here
        payableWallet = payable(
            address(0x3811866A85b0e42B437B36BB4FD2f05e4c3b16f8)
        );

        // @todo Add Inu's contract reference here
        tlWhitelist = iWhitelist(0x1A3b1D5e9678d9e71271f2c5017A10D3Afb7d296);
    }

    /** *********************************** **/
    /** ********* Internal Functions ****** **/
    /** *********************************** **/
    function isContract(address account) internal view returns (bool) {
        uint256 size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }

    /** *********************************** **/
    /** ********* Minting Functions ******* **/
    /** *********************************** **/
    function mintTokens(uint256 quantity) private {
        walletMints[msg.sender] += quantity;
        for (uint256 i = 0; i < quantity; i++) {
            uint256 newTokenId = totalSupply + 1;
            _safeMint(msg.sender, newTokenId);
            totalSupply++;
        }
    }

    function defaultMintingRules(uint256 value, uint256 quantity) private view {
        require(isContract(msg.sender) == false, "Cannot mint from a contract");
        require(value == getPrice(quantity), "wrong value");
        require(totalSupply < maxSupply, "sold out");
        require(totalSupply + quantity <= maxSupply, "exceeds max supply");
        require(quantity <= maxPerTxn, "exceeds max per txn");
        require(
            walletMints[msg.sender] + quantity <= maxPerWallet,
            "exceeds max per wallet"
        );
    }

    function mintWhitelist(uint256 quantity, bytes32[] memory proof)
        public
        payable
    {
        require(whitelistMintingEnabled, "minting not enabled");
        require(
            tlWhitelist.isWhitelisted(msg.sender, proof),
            "address supplied is not on the whitelist"
        );
        require(tx.origin == msg.sender, "Contracts not allowed");

        defaultMintingRules(msg.value, quantity);
        mintTokens(quantity);
    }

    function mintPublic(uint256 quantity) public payable {
        require(
            block.timestamp >= startSaleTimestamp,
            "official sale has not started"
        );
        require(publicMintingEnabled, "minting not enabled");
        require(tx.origin == msg.sender, "Contracts not allowed");
        defaultMintingRules(msg.value, quantity);
        mintTokens(quantity);
    }

    function reserveTokens(uint256 tokensToReserve) public onlyOwner {
        mintTokens(tokensToReserve);
    }

    /** *********************************** **/
    /** ********* Owner Functions ********* **/
    /** *********************************** **/
    function setEvolutionAddress(address _address) external onlyOwner {
        evolutionAddress = _address;
    }

    function setPriceInWei(uint256 _price) external onlyOwner {
        ethPrice = _price;
    }

    function setMaxSupply(uint256 _maxSupply) external onlyOwner {
        maxSupply = _maxSupply;
    }

    function setMaxPerWallet(uint256 _maxPerWallet) external onlyOwner {
        maxPerWallet = _maxPerWallet;
    }

    function setMaxPerTxn(uint256 _maxPerTxn) external onlyOwner {
        maxPerTxn = _maxPerTxn;
    }

    function toggleBurningEnabled() external onlyOwner {
        burningEnabled = !burningEnabled;
    }

    function toggleWhitelistMintingEnabled() external onlyOwner {
        whitelistMintingEnabled = !whitelistMintingEnabled;
    }

    function togglePublicMintingEnabled() external onlyOwner {
        publicMintingEnabled = !publicMintingEnabled;
    }

    function setStartSaleTimestamp(uint256 _startSaleTimestamp)
        external
        onlyOwner
    {
        startSaleTimestamp = _startSaleTimestamp;
    }

    function setBaseURI(string memory _newBaseURI) external onlyOwner {
        _baseURI_ = _newBaseURI;
    }

    function setPayableWallet(address _payableWallet) external onlyOwner {
        payableWallet = payable(_payableWallet);
    }

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        payable(payableWallet).transfer(balance);
    }

    /** *********************************** **/
    /** ***** 0xInuarashi Functions ******* **/
    /** *** https://twitter.com/0xinuarashi **/
    /** *********************************** **/
    function ownerOf(uint256 tokenId)
        public
        view
        virtual
        override
        returns (address)
    {
        return _owners[tokenId];
    }

    function walletOfOwner(address address_)
        public
        view
        returns (uint256[] memory)
    {
        uint256 _balance = balanceOf(address_);
        uint256[] memory _tokens = new uint256[](_balance);
        uint256 _index;

        for (uint256 i = 0; i < maxSupply; i++) {
            if (address_ == ownerOf(i)) {
                _tokens[_index] = i;
                _index++;
            }
        }

        return _tokens;
    }

    function multiTransferFrom(
        address from_,
        address to_,
        uint256[] memory tokenIds_
    ) public {
        for (uint256 i = 0; i < tokenIds_.length; i++) {
            transferFrom(from_, to_, tokenIds_[i]);
        }
    }

    function multiSafeTransferFrom(
        address from_,
        address to_,
        uint256[] memory tokenIds_,
        bytes memory data_
    ) public {
        for (uint256 i = 0; i < tokenIds_.length; i++) {
            safeTransferFrom(from_, to_, tokenIds_[i], data_);
        }
    }

    /** *********************************** **/
    /** ******** Public Functions ********* **/
    /** *********************************** **/
    function getWalletMintCount(address addr) public view returns (uint256) {
        return walletMints[addr];
    }

    function getYieldRate(uint256 tokenId_) public view returns (uint256) {
        if (evolutionAddress != address(0x0)) {
            return iEvolution(evolutionAddress).getYieldRate(tokenId_);
        } else {
            revert("Evolution not enabled yet!");
        }
    }

    function getPrice(uint256 quantity) public view returns (uint256) {
        return ethPrice * quantity;
    }

    function burn(uint256 tokenId) public {
        require(burningEnabled, "burning enabled");
        require(
            _isApprovedOrOwner(_msgSender(), tokenId),
            "caller is not owner nor approved"
        );
        _burn(tokenId);
    }

    function remainingSupply() public view returns (uint256) {
        return maxSupply - totalSupply;
    }

    function _baseURI() internal view override returns (string memory) {
        return _baseURI_;
    }
}
function mintTokens(uint256 quantity) private {
    walletMints[msg.sender] += quantity;
    for (uint256 i = 0; i < quantity; i++) {
        uint256 newTokenId = totalSupply + 1;
        _safeMint(msg.sender, newTokenId);
        totalSupply++;
    }
}

/** *********************************** **/