# Created by: Ausar686
# https://github.com/Ausar686

# from typing import Any, Union, Tuple, Iterable
# from datetime import datetime

# import pandas as pd
# from pandas.core.frame import DataFrame
# from pandas.core.series import Series


class BaseStorage:
    pass
#     _base_columns = ["date", "time", "datetime"]
#     _base_indexed_columns = ["date"]
#     _base_multivalue_columns = []
#     _columns = []
#     _indexed_columns = []
#     _multivalue_columns = []
    
#     def __init__(self, df: Union[DataFrame,str]=None):
#         self._index_col = "index"
#         self._columns.extend(self._base_columns)
#         self._indexed_columns.extend(self._base_indexed_columns)
#         self._multivalue_columns.extend(self._base_multivalue_columns)
#         if df is None:
#             self.data = pd.DataFrame(columns=self._columns)
#             return
#         elif isinstance(df, str):
#             self.from_csv(df)
#         else:
#             self.data = df.copy()
#         extra_columns = [column for column in self.data.columns if column not in self._columns]
#         self.data.drop(extra_columns, axis=1, inplace=True)
#         for column in self._columns:
#             if column == "datetime":
#                 self.data["datetime"] = pd.to_datetime(self.data["date"] + ' ' + self.data["time"])
#             if column not in self.data.columns:
#                 self.data[column] = None
#         return
    
#     def create(self, dct: dict) -> dict:
#         note = {key: self.iter_to_str(value) if key in self._multivalue_columns
#                 else value for key, value in dct.items()}
#         now = datetime.now()
#         note["date"] = now.date().strftime("%Y-%m-%d")
#         note["time"] = now.time().strftime("%H:%M")
#         note["datetime"] = note["date"] + ' ' + note["time"]
#         return note
    
#     def add(self, note: dict) -> None:
#         new_row = pd.Series(note)
#         try:
#             index = self.data.index[-1] + 1
#         except IndexError:
#             index = 0
#         self.data.loc[index] = new_row
#         return
    
#     def append(self, dct: dict) -> None:
#         # A convinient way to add a new note from the dict to the storage.
#         # This method consequently performs 'create' and 'add' methods.
#         note = self.create(dct)
#         self.add(note)
#         return
    
#     def delete(self, index: int) -> None:
#         try:
#             self.data.drop(index, inplace=True)
#         except KeyError:
#             print(f"[INFO]: No row with index {index} found. Ignored.")
#         return
    
#     def edit(self, index: int=None, note: Union[dict, Series]=None) -> None:
#         if index is None and note is None:
#             return
#         # If index is None, we are trying to edit a specific row, metioned in note
#         if index is None: 
#             index = note.name
#         if index not in self.data.index:
#             print(f"[INFO]: No row with index {index} found. Ignored.")
#             return
#         for key in note:
#             if key not in self._columns:
#                 continue
#             value = note[key]
#             if key in self._multivalue_columns and self.is_nonstr_iterable(value):
#                 value = self.iter_to_str(value)
#             self.data.loc[index, key] = value
#         return
    
#     def _base_get(self, 
#                   *, 
#                   by: str,
#                   value: Any=None,
#                   search_type: str="union",
#                   date_search: str="precise") -> Tuple[bool, Union[DataFrame, Series]]:
#         # Performs basic search, which is similar to all subclasses.
#         # 'value' is not specified. Return all data 
#         if value is None:
#             return True, self.data
#         # Search by index via 'loc' method
#         elif by == "index":
#             return True, self.data.loc[value]
#         elif by == "date":
#             if date_search == "precise":
#                 return True, self._eq_search(by, value)
#             elif date_search == "between":
#                 date_start = value[0]
#                 date_end = value[1]
#                 return True, self._date_search(date_start, date_end)
#             else:
#                 raise ValueError("Parameter 'date_search' must be either 'precise' or 'between'.")
#         # 'by' is either a string or non-iterable object
#         # and 'value' is a non-string iterable object
#         elif not self.is_nonstr_iterable(by) and self.is_nonstr_iterable(value):
#             if search_type not in ["union", "intersection"]:
#                 raise ValueError("Parameter 'search_type' must be either 'union' or 'intersection'.")
#             res_idx = None
#             for i, value_ in enumerate(value):
#                 # Iterate over values in 'value'
#                 # And apply index union/intersection among results
#                 # The type of operation depends on 'search_type' parameter
#                 current_res = self.get(by=by, value=value_)
#                 if i == 0:
#                     res_idx = current_res.index
#                 else:
#                     res_idx = res_idx.__getattribute__(search_type)(current_res.index)
#             # Return rows with indexes in 'res_idx'
#             return True, self.data.loc[res_idx]
#         elif self.is_nonstr_iterable(by):
#             # Group arguments into pairs column-value
#             pairs = zip(by, value)
#             res_idx = None
#             for i, pair in enumerate(pairs):
#                 # Iterate over pairs in zip
#                 # And apply index intersection among results
#                 by_, value_ = pair
#                 current_res = self.get(by=by_, value=value_)
#                 if i == 0:
#                     res_idx = current_res.index
#                 else:
#                     res_idx = res_idx.intersection(current_res.index)
#             # Return rows with indexes in 'res_idx'
#             return True, self.data.loc[res_idx]
# #             raise NotImplementedError("Multiple search will be implemented in upcoming updates.")
#         elif by not in self._indexed_columns:
#             raise ValueError("Invalid search argument.")
#         return False, None
            
#     def _eq_search(self, column: str, value: Any) -> DataFrame:
#         # Columnwise equality-based search.
#         return self.data[self.data[column]==value]
    
#     def _substr_search(self, column: str, value: Any) -> DataFrame:
#         # Return all rows, where the following column cell
#         return self.data[self.data[column].str.contains(value)]
    
#     def _date_search(self, date_start: str, date_end: str):
#         # Return all rows, where date is in between 'date_start' and 'date_end'
#         return self.data[(date_start <= self.data.date) & (self.data.date <= date_end)]
    
#     def get(self, 
#             *,
#             by: str,
#             value: Any=None,
#             search_type: str="union",
#             date_search: str="precise") -> DataFrame:
#         # Define a 'get' method for subclasses.
#         # Implementation depends on a specific subclass.
#         # It is strongly recommended to use '_base_get' method
#         # in the beginning of 'get' method in any subclass
#         pass
    
#     @staticmethod
#     def is_iterable(obj: Any) -> bool:
#         try:
#             iter(obj)
#             return True
#         except TypeError:
#             return False
        
#     @classmethod
#     def is_nonstr_iterable(cls, obj: Any) -> bool:
#         return cls.is_iterable(obj) and not isinstance(obj, str)
    
#     @classmethod
#     def iter_to_str(cls, lst: Iterable[str]) -> str:
#         if cls.is_nonstr_iterable(lst):
#             return '|'.join(lst)
#         return lst

#     def from_csv(self, path: str) -> None:
#         self.data = pd.read_csv(path, index_col=self._index_col)
#         return
    
#     def to_csv(self, path: str) -> None:
#         self.data.drop_duplicates(self._columns, keep="last").to_csv(path, index_label=self._index_col)
#         return
    
#     def load(self, path: str) -> None:
#         # An alias for 'from_csv' method for standartized interface
#         self.from_csv(path)
#         return
    
#     def to_disk(self, path: str) -> None:
#         # An alias for 'to_csv' method for standartized interface
#         self.to_csv(path)
#         return
        
#     def __repr__(self):
#         return repr(self.data)
    
#     def __str__(self):
#         return str(self.data)