from typing import List, Tuple

from cad_parser.errors import ExocadParserError
from cad_parser.definitions import Order, Element, Tooth, Patient, ScanFile
from cad_parser.utils import consolidate_colors, parse_document, get_text

from pathlib import Path

import logging
logger = logging.getLogger(__name__)

FORMAT_NAME = 'exocad'

def is_supported(file_path: Path) -> bool:
    '''
    Is the given file path a supported ExoCAD project?

    Parameters
    ----------
    file_path : Path
        The file path to check.

    Returns
    -------
    bool
        True if the file path is a supported ExoCAD project, False otherwise.
    '''
    return next(file_path.glob('*.dental[pP]roject'), False) and (
        next(file_path.glob('*.construction[iI]nfo'), False) or
        next(file_path.glob('*.model[iI]nfo'), False) or
        next(file_path.glob('*.scan[iI]nfo'), False) or
        next(file_path.glob('*.partial[iI]nfo'), False)
    )

def parse(project_path: Path) -> Tuple[Path, Path, Path, Path, Path, Order]:
    '''
    Parse an exocad project and return an `Order` object.

    The process of creating an exocad construction follows the following steps:
    1. Scan the patient's dental model resulting in a `.scanInfo` file.
    2. Create a model from the scanned model resulting in a `.modelInfo` file.
    3a. Either create a construction from the model resulting in a `.constructionInfo` file.
    3b. Or create a partial construction from the model resulting in a `.partialInfo` file.

    Thus, the order contains the elements defined either in the `constructionInfo` if present, or in the `partialInfo` file.

    Parameters
    ----------
    project_path : Path
        The path to the exocad project directory.

    Raises
    ------
    ExocadParseError
        If none of the required files are missing or if they cannot be parsed correctly.

    Returns
    -------
    dental_project_path : Path
        The path to the exocad project file.
    scan_info_path : Path
        The path to the `.scanInfo` file.
    model_info_path : Path
        The path to the `.modelInfo` file.
    partial_info_path : Path
        The path to the `.partialInfo` file.
    construction_info_path : Path
        The path to the `.constructionInfo` file.
    order : Order
        The order object containing the elements, models and scans defined in the exocad project.

    '''
    if not is_supported(project_path):
        raise ExocadParserError(f"Unsupported exocad project: {project_path}")
    
    project = None
    patient = None
    comment = None
    practice_id = None
    elements = []
    models = []
    scan_files = []
    loaded_scene_file = None

    # Get basic information
    if dental_project_path := next(project_path.glob('*.dentalProject'), False):
        patient = parse_dental_project(dental_project_path)

    # get infomation if the project is in the scan step
    if scan_info_path := next(project_path.glob('*.scanInfo'), False):
        scan_order = parse_scan_info(scan_info_path)
        project = scan_order.project
        comment = scan_order.comment
        practice_id = scan_order.practice_id
        scan_files = scan_order.scan_files

    # get information if the project is in the model step
    if model_info_path := next(project_path.glob('*.modelInfo'), False):
        model_order = parse_model_info(model_info_path)
        project = model_order.project
        comment = model_order.comment
        practice_id = model_order.practice_id
        models = model_order.models
        scan_files = model_order.scan_files
        loaded_scene_file = model_order.loaded_scene_file

    if partial_info_path := next(project_path.glob('*.partialInfo'), False):
        elements = parse_partial_info(partial_info_path)

    # get the information if the project is in the construction step
    if construction_info_path := next(project_path.glob('*.constructionInfo'), False):
        construction_order = parse_construction_info(construction_info_path)
        project = construction_order.project
        comment = construction_order.comment
        practice_id = construction_order.practice_id
        elements = construction_order.elements
        scan_files = construction_order.scan_files
        loaded_scene_file = construction_order.loaded_scene_file

    order = Order(
        format=FORMAT_NAME,
        project=project,
        patient=patient,
        comment=comment,
        practice_id=practice_id,
        elements=elements,
        models=models,
        scan_files=scan_files,
        loaded_scene_file=loaded_scene_file
    )

    return dental_project_path, scan_info_path, model_info_path, partial_info_path, construction_info_path, order
    
