contract LeapNFoundersDeedWorld is ERC721, Ownable, ReentrancyGuard {
    using Counters for Counters.Counter;

    uint256 public constant MAX_SUPPLY = 6000;
    uint256 public constant MAX_PRESALE_SUPPLY = 1500;
    uint256 public constant MAX_PER_PURCHASE = 10;
    uint256 public constant SALE_PRICE = 0.15 ether;
    uint256 public constant MAX_WALLET_PRESALE_PURCHASE = 5;
    uint256 public constant OG_MINTED_QTY = 85;

    string public baseURI = "ipfs://QmRb3GBgwKSZGEp8Gto5wTULzCfzmydXaZ7ic6Dq4sSmoj";

    bool public isActive = false;
    bool public presaleIsActive = false;
    uint256 public totalPreSaleSold = 0;
    bool public metadataLocked = false;

    // Payout addresses
    address private constant _ferb = 0x4DFA7FF4a33F2625495867a373caa4d165d37F37; // Founder
    address private constant _wickedDig = 0xc36596E05312410fC41337eC6c2914A4FCA8d6C1; // The Believer
    address private constant _picoPyro = 0x7560749351EA673a7054b746Ff49c794597258f6; // Solidity dev    
    address private constant _frederick = 0x15557c8b7246C38EE71eA6dc69e4347F5DAc2104; // Web dev
    address private constant _kingOfTheJungle = 0x4d1A642fb0e840842aeed1Fafab35a26b32c3098; // Web dev
    address private constant _reasearchAndDevelopment = 0x1e2d1C0542cBAFdB54E9E1352a465b818dFee20A; // R&D
    address private constant _marketing = 0x574617AC8752A708640F5a69adfb89eC57c7a861; // Marketing
    address private constant _comm = 0xF8b304d259bD8687311129db18264E11E585f45a; // Comm
        
    uint256 private constant _totalShares = 100000;
    
    bool private _adminsSet = false;    
    uint256 private _ogQtyMinted = 0;    
    
    address[8] private _shareholders = [_ferb,_wickedDig,_picoPyro,_frederick,_kingOfTheJungle,_reasearchAndDevelopment,_marketing,_comm];
    
    Counters.Counter private _tokenIds;
    
    address[2] private _admins;
    mapping(address => uint256) private _shares;
    mapping(address => bool) private _whitelist;
    
    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 are required");
        
        _shares[_ferb] = 49285;
        _shares[_wickedDig] = 24515;
        _shares[_picoPyro] = 3000;
        _shares[_frederick] = 100;
        _shares[_kingOfTheJungle] = 100;
        _shares[_reasearchAndDevelopment] = 1000;
        _shares[_marketing] = 1000;
        _shares[_comm] = 1000;
    }

    function addToWhitelist(address[] calldata whitelistAddresses) external onlyOwnerOrAdmin {
        for (uint256 i = 0; i < whitelistAddresses.length; i++) {
            if(!isOnWhitelist(whitelistAddresses[i])){
                _whitelist[whitelistAddresses[i]] = true;
            }
        }
    }

    function mint(uint256 qty) external payable nonReentrant {
        require(isActive, "Minting is not active");
        require(
            qty < MAX_PER_PURCHASE + 1,
            "Cannot purchase more than the per-order limit"
        );
        require(msg.value == (SALE_PRICE * qty), "Incorrect payment amount");

        mint(_msgSender(), qty);
    }

    // The first 85 go to the founder for distributing to original holders
    function mintDistributions(uint256 qty) external onlyOwner {
        require(qty + _ogQtyMinted < OG_MINTED_QTY + 1, "Qty would exceed the amount of OG mints");
        
        _ogQtyMinted += qty;

        mint(_ferb, qty);
    }

    function mintFromPresale(uint256 qty) external payable presaleActive onlyWhitelisted {
        require(qty < MAX_WALLET_PRESALE_PURCHASE + 1, "Qty would exceed the maximum per-address presale limit");
        require(totalPreSaleSold + qty < MAX_PRESALE_SUPPLY + 1, "Qty would exceed the total presale limit");
        require(msg.value == (SALE_PRICE * qty), "Incorrect payment amount");
        require(qty + ERC721.balanceOf(_msgSender()) < MAX_WALLET_PRESALE_PURCHASE + 1, "The requested qty would exceed your per-address presale limit");

        totalPreSaleSold += qty;

        mint(_msgSender(), qty);
    }

     function mint(address recipient, uint256 qty) private {
        require((totalSold() + qty) < MAX_SUPPLY + 1, "Requested quantity would exceed total supply");

        for (uint256 i = 0; i < qty; i++) {
            _tokenIds.increment();
            uint256 tokenId = totalSold();
            _safeMint(recipient, tokenId);
            
            emit Mint(recipient, tokenId);
        }
    }

    function isOnWhitelist(address whitelistAddress) public view returns (bool) {
        return _whitelist[whitelistAddress];
    }

    function totalSold() public view returns (uint256) {
        return _tokenIds.current();
    }
    
    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        require(tokenId > 0, "Token Id = 0");

        string memory base = baseURI;
        return
            bytes(base).length > 0
                ? string(
                    abi.encodePacked(base, "/", uint2str(tokenId), ".json")
                )
                : "";
    }

    /**
     * @dev Distrubtes the balance to all shareholders as defined in the ctor and 
     * then the remainder goes to the contract owner
     */
    function cashOut() external onlyOwner nonReentrant {
        require(address(this).balance > 0, "Negative Ghost Rider, this pattern is...ummm...empty");

        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);
        }

        payable(_msgSender()).transfer(balance);
    }
    
    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 setActive(bool active) external onlyOwnerOrAdmin ogHolderTokensMinted {
        isActive = active;
    }

    function setPreSaleActive(bool active) external onlyOwnerOrAdmin ogHolderTokensMinted {
        presaleIsActive = active;
    }
    
    function setMetadataUrl(string calldata newUrl) external onlyOwnerOrAdmin {
        require(!metadataLocked, "Metadata url is locked");
        require(bytes(newUrl).length > 0, "newUrl cannot be empty");
        
        baseURI = newUrl;
    }
    
    function lockMetadata() external onlyOwner {
        require(!isActive && !presaleIsActive, "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"
        );
        _;
    }

    modifier onlyWhitelisted() {
        require(isOnWhitelist(_msgSender()), "Sorry, you are not on the whitelist");
        _;
    }

    modifier presaleActive() {
        require(presaleIsActive, "Presale minting is not currently available");
        _;
    }

    modifier ogHolderTokensMinted {
        require(_ogQtyMinted == OG_MINTED_QTY, "The OG tokens have not been minted");
        _;
    }

    /**
     * @dev Do not allow ownership renouncing
     */
    function renounceOwnership() public override(Ownable) onlyOwner { }

    // https://stackoverflow.com/a/47137572
    function uint2str(uint256 _i)
        private
        pure
        returns (string memory _uintAsString)
    {
        if (_i == 0) {
            return "0";
        }
        uint256 j = _i;
        uint256 len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint256 k = len;
        while (_i != 0) {
            k = k - 1;
            uint8 temp = (48 + uint8(_i - (_i / 10) * 10));
            bytes1 b1 = bytes1(temp);
            bstr[k] = b1;
            _i /= 10;
        }
        return string(bstr);
    }
}
function uint2str(uint256 _i)
    private
    pure
    returns (string memory _uintAsString)
{
    if (_i == 0) {
        return "0";
    }
    uint256 j = _i;
    uint256 len;
    while (j != 0) {
        len++;
        j /= 10;
    }
    bytes memory bstr = new bytes(len);
    uint256 k = len;
    while (_i != 0) {
        k = k - 1;
        uint8 temp = (48 + uint8(_i - (_i / 10) * 10));
        bytes1 b1 = bytes1(temp);
        bstr[k] = b1;
        _i /= 10;
    }
    return string(bstr);
}

// https://stackoverflow.com/a/47137572