import json
import re
from pathlib import Path

from src.engines.core.base_engine import *

logger = logging.getLogger()

_creationflags = subprocess.CREATE_NO_WINDOW if platform.system() == 'Windows' else 0


class Blender(BaseRenderEngine):

    install_paths = ['/Applications/Blender.app/Contents/MacOS/Blender']
    binary_names = {'linux': 'blender', 'windows': 'blender.exe', 'macos': 'Blender'}

    @staticmethod
    def downloader():
        from src.engines.blender.blender_downloader import BlenderDownloader
        return BlenderDownloader

    @staticmethod
    def worker_class():
        from src.engines.blender.blender_worker import BlenderRenderWorker
        return BlenderRenderWorker

    def ui_options(self):
        options = [
            {'name': 'engine', 'options': self.supported_render_engines()},
            {'name': 'render_device', 'options': ['Any', 'GPU', 'CPU']},
        ]
        return options

    def supported_extensions(self):
        return ['blend']

    def version(self):
        version = None
        try:
            render_path = self.engine_path()
            if render_path:
                ver_out = subprocess.check_output([render_path, '-v'], timeout=SUBPROCESS_TIMEOUT,
                                                  creationflags=_creationflags)
                version = ver_out.decode('utf-8').splitlines()[0].replace('Blender', '').strip()
        except Exception as e:
            logger.error(f'Failed to get Blender version: {e}')
        return version

    def get_output_formats(self):
        format_string = self.get_help().split('Format Options')[-1].split('Animation Playback Options')[0]
        formats = re.findall(r"'([A-Z_0-9]+)'", format_string)
        return formats

    def run_python_expression(self, project_path, python_expression, timeout=None):
        if os.path.exists(project_path):
            try:
                return subprocess.run([self.engine_path(), '-b', project_path, '--python-expr', python_expression],
                                      capture_output=True, timeout=timeout, creationflags=_creationflags)
            except Exception as e:
                err_msg = f"Error running python expression in blender: {e}"
                logger.error(err_msg)
                raise ChildProcessError(err_msg)
        else:
            raise FileNotFoundError(f'Project file not found: {project_path}')

    def run_python_script(self, script_path, project_path=None, timeout=None):

        if project_path and not os.path.exists(project_path):
            raise FileNotFoundError(f'Project file not found: {project_path}')
        elif not os.path.exists(script_path):
            raise FileNotFoundError(f'Python script not found: {script_path}')

        try:
            command = [self.engine_path(), '-b', '--python', script_path]
            if project_path:
                command.insert(2, project_path)
            result = subprocess.run(command, capture_output=True, timeout=timeout, creationflags=_creationflags)
            return result
        except subprocess.TimeoutExpired:
            err_msg = f"Timed out after {timeout}s while running python script in blender: {script_path}"
            logger.error(err_msg)
            raise TimeoutError(err_msg)
        except Exception as e:
            err_msg = f"Error running python script in blender: {e}"
            logger.error(err_msg)
            raise ChildProcessError(err_msg)

    def get_project_info(self, project_path, timeout=10):
        scene_info = {}
        try:
            script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_file_info.py')
            results = self.run_python_script(project_path=project_path, script_path=Path(script_path),
                                             timeout=timeout)
            result_text = results.stdout.decode()
            for line in result_text.splitlines():
                if line.startswith('SCENE_DATA:'):
                    raw_data = line.split('SCENE_DATA:')[-1]
                    scene_info = json.loads(raw_data)
                    break
                elif line.startswith('Error'):
                    logger.error(f"get_scene_info error: {line.strip()}")
        except Exception as e:
            msg = f'Error getting file details for .blend file: {e}'
            logger.error(msg)
            raise ChildProcessError(msg)
        return scene_info

    def pack_project_file(self, project_path, timeout=None):
        # Credit to L0Lock for pack script - https://blender.stackexchange.com/a/243935
        try:
            logger.info(f"Starting to pack Blender file: {project_path}")
            script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'pack_project.py')
            results = self.run_python_script(project_path=project_path, script_path=Path(script_path),
                                             timeout=timeout)

            result_text = results.stdout.decode()
            dir_name = os.path.dirname(project_path)

            # report any missing textures
            not_found = re.findall("(Unable to pack file, source path .*)\n", result_text)
            for err in not_found:
                raise ChildProcessError(err)

            p = re.compile('Saved to: (.*)\n')
            match = p.search(result_text)
            if match:
                new_path = os.path.join(dir_name, match.group(1).strip())
                logger.info(f'Blender file packed successfully to {new_path}')
                return new_path
            return project_path
        except Exception as e:
            msg = f'Error packing .blend file: {e}'
            logger.error(msg)
            raise ChildProcessError(msg)
        return None

    def get_arguments(self):
        help_text = subprocess.check_output([self.engine_path(), '-h'], creationflags=_creationflags).decode('utf-8')
        lines = help_text.splitlines()

        options = {}
        current_category = None
        current_option = None

        for line in lines:
            line = line.strip()
            # Check if line starts with - or --, indicating a new option
            if line.endswith('Options:'):
                current_category = line.split('Options')[0].strip()
                options[current_category] = {}
            elif line.startswith("-") or line.startswith("/"):
                parts = line.split(' or ')
                flag = parts[-1]  # Choose the verbose option
                has_argument = '<' in flag and '>' in flag
                flag = flag.split(' <')[0]  # Remove argument placeholder
                current_option = flag.strip("--") or flag
                options[current_category][current_option] = {
                    'flag': flag, 'description': '', 'takes_argument': has_argument
                }
            elif line == "" and current_option is not None:
                current_option = None
            elif current_option is not None:
                d = options[current_category][current_option]['description']
                d = d + (' ' if d else '') + line
                options[current_category][current_option]['description'] = d

        return options

    def system_info(self):
        return {'render_devices': self.get_render_devices(), 'engines': self.supported_render_engines()}

    def get_render_devices(self):
        script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'get_system_info.py')
        results = self.run_python_script(script_path=script_path)
        output = results.stdout.decode()
        match = re.search(r"GPU DATA:(\[[\s\S]*\])", output)
        if match:
            gpu_data_json = match.group(1)
            gpus_info = json.loads(gpu_data_json)
            return gpus_info
        else:
            logger.error("GPU data not found in the output.")

    def supported_render_engines(self):
        engine_output = subprocess.run([self.engine_path(), '-b', '-E', 'help'], timeout=SUBPROCESS_TIMEOUT,
                                       capture_output=True, creationflags=_creationflags).stdout.decode('utf-8').strip()
        render_engines = [x.strip() for x in engine_output.split('Blender Engine Listing:')[-1].strip().splitlines()]
        return render_engines

    def perform_presubmission_tasks(self, project_path):
        packed_path = self.pack_project_file(project_path, timeout=120)
        return packed_path


if __name__ == "__main__":
    x = Blender().get_render_devices()
    print(x)
