# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/utils.ipynb.

# %% auto 0
__all__ = ['T', 'set_seed', 'most_common', 'context_chdir', 'generate_time_id', 'chunk_random', 'jprint', 'is_in', 'flatten_dict',
           'unflatten_dict', 'NestedDict', 'split_camel_case']

# %% ../nbs/utils.ipynb 3
import json
import os
import random
import re
from contextlib import contextmanager
from operator import eq
from pathlib import Path
from typing import Dict, List, Tuple, Union, Iterable, TypeVar, Generator

import numpy as np
import pandas as pd
from fastcore.basics import patch

# %% ../nbs/utils.ipynb 4
def set_seed(seed):
    np.random.seed(seed%(2**32-1))
    random.seed(seed)

# %% ../nbs/utils.ipynb 6
from collections import Counter

def most_common(lst):
    """returns the most common element of a collection"""
    return Counter(lst).most_common(1)[0][0]

# %% ../nbs/utils.ipynb 8
@patch
def ls_sorted(self:Path):
    "ls but sorts files by name numerically"
    return self.ls().sorted(key=lambda f: int(f.with_suffix('').name))

# %% ../nbs/utils.ipynb 9
# ref: https://dev.to/teckert/changing-directory-with-a-python-context-manager-2bj8
@contextmanager
def context_chdir(path: Union[Path, str]):
    """Sets the cwd within the context"""
    origin = Path().absolute()
    try:
        os.chdir(path)
        yield
    finally:
        os.chdir(origin)

# %% ../nbs/utils.ipynb 11
from datetime import datetime

def generate_time_id(dt=None):
    """generates a string id from given datetime or now"""
    return (dt or datetime.now()).isoformat().rsplit('.', 1)[0].replace(':', '-')

# %% ../nbs/utils.ipynb 13
T = TypeVar("T")


def chunk_random(lst: List[T], min_chunk: int = 2, max_chunk: int = 4) -> Generator[List[T], None, None]:
    """
    Splits a list into random-sized chunks.

    Args:
        lst (list): The list to be split into chunks.
        min_chunk (int, optional): The minimum size of each chunk. Defaults to 2.
        max_chunk (int, optional): The maximum size of each chunk. Defaults to 4.

    Yields:
        list: A chunk of the original list.

    Returns:
        list: A list of chunks.

    """
    # Ensure the list has at least the minimum number of elements required for a chunk
    if len(lst) < min_chunk:
        return [lst]

    i = 0  # Initialize an index to traverse the list
    while i < len(lst):
        if len(lst) - i < min_chunk:
            break
        # Determine the size of the next chunk
        chunk_size = random.randint(min_chunk, min(max_chunk, len(lst) - i))
        # Add the chunk to the list of chunks
        yield lst[i : i + chunk_size]
        # Increment the index by the size of the chunk just added
        i += chunk_size


# %% ../nbs/utils.ipynb 15
def jprint(obj, indent=2, **kwargs):
    print(json.dumps(obj, indent=indent), **kwargs)

# %% ../nbs/utils.ipynb 17
def is_in(target, collection: Iterable, eq_fn=eq) -> bool:
    for item in collection:
        if eq_fn(item, target):
            return True
    return False

# %% ../nbs/utils.ipynb 19
def flatten_dict(d: Dict, sep='.') -> Dict:
    records = pd.json_normalize(d, sep=sep).to_dict(orient='records')
    if len(records):
        return records[0]
    return {}

def unflatten_dict(d: Dict, sep='.') -> Dict:
    res = {}
    for k, v in d.items():
        subkeys = k.split(sep)
        container = res
        for subkey in subkeys[:-1]:
            if subkey not in container:
                container[subkey] = {}
            container = container[subkey]
        container[subkeys[-1]] = v
    return res

# %% ../nbs/utils.ipynb 22
class NestedDict(dict):
    def __init__(self, data, sep='.'):
        super().__init__(data)
        self.sep = sep
    
    def at(self, keys: Union[str, List, Tuple], default=None):
        if isinstance(keys, str):
            keys = keys.split(self.sep)
        node = self
        for key in keys:
            if key not in node:
                return default
            node = node.get(key)
        return node

    def set(self, keys: Union[str, List, Tuple], value):
        if isinstance(keys, str):
            keys = keys.split(self.sep)
        node = self
        last_key = keys.pop()
        for key in keys:
            if key not in node:
                node[key] = dict()
            node = node[key]
        node[last_key] = value

    def flat(self) -> Dict:
        return flatten_dict(self, sep=self.sep)
    
    @classmethod
    def from_flat_dict(cls, data, sep='.'):
        return cls(unflatten_dict(data, sep=sep))
     

# %% ../nbs/utils.ipynb 25
def split_camel_case(input_str):
    # Use regular expression to find word boundaries in camel case
    matches = re.finditer('.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', input_str)
    # Extract the words and return as a list
    return [m.group(0) for m in matches]