def parse_dental_project(dental_project_path: Path) -> Patient:
    '''
    Parse a dental project file and extract the necessary information.

    Parameters
    ----------
    dental_project_path : Path
        The path to the dental project file.

    Returns
    -------
    Patient
        The parsed patient information.
    '''
    dp_doc = parse_document(dental_project_path.open('rb'))

    patient_id = get_text(dp_doc, 'Patient/PatientId')
    patient_first_name = get_text(dp_doc, 'Patient/PatientFirstName')
    patient_last_name = get_text(dp_doc, 'Patient/PatientName')

    return Patient(id=patient_id, first_name=patient_first_name, last_name=patient_last_name)

def parse_construction_info(construction_info_path: Path) -> Order:
    ci_doc = parse_document(construction_info_path.open('rb'))

    project_name = get_text(ci_doc, "ProjectName")
    practice_id = get_text(ci_doc, "PracticeId")
    patient_id = get_text(ci_doc, "PatientId")
    comment = get_text(ci_doc, "Notes")

    loaded_scene_file = None
    # we need to look for the most recent changed file, as the workflow of 
    # exocad users is to save their intermediate progress in a separate file.
    recentModification = 0
    for scene_file in construction_info_path.parent.glob('*.dentalCAD'):
        modified = scene_file.stat().st_mtime
        if modified > recentModification:
            recentModification = modified
            loaded_scene_file = scene_file.name

    elements = []
    for element in ci_doc.findall("ConstructionFileList/ConstructionFile"):
        file_name = get_text(element, "Filename")
        material = get_text(element, "Material")
        color = None
        height = float(get_text(element, "Height"))
        is_screw_retained = get_text(element, "IsScrewRetained")
        part_type = get_text(element, "PartType")

        teeth = []
        tooth_numbers = [tooth.text for tooth in element.findall('ToothNumbers/int')]

        for tooth_number in tooth_numbers:
            tooth_nodes = ci_doc.xpath(f'Teeth/Tooth[contains(Number,"{tooth_number}")]')

            if len(tooth_nodes) == 0:
                raise ExocadParserError(f'Tooth #{tooth_number} missing')

            tooth_node = next(iter(tooth_nodes))

            tooth_color = get_text(tooth_node, 'ToothColor')
            tooth_type = get_text(tooth_node, 'ReconstructionType')
            tooth_is_anatomical = None
            tooth_implant_type = get_text(tooth_node, 'ImplantType')
            implant_system = None

            if is_screw_retained == 'true':
                implant_system = get_text(tooth_node, 'ImplantLibraryEntryDescriptor')

            tooth = Tooth(fdi=tooth_number, color=tooth_color, type=tooth_type,
                    is_anatomical=tooth_is_anatomical, implant_type=tooth_implant_type, implant_system=implant_system)
            teeth.append(tooth)

        color = consolidate_colors(teeth)

        element = Element(file_name=file_name, material=material, color=color, height=height,
                is_screw_retained=is_screw_retained, type=part_type, teeth=teeth)
        elements.append(element)

    scanFiles = []
    for scanFile in ci_doc.findall("ScanFiles/ScanFile"):
        part_type = get_text(scanFile, 'PartType')
        file_name = get_text(scanFile, 'FileName')

        scanFiles.append(ScanFile(file_name=file_name, type=part_type))

    patient = Patient(id=patient_id, last_name=None, first_name=None)

    return Order(format=FORMAT_NAME, project=project_name, patient=patient, comment=comment,
        practice_id=practice_id, elements=elements, models=[], scan_files=scanFiles, loaded_scene_file=loaded_scene_file)

def parse_model_info(model_info_path: Path) -> Order:
    mi_doc = parse_document(model_info_path.open('rb'))

    project_name = get_text(mi_doc, "ProjectName")
    practice_id = get_text(mi_doc, "PracticeId")
    patient_id = get_text(mi_doc, "PatientId")
    comment = get_text(mi_doc, "Notes")
    is_screw_retained = None

    models = []
    for element in mi_doc.findall("ConstructionFileList/ConstructionFile"):
        file_name = get_text(element, "Filename")
        material = get_text(element, "Material")
        color = None
        height = float(get_text(element, "Height"))
        element_type = get_text(element, 'PartType')

        teeth = []
        tooth_numbers = [tooth.text for tooth in element.findall('ToothNumbers/int')]
        for tooth_number in tooth_numbers:
            tooth_nodes = mi_doc.xpath(f'Teeth/Tooth[contains(Number,"{tooth_number}")]')

            if len(tooth_nodes) == 0:
                raise ExocadParserError(f'Tooth #{tooth_number} missing')
            
            tooth_node = next(iter(tooth_nodes))
            tooth_color = get_text(tooth_node, 'ToothColor')
            tooth_type = get_text(tooth_node, 'ReconstructionType')
            tooth_is_anatomical = None
            tooth_implant_type = None
            implant_system = None

            tooth = Tooth(fdi=tooth_number, color=tooth_color, type=tooth_type,
                    is_anatomical=tooth_is_anatomical, implant_type=tooth_implant_type, implant_system=implant_system)
            teeth.append(tooth)

        color = consolidate_colors(teeth)

        model = Element(file_name=file_name, material=material, color=color, height=height,
                is_screw_retained=is_screw_retained, type=element_type, teeth=teeth)
        models.append(model)

    scanFiles = []
    for scanFile in mi_doc.findall("ScanFiles/ScanFile"):
        part_type = get_text(scanFile, 'PartType')
        file_name = get_text(scanFile, 'FileName')

        scanFiles.append(ScanFile(file_name=file_name, type=part_type))

    patient = Patient(id=patient_id, last_name=None, first_name=None)

    return Order(format=FORMAT_NAME, project=project_name, patient=patient, comment=comment,
        practice_id=practice_id, elements=[], models=models, scan_files=scanFiles, loaded_scene_file=None)

def parse_scan_info(scan_info_path: Path) -> Order:
    si_doc = parse_document(scan_info_path.open('rb'))

    project_name = get_text(si_doc, "ProjectName")
    practice_id = get_text(si_doc, "PracticeId")
    patient_id = get_text(si_doc, "PatientId")
    comment = get_text(si_doc, "Notes")

    scanFiles = []
    for scanFile in si_doc.findall("ScanFiles/ScanFile"):
        part_type = get_text(scanFile, 'PartType')
        file_name = get_text(scanFile, 'FileName')

        scanFiles.append(ScanFile(file_name=file_name, type=part_type))

    patient = Patient(id=patient_id, last_name=None, first_name=None)

    return Order(format=FORMAT_NAME, project=project_name, patient=patient, comment=comment,
        practice_id=practice_id, elements=[], models=[], scan_files=scanFiles, loaded_scene_file=None)

def parse_partial_info(partial_info_path: Path) -> List[Element]:

    si_doc = parse_document(partial_info_path.open('rb'))

    elements = []
    for element in si_doc.findall("PartialFileList/PartialFile"):
        file_name = get_text(element, 'FileName')
        material = get_text(element, 'Material')
        element_type = None

        claps_curve = False
        for buildParameter in si_doc.findall("BuildParameterLists/BuildParameters"):
            if buildParameter.get('type') == 'ClaspCurve':
                claps_curve = True
                break

        element_type = 'modelclip' if claps_curve else 'framework'

        element = Element(file_name=file_name, material=material, color=None, height=None,
                is_screw_retained=None, teeth=[], type=element_type)
        elements.append(element)
    return elements