contract DaijuKingz is ERC721, Ownable {
    using SafeMath for uint256;
    using Address for address;
    using Counters for Counters.Counter;

    Counters.Counter private TotalSupplyFix;

    ICW public CW;
    IDoodles public Doodles;
    IKaijuKingz public KaijuKingz;
    ISOS public SOS;
    
    string private baseURI;

    uint256 constant public reserveMax = 50; // the amount of daijus the team will reserve for community events, staff, etc.
    uint256 public maxSupply = 11110; // max genesis + max bred daijus
    uint256 public maxGenCount = 5555; // the max genesis count. if theres remaining supply after sale ends, the genesis daijukingz will be less then this number
    uint256 public maxFreeMints = 124; // the max free mints that can be claimed for kaiju and doodle holders
    uint256 public maxGivewayMint = 676; // the max free mints that are from the whitelisted users
    
    // counters
    uint256 public reserveCount = 0;
    uint256 public freeMintCount = 0;
    uint256 public giveawayMintCount = 0;
    uint256 public genCount = 0;
    uint256 public babyCount = 0;

    // settings for breeding & sales
    uint256 public price = 0.05 ether; 
    uint256 public breedPrice = 7500 ether;
    uint256 public increasePerBreed = 0 ether;
    uint256 public saleStartTimestamp;
    uint256 public freeEndTimestamp;
    address public sosWallet; 
    uint256 public sosPrice = 0.05 ether;
    bool public transfersEnabled = false;
    bool public breedingEnabled = false;

    // tracks all of the wallets that interact with the contract
    mapping(address => uint256) public balanceGenesis;
    mapping(address => uint256) public balanceBaby;
    mapping(address => uint256) public freeMintList;
    mapping(address => uint256) public giveawayMintList;
    
    /*
     * DaijuOwner
     * checks if the token IDs provided are owned by the user interacting with the contract (used for breeding)
     */
    modifier DaijuOwner(uint256 DaijuId) {
        require(ownerOf(DaijuId) == msg.sender, "Cannot interact with a DaijuKingz you do not own");
        _;
    }

    /*
     * IsSaleActive
     * checks if the mint is ready to begin
     */
    modifier IsSaleActive() {
        require(saleStartTimestamp != 0 && block.timestamp > saleStartTimestamp, "Cannot interact because sale has not started");
        _;
    }

    /*
     * IsFreeMintActive
     * checks if the free mint end timestamp isn't met
     */
    modifier IsFreeMintActive {
        require(block.timestamp < freeEndTimestamp, "Free Minting period has ended!");
        _;
    }

    /*
     * AreTransfersEnabled
     * checks if we want to allow opensea to transfer any tokens
     */
    modifier AreTransfersEnabled() {
        require(transfersEnabled, "Transfers aren't allowed yet!");
        _;
    }

    constructor() ERC721("DaijuKingz", "Daiju") {}

    /*
     * mintFreeKaijuList
     * checks the walelt for a list of kaijukingz they hold, used for checking if they
     * can claim a free daijuking
     */
    function mintFreeKaijuList(address _address) external view returns(uint256[] memory) {
        return KaijuKingz.walletOfOwner(_address);
    }

    /*
     * mintFreeDoodleList
     * gets the amount of doodles a wallet holds, used for checking if they are elligble
     * for claiming a free daijuking
     */
    function mintFreeDoodleList(address _address) public view returns(uint256) {
        uint256 count = Doodles.balanceOf(_address);

        return count;
    }

    /*
     * checkIfFreeMintClaimed
     * checks if the address passed claimed a free mint already
     */
    function checkIfFreeMintClaimed(address _address) public view returns(uint256) {
        return freeMintList[_address];
    }

    /*
     * checkIfGiveawayMintClaimed
     * checks if they claimed their giveaway mint
     */
    function checkIfGiveawayMintClaimed(address _address) public view returns(uint256) {
        return giveawayMintList[_address];
    }

    /*
     * addGiveawayWinners
     * adds the wallets to the giveaway list so they can call mintFreeGiveaway()
     */
    function addGiveawayWinners(address[] calldata giveawayAddresses) external onlyOwner {
        for (uint256 i; i < giveawayAddresses.length; i++) {
            giveawayMintList[giveawayAddresses[i]] = 1;
        }
    }

    /*
     * mintFreeDoodle
     * mints a free daijuking using a Doodles token - can only be claimed once per wallet
     */
    function mintFreeDoodle() public payable IsSaleActive IsFreeMintActive {
        uint256 supply = TotalSupplyFix.current();

        require(supply <= maxGenCount, "All Genesis Daijukingz were claimed!");
        require(mintFreeDoodleList(msg.sender) >= 1, "You don't own any Doodles!");
        require((supply + 1) <= maxFreeMints, "All the free mints have been claimed!");
        require(freeMintList[msg.sender] < 1, "You already claimed your free daijuking!");
        require(!breedingEnabled, "Minting genesis has been disabled!");

        _safeMint(msg.sender, supply);
        TotalSupplyFix.increment();

        freeMintList[msg.sender] = 1;
        balanceGenesis[msg.sender]++;
        genCount++;
        freeMintCount += 1;
    }

    /*
     * mintFreeKaiju
     * mints a free daijuking using a KaijuKingz token - can only be claimed once per wallet
     */
    function mintFreeKaiju() public payable IsSaleActive IsFreeMintActive {
        uint256 supply = TotalSupplyFix.current();
        uint256[] memory list = KaijuKingz.walletOfOwner(msg.sender);

        require(supply <= maxGenCount, "All Genesis Daijukingz were claimed!");
        require((supply + 1) <= maxFreeMints, "All the free mints have been claimed!");
        require(freeMintList[msg.sender] < 1, "You already claimed your free daijuking!");
        require(list.length >= 1, "You don't own any KaijuKingz!");
        require(!breedingEnabled, "Minting genesis has been disabled!");

        _safeMint(msg.sender, supply);
        TotalSupplyFix.increment();

        freeMintList[msg.sender] = 1;
        balanceGenesis[msg.sender]++;
        genCount++;
        freeMintCount += 1;
    }

    /*
     * mintFreeGiveaway
     * this is for the giveaway winners - allows you to mint a free daijuking
     */
    function mintFreeGiveaway() public payable IsSaleActive IsFreeMintActive {
        uint256 supply = TotalSupplyFix.current();

        require(supply <= maxGenCount, "All Genesis Daijukingz were claimed!");
        require((supply + 1) <= maxGivewayMint, "All giveaway mints were claimed!");
        require(giveawayMintList[msg.sender] >= 1, "You don't have any free mints left!");
        require(!breedingEnabled, "Minting genesis has been disabled!");

        _safeMint(msg.sender, supply);
        TotalSupplyFix.increment();

        giveawayMintList[msg.sender] = 0;
        balanceGenesis[msg.sender]++;
        genCount++;
        giveawayMintCount += 1;
    }

    /*
     * mint
     * mints the daijuking using ETH as the payment token
     */
    function mint(uint256 numberOfMints) public payable IsSaleActive {
        uint256 supply = TotalSupplyFix.current();
        require(numberOfMints > 0 && numberOfMints < 11, "Invalid purchase amount");

        if (block.timestamp <= freeEndTimestamp) {
            require(supply.add(numberOfMints) <= (maxGenCount - (maxFreeMints + maxGivewayMint)), "Purchase would exceed max supply");
        } else {
            require(supply.add(numberOfMints) <= maxGenCount, "Purchase would exceed max supply");
        }
        
        require(price.mul(numberOfMints) == msg.value, "Ether value sent is not correct");
        require(!breedingEnabled, "Minting genesis has been disabled!");

        for (uint256 i; i < numberOfMints; i++) {
            _safeMint(msg.sender, supply + i);
            balanceGenesis[msg.sender]++;
            genCount++;
            TotalSupplyFix.increment();
        }
    }

    /*
     * mintWithSOS
     * allows the user to mint a daijuking using the $SOS token
     * note: user must approve it before this can be called
     */
    function mintWithSOS(uint256 numberOfMints) public payable IsSaleActive {
         uint256 supply = TotalSupplyFix.current();
        
        require(numberOfMints > 0 && numberOfMints < 11, "Invalid purchase amount");
        require(supply.add(numberOfMints) <= maxGenCount, "Purchase would exceed max supply");
        require(sosPrice.mul(numberOfMints) < SOS.balanceOf(msg.sender), "Not enough SOS to mint!");
        require(!breedingEnabled, "Minting genesis has been disabled!");

        SOS.transferFrom(msg.sender, sosWallet, sosPrice.mul(numberOfMints));

        for (uint256 i; i < numberOfMints; i++) {
            _safeMint(msg.sender, supply + i);
            balanceGenesis[msg.sender]++;
            genCount++;
            TotalSupplyFix.increment();
        }
    }

    /*
     * breed
     * allows you to create a daijuking using 2 parents and some $COLORWASTE, 18+ only
     * note: selecting is automatic from the DAPP, but if you use the contract make sure they are two unique token id's that aren't children...
     */
    function breed(uint256 parent1, uint256 parent2) public DaijuOwner(parent1) DaijuOwner(parent2) {
        uint256 supply = TotalSupplyFix.current();

        require(breedingEnabled, "Breeding isn't enabled yet!");
        require(supply < maxSupply, "Cannot breed any more baby Daijus");
        require(parent1 < genCount && parent2 < genCount, "Cannot breed with baby Daijus (thats sick bro)");
        require(parent1 != parent2, "Must select two unique parents");

        // burns the tokens for the breeding cost
        CW.burn(msg.sender, breedPrice + increasePerBreed * babyCount);

        // adds the baby to the total supply and counter
        babyCount++;
        TotalSupplyFix.increment();

        // mints the baby daijuking and adds it to the balance for the wallet
        _safeMint(msg.sender, supply);
        balanceBaby[msg.sender]++;
    }

    /*
     * reserve
     * mints the reserved amount 
     */
    function reserve(uint256 count) public onlyOwner {
        uint256 supply = TotalSupplyFix.current();

        require(reserveCount + count < reserveMax, "cannot reserve more");

        for (uint256 i = 0; i < count; i++) {
            _safeMint(msg.sender, supply + i);
            balanceGenesis[msg.sender]++;
            genCount++;
            TotalSupplyFix.increment();
            reserveCount++;
        }
    }

    /*
     * totalSupply
     * returns the current amount of tokens, called by DAPPs
     */
    function totalSupply() external view returns(uint256) {
        return TotalSupplyFix.current();
    }

    /*
     * getTypes
     * gets the tokens provided and returns the genesis and baby daijukingz
     *
     * genesis is 0
     * baby is 1
     */
    function getTypes(uint256[] memory list) external view returns(uint256[] memory) {
        uint256[] memory typeList = new uint256[](list.length);

        for (uint256 i; i < list.length; i++){
            if (list[i] >= genCount) {
                typeList[i] = 1;
            } else {
                typeList[i] = 0;
            }
        }

        return typeList;
    }

    /*
     * walletOfOwner
     * returns the tokens that the user owns
     */
    function walletOfOwner(address owner) public view returns(uint256[] memory) {
        uint256 tokenCount = balanceOf(owner);
        uint256 supply = TotalSupplyFix.current();
        uint256 index = 0;

        uint256[] memory tokensId = new uint256[](tokenCount);
    
        for (uint256 i; i < supply; i++) {
            if (ownerOf(i) == owner) {
                tokensId[index] = i;
                index++;
            }
        }

        return tokensId;
    }

    /*
     * withdraw
     * takes out the eth from the contract to the deployer
     */
    function withdraw() public onlyOwner {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }

    /*
     * updateSaleTimestamp
     * set once we're ready release
     */
    function updateSaleTimestamp(uint256 _saleStartTimestamp) public onlyOwner {
        saleStartTimestamp = _saleStartTimestamp;
    }

    /*
     * updateFreeEndTimestamp
     * set once saleStartTimestamp is set for release
     */
    function updateFreeEndTimestamp(uint256 _freeEndTimestamp) public onlyOwner {
        freeEndTimestamp = _freeEndTimestamp;
    }

    /*
     * setBreedingCost
     * sets the breeding cost to the new
     */
    function setBreedingCost(uint256 _breedPrice) public onlyOwner {
        breedPrice = _breedPrice;
    }

    /*
     * setBreedingCostIncrease
     * change the rate based on the breeding cost for each new baby mint
     */
    function setBreedingCostIncrease(uint256 _increasePerBreed) public onlyOwner {
        increasePerBreed = _increasePerBreed;
    }

    /*
     * setBreedingState
     * changes whether breeding is enabled or not, by default its disabled
     */
    function setBreedingState(bool _state) public onlyOwner {
        breedingEnabled = _state;
    }

    /*
     * setPrice
     * sets the eth mint price (shouldn't ever be called but who knows)
     */
    function setPrice(uint256 _price) public onlyOwner {
        price = _price;
    }
    
    /*
     * setSOSPrice
     * sets the price for $SOS mint, most likely will only be set once before the release
     */
    function setSOSPrice(uint256 _sosPrice) public onlyOwner {
        sosPrice = _sosPrice;
    }

    // 46524939000000000000000000
    // 4652493900000000000000000
    /*
     * setSOSWallet
     * sets the deposit wallet for $SOS mints
     */
    function setSOSWallet(address _address) public onlyOwner {
        sosWallet = _address;
    }

    /*
     * setBaseURI
     * sets the metadata url once its live
     */
    function setBaseURI(string memory _newURI) public onlyOwner {
        baseURI = _newURI;
    }

    /*
     * _baseURI
     * returns the metadata url to any DAPPs that use it (opensea for instance)
     */
    function _baseURI() internal view override returns (string memory) {
        return baseURI;
    }

    /*
     * setTransfersEnabled
     * enables opensea transfers - can only be called once
     */
    function setTransfersEnabled() external onlyOwner {
        transfersEnabled = true;
    }

    /*
     * setCWInterface
     * links the interface to the $COLORWASTE smart contract once it's deployed
     */
    function setCWInterface(address _address) external onlyOwner {
        CW = ICW(_address);
    }

    /*
     * setKaijuInterface
     * links the interface to the KaijuKingz contract
     */
    function setKaijuInterface(address _address) public onlyOwner {
        KaijuKingz = IKaijuKingz(_address);
    }

    /*
     * setDoodleInterface
     * links the interface to the Doodles contract
     */
    function setDoodleInterface(address _address) public onlyOwner {
        Doodles = IDoodles(_address);
    }

    /*
     * setSOSInterface
     * links the interface to the $SOS contract
     */
    function setSOSInterface(address target) public onlyOwner {
        SOS = ISOS(target);
    }

    /*
     * Opensea methods for transfering the tokens
     */
    function transferFrom(address from, address to, uint256 tokenId) public override AreTransfersEnabled {
        CW.updateBalance(from, to);

        if (tokenId >= maxGenCount) {
            balanceBaby[from]--;
            balanceBaby[to]++;
        } else {
            balanceGenesis[from]--;
            balanceGenesis[to]++;
        }

        ERC721.transferFrom(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) public override AreTransfersEnabled {
        safeTransferFrom(from, to, tokenId, "");
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override AreTransfersEnabled {
        CW.updateBalance(from, to);

        if (tokenId >= maxGenCount) {
            balanceBaby[from]--;
            balanceBaby[to]++;
        } else {
            balanceGenesis[from]--;
            balanceGenesis[to]++;
        }

        ERC721.safeTransferFrom(from, to, tokenId, data);
    }
}
/*
 * DAIJUKINGZ
 */
function setBreedingCost(uint256 _breedPrice) public onlyOwner {
    breedPrice = _breedPrice;
}

/*
 * setBreedingCost
 * sets the breeding cost to the new
 */