contract ChibiGalaxy is ERC721, Ownable {

    using Address for address;
    using MerkleProof for bytes32[];

    string public baseTokenURI;

    uint public preMintPrice = 0.06 ether;
    uint public publicMintPrice = 0.08 ether;
    uint public collectionSize = 7000;
    uint public maxItemsPerTx = 5;
    uint public giveawayCount = 0;
    uint public giveawayMaxItems = 100;
    uint public rareTokenCount = 0;
    uint public maxRareTokens = 140;
    uint public nextRareTokenIdToClaim = 0;
    uint public totalSupply = 0;
    uint public maxItemsForSpecialMint = 2;

    bool public onlyWhitelistedCanMint = true;

    bool public preMintPaused = true;
    bool public publicMintPaused = true;

    bytes32 generation1HoldersMerkleRoot;
    bytes32 whitelistMerkleRoot;
    bytes32 specialEventWhitelistMerkleRoot;

    mapping(address => uint) public ownerRareTokens;
    mapping(address => uint) public specialMintTokens;

    event Mint(address indexed owner, uint256 tokenId);

    Generation1s gen1ContractInstance;
    Generation2s gen2ContractInstance;

    constructor() ERC721("Chibi Galaxy", "GALACTIC CHIBI") {
        gen1ContractInstance = Generation1s(0x4Ef0Fe82B42B6104BbcEB69E764AbD2050aCfdd4);
        gen2ContractInstance = Generation2s(0xC49a9AB342b6ea66792D4110e9cA0ab36e3a5674);
    }

    /*
    GIVEAWAY FUNCTIONS - START
    */
    function giveawayMint(address to, uint amount) external onlyOwner {
        require(giveawayCount + amount <= giveawayMaxItems, "giveawayMint: Surpasses cap");
        _mintWithoutValidation(to, amount);
        giveawayCount += amount;
    }
    /*
    GIVEAWAY FUNCTIONS - END
    */

    /*
    RARE MINT FUNCTIONS - START
    */
    function setGeneration1HoldersMerkleRoot(bytes32 _generation1HoldersMerkleRoot) public onlyOwner {
        generation1HoldersMerkleRoot = _generation1HoldersMerkleRoot;
    }

    function isAddressAGeneration1Holder(bytes32[] memory proof, address _address, uint balance) public view returns (bool) {
        return proof.verify(generation1HoldersMerkleRoot, keccak256(abi.encodePacked(_address, balance)));
    }

    function rareMintAmount() public view returns (uint) {
        uint numGen1sOwned = numGeneration1sOwned();
        uint numGen2sOwned = numGeneration2sOwned();
        return calculateRareMintAmount(numGen1sOwned, numGen2sOwned);
    }

    function rareMint(uint amount) external onlyOwner {
        require(rareTokenCount + amount <= maxRareTokens, "surpass cap");
        nextRareTokenIdToClaim = totalSupply + 1;
        _mintWithoutValidation(address(this), amount);
        ownerRareTokens[address(this)] += amount;
        rareTokenCount += amount;
    }

    function transferRareTokensToOwner() external onlyOwner {
        uint amount = ownerRareTokens[address(this)];
        for (uint i = 0; i < amount; i++){
            _transfer(address(this), msg.sender, nextRareTokenIdToClaim + i);
        }
        ownerRareTokens[msg.sender] += amount;
        ownerRareTokens[address(this)] -= amount;
    }

    function claimRareToken(bytes32[] memory proof) public {
        require(ownerRareTokens[address(this)] > 0, "all claimed");

        // wallet should be part of the gen1 snapshot
        uint numGen1sOwned = numGeneration1sOwned();

        require(isAddressAGeneration1Holder(proof, msg.sender, numGen1sOwned), "not part of snapshot");

        uint numGen2sOwned = numGeneration2sOwned();

        uint amount = calculateRareMintAmount(numGen1sOwned, numGen2sOwned);

        // not eligible to claim any rares
        require((amount > 0 && ownerRareTokens[msg.sender] < amount), "not eligible");

        // a wallet with a gen 1 and 2 can claim a token.
        for (uint i = 0; i < amount; i++) {
            _transfer(address(this), msg.sender, nextRareTokenIdToClaim + i);
        }

        ownerRareTokens[msg.sender] += amount;
        ownerRareTokens[address(this)] -= amount;
        nextRareTokenIdToClaim += amount;
    }
    /*
    RARE MINT FUNCTIONS - END
    */

    /*
    PRE MINT - START
    */
    function setPreMintPaused(bool _preMintPaused) public onlyOwner {
        preMintPaused = _preMintPaused;
    }

    function preMint() external payable {
        require(!preMintPaused, "mint paused");
        // verify that the client sent enough eth to pay for the mint
        uint remainder = msg.value % preMintPrice;
        require(remainder == 0, "send a divisible amount of eth");

        // calculate the amount of tokens we are minting based on the amount of eth sent
        uint amount = msg.value / preMintPrice;
        require(amount > 0, "amount to mint is 0");

        uint numGen1sOwned = numGeneration1sOwned();
        uint numGen2sOwned = numGeneration2sOwned();

        // only gen 1 or gen 2 owners can pre mint.
        require((numGen1sOwned > 0 || numGen2sOwned > 0), "not eligible");

        // calculate the max number of items to mint. (3 passes per gen 1, 1 pass per gen 2)
        uint maxItemsForPreMint = (numGen1sOwned * 3) + (numGen2sOwned * 1);

        uint numGeneration3sOwned = balanceOf(msg.sender);
        require((numGeneration3sOwned + amount) <= maxItemsForPreMint, "surpass pre mint cap");

        _mintWithoutValidation(msg.sender, amount);
    }
    /*
    PRE MINT - END
    */

    /*
     SPECIAL EVENT MINT FUNCTIONS - BEGIN
     */
    function setSpecialEventMintInfo(uint _maxItemsForSpecialMint, bytes32 _specialEventWhitelistMerkleRoot) public onlyOwner {
        specialEventWhitelistMerkleRoot = _specialEventWhitelistMerkleRoot;
        maxItemsForSpecialMint = _maxItemsForSpecialMint;
    }

    function isAddressWhitelistedForSpecialEvent(bytes32[] memory proof, address _address) public view returns (bool) {
        return isAddressInMerkleRoot(specialEventWhitelistMerkleRoot, proof, _address);
    }

    function specialEventMint(bytes32[] memory proof) external payable {
        require(!preMintPaused, "mint paused");
        require(isAddressWhitelistedForSpecialEvent(proof, msg.sender), "not eligible");

        // verify that the client sent enough eth to pay for the mint
        uint remainder = msg.value % publicMintPrice;
        require(remainder == 0, "send a divisible amount of eth");

        // calculate the amount of tokens we are minting based on the amount of eth sent
        uint amount = msg.value / publicMintPrice;
        require(amount > 0, "amount to mint is 0");

        require(specialMintTokens[msg.sender] < maxItemsForSpecialMint, "already minted max");
        require(specialMintTokens[msg.sender] + amount < maxItemsForSpecialMint, "will surpass special mint cap");

        _mintWithoutValidation(msg.sender, amount);
    }
    /*
    SPECIAL EVENT MINT FUNCTIONS - END
    */

    /*
    PUBLIC MINT FUNCTIONS - START
    */
    function setPublicMintInfo(bool _publicMintPaused, bool _onlyWhitelistedCanMint, bytes32 _whitelistMerkleRoot) public onlyOwner {
        publicMintPaused = _publicMintPaused;
        whitelistMerkleRoot = _whitelistMerkleRoot;
        onlyWhitelistedCanMint = _onlyWhitelistedCanMint;
    }

    function isAddressWhitelisted(bytes32[] memory proof, address _address) public view returns (bool) {
        return isAddressInMerkleRoot(whitelistMerkleRoot, proof, _address);
    }

    function publicMint(bytes32[] memory proof) external payable {
        // publicMintPaused is set by owner calling pausePublicMint and unpausePublicMint
        require(!publicMintPaused, "mint paused");
        // verify that the client sent enough eth to pay for the mint
        uint remainder = msg.value % publicMintPrice;
        require(remainder == 0, "send a divisible amount of eth");

        // calculate the amount of tokens we are minting based on the amount of eth sent
        uint amount = msg.value / publicMintPrice;

        require(amount <= maxItemsPerTx, "max 5 per tx");

        // onlyWhitelistedCanMint is set by calling setOnlyWhitelistedCanMint
        if (onlyWhitelistedCanMint){
            uint numGen1sOwned = numGeneration1sOwned();
            uint numGen2sOwned = numGeneration2sOwned();
            // perform the following only for wallets that don't have a gen 1 or gen 2
            if (numGen1sOwned == 0 && numGen2sOwned == 0) {
                // should not throw if user is whitelisted, the amount being minted is equal to their whitelisted value and they
                // have not minted already.
                require(isAddressWhitelisted(proof, msg.sender), "not eligible to mint");
            }
        }

        _mintWithoutValidation(msg.sender, amount);
    }
    /*
    PUBLIC MINT FUNCTIONS - END
    */

    /*
    HELPER FUNCTIONS - START
    */
    function _mintWithoutValidation(address to, uint amount) internal {
        require((totalSupply + amount) <= collectionSize, "sold out");
        for (uint i = 0; i < amount; i++) {
            totalSupply += 1;
            _mint(to, totalSupply);
            emit Mint(to, totalSupply);
        }
    }

    function calculateRareMintAmount(uint numGen1sOwned, uint numGen2sOwned) internal view returns (uint){
        if (numGen1sOwned > 0 && numGen2sOwned > 0){
            // the number of rares minted should be the lesser of gen 1s owned to gen 2s owned
            uint amount = numGen1sOwned;
            if (numGen1sOwned > numGen2sOwned) {
                amount = numGen2sOwned;
            }
            return amount - ownerRareTokens[msg.sender];
        }
        return 0;
    }

    function numGeneration1sOwned() public view returns (uint256) {
        return gen1ContractInstance.balanceOf(msg.sender);
    }

    function numGeneration2sOwned() public view returns (uint256) {
        return gen2ContractInstance.balanceOf(msg.sender);
    }

    function isAddressInMerkleRoot(bytes32 merkleRoot, bytes32[] memory proof, address _address) internal pure returns (bool) {
        // specialEventWhitelistMerkleRoot is set by calling setSpecialEventWhitelistMerkleRoot
        return proof.verify(merkleRoot, keccak256(abi.encodePacked(_address)));
    }
    /*
    HELPER FUNCTIONS - END
    */

    /*
    ADMIN FUNCTIONS - BEGIN
    */
    function setMintInfo(uint _preMintPrice, uint _publicMintPrice) public onlyOwner {
        preMintPrice = _preMintPrice;
        publicMintPrice = _publicMintPrice;
    }

    function setMaxItemsPerTrx(uint _maxItemsPerTrx) public onlyOwner {
        maxItemsPerTx = _maxItemsPerTrx;
    }

    function setBaseTokenURI(string memory _baseTokenURI) external onlyOwner {
        baseTokenURI = _baseTokenURI;
    }

    function withdrawAll() external onlyOwner {
        uint amount1 = address(this).balance * 200 / 1000;
        uint amount2 = address(this).balance * 800 / 1000;
        uint amount3 = amount2 * 1/2;
        uint amount4 = amount2 * 1/4;
        uint amount5 = amount2 * 1/4;

        sendEth(0xAc0B0CD0268B5A9166De11A94E931E8e0cAD1DbB, amount1);
        sendEth(0x1D70A61565a309a25d6ddA87dCf649873ee36E39, amount3);
        sendEth(0xb5966228209779d3c0C826b775172De7E9a31538, amount4);
        sendEth(0xf415770F7aF765f823296BA7294c6d72217C8Af5, amount5);
    }

    function sendEth(address to, uint amount) internal {
        (bool success,) = to.call{value: amount}("");
        require(success, "Failed to send ether");
    }
    /*
    ADMIN FUNCTIONS - END
    */

    /*
    REQUIRED BY SOLIDITY - START
    */
    function _burn(uint256 tokenId) internal override(ERC721) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId) public view override(ERC721) returns (string memory) {
        return string(abi.encodePacked(baseTokenURI, Strings.toString(tokenId)));
    }
    /*
    REQUIRED BY SOLIDITY - END
    */
}
function giveawayMint(address to, uint amount) external onlyOwner {
    require(giveawayCount + amount <= giveawayMaxItems, "giveawayMint: Surpasses cap");
    _mintWithoutValidation(to, amount);
    giveawayCount += amount;
}

/*
 GIVEAWAY FUNCTIONS - START
 */