from typing import List
import requests
from pathlib import Path
from io import BytesIO
from zipfile import ZipFile
from cad_parser import exocad, threeshape
from cad_parser.definitions import Order, Element, Patient, Tooth

import logging
logger = logging.getLogger(__name__)

class Project(object):
    '''
    Represents a project folder.
    '''
    def __init__(self, projectPath: Path):
        '''
        Parameters
        ----------
        projectPath: Path
            The path to the project folder

        Raises
        ------
        FileNotFoundError:
            If either the project folder, a `.constructionInfo` file, or `.dentalProject` file does no exists inside the project folder.
        '''
        if not projectPath.exists() or not projectPath.is_dir():
            logger.error(f'Project folder "{projectPath.absolute()}" does not exists or is not a folder.')
            raise FileNotFoundError(f'Project folder "{projectPath.absolute()}" does not exists or is not a folder.')
        self._projectPath = projectPath

        # default variables
        self._dentalProjectPath = None
        self._constructionInfoPath = None
        self._sceneFilePath = None
        self._lowerJaw = None
        self._upperJaw = None
        self._scanInfoPath = None
        self._modelInfoPath = None
        self._partialInfoPath = None
        self._threeshapePath = None

        logger.info(f'Project location: {self._projectPath}')
        cadOrder = self.parseProject(projectPath)

        self.order = self.createOrder(cadOrder)

        self.filesWithinProjectFolder = self._getFileList()
        self.trackedFiles = [projectPath / element['file'] for element in self.order['elements']]
        self.trackedFiles += [projectPath / model['file'] for model in self.order['models']]
        self.untrackedFiles = [
            file for file in self.filesWithinProjectFolder 
            if file not in self.trackedFiles 
            and file.suffix == '.stl'
            and file != self._lowerJaw 
            and file != self._upperJaw
        ]

    @property
    def projectPath(self) -> Path:
        return self._projectPath
    
    @property
    def dentalProjectPath(self) -> Path:
        return self._dentalProjectPath
    
    @property
    def constructionInfoPath(self) -> Path:
        return self._constructionInfoPath
    
    @property
    def sceneFilePresent(self) -> bool:
        return self._sceneFilePath is not None and ( self._lowerJaw is not None or self._upperJaw is not None)
    
    @property
    def sceneFilePath(self) -> Path:
        return self._sceneFilePath

    @property
    def lowerJaw(self) -> Path:
        return self._lowerJaw
    
    @property
    def upperJaw(self) -> Path:
        return self._upperJaw
    
    @property
    def scanInfoPath(self) -> Path:
        return self._scanInfoPath
    
    @property
    def modelInfoPath(self) -> Path:
        return self._modelInfoPath
    
    @property
    def partialInfoPath(self) -> Path:
        return self._partialInfoPath
    
    @property
    def threeshapePath(self) -> Path:
        return self._threeshapePath
    
    def parseProject(self, projectPath: Path) -> Order:
        if exocad.is_supported(projectPath):
            dentalProjectPath, scanInfoPath, modelInfoPath, partialInfoPath, constructionInfoPath, cadOrder = exocad.parse(projectPath)
            self._dentalProjectPath = dentalProjectPath
            self._scanInfoPath = scanInfoPath
            self._modelInfoPath = modelInfoPath
            self._partialInfoPath = partialInfoPath
            self._constructionInfoPath = constructionInfoPath

            if cadOrder.loaded_scene_file:
                self._sceneFilePath = self._projectPath / cadOrder.loaded_scene_file
                for scanFile in cadOrder.scan_files:
                    if scanFile.type == 'UnsegmentedJaw':
                        self._lowerJaw = self._projectPath / scanFile.file_name
                    if scanFile.type == 'Antagonist':
                        self._upperJaw = self._projectPath / scanFile.file_name

                if not self._lowerJaw and not self._upperJaw:
                    logger.error(f'Project folder "{projectPath.absolute()}" does not has a "lowerJaw" or "upperJaw" file althought a scene file is present.')
                    raise FileNotFoundError(f'Project folder "{projectPath.absolute()}" does not has a "lowerJaw" or "upperJaw" file althought a scene file is present.')

            return cadOrder
        
        if threeshape.is_supported(projectPath):
            threeshapePath, cadOrder = threeshape.parse(projectPath)
            self._threeshapePath = threeshapePath
            return cadOrder
        
        raise NotImplementedError(f'Project `{projectPath}` is not supported.')
    
    def createOrder(self, cadOrder: Order) -> None:
        order =  {
            'id': None,
            'orderNumber': 0,
            'projectName': cadOrder.project,
            'deliveryType': None,
            'comment': cadOrder.comment,
            'creationMethod': 'client',
            'fastline': False,
            'alternativeDeliveryAddress': None,
            'elements': [],
            'models': [],
            'isAIO': None
        }
        for cadElement in cadOrder.elements:
            element = {
                'id': None,
                'type': cadElement.type,
                'material': cadElement.material,
                'color': cadElement.color,
                'height': cadElement.height,
                'comment': '',
                'file': cadElement.file_name,
                'teeth': [],
                'amenities': []
            }

            order['elements'].append(element)

            for tooth in cadElement.teeth:
                element['teeth'].append({
                    'id': None,
                    'fdi': tooth.fdi,
                    'type': tooth.type,
                    'implant_type': tooth.implant_type,
                    'implant_system': tooth.implant_system
                })

        for cadModel in cadOrder.models:
            model = {
                'id': None,
                'type': cadModel.type,
                'material': cadModel.material,
                'color': cadModel.color,
                'height': cadModel.height,
                'comment': '',
                'file': cadModel.file_name,
                'teeth': []
            }
            
            order['models'].append(model)

            for tooth in cadModel.teeth:
                model['teeth'].append({
                    'id': None,
                    'fdi': tooth.fdi,
                    'type': tooth.type,
                    'implant_type': None,
                    'implant_system': None
                })

        return order
    
    def update(self, modifiedOrder: dict) -> None:
        order = {
            'id': None,
            'orderNumber': 0,
            'projectName': modifiedOrder['projectName'],
            'deliveryType': modifiedOrder['deliveryType'],
            'comment': modifiedOrder['comment'],
            'creationMethod': 'client',
            'fastline': modifiedOrder['fastline'],
            'alternativeDeliveryAddress': modifiedOrder['alternativeDeliveryAddress'],
            'elements': [],
            'isAIO': modifiedOrder['isAIO']
        }
        for newElement in modifiedOrder['elements']:
            element = {
                'id': None,
                'type': newElement['type'],
                'material': newElement['material'],
                'color': newElement['color'],
                'height': newElement['height'],
                'comment': newElement['comment'],
                'file': newElement['file'],
                'teeth': [],
                'amenities': newElement['amenities'] if 'amenities' in newElement else []
            }
            order['elements'].append(element)

            for tooth in newElement['teeth']:
                element['teeth'].append({
                    'id': None,
                    'fdi': tooth['fdi'],
                    'type': tooth['type'],
                    'implant_type': tooth['implant_type'] if 'implant_type' in tooth else None,
                    'implant_system': tooth['implant_system'] if 'implant_system' in tooth else None
                })
        
        self.order = order

    def getPoints(self) -> List[Path]:
        return list(self._projectPath.glob('*.pts'))
    
    def getImplantDirectionFiles(self) -> List[Path]:
        return list(self._projectPath.glob('*ImplantDirectionPosition*.xml'))

    def compressFiles(self, files: List[Path]) -> BytesIO:
        '''
        Compresses the files into one zip and return the raw Bytes.

        Parameters
        ----------
        files : List[Path]
            Files to zip.

        Returns
        -------
            Compressed files in raw Bytes format.
        '''
        archive = BytesIO()
        archive.name = 'Points.zip'
        with ZipFile(archive, 'w') as zipArchive:
            for file in files:
                logger.info(f'Compressing {file.name} into points zip.')
                zipArchive.write(file, file.name)

        archive.seek(0)
        return archive
    
    def getFile(self, fileName: str) -> Path | None:
        for file in self.filesWithinProjectFolder:
            if file.name == fileName:
                return file
            
        return None

    def _getMetaFilePaths(self) -> tuple[Path, Path]:
        '''
        Search within the project folder for the meta files `.constructionInfo` and `.dentalProject`.

        Returns
        -------
        (Path, Path):
            The paths to the desired files.
        '''
        # search for the desired files
        # TODO: handle exceptions
        constructionInfoPath = next(self._projectPath.glob('*.constructionInfo'), None)
        dentalProjectPath = next(self._projectPath.glob('*.dentalProject'), None)

        # use the most recent scene file
        sceneFiles = self._projectPath.glob('*.dentalCAD')
        recentModification = 0
        sceneFile = None
        for sceneFile in sceneFiles:
            modified = sceneFile.stat().st_mtime
            if modified > recentModification:
                recentModification = modified
                sceneFile = sceneFile

        return constructionInfoPath, dentalProjectPath, sceneFile
    
    def _getFileList(self) -> List[Path]:
        '''
        Returns a list of contained files within this project.

        Returns
        -------
        list:
            A list of paths representing the files inside the project.
        '''
        return list(self._projectPath.iterdir())
    
    @property
    def projectFiles(self):
        '''
        Getter for the project files.
        '''
        return self.filesWithinProjectFolder
