contract StreetPuppies is ERC721Enumerable, Ownable {
    using Strings for uint256;
    
    uint256 public constant MAX_PURCHASE = 5;
    uint256 public enableGenesis;
    uint256 public enablePresales;
    uint256 public immutable maxSupply;
    uint256 public maxGSupply;
    string currentcontractURI = "https://metadata.streetdawgs.io/contract/puppies";
    uint256 public genesisPrice = 0.05 ether;
    uint256 public presalePrice = 0;
    uint256 public ddPrice = 0;
    mapping (address => uint) public presaleAddress;
    address public winner;
    string  private _baseTokenURI;

    event Mint(
        address indexed to, 
        uint256 indexed id);

    constructor(
        uint256 allowPresales, 
        uint256 allowGenesis, 
        uint256 maxPuppies,
        uint256 maxGenesisSupply
        ) 
        ERC721("StreetPuppies", "SP") {
        enablePresales = allowPresales;
        enableGenesis = allowGenesis;
        maxSupply = maxPuppies;
        maxGSupply = maxGenesisSupply;
    }
    
    function createPuppies(uint amount) external payable returns (bool) {
        require(enableGenesis == 1, "CP: Sale not started");
        require(amount > 0, "CP: value > zero required");
        require(amount <= MAX_PURCHASE, "CP: value must not be larger than allowed");
        require(totalSupply() + amount <= maxGSupply, "CP: creation must not exceed maximum supply");
        require(genesisPrice * amount == msg.value, "CP: value must match accumulated price");
        
        uint256 tokensGenerated = 0;
        for (uint256 i = 0; i < amount; i++) {
            uint256 mintIndex = totalSupply();
            if (totalSupply() < maxGSupply) {
                _safeMint(_msgSender(), mintIndex);
                emit Mint(_msgSender(), mintIndex);
                tokensGenerated++;
            }
        }
        if(tokensGenerated < amount) {
            // amount to refund
            uint256 excess = amount - tokensGenerated;
            uint256 returnAmount = genesisPrice * excess;
            payable(_msgSender()).transfer(returnAmount);
        }
        return true;
    }
    
    /*
    * Send free token to an array of addresses when ever want to as owner of the contract
    */
    function airDropPuppies(address[] calldata addresses) external onlyOwner returns (bool) {
        // uint count = addresses.length();
        uint arrayLength = addresses.length;
        uint256 mintIndex = totalSupply();
        for (uint i=0; i<arrayLength; i++) {
            if (totalSupply() < maxSupply) {
                _safeMint(addresses[i], mintIndex);
                emit Mint(addresses[i], mintIndex);
                mintIndex = totalSupply();
            }
        }
        return true;
    }
    
    function createPPuppies() external payable returns (bool) {
        require(enablePresales == 1, "CPP: Sale not started");
        require(presalePrice  == msg.value, "CPP: value must match price");
        require(presaleAddress[_msgSender()] > 0, "CPP: Not Authorised to buy");
        require(totalSupply() <= maxSupply, "CPP: creation must not exceed maximum supply");
        uint256 mintIndex = totalSupply();
        if (totalSupply() < maxGSupply) {
            _safeMint(_msgSender(), mintIndex);
            presaleAddress[_msgSender()] -= 1;
            emit Mint(_msgSender(), mintIndex);
        }
        return true;
    }
    
    function createDDPuppies() external payable returns (bool) {
        require(totalSupply() < maxSupply, "CDDP: creation must not exceed maximum supply");
        require((ddPrice == msg.value), "CDDP: value must match the daily drop price");
        require(winner == _msgSender(), "CDDP: buyer address should be DD winner");

        uint256 mintIndex = totalSupply();
        _safeMint(_msgSender(), mintIndex);
        emit Mint(_msgSender(), mintIndex);
        
        delete winner;
        return true;
    }
    
    function registerForPreSale(address[] calldata addresses) external onlyOwner returns (bool){
        uint arrayLength = addresses.length;
        for (uint i=0; i<arrayLength; i++) {
            presaleAddress[addresses[i]] += 1; 
        }
        return true;
    }
    
    function withdrawFunds(uint256 withdrawAmount) external onlyOwner returns (bool) {
        uint256 balance = address(this).balance;
        require(balance > 0, "withdrawFunds: must have funds to withdraw");
        require(withdrawAmount <= balance, "Can not withdraw more than available");
        payable(msg.sender).transfer(withdrawAmount);
        return true;
    }
    
    function transferFund(uint256 transferAmount, address transferTo) external onlyOwner returns (bool){
        uint256 amount = address(this).balance;
        require(transferAmount <= amount, "Can not withdraw more than available");
        (bool success,) = transferTo.call{value: transferAmount}("");
        require(success, "Failed to send ether");
        return true;
    }

    function setBaseURI(string memory baseURI) external onlyOwner returns (bool) {
        _baseTokenURI = baseURI;
        return true;
    }
    
    function updateContractURI(string memory newContractURI) external onlyOwner returns (bool){
        currentcontractURI = newContractURI;
        return true;
    }
    
    function setDDWinnerAndPrice(address addr, uint256 amount) external onlyOwner returns (bool) {
        require(addr != address(0), "setWinnerAndPrice: winner must not be the zero address");
        _setDDWinner(addr);
        _setDDPrice(amount);
        return true;
    }
    
    function updatePreSaleList(address preAdd, uint allowedCount) external onlyOwner returns (bool){
        presaleAddress[preAdd] = allowedCount;
        return true;
    }
    
    function setGPrice(uint256 amount) external onlyOwner returns (bool) {
        genesisPrice = amount;
        return true;
    }
    
    function setPPrice(uint256 amount) external onlyOwner returns (bool) {
        presalePrice = amount;
        return true;
    }

    function _setDDWinner(address addr) private {
        winner = addr;
    }

    function _setDDPrice(uint256 amount) private {
        ddPrice = amount;
    }
    
    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }
    
    function contractURI() public view returns (string memory) {
        return currentcontractURI;
    }
    
    function updateGenesis(uint status) external onlyOwner returns (bool) {
        enableGenesis = status;
        return true;
    }
    
    function updatePresales(uint status) external onlyOwner returns (bool) {
        enablePresales = status;
        return true;
    }
    
    function updateMaxGenesisCount(uint256 cnt) external onlyOwner returns (bool) {
        maxGSupply = cnt;
        return true;
    }

}
// Accessing the Ownable method ensures that only the creator of this Smart contract can interact with it
function airDropPuppies(address[] calldata addresses) external onlyOwner returns (bool) {
    // uint count = addresses.length();
    uint arrayLength = addresses.length;
    uint256 mintIndex = totalSupply();
    for (uint i=0; i<arrayLength; i++) {
        if (totalSupply() < maxSupply) {
            _safeMint(addresses[i], mintIndex);
            emit Mint(addresses[i], mintIndex);
            mintIndex = totalSupply();
        }
    }
    return true;
}

/*
 * Send free token to an array of addresses when ever want to as owner of the contract
 */