import logging
import os
import platform
import subprocess
from typing import Optional, List, Dict, Any, Type

logger = logging.getLogger()
SUBPROCESS_TIMEOUT = 10


class BaseRenderEngine(object):
    """Base class for render engines. This class provides common functionality and structure for various rendering
    engines. Create subclasses and override the methods marked below to add additional engines

    Attributes:
        install_paths (list): A list of default installation paths where the render engine
        might be found. This list can be populated with common paths to help locate the
        executable on different operating systems or environments.
    """

    install_paths: List[str] = []

    # --------------------------------------------
    # Required Overrides for Subclasses:
    # --------------------------------------------

    def __init__(self, custom_path: Optional[str] = None) -> None:
        """Initialize the render engine.

        Args:
            custom_path: Optional custom path to the engine executable.
            
        Raises:
            FileNotFoundError: If the engine executable cannot be found.
        """
        self.custom_engine_path: Optional[str] = custom_path
        if not self.engine_path() or not os.path.exists(self.engine_path()):
            raise FileNotFoundError(f"Cannot find path to engine for {self.name()} instance: {self.engine_path()}")

        if not os.access(self.engine_path(), os.X_OK):
            logger.warning(f"Path is not executable. Setting permissions to 755 for {self.engine_path()}")
            os.chmod(self.engine_path(), 0o755)

    def version(self) -> str:
        """Return the version number as a string.

        Returns:
            str: Version number.

        Raises:
            NotImplementedError: If not overridden.
        """
        raise NotImplementedError(f"version not implemented for {self.__class__.__name__}")

    def get_project_info(self, project_path: str, timeout: int = 10) -> Dict[str, Any]:
        """Extracts detailed project information from the given project path.

        Args:
            project_path (str): The path to the project file.
            timeout (int, optional): The maximum time (in seconds) to wait for the operation. Default is 10 seconds.

        Returns:
            dict: A dictionary containing project information (subclasses should define the structure).

        Raises:
            NotImplementedError: If the method is not overridden in a subclass.
        """
        raise NotImplementedError(f"get_project_info not implemented for {self.__class__.__name__}")

    @classmethod
    def get_output_formats(cls) -> List[str]:
        """Returns a list of available output formats supported by the engine.

        Returns:
            list[str]: A list of strings representing the available output formats.
        """
        raise NotImplementedError(f"get_output_formats not implemented for {cls.__name__}")

    @staticmethod
    def worker_class() -> Type[Any]:  # override when subclassing to link worker class
        raise NotImplementedError("Worker class not implemented")

    # --------------------------------------------
    # Optional Overrides for Subclasses:
    # --------------------------------------------

    def supported_extensions(self) -> List[str]:
        """Return a list of file extensions supported by this engine.

        Returns:
            List[str]: List of supported file extensions (e.g., ['.blend', '.mp4']).
        """
        return []

    def get_help(self) -> str:
        """Retrieves the help documentation for the engine.

        This method runs the engine's help command (default: '-h') and captures the output.
        Override this method if the engine uses a different help flag.

        Returns:
            str: The help documentation as a string.

        Raises:
            FileNotFoundError: If the engine path is not found.
        """
        path = self.engine_path()
        if not path:
            raise FileNotFoundError(f"Engine path not found: {path}")
        creationflags = subprocess.CREATE_NO_WINDOW if platform.system() == 'Windows' else 0
        help_doc = subprocess.check_output([path, '-h'], stderr=subprocess.STDOUT,
                                           timeout=SUBPROCESS_TIMEOUT, creationflags=creationflags).decode('utf-8')
        return help_doc

    def system_info(self) -> Dict[str, Any]:
        """Return additional information about the system specfic to the engine (configured GPUs, render engines, etc)

        Returns:
            dict: A dictionary with engine-specific system information
        """
        return {}

    def perform_presubmission_tasks(self, project_path: str) -> str:
        """Perform any pre-submission tasks on a project file before uploading it to a server (pack textures, etc.)

        Override this method to:
            1. Copy the project file to a temporary location (DO NOT MODIFY ORIGINAL PATH).
            2. Perform additional modifications or tasks.
            3. Return the path to the modified project file.

        Args:
            project_path (str): The original project file path.

        Returns:
            str: The path to the modified project file.
        """
        return project_path

    def get_arguments(self) -> Dict[str, Any]:
        """Return command-line arguments for this engine.

        Returns:
            Dict[str, Any]: Dictionary of argument specifications.
        """
        return {}

    @staticmethod
    def downloader() -> Optional[Any]:
        """Return the downloader class for this engine.

        Returns:
            Optional[Any]: Downloader class instance, or None if no downloader is used.
        """
        return None

    def ui_options(self) -> Dict[str, Any]:
        """Return UI configuration options for this engine.

        Returns:
            Dict[str, Any]: Dictionary of UI options and their configurations.
        """
        return {}

    # --------------------------------------------
    # Do Not Override These Methods:
    # --------------------------------------------

    def engine_path(self) -> Optional[str]:
        """Return the path to the engine executable.

        Returns:
            Optional[str]: Path to the engine executable, or None if not found.
        """
        return self.custom_engine_path or self.default_engine_path()

    @classmethod
    def name(cls) -> str:
        """Return the name of this engine.

        Returns:
            str: Engine name in lowercase.
        """
        return str(cls.__name__).lower()

    @classmethod
    def default_engine_path(cls) -> Optional[str]:
        """Find the default path to the engine executable.

        Returns:
            Optional[str]: Default path to the engine executable, or None if not found.
        """
        path: Optional[str] = None
        try:  # Linux and macOS
            path = subprocess.check_output(['which', cls.name()], timeout=SUBPROCESS_TIMEOUT).decode('utf-8').strip()
        except (subprocess.CalledProcessError, FileNotFoundError):
            for p in cls.install_paths:
                if os.path.exists(p):
                    path = p
        except Exception as e:
            logger.exception(e)
        return path
