contract BLOCKRUNRS_NFT is ERC721Enumerable, Ownable {
    using Address for address;
    using Strings for uint256;

    uint256 public constant MAX_TOTAL_SUPPLY = 9_999;
    uint256 public constant MAX_ROLLING_SUPPLY = 9_899; // Includes max presale supply
    uint256 public constant MAX_PRESALE_SUPPLY = 6_250;
    uint256 public constant MAX_PRIVATE_SUPPLY = 100;

    uint256 public constant ROLLING_PURCHASE_LIMIT = 20;
    uint256 public constant PRESALE_PURCHASE_LIMIT = 5;

    bool public rollingPurchasesEnabled = false;
    bool public presale1PurchasesEnabled = false;
    bool public presale2PurchasesEnabled = false;

    address private _manager;

    uint256 private _rollingSupply;
    uint256 private _presaleSupply;
    uint256 private _privateSupply;

    string private _tokenBaseURI;

    mapping(address => uint256) private _presalePurchasesByAccount;
    mapping(address => uint256) private _rollingPurchasesByAccount;

    // 0 = false, 1 = true
    mapping(address => uint8) private _isPresale1Account;
    mapping(address => uint8) private _isPresale2Account;
    mapping(address => uint8) private _isPurchasing;

    FlexPaymentDivider private immutable _paymentHandler;

    constructor(
        string memory name,
        string memory symbol,
        string memory tokenBaseURI_,
        address payable[] memory payoutAccounts_,
        uint256[] memory payoutPercentages_
    ) ERC721(name, symbol) {
        _tokenBaseURI = tokenBaseURI_;
        _paymentHandler = new FlexPaymentDivider(payoutAccounts_, payoutPercentages_);
    }

    /* MODIFIERS */

    /**
     * @dev Throws if called by any account that is not the owner or manager.
     */
    modifier onlyAuthorized() {
        require(isAuthorized(_msgSender()));
        _;
    }

    /* GETTERS */

    function manager() external view returns (address) {
        return _manager;
    }

    function paymentHandler() external view returns (address) {
        return address(_paymentHandler);
    }

    function rollingSupplyAvailable() public view returns (uint256) {
        uint256 available = MAX_ROLLING_SUPPLY - _presaleSupply - _rollingSupply;
        if (available < 0) {
            return 0;
        }
        return available;
    }

    function presaleSupplyAvailable() public view returns (uint256) {
        if (_presaleSupply > MAX_PRESALE_SUPPLY) {
            return 0;
        }
        return MAX_PRESALE_SUPPLY - _presaleSupply;
    }

    function privateSupplyAvailable() public view returns (uint256) {
        if (_privateSupply > MAX_PRIVATE_SUPPLY) {
            return 0;
        }
        return MAX_PRIVATE_SUPPLY - _privateSupply;
    }

    function isPresale1Account(address account) public view returns (bool) {
        return _isPresale1Account[account] == 1;
    }

    function isPresale2Account(address account) public view returns (bool) {
        return _isPresale2Account[account] == 1;
    }

    function isAuthorized(address account) public view returns (bool) {
        return account == owner() || account == _manager;
    }

    /* PAYABLE */

    function purchasePresale(
        uint256 amount,
        uint8 safeMode,
        uint8 presaleType
    ) external payable {
        require(amount > 0);
        require(
            !isPurchasing(_msgSender())
        );
        require(
            totalSupply() < MAX_TOTAL_SUPPLY
        );
        require(
            amount <= presaleSupplyAvailable()
        );
        require(
            _presalePurchasesByAccount[_msgSender()] + amount <= PRESALE_PURCHASE_LIMIT
        );

        if (presaleType == 2) {
            require(
                presale2PurchasesEnabled
            );
            require(
                isPresale2Account(_msgSender())
            );
            require(
                msg.value >= 0.04 ether * amount
            );
        } else {
            require(
                presale1PurchasesEnabled
            );
            require(
                isPresale1Account(_msgSender())
            );
            require(
                msg.value >= 0.05 ether * amount
            );
        }

        _isPurchasing[_msgSender()] = 1;

        _handleDeposit(msg.value, safeMode);

        for (uint256 i = 0; i < amount; i++) {
            uint256 tokenId = 1 + totalSupply();
            _presaleSupply += 1;
            _presalePurchasesByAccount[_msgSender()] += 1;
            _safeMint(_msgSender(), tokenId);
        }

        _isPurchasing[_msgSender()] = 0;
    }

    function purchase(uint256 amount, uint8 safeMode) external payable {
        require(amount > 0);
        require(
            !isPurchasing(_msgSender())
        );
        require(
            totalSupply() < MAX_TOTAL_SUPPLY
        );
        require(
            amount <= rollingSupplyAvailable()
        );
        require(
            _rollingPurchasesByAccount[_msgSender()] + amount <= ROLLING_PURCHASE_LIMIT
        );
        require(rollingPurchasesEnabled);
        require(
            msg.value >= 0.05 ether * amount,
            "BLOCKRUNRS: Not enough Ether provided"
        );

        _isPurchasing[_msgSender()] = 1;

        _handleDeposit(msg.value, safeMode);

        for (uint256 i = 0; i < amount; i++) {
            uint256 tokenId = 1 + totalSupply();
            _rollingSupply += 1;
            _rollingPurchasesByAccount[_msgSender()] += 1;
            _safeMint(_msgSender(), tokenId);
        }

        _isPurchasing[_msgSender()] = 0;
    }

    /* PRIVILEGED */

    function gift(
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external onlyAuthorized {
        require(
            recipients.length == amounts.length,
            "BLOCKRUNRS: Input lengths must match"
        );
        require(
            totalSupply() < MAX_TOTAL_SUPPLY,
            "BLOCKRUNRS: Maximum supply reached"
        );

        uint256 _privateSupplyAvailable = privateSupplyAvailable();
        uint256 totalAmount;

        for (uint256 i = 0; i < recipients.length; i++) {
            address recipient = recipients[i];
            uint256 amount = amounts[i];
            for (uint256 j = 0; j < amount; j++) {
                uint256 tokenId = 1 + totalSupply();
                _privateSupply += 1;
                _safeMint(recipient, tokenId);
            }
        }

        require(
            totalAmount <= _privateSupplyAvailable,
            "BLOCKRUNRS: Amounts exceed supply available"
        );
    }

    function startPresale1() external onlyAuthorized {
        presale1PurchasesEnabled = true;
    }

    function stopPresale1() external onlyAuthorized {
        presale1PurchasesEnabled = false;
    }

    function startPresale2() external onlyAuthorized {
        presale2PurchasesEnabled = true;
    }

    function stopPresale2() external onlyAuthorized {
        presale2PurchasesEnabled = false;
    }

    function startRollingSale() external onlyAuthorized {
        rollingPurchasesEnabled = true;
    }

    function stopRollingSale() external onlyAuthorized {
        rollingPurchasesEnabled = false;
    }

    function addPresaleAccounts(
        address[] calldata accounts,
        uint8 presaleType
    ) external onlyAuthorized {
        if (presaleType == 2) {
            for (uint256 i = 0; i < accounts.length; i++) {
                require(
                    accounts[i] != address(0),
                    "BLOCKRUNRS: Address 0 not allowed"
                );
                _isPresale2Account[accounts[i]] = 1;
            }
        } else {
            for (uint256 i = 0; i < accounts.length; i++) {
                require(
                    accounts[i] != address(0),
                    "BLOCKRUNRS: Address 0 not allowed"
                );
                _isPresale1Account[accounts[i]] = 1;
            }
        }
    }

    function removePresaleAccounts(
        address[] calldata accounts,
        uint8 presaleType
    ) external onlyAuthorized {
        if (presaleType == 2) {
            for (uint256 i = 0; i < accounts.length; i++) {
                require(
                    isPresale2Account(accounts[i]),
                    "BLOCKRUNRS: One of the accounts is not a presale 2 account"
                );
                _isPresale2Account[accounts[i]] = 0;
            }
        } else {
            for (uint256 i = 0; i < accounts.length; i++) {
                require(
                    isPresale1Account(accounts[i]),
                    "BLOCKRUNRS: One of the accounts is not a presale 1 account"
                );
                _isPresale1Account[accounts[i]] = 0;
            }
        }
    }

    function setTokenBaseURI(string calldata URI) external onlyAuthorized {
        _tokenBaseURI = URI;
    }

    /**
     * @dev Set to the 0 address to revoke the current manager's authorization.
     */
    function setManager(address manager_) external onlyOwner {
        _manager = manager_;
    }

    /* INTERNAL */

    /**
     * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
     * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
     * by default, can be overriden in child contracts.
     */
    function _baseURI() internal view override returns (string memory) {
        return _tokenBaseURI;
    }

    /* PRIVATE */

    function _handleDeposit(uint256 value, uint8 safeMode) private {
        if (safeMode > 0) { // >0 == true, 0 == false
            _depositAsPull(value);
        } else {
            _depositAsPush(value);
        }
    }

    function _depositAsPush(uint256 value) private {
        _paymentHandler.deposit{value: value}();
        _paymentHandler.disperse();
    }

    function _depositAsPull(uint256 value) private {
        _paymentHandler.deposit{value: value}();
    }

    function isPurchasing(address account) private view returns (bool) {
        return _isPurchasing[account] == 1;
    }
}
/**
 * @notice Token IDs are positive integers starting at 1.
 */
function manager() external view returns (address) {
    return _manager;
}

/* GETTERS */