# AUTOGENERATED! DO NOT EDIT! File to edit: 03_Structure.ipynb (unless otherwise specified).

__all__ = ['MetaID', 'MongoObj']

# Cell
from bson.objectid import ObjectId
import warnings
import pandas as pd

# for storing the data localy
import kutility.core
import json
import os

from math import isnan

class MetaID(type):
    """
        Reference MongoObj with the same unique ID of MongoDB : ObjectId
    """

    _register={} # { ObjectId('str(ObjectID)'): obj }

    # ------- Data ------

    @property
    def data(cls):
        """
            Pandas Dataframe representation of the data
        """
        l_data=[obj.__Mongo_dict__ for obj in cls]
        if l_data :
            df=pd.DataFrame(l_data)
            df=df.set_index('_id',drop=False)
            df['generation_time_utc']=df._id.apply(lambda _id : ObjectId(_id).generation_time)
            return(df)

    #       Basic data queries for joining data beween classes
    def data_group_by(cls,key):
        df=cls.data
        key_ids=df[key].unique()
        return({key_id:df[df[key]==key_id] for key_id in key_ids})

    def last_data_group_by(cls,key):
        """
            last data available unique for the key
        """
        temp={key_id : df.tail(1) for key_id,df in cls.data_group_by(key).items()}
        df=pd.concat(temp.values()).set_index(key,drop=False)
        return(df)


    def add_data(cls,data,uniq_keys=['url']):
        """
            Add data from a pandas table without duplicates on uniq_keys
        """

        data_registered=cls.data

        # Approach chose : concatenation of pandas data ; suppress the right duplicates ; create the new offers

        if data_registered is None : # keep the most recent offer if no data is registered
            temp_data=pd.concat([data]).drop_duplicates(subset=uniq_keys, keep='first')
            temp_data['_id']=None
        else : # keep the first offer registered (thus the 'last' in term of time) if there is duplicates
            temp_data=pd.concat([data,data_registered]).drop_duplicates(subset=uniq_keys,keep='last')

        # Creating the new instances
        new_data=temp_data['_id'].isna() # mask on new data (not having an _id)

        l_new_data=[]
        for id_index,row in temp_data[new_data].iterrows() :
            row=dict(row)

            # -- Suppressing row[key] : 'nan'
            del_keys=[]
            for key , value in row.items():
                if str(value) in ['nan','NaT']:
                    del_keys.append(key)
            for key in del_keys :
                del row[key]


            l_new_data.append(cls(**row))


        return(l_new_data)

    # ______ Basic data queries

    # _______ Data ________


    def from_id(cls,_id): # could be define @classmethod in MongoObj class
        """
            Retrieve a live instance of MongoObj from its _id
        """
        def r_search(l_classes=[cls]):
            """
                Look in every register of subclasses recurssively
            """
            if l_classes :
                for cls_ in l_classes :
                    obj=cls_._register.get(ObjectId(_id))
                    if obj :
                        return(obj)
                    return(r_search(cls_.__subclasses__()))

        return(r_search())


    # ---- For iteration ----

    def invariant_lecture(cls,objs):
        """
            Method to overide for iterate according a lecture method (sort, not taking into account certain class)
        """
        return(objs)


    def __iter__(cls):
        objs=list(cls._register.values()).copy()
        return(iter(cls.invariant_lecture(objs)))

    # ______ For iteration _______

    def __len__(cls):
        """ Length of the class equal to the lenght of iterations THUS see sensible to defintion of invariant_lecture classmethod
        """
        return(len(cls._register))


    # ----- Storing the MongoObj localy ----

    def json_dumps(cls,folder_path='.dev'):

        file_name=f'{folder_path}/{cls.__name__}.json'

        kutility.core.create_file_path(file_name) # ensure that the folder structure to create the file is created


        with open(file_name,'w', encoding='utf-8') as f :
            json.dump([obj.__Mongo_dict__ for obj in cls]
                       ,f, ensure_ascii=False, indent=4)

    def json_loads(cls,folder_path='.dev',globals_=globals(),file_name=None):

        if not file_name :
            file_name=f'{folder_path}/{cls.__name__}.json'

        if not os.path.exists(file_name) :
            warnings.warn(f'Loading of {cls.__name__} failed ! no file {file_name}')
            return

        with open(file_name,'r') as f :
            data=json.load(f)
        for obj_doc in data :
            cls.create_instance(obj_doc,globals_)

    #_______ Storing the MongoObj localy _____





class MongoObj(object,metaclass=MetaID):

    def __new__(cls,_id=None,**kargs):

        if kargs.get('python_type'):
            if not kargs['python_type']==str(cls.__name__) :
                raise Exception(f"Try to load {kargs['python_type']} in obj of type {str(cls.__name__)}")

        if _id :
            _id=ObjectId(str(_id))

        instance=cls.from_id(_id)

        if not instance :
            instance=super().__new__(cls)
            instance._id=_id if _id else ObjectId()
            cls._register[instance._id]=instance

        return instance

    def init_att(self,att_name,att_value):
        """
            Rigth way to initialize attribute with not None value accordingly to a scheme especialy when they come from **rows
        """
        if att_value is not None : # Load new values (not None) of the data
            self.__dict__[att_name]=att_value
        if att_name not in self.__dict__ : # Look if the attribute is created and create it with none if necessary
            self.__dict__[att_name]=None

    @classmethod
    def invariant_lecture(cls,objs): # Overide of the MetaID
        """
            iterate according a lecture method :
                * only instance of this class (class or sub-classes)
                * sort according to generation_time of MongoObj
        """
        # ensure to iterate only on istance of the class (class and all sub-classes)
        objs=[obj for obj in objs if isinstance(obj,cls)]
        # sorte the iteration accoriding to generation_time
        objs=sorted(objs,key= lambda obj : obj.generation_time)
        return(objs)

    @classmethod
    def create_instance(cls,mongo_doc,globals_=None):
        """
            Dynamicaly create instance from a stored __Mongo_dict__ dictionary (stored in json file or mongodb)
        """
        # kclass=globals()["class_name"] ; instance=kclass()
        if not globals_ :
            globals_=globals()

        return(globals_[mongo_doc.get('python_type')](**mongo_doc))

    @property
    def __Mongo_dict__(self):
        """
            Storable version of the MongoObj (it replace in particular every reference to OTHER MongoObj by there _id)
        """

        def func(obj):
            # IF the object have an _id THEN replace the obj by a reference to its _id
            if isinstance(obj,ObjectId):
                return(str(obj))

            if isinstance(obj,MongoObj):
                warnings.warn('MongoObj should not contain other refrence them as ObjectId and use MongoObj()')
                return(str(obj._id))

            # IF Not an insatance of MongoObj implement __Mongo__dict___
            if isinstance(obj,object):
                if '__Mongo_dict__' in type(obj).__dir__(obj):
                    return(obj.__Mongo_dict__)

            return(obj)


        def rec_dumps(obj,func=func):
            if isinstance(obj,dict) :
                return({rec_dumps(key):rec_dumps(value) for key,value in obj.items()})
            elif isinstance(obj,list) :
                return([rec_dumps(e) for e in obj])
            else :
                return(func(obj))

        d=self.__dict__.copy()
        d['python_type']=str(type(self).__name__)

        return(rec_dumps(d))

    @property
    def generation_time(self):
        """
            Return the generation time of the obj from the one of its _id : ObjectId
        """
        return(ObjectId(self._id).generation_time)


