contract TheRandomWalk is ERC721, Ownable {
    using Strings for uint256;

    // Token counter
    uint public lastTokenId;
    // Starting supply
    uint256 public totalSupply = 1024;
    // Starting price
    uint256 public currentPrice;
    // Base URI for metadata
    string private metadataBaseURI;
    // Public sale status
    bool public onSale = false;

    // Referral program
    address public defaultReferrer;
    uint8 public rewardsPct = 20;
    mapping(address => uint) public referralCount;
    mapping(address => uint) public rewardsPaid;
    address[] private _referrers;
    uint public totalReferralCount;
    uint private _totalRoyaltiesReceived;
    uint private _totalRewardsPaid;
    event RewardPaid(address referrer, uint amount);
    event RewardPaymentFailed(address referrer, uint amount);

    constructor(uint _price) ERC721("Algorithms. Random Walk", "ALGRW") {
        currentPrice = _price;
    }

    function mint(uint qty, address referrer) external payable {
        require(onSale, "Not on sale");
        require(lastTokenId + qty <= totalSupply, "Not enough supply");
        require(msg.value >= qty * currentPrice, "Insufficient funds");

        // Set referrer, if any
        // Use defaultReferrer if specified
        if (referrer == address(0) && defaultReferrer != address(0))
            referrer = defaultReferrer;
        if (referrer != address(0) && referrer != msg.sender) {
            if (referralCount[referrer] == 0)
                _referrers.push(referrer);
            referralCount[referrer] += qty;
            totalReferralCount += qty;
        }

        for (uint i = 0; i < qty; i++) {
            lastTokenId++;
            _safeMint(msg.sender, lastTokenId);
        }
    }

    // Frontend-only call
    function listTokens(address _addr) external view returns (uint[] memory) {
        uint cnt = balanceOf(_addr);
        if (cnt == 0) {
            return new uint[](0);
        } else {
            uint[] memory result = new uint[](cnt);
            uint resIdx = 0;
            for (uint i = 1; i <= lastTokenId; i++) {
                if (ownerOf(i) == _addr) {
                    result[resIdx] = i;
                    cnt--;
                    if (cnt == 0)
                        return result;
                    else
                        resIdx++;
                }
            }
            return result;
        }
    }

    // While minting, we'll have the metadata load from our website.
    // Once all is minted, we will upload all metadata to IPFS and set base URI
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        if (bytes(metadataBaseURI).length == 0)
            return string(abi.encodePacked("https://generart.io/collections/trw/", tokenId.toString(), ".json"));
        else
            return string(abi.encodePacked(metadataBaseURI, tokenId.toString()));
    }

    // Rewards payable to caller.
    // Includes mint rewards and royalties, less already paid rewards
    function payableRewards() public view returns (uint) {
        return referralCount[msg.sender] * (currentPrice + _totalRoyaltiesReceived / lastTokenId) * rewardsPct / 100 - rewardsPaid[msg.sender];
    }

    // Anyone can claim their reward.
    // Reentrancy-protected
    function claimRewards() external {
        uint toPay = payableRewards();
        require(toPay > 0, "Insufficient funds");
        rewardsPaid[msg.sender] += toPay;
        _totalRewardsPaid += toPay;

        (bool sent,) = msg.sender.call{value : toPay}("");
        if (sent)
            emit RewardPaid(msg.sender, toPay);
        else
            emit RewardPaymentFailed(msg.sender, toPay);
        require(sent);
    }

    // Every unidentified deposit to the contract
    // will be considered as royalty
    receive() external payable {
        _totalRoyaltiesReceived += msg.value;
    }

    //
    // Owner(s) functions
    //

    function updatePricing(uint256 _price) external onlyOwner {
        require(_price > 0, "Invalid price");
        currentPrice = _price;
    }

    function lockedRewardsAmount() public view onlyOwner returns (uint) {
        return totalReferralCount * (currentPrice + _totalRoyaltiesReceived / lastTokenId) * rewardsPct / 100 - _totalRewardsPaid;
    }

    function availableToWithdraw() public view onlyOwner returns (uint) {
        if (address(this).balance > lockedRewardsAmount())
            return address(this).balance - lockedRewardsAmount();
        else
            return 0;
    }

    // This function locks unpaid referral rewards (as defined in availableToWithdraw)
    // and ensures funds always remain available in the contract and claimable.
    // It then withdraws the specified amount to specified payee
    function withdraw(address payee, uint amount) external onlyOwner {
        if (amount == 0)
            amount = availableToWithdraw();
        else
            require(amount <= availableToWithdraw(), "Insufficient funds");
        require(amount > 0, "Insufficient funds");
        (bool sent,) = payee.call{value : amount}("");
        require(sent);
    }

    function mintFor(address[] memory to, uint[] memory qty) external onlyOwner {
        require(to.length == qty.length);

        for (uint16 i = 0; i < to.length; i++)
            for (uint16 j = 0; j < qty[i]; j++) {
                require(lastTokenId < totalSupply, "Not enough supply");
                lastTokenId++;
                _safeMint(to[i], lastTokenId);
            }
    }

    function setMetadataBaseUri(string memory uri) external onlyOwner {
        metadataBaseURI = uri;
    }

    function getReferrers() external view onlyOwner returns (address[] memory) {
        return _referrers;
    }

    function setDefaultReferrer(address _ref) external onlyOwner {
        defaultReferrer = _ref;
    }

    function setRewardPct(uint8 pct) external onlyOwner {
        rewardsPct = pct;
    }

    function setOnSale(bool _onSale) external onlyOwner {
        onSale = _onSale;
    }
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
function payableRewards() public view returns (uint) {
    return referralCount[msg.sender] * (currentPrice + _totalRoyaltiesReceived / lastTokenId) * rewardsPct / 100 - rewardsPaid[msg.sender];
}

// Rewards payable to caller.
// Includes mint rewards and royalties, less already paid rewards