contract PixelPugs is ERC721Enumerable, Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    using ECDSA for bytes32;
    using Address for address;
    enum PugType{PUG, OG_PUG}
    
    IERC20 public yieldToken;
    
    // Minting
    bool public presaleActive = false;
    bool public saleActive = false;
    uint256 public maxMintPerAccount = 10;
    uint256 public price = 0.05 ether;
    uint256 public constant MAX_SUPPLY = 9999;
    
    uint256 public nameChangePrice = 300 ether;
	uint256 public bioChangePrice = 100 ether;
	uint256 public upgradePrice = 1000 ether;
	
	address public constant burnAddress = address(0x000000000000000000000000000000000000dEaD);

    // Base URI
    string private baseURI;
    
    mapping(uint256 => string) public _bio;

	// Mapping from token ID to name
	mapping (uint256 => string) private _tokenName;

	// Mapping if certain name string has already been reserved
	mapping (string => bool) private _nameReserved;
	
	mapping (uint256 => PugType) public tokenIdToPugType;

    // Whitelisting verification
    mapping (address => uint256) public presaleClaimed;
    address private signVerifier;

    event Mint(address recipient, uint256 tokenId);
    event NameChange (uint256 indexed tokenId, string newName);
	event BioChange (uint256 indexed tokenId, string bio);

    constructor() ERC721("PixelPugs", "PUG") {
        signVerifier = 0x759670b67cB21dFFd8655d0C8D5C751f36449F0D;
        baseURI = "";
    }
    
    function setYieldToken(address tokenAddress) external onlyOwner {
        yieldToken = IERC20(tokenAddress);
    }
    
    function setNameChangePrice(uint256 _nameChangePrice) public onlyOwner {
        nameChangePrice = _nameChangePrice;
    }
    
    function setBioChangePrice(uint256 _bioChangePrice) public onlyOwner {
        bioChangePrice = _bioChangePrice;
    }
    
    function setUpgradePrice(uint256 _upgradePrice) public onlyOwner {
        upgradePrice = _upgradePrice;
    }
    
    /**
     * @dev Returns name of the NFT at index.
     */
    function tokenNameByIndex(uint256 index) public view returns (string memory) {
        return _tokenName[index];
    }
    
    /**
     * @dev Returns bio of the NFT at index.
     */
    function tokenBioByIndex(uint256 index) public view returns (string memory) {
        return _bio[index];
    }
    
    function pugTypeByIndex(uint256 index) public view returns (PugType) {
        return tokenIdToPugType[index];
    }

    /**
     * @dev Returns if the name has been reserved.
     */
    function isNameReserved(string memory nameString) public view returns (bool) {
        return _nameReserved[toLower(nameString)];
    }
    
    /**
     * @dev Changes the name for pug tokenId
     */
    function changeName(uint256 tokenId, string memory newName) external nonReentrant {
        address owner = ownerOf(tokenId);
        require(address(yieldToken) != address(0), "Yieldtoken not set");
        require(yieldToken.balanceOf(_msgSender()) >= nameChangePrice, "Not enough tokens in wallet");
        require(_msgSender() == owner, "ERC721: caller is not the owner");
        require(validateName(newName, 25) == true, "Not a valid new name");
        require(sha256(bytes(newName)) != sha256(bytes(_tokenName[tokenId])), "New name is same as the current one");
        require(isNameReserved(newName) == false, "Name already reserved");
        
        yieldToken.transferFrom(msg.sender, address(this), nameChangePrice);
        // If already named, dereserve old name
        if (bytes(_tokenName[tokenId]).length > 0) {
            toggleReserveName(_tokenName[tokenId], false);
        }
        toggleReserveName(newName, true);
        _tokenName[tokenId] = newName;
        emit NameChange(tokenId, newName);
    }
    
    function changeBio(uint256 tokenId, string memory bio) external nonReentrant {
		address owner = ownerOf(tokenId);
		require(address(yieldToken) != address(0), "Yieldtoken not set");
		require(yieldToken.balanceOf(_msgSender()) >= bioChangePrice, "Not enough tokens in wallet");
		require(_msgSender() == owner, "ERC721: caller is not the owner");
		require(validateName(bio, 280) == true, "Not a valid bio");
        
        yieldToken.transferFrom(msg.sender, address(this), bioChangePrice);
        
		_bio[tokenId] = bio;
		emit BioChange(tokenId, bio);
	}
	
	function upgradePug(uint256 tokenId) external nonReentrant {
	    address owner = ownerOf(tokenId);
	    require(address(yieldToken) != address(0), "Yieldtoken not set");
	    require(yieldToken.balanceOf(_msgSender()) >= upgradePrice, "Not enough tokens in wallet");
	    require(_msgSender() == owner, "ERC721: caller is not the owner");
	    
	    yieldToken.transferFrom(msg.sender, address(this), upgradePrice);
	    
	    PugType pugType = PugType(1);
        tokenIdToPugType[tokenId] = pugType;
	}

    function togglePresale() external onlyOwner {
        presaleActive = !presaleActive;
    }

    function toggleSale() external onlyOwner {
        saleActive = !saleActive;
    }

    // @dev Generate hash to prove whitelist eligibility
    function getSigningHash(address recipient) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(recipient));
    }

    // @dev Sets a new signature verifier
    function setSignVerifier(address verifier) external onlyOwner {
        signVerifier = verifier;
    }

    // @dev Dynamically set the max mints a user can do in the main sale
    function setMaxMintPerAccount(uint256 maxMint) external onlyOwner {
        maxMintPerAccount = maxMint;
    }

    function isValidSignature(address recipient, bytes memory sig) private view returns (bool) {
        bytes32 message = getSigningHash(recipient).toEthSignedMessageHash();
        return ECDSA.recover(message, sig) == signVerifier;
    }

    // @dev Returns number of remaining mints for approved address
    function getAvailablePresaleMints(address recipient, bytes memory sig) external view returns (uint256) {
        require(isValidSignature(recipient, sig), "Account is not authorized for presale");
        return maxMintPerAccount - presaleClaimed[msg.sender];
    }

    function presaleMint(bytes memory sig, uint256 numberOfMints) external payable nonReentrant {
        uint256 supply = totalSupply();
        uint256 claimed = presaleClaimed[msg.sender];

        require(presaleActive, "Presale must be active to mint");
        require(isValidSignature(msg.sender, sig), "Account is not authorized for presale");
        require(numberOfMints <= maxMintPerAccount - claimed, "Amount exceeds mintable limit");
        require(numberOfMints > 0, "The minimum number of mints is 1");
        require(supply.add(numberOfMints) <= MAX_SUPPLY, "Further minting would exceed max supply");
        require(price.mul(numberOfMints) == msg.value, "Ether value sent is not correct");
        require(address(this).balance >= msg.value, "Insufficient balance to mint");

        presaleClaimed[msg.sender] = claimed + numberOfMints;

        for (uint256 i; i < numberOfMints; i++) {
            uint256 tokenId = supply + i;
            emit Mint(msg.sender, tokenId);
            _safeMint(msg.sender, tokenId);
            PugType pugType = PugType(0);
            tokenIdToPugType[tokenId] = pugType;
        }
    }

    function mint(uint256 numberOfMints) public payable nonReentrant {
        uint256 supply = totalSupply();

        require(saleActive, "Sale must be active to mint");
        require(numberOfMints <= maxMintPerAccount, "Amount exceeds mintable limit");
        require(numberOfMints > 0, "The minimum number of mints is 1");
        require(supply.add(numberOfMints) <= MAX_SUPPLY, "Further minting would exceed max supply");
        require(price.mul(numberOfMints) == msg.value, "Ether value sent is not correct");
        require(address(this).balance >= msg.value, "Insufficient balance to mint");

        for (uint256 i; i < numberOfMints; i++) {
            uint256 tokenId = supply + i;
            emit Mint(msg.sender, tokenId);
            _safeMint(msg.sender, tokenId);
            PugType pugType = PugType(0);
            tokenIdToPugType[tokenId] = pugType;
        }
    }

    // @dev Check if sale has been sold out
    function isSaleFinished() private view returns (bool) {
        return totalSupply() >= MAX_SUPPLY;
    }

    // @dev List tokens per owner
    function walletOfOwner(address owner) external view returns (uint256[] memory) {
        uint256 tokenCount = balanceOf(owner);

        uint256[] memory tokensId = new uint256[](tokenCount);
        for(uint256 i; i < tokenCount; i++){
            tokensId[i] = tokenOfOwnerByIndex(owner, i);
        }
        return tokensId;
    }

    function setPrice(uint256 newPrice) public onlyOwner {
        price = newPrice;
    }
    
	function ownerUpgradePug(uint256 tokenId) external onlyOwner {
	    PugType pugType = PugType(1);
        tokenIdToPugType[tokenId] = pugType;
	}

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "ETH balance is 0");
        payable(msg.sender).transfer(balance);
    }
    
    function withdrawYieldToken() external onlyOwner {
        uint balance = yieldToken.balanceOf(address(this));
        require(balance > 0, "Yieldtoken balance is 0");
        yieldToken.transfer(msg.sender, balance);
    }
    
    function burnYieldToken() external onlyOwner {
        uint balance = yieldToken.balanceOf(address(this));
        require(balance > 0, "Yieldtoken balance is 0");
        yieldToken.transfer(burnAddress, balance);
    }

    // @dev Private mint function reserved for company.
    function ownerMintToAddress(address recipient, uint256 numberOfMints) external onlyOwner nonReentrant {
        uint256 supply = totalSupply();

        require(numberOfMints > 0, "The minimum number of mints is 1");
        require(supply.add(numberOfMints) <= MAX_SUPPLY, "Further minting would exceed max supply");

        for (uint256 i; i < numberOfMints; i++) {
            uint256 tokenId = supply + i;
            emit Mint(recipient, tokenId);
            _safeMint(recipient, tokenId);
        }
    }

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

    function _baseURI() internal view override returns (string memory) {
        return baseURI;
    }
    
    /**
     * @dev Reserves the name if isReserve is set to true, de-reserves if set to false
     */
    function toggleReserveName(string memory str, bool isReserve) internal {
        _nameReserved[toLower(str)] = isReserve;
    }

    /**
     * @dev Check if the name string is valid (Alphanumeric and spaces without leading or trailing space)
     */
    function validateName(string memory str, uint256 maxChars) public pure returns (bool){
        bytes memory b = bytes(str);
        if(b.length < 1) return false;
        if(b.length > maxChars) return false; // Cannot be longer than maxChars
        if(b[0] == 0x20) return false; // Leading space
        if (b[b.length - 1] == 0x20) return false; // Trailing space

        bytes1 lastChar = b[0];

        for(uint i; i<b.length; i++){
            bytes1 char = b[i];

            if (char == 0x20 && lastChar == 0x20) return false; // Cannot contain continous spaces

            if(
                !(char >= 0x30 && char <= 0x39) && //9-0
                !(char >= 0x41 && char <= 0x5A) && //A-Z
                !(char >= 0x61 && char <= 0x7A) && //a-z
                !(char == 0x20) //space
            )
                return false;

            lastChar = char;
        }

        return true;
    }
    
    /**
     * @dev Converts the string to lowercase
     */
    function toLower(string memory str) public pure returns (string memory){
        bytes memory bStr = bytes(str);
        bytes memory bLower = new bytes(bStr.length);
        for (uint i = 0; i < bStr.length; i++) {
            // Uppercase character
            if ((uint8(bStr[i]) >= 65) && (uint8(bStr[i]) <= 90)) {
                bLower[i] = bytes1(uint8(bStr[i]) + 32);
            } else {
                bLower[i] = bStr[i];
            }
        }
        return string(bLower);
    }
}
/**
 * @title Crypto Zombiez contract
 * @dev Extends ERC721 Non-Fungible Token Standard basic implementation
 */
function setMaxMintPerAccount(uint256 maxMint) external onlyOwner {
    maxMintPerAccount = maxMint;
}

// @dev Dynamically set the max mints a user can do in the main sale