contract MachineElves is ERC721Enumerable, Ownable, ReentrancyGuard {
    using Strings for uint256;
    using ECDSA for bytes32;

    uint256 public constant MAX_SUPPLY = 5555;
    uint256 public constant SALE_PRICE = 0.08 ether;
    uint256 public constant MAX_PER_PURCHASE = 10; // This is a per-address max.  No addresses can mint more than 10 in total (including deed holder claimed tokens and presale purchased tokens)
    uint256 public constant MAX_PER_PRESALE_PURCHASE = 5; // This is a per-address max.  No address can mint more than 5 during the presale (including deed holder claimed tokens)
    uint256 public constant RESERVE_QTY = 32; // 25 for the vault, 7 for giveaways
    
    bool public publicMintActive = false;
    bool public presaleMintActive = false;
    bool public foundersDeedClaimActive = false;
    bool public metadataLocked = false;
    
    // Payout addresses
    address private constant _ferb = 0x4DFA7FF4a33F2625495867a373caa4d165d37F37; // Founder
    address private constant _wickedDig = 0xc36596E05312410fC41337eC6c2914A4FCA8d6C1; // Creator
    address private constant _picoPyro = 0x86190c50d1dD3Eeb2Ab1dbb8E7195F2195AF321f; // Solidity dev
    address private constant _jups = 0xDB408786E162f7aF1e2B062b458939cE89E1c3dD; // Artist
    address private constant _machineElves = 0x31522146C57EaaeB278FbFfC97A94214b0f95404; // Community/Project
    address private constant _friendly = 0x689A19F57077F682A1D7cc19D9066F1a834721a2; // marketing
    address private constant _duo = 0x9Da3f811143ED2208085f460754b32788913a788; // marketing

    uint256 private constant _totalShares = 10000;

    string private _baseUri = "";
    
    address private _signerAddress = 0xd821bd8F420F842b0EfCc7488573A5C0Ffd5842A;

    bool private _adminsSet = false;
    uint256 private _qtyReserved = 0;

    address[7] private _shareholders = [_ferb, _wickedDig, _picoPyro, _jups, _machineElves, _friendly, _duo];
    address[2] private _admins;

    IHolderValidation private _foundersDeedInstance;

    mapping(address => uint256) private _shares;
    mapping(address => SnapshotAddressMinted) private _foundersDeedHolderSnapshot;
    mapping(string => bool) private _usedNonces;

    event Mint(address indexed recipient, uint256 indexed tokenId);

    constructor(string memory name, string memory symbol) ERC721(name, symbol) {
        require(bytes(name).length > 0 && bytes(symbol).length > 0,"Name and Symbol");

        _shares[_ferb] = 1400;
        _shares[_wickedDig] = 1200;
        _shares[_picoPyro] = 500;
        _shares[_jups] = 3300;
        _shares[_machineElves] = 2600;
        _shares[_friendly] = 500;
        _shares[_duo] = 500;
    }

    function foundersDeedClaim() external nonReentrant{
        require(foundersDeedClaimActive, "Founders Deed claiming not active");
        require(_foundersDeedHolderSnapshot[_msgSender()].Allowed, "Address not whitelisted");
        require(!_foundersDeedHolderSnapshot[_msgSender()].Claimed, "Already claimed");
        require(_foundersDeedInstance.balanceOf(_msgSender()) > 0, "Not a Founders Deed holder");

        internalMint(_msgSender(), 1);        
        _foundersDeedHolderSnapshot[_msgSender()].Claimed = true;
    }

    function mint(uint256 qty) external payable nonReentrant {
        require(publicMintActive, "Minting is not active");
        require(qty > 0, "Quantity cannot be zero");
        require(msg.value == (SALE_PRICE * qty), "Incorrect payment amount");
        require(balanceOf(_msgSender())+qty <= MAX_PER_PURCHASE, "Max of 10 mints per address");

        internalMint(_msgSender(), qty);
    }

    function presaleMint(bytes32 hash, bytes memory sig, string calldata nonce, uint256 qty) external payable nonReentrant {
        require(presaleMintActive, "Presale minting is not active");
        require(_usedNonces[nonce] == false, "Nonce has already been used");
        require(matchAddresSigner(hash, sig), "No direct minting");
        require(hashTransaction(_msgSender(), nonce) == hash, "Hash check failed");
        require(qty > 0, "Quantity cannot be zero");
        require(msg.value == (SALE_PRICE * qty), "Incorrect payment amount");
        require(balanceOf(_msgSender())+qty <= MAX_PER_PRESALE_PURCHASE, "Max of 5 presale mints per address");

        _usedNonces[nonce] = true;
        internalMint(_msgSender(), qty);
    }

    function distributeFromReserve(uint256 qty, address recipient) external nonReentrant onlyOwner {
        require((totalSupply() + qty + RESERVE_QTY) <= MAX_SUPPLY,"Exceeds total supply");
        require(_qtyReserved + qty <= RESERVE_QTY, "Exceed reserve quantity");

        for (uint256 i = 0; i < qty; i++) {
            _qtyReserved++;
            internalMint(recipient, 1);
        }
    }

    function internalMint(address recipient, uint256 qty) private {
        require(tx.origin == _msgSender(), "Only EOA");
        require((totalSupply() + qty + RESERVE_QTY) <= MAX_SUPPLY, "Exceeds total supply");
        require(qty <= MAX_PER_PURCHASE, "Exceeds per transaction limit");        
        
        for (uint256 i = 0; i < qty; i++) {
            uint256 tokenId = totalSupply() + 1;

            _safeMint(recipient, tokenId);
            emit Mint(recipient, tokenId);
        }
    }

    function hashTransaction(address sender, string calldata nonce) private view returns (bytes32) {
        bytes32 hash = keccak256(
            abi.encodePacked(
                "\x19Ethereum Signed Message:\n32",
                keccak256(abi.encodePacked(address(this), sender, nonce))
            )
        );
        return hash;
    }
    
    function matchAddresSigner(bytes32 hash, bytes memory signature) private view returns (bool) {
        return _signerAddress == hash.recover(signature);
    }

    /**
     * @dev Distrubtes the current balance held by the contract to all shareholders in the percentages defined
     * in the constructor. The remainder, if any, goes to the contract owner
     */
    function distributeFunds() external onlyOwner nonReentrant {
        require(address(this).balance > 0, "Insufficient balance");

        uint256 originalAmt = address(this).balance;
        uint256 balance = originalAmt;

        for (uint256 i = 0; i < _shareholders.length; i++) {
            address payee = _shareholders[i];
            uint256 payeeShares = _shares[payee];
            uint256 paymentAmount = (originalAmt * payeeShares) / _totalShares;

            balance -= paymentAmount;

            payable(payee).transfer(paymentAmount);
        }

        if(balance > 0) {
            payable(_msgSender()).transfer(balance);
        }
    }

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

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "Token does not exist");

        return string(abi.encodePacked(_baseUri, "/", tokenId.toString(), ".json"));
    }

    function tokensOfOwner(address owner) external view returns (uint256[] memory) {
        uint256 tokenCount = balanceOf(owner);
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(owner, index);
            }
            return result;
        }
    }
    
    function canClaim(address addr) external view returns (bool) {
        return _foundersDeedHolderSnapshot[addr].Allowed && !_foundersDeedHolderSnapshot[addr].Claimed;
    }

    function burn(uint256 tokenId) public {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "Caller is not owner nor approved");
        _burn(tokenId);
    }

    function setSignerAddress(address addr) external onlyOwner {
        _signerAddress = addr;
    }

    function setBaseURI(string calldata baseURI) external onlyOwnerOrAdmin{
        require(!metadataLocked, "Metadata url is locked");
        require(bytes(baseURI).length > 0, "baseURI is required");

        _baseUri = baseURI;
    }

    function setAdmins(address[2] calldata adminAddresses) external onlyOwner {
        for (uint256 i = 0; i < adminAddresses.length; i++) {
            address admin = adminAddresses[i];

            require(admin != address(0), "Nope, that's not a good idea");
            _admins[i] = admin;
        }

        _adminsSet = true;
    }

    function addDeedholderAddresses(address[] calldata addresses) external onlyOwnerOrAdmin {
        for (uint256 i = 0; i < addresses.length; i++) {
            _foundersDeedHolderSnapshot[addresses[i]] = SnapshotAddressMinted(true, false);
        }
    }

    /**
     * @dev Creates a "limted functionality" instance of the contract for the specified address
     */
    function setFoundersDeedInstance(address instanceAddress) external onlyOwner {
        _foundersDeedInstance = IHolderValidation(instanceAddress);
    }

    function setPublicSaleActive(bool active) external onlyOwnerOrAdmin {
        if (active) {
            require(bytes(_baseUri).length > 0, "Metadata url has not been set");
        }
        publicMintActive = active;
    }
    
    function setPresaleActive(bool active) external onlyOwnerOrAdmin {
        if (active) {
            require(bytes(_baseUri).length > 0, "Metadata url has not been set");
        }
        presaleMintActive = active;
    }

    function setFoundersDeedClaimActive(bool active) external onlyOwnerOrAdmin{
        if (active) {
            require(bytes(_baseUri).length > 0, "Metadata url has not been set");
            require(address(_foundersDeedInstance) != address(0), "Founders Deed instance not set");
        }
        foundersDeedClaimActive = active;
    }

    function lockMetadataUrl() external onlyOwner {
        require(!(publicMintActive || presaleMintActive || foundersDeedClaimActive), "Cannot lock the metadata while the contract is active");
        metadataLocked = true;
    }

    /**
     * @dev Throws if called by any account other than the owner or one of the predefined admins.
     */
    modifier onlyOwnerOrAdmin() {
        require(
            _msgSender() == owner() ||
                (_adminsSet &&
                    (_msgSender() == _admins[0] || _msgSender() == _admins[1])),
            "Caller is not the owner or an admin"
        );
        _;
    }

}
function setFoundersDeedInstance(address instanceAddress) external onlyOwner {
    _foundersDeedInstance = IHolderValidation(instanceAddress);
}

/**
 * @dev Creates a "limted functionality" instance of the contract for the specified address
 */