try:
    import numpy as np
except ImportError as e:
    import sys
    sys.stderr.write(f"An error occurred while importing numpy: {str(e)}\n")
    del sys

try:
    import os
except ImportError as e:
    import sys
    sys.stderr.write(f"An error occurred while importing os: {str(e)}\n")
    del sys

try:
    from datetime import datetime
except ImportError as e:
    import sys
    sys.stderr.write(f"An error occurred while importing datetime: {str(e)}\n")
    del sys

try:
    import json
except ImportError as e:
    import sys
    sys.stderr.write(f"An error occurred while importing datetime: {str(e)}\n")
    del sys

class FileManager:
    def __init__(self, file_location:str=None, name:str=None):
        if name is not None and not isinstance(name, str):
            raise TypeError("(!) name must be a string")
        self._name = name
        self._metaData = {}
        self._comment = ''
        if file_location is not None and not isinstance(file_location, str):
            raise TypeError("(!) file_location must be a string")
        self._file_location = file_location   
        self._original_file_location = None
        
        #self._FM_attrs = set(vars(self).keys())
        
    def __getattr__(self, name):
        # Check if the attribute exists with a leading underscore
        private_name = f"_{name}"
        if private_name in self.__dict__:
            return getattr(self, private_name)
        else:
            raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        # Check if the attribute exists with a leading underscore
        private_name = f"_{name}"
        if private_name in self.__dict__:
            setattr(self, private_name, value)
        else:
            super().__setattr__(name, value)
            
    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("(!) name must be a string")
        self._name = value

    @name.deleter
    def name(self):
        print("Deleting name")
        del self._name

    @property
    def file_location(self):
        return self._file_location

    @file_location.setter
    def file_location(self, value):
        if not isinstance(value, str):
            raise TypeError("(!) file_location must be a string")
        self._file_location = value
        if self._original_file_location == None: 
            self._original_file_location = self._file_location

    @file_location.deleter
    def file_location(self):
        print("Deleting file_location")
        del self._file_location

    @staticmethod
    def is_number(s):
        try:
            float(s)
            return True
        except ValueError:
            return False

    def isINT(self, num): return self.is_number(num) and abs(num - int(num)) < 0.0001 

    def is_close(self, point_a, point_b, threshold:float=1e-5) -> bool:
        """Check if two points are close to each other within a specified threshold."""
        return np.linalg.norm(point_a - point_b) < threshold

    def file_exists(self, file_path:str) -> bool:
        """Check if a file exists."""
        return os.path.exists(file_path)

    def create_directories_for_path(self, path:str):
        """
        Create any directories needed to ensure that the given path is valid.

        Parameters:
        - path (str): The file or directory path to validate.
        """
        if not os.path.exists(path):
            try:
                os.makedirs(path, exist_ok=True)
            except PermissionError:
                print(f"Permission denied: Could not create directory at {path}")
                # Exit or handle the error appropriately
                exit(1)

    def read_file(self, file_location:str=None, strip=True):
        file_location = file_location if type(file_location) == str else self._file_location

        if file_location is None:
            raise ValueError("(!) File location is not set.")
        
        try:
            with open(file_location, 'r') as file:
                if strip:
                    for line in file:
                        yield line.strip()  # strip() elimina espacios y saltos de línea al principio y al final
                else:
                    for line in file:
                        yield line  # strip() elimina espacios y saltos de línea al principio y al final
        except FileNotFoundError:
            raise FileNotFoundError(f"(!) File not found at {file_location}")
        except IOError:
            raise IOError(f"(!) Error reading file at {file_location}")
        except Exception as e:
            print(f"Error inesperado: {e}")

    def save_to_json(self, data, file_path:str) -> bool:
        """
        Save data to a JSON file.

        Args:
            data (dict): The data to be saved as a Python dictionary.
            file_path (str): The path to the JSON file where the data will be saved.

        Returns:
            bool: True if the data was successfully saved, False otherwise.

        Raises:
            FileNotFoundError: If the directory specified in 'file_path' does not exist.
            IOError: If there was an issue writing the data to the JSON file.

        Example:
            data = {"name": "John", "age": 30, "city": "New York"}
            success = save_to_json(data, "data.json")
            if success:
                print("Data saved successfully.")
            else:
                print("Failed to save data.")
        """
        try:
            # Serialize the data as JSON and write it to the file
            with open(file_path, 'w') as json_file:
                json.dump(data, json_file, indent=4)
            return True
        except FileNotFoundError:
            raise FileNotFoundError("The directory specified in 'file_path' does not exist.")
        except MemoryError:
            print("Memory Error occurred")
        except IOError as e:
            print(f"There was an issue writing the data to the JSON file. I/O error({e.errno}): {e.strerror}")
        except Exception as e:
            print(f"Unexpected error: {e}")

        return True

    def loadStoreManager(self, manager_class, file_name:str, attribute_name: str, read_method:str, v:bool=False):
        """Generic function to read a VASP file."""
        file_path = f"{self.file_location}/{file_name}"
        if not self.file_exists(file_path):
            return
        #try:
        manager = manager_class(file_path)
        getattr(manager, read_method)()  # Call the appropriate read method
        setattr(self, attribute_name, manager)
        self._loaded[file_name] = True
        if v:
            print(f'File readed : {file_name}')
        #except Exception as e:
        #    if v:
        #        print(f"ERROR :: Cannot load {file_name}. Reason: {e}")

    def copy(self, file_location:str=None, original_file_location:str=None, buffer_size=1024*1024) -> bool:
        """
        Universal copy method that chooses an available copy method.
        
        Parameters:
        - dest_path (str): The destination path where the file will be copied.
        - buffer_size (int): The buffer size for manual copy if needed.
        
        Returns:
        - bool: True if the file was successfully copied, otherwise False.
        """
        file_location  = file_location if file_location else self.file_location
        original_file_location  = original_file_location if original_file_location else self.original_file_location

        if original_file_location is None:
            print("Original file path is not set.")
            return False
        try:
            os.system(f"cp {original_file_location} {file_location}")
            return True
        except Exception as e:
            if verbosity: print(f"An error occurred while copying the file: {e}")
            return False 

    def copy_file_manual(self, file_location:str=None, original_file_location:str=None, buffer_size:int=1024*1024) -> bool:
        """
        Manually copy a file in a memory-efficient manner.
        
        Parameters:
        - dest_path (str): The destination path where the file will be copied.
        - buffer_size (int): The buffer size for the copy operation.
        
        Returns:
        - bool: True if the file was successfully copied, otherwise False.
        """
        try:
            with open(self.original_file_path, 'rb') as src, open(dest_path, 'wb') as dest:
                chunk = src.read(buffer_size)
                while chunk:
                    dest.write(chunk)
                    chunk = src.read(buffer_size)
            return True
        except Exception as e:
            print(f"An error occurred while copying the file: {e}")
            return False

    def _copy_shared_attributes(self, shared_attributes, source, destination):
        """
        Copies shared attributes from the source object to the destination object.

        :param shared_attributes: List of attribute names to be copied.
        :param source: The source object from which to copy attributes.
        :param destination: The destination object to which attributes are copied.
        """
        for attr in shared_attributes:
            if hasattr(source, attr):
                setattr(destination, attr, getattr(source, attr))

