contract MODC is ERC721Enumerable, Ownable, PriceFetch {
    using Strings for uint256;

    string baseURI;
    string public baseExtension = ".json";
    uint256 public cost = 0.09 ether; // default cost for public mint. can be updated
    uint256 public presaleCost = 0.06 ether; // default cost for pre-sale mint. can be updated
    uint256 public maxSupply = 4000; // maxSupply is fixed to 4000
    uint256 public presaleMaxSupply = 400; // presale supply is fixed to 10% of total supply
    uint256 public maxMintAmount = 10; // each address can only mint 10 MODC in public mint
    uint256 public maxPresaleMintAmount = 3; // each address can only mint 3 MODC in pre-sale
    uint256 public timeDeployed;
    uint256 public presaleTime; 
    uint256 public presaleToPublic; // duration of pre-sale 
    uint256 public allowMintingAfter = 0;
    uint256 public minimumAdditionalDonation = 0.01 ether; 
    address public donations; // address for donations to Ocean NGOs 
    bool public isPaused = false;
    bool public isRevealed = true;
    string public notRevealedUri;

    // pre-sale whitelist mapping
    mapping(address => bool) public onAllowList;

    // maps the USD value that a specific NFT contributed to the donations
    mapping(uint => uint) public tokenOceanDonationsUsd; 

    constructor(
        string memory _name,
        string memory _symbol,
        string memory _initBaseURI,
        string memory _initNotRevealedUri,
        uint256 _revealTime,
        uint256 _presaleToPublic,
        address _donations
    ) ERC721(_name, _symbol) {
        if (_revealTime > block.timestamp) {
            allowMintingAfter = _revealTime - block.timestamp;
            presaleTime = allowMintingAfter - _presaleToPublic; 
        }
        donations = _donations;
        presaleToPublic = _presaleToPublic;
        timeDeployed = block.timestamp;
        setBaseURI(_initBaseURI);
        setNotRevealedURI(_initNotRevealedUri);
    }

    // internal
    function _baseURI() internal view virtual override returns (string memory) {
        return baseURI;
    }

    // public sale 
    function mint(uint256 _mintAmount) public payable {
        require(
            block.timestamp >= timeDeployed + allowMintingAfter,
            "Minting not allowed yet"
        );

        require(balanceOf(msg.sender) <= 9, "Only 10 mint per account");

        uint256 supply = totalSupply();
        require(!isPaused);
        require(_mintAmount > 0);
        require(_mintAmount <= maxMintAmount);
        require(supply + _mintAmount <= maxSupply);

        if (msg.sender != owner()) {
            require(msg.value >= cost * _mintAmount);
        }

        uint256 currentPrice = getLatestPrice(); 

        for (uint256 i = 1; i <= _mintAmount; i++) {
            _safeMint(msg.sender, supply + i);
            tokenOceanDonationsUsd[(supply + i)] = ((cost * currentPrice) * 70 / 100);
        }
    }

    // pre-sale 
    function presaleMint(uint256 _mintAmount) public payable {
        require(
            block.timestamp >= timeDeployed + presaleTime,
            "Pre-sale minting not allowed yet"
        );

        require( block.timestamp < timeDeployed + allowMintingAfter,
            "Presale is over"); 
        
        require(onAllowList[msg.sender], "You are not in allowlist");

        require(balanceOf(msg.sender) <= 3, "Only 3 pre-sale mint per account");

        uint256 supply = totalSupply();
        require(!isPaused);
        require(_mintAmount > 0);
        require(_mintAmount <= maxPresaleMintAmount);
        require(supply + _mintAmount <= maxSupply);

        if (msg.sender != owner()) {
            require(msg.value >= presaleCost * _mintAmount);
        }

        uint256 currentPrice = getLatestPrice(); 

        for (uint256 i = 1; i <= _mintAmount; i++) {
            _safeMint(msg.sender, supply + i);
            tokenOceanDonationsUsd[(supply + i)] = ((presaleCost * currentPrice) * 70 / 100);
        }
    }

    // add impact of MODC by donating additional ETH
    function increaseImpact(uint tokenId) public payable {
        require(msg.sender == ownerOf(tokenId), "only owner of token can increase impact");
        require(msg.value >= minimumAdditionalDonation , "minimum additional donation is 0.01 ETH");

        uint256 currentPrice = getLatestPrice(); 
        tokenOceanDonationsUsd[tokenId] = (tokenOceanDonationsUsd[tokenId] + (msg.value * currentPrice)); 

    }

    function walletOfOwner(address _owner)
        public
        view
        returns (uint256[] memory)
    {
        uint256 ownerTokenCount = balanceOf(_owner);
        uint256[] memory tokenIds = new uint256[](ownerTokenCount);
        for (uint256 i; i < ownerTokenCount; i++) {
            tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
        }
        return tokenIds;
    }

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

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

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

    function getSecondsUntilMinting() public view returns (uint256) {
        if (block.timestamp < timeDeployed + allowMintingAfter) {
            return (timeDeployed + allowMintingAfter) - block.timestamp;
        } else {
            return 0;
        }
    }

    // add address to the whitelist for presale
    function addToAllowList(address[] calldata addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++)
            onAllowList[addresses[i]] = true;
    }

    // remove address from whitelist for presale
    function removeFromAllowList(address[] calldata addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++)
            onAllowList[addresses[i]] = false;
    }

    // returns how much donations one MODC accounts for
    function getTokenOceanDonations(uint256 tokenId) public view returns (uint256) {
        return tokenOceanDonationsUsd[tokenId]; 
    }

    // check wether address is on whitelist for presale
    function getOnAllowList(address user) public view returns (bool) {
        return onAllowList[user]; 
    }

    //only owner
    function setIsRevealed(bool _state) public onlyOwner {
        isRevealed = _state;
    }

    function setLaunchDate(uint256 _newLaunchDate, uint256 _newPresaleToPublic) public onlyOwner {
        allowMintingAfter = (_newLaunchDate - timeDeployed);
        presaleToPublic = _newPresaleToPublic; 
        presaleTime = allowMintingAfter - presaleToPublic; 
    }

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

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

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

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

    function setBaseExtension(string memory _newBaseExtension)
        public
        onlyOwner
    {
        baseExtension = _newBaseExtension;
    }

    function setIsPaused(bool _state) public onlyOwner {
        isPaused = _state;
    }

    function withdrawETH() external onlyOwner {
        uint256 balance = address(this).balance;

        uint256 _30_percent = (balance * 0.30 ether) / 1 ether;
        uint256 _70_percent = (balance * 0.70 ether) / 1 ether;

        payable(msg.sender).transfer(_30_percent);
        payable(donations).transfer(_70_percent);
    }
}
function increaseImpact(uint tokenId) public payable {
    require(msg.sender == ownerOf(tokenId), "only owner of token can increase impact");
    require(msg.value >= minimumAdditionalDonation , "minimum additional donation is 0.01 ETH");

    uint256 currentPrice = getLatestPrice(); 
    tokenOceanDonationsUsd[tokenId] = (tokenOceanDonationsUsd[tokenId] + (msg.value * currentPrice)); 

}

// add impact of MODC by donating additional ETH