import glob, os
from collections import OrderedDict, namedtuple
from os.path import basename, exists, dirname, join, expanduser
from typing import Optional, Union
from pathlib import Path


def version() -> str:
    # - check current version
    version = 'Dev'
    try: 
        import importlib_metadata
        version = importlib_metadata.version('qube2')
    except:
        pass

    return version


from ._pyxreloader import pyx_install_loader

def install_pyx_recompiler_for_dev():
    if version().lower() == 'dev':
        print(f" >  [{green('dev')}] {red('installed cython rebuilding hook')}")
        pyx_install_loader(['qubx.core', 'qubx.ta', 'qubx.data', 'qubx.strategies'])


def runtime_env():
    """
    Check what environment this script is being run under
    :return: environment name, possible values:
             - 'notebook' jupyter notebook
             - 'shell' any interactive shell (ipython, PyCharm's console etc)
             - 'python' standard python interpreter
             - 'unknown' can't recognize environment
    """
    try:
        from IPython import get_ipython
        shell = get_ipython().__class__.__name__

        if shell == 'ZMQInteractiveShell':  # Jupyter notebook or qtconsole
            return 'notebook'
        elif shell.endswith('TerminalInteractiveShell'):  # Terminal running IPython
            return 'shell'
        else:
            return 'unknown'  # Other type (?)
    except (NameError, ImportError):
        return 'python'  # Probably standard Python interpreter

_QUBX_FLDR = None

def get_local_qubx_folder() -> str:
    global _QUBX_FLDR

    if _QUBX_FLDR is None:
        _QUBX_FLDR = makedirs(os.getenv('QUBXSTORAGE', os.path.expanduser('~/.qubx')))

    return _QUBX_FLDR


def add_project_to_system_path(project_folder:str = '~/projects'):
    """
    Add path to projects folder to system python path to be able importing any modules from project
    from test.Models.handy_utils import some_module
    """
    import sys
    from os.path import expanduser, relpath
    from pathlib import Path
    
    # we want to track folders with these files as separate paths
    toml = Path('pyproject.toml')
    src = Path('src')
    
    try:
        prj = Path(relpath(expanduser(project_folder)))
    except ValueError as e:
        # This error can occur on Windows if user folder and python file are on different drives
        print(f"Qube> Error during get path to projects folder:\n{e}")
    else:
        insert_path_iff = lambda p: sys.path.insert(0, p.as_posix()) if p.as_posix() not in sys.path else None
        if prj.exists():
            insert_path_iff(prj)
            
            for di in prj.iterdir():
                _src = di / src
                if (di / toml).exists():
                    # when we have src/
                    if _src.exists() and _src.is_dir():
                        insert_path_iff(_src)
                    else:
                        insert_path_iff(di)
        else:
            print(f'Qube> Cant find {project_folder} folder for adding to python path !')


def is_localhost(host):
    return host.lower() == 'localhost' or host == '127.0.0.1'


def __wrap_with_color(code):
    def inner(text, bold=False):
        c = code
        if bold:
            c = "1;%s" % c
        return "\033[%sm%s\033[0m" % (c, text)

    return inner


red, green, yellow, blue, magenta, cyan, white = (
    __wrap_with_color('31'),
    __wrap_with_color('32'),
    __wrap_with_color('33'),
    __wrap_with_color('34'),
    __wrap_with_color('35'),
    __wrap_with_color('36'),
    __wrap_with_color('37'),
)


class Struct:
    """
    Dynamic structure (similar to matlab's struct it allows to add new properties dynamically)

    >>> a = Struct(x=1, y=2)
    >>> a.z = 'Hello'
    >>> print(a)

    Struct(x=1, y=2, z='Hello')
    
    >>> Struct(a=234, b=Struct(c=222)).to_dict()
    
    {'a': 234, 'b': {'c': 222}}

    >>> Struct({'a': 555}, a=123, b=Struct(c=222)).to_dict()

    {'a': 123, 'b': {'c': 222}}
    """

    def __init__(self, *args, **kwargs):
        _odw = OrderedDict(**kwargs)
        if args:
            if isinstance(args[0], dict):
                _odw = OrderedDict(Struct.dict2struct(args[0]).to_dict()) | _odw
            elif isinstance(args[0], Struct):
                _odw = args[0].to_dict() | _odw
        self.__initialize(_odw.keys(), _odw.values())

    def __initialize(self, fields, values):
        self._fields = list(fields)
        self._meta = namedtuple('Struct', ' '.join(fields))
        self._inst = self._meta(*values)

    def fields(self) -> list:
        return self._fields

    def __getitem__(self, idx: int):
        return getattr(self._inst, self._fields[idx])

    def __getattr__(self, k):
        return getattr(self._inst, k)

    def __or__(self, other: Union[dict, 'Struct']):
        if isinstance(other, dict):
            other = Struct.dict2struct(other)
        elif not isinstance(other, Struct):
            raise ValueError(f"Can't union with object of {type(other)} type ")
        for f in other.fields():
            self.__setattr__(f, other.__getattr__(f))
        return self

    def __dir__(self):
        return self._fields

    def __repr__(self):
        return self._inst.__repr__()

    def __setattr__(self, k, v):
        if k not in ['_inst', '_meta', '_fields']:
            new_vals = {**self._inst._asdict(), **{k: v}}
            self.__initialize(new_vals.keys(), new_vals.values())
        else:
            super().__setattr__(k, v)

    def __getstate__(self):
        return self._inst._asdict()

    def __setstate__(self, state):
        self.__init__(**state)

    def __ms2d(self, m) -> dict:
        r = {}
        for f in m._fields:
            v = m.__getattr__(f)
            r[f] = self.__ms2d(v) if isinstance(v, Struct) else v
        return r

    def to_dict(self) -> dict:
        """
        Return this structure as dictionary
        """
        return self.__ms2d(self)

    def copy(self) -> 'Struct':
        """
        Returns copy of this structure
        """
        return Struct(self.to_dict())

    @staticmethod
    def dict2struct(d: dict) -> 'Struct':
        """
        Convert dictionary to structure
        >>> s = dict2struct({'f_1_0': 1, 'z': {'x': 1, 'y': 2}})
        >>> print(s.z.x)
        1
        """
        m = Struct()
        for k, v in d.items():
            # skip if key is not valid identifier
            if not k.isidentifier():
                print(f"Struct> {k} doesn't look like as identifier - skip it")
                continue
            if isinstance(v, dict):
                v = Struct.dict2struct(v)
            m.__setattr__(k, v)
        return m


def makedirs(path: str, *args) -> str:
    path = os.path.expanduser(os.path.join(*[path, *args]))
    if not exists(path):
        os.makedirs(path)
    return path