from pathlib import Path
from typing import Tuple
import re

from cad_parser.errors import ThreeOXParserError
from cad_parser.definitions import Order, Element, Tooth, Patient, ScanFile
from cad_parser.utils import parse_document, unn_to_fdi

FORMAT_NAME = '3ox'

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('*.3ox'), False)

def parse(project_path: Path) -> Tuple[Path, Order]:
    if not is_supported(project_path):
        raise ThreeOXParserError(f"Unsupported threeshape project: {project_path}")
    
    if threeshape_path := next(project_path.glob('*.3ox'), False):
        order = parse_3ox(threeshape_path)

    return threeshape_path, order

def parse_3ox(threeshape_file: Path):
    threeox_doc = parse_document(threeshape_file.open('rb'))
    root = threeox_doc.getroot()
    nsmap = root.nsmap

    # What the ...?
    nsmap['ns'] = nsmap.get(None)
    del nsmap[None]

    # FIXME: some versions don't seem to have OrderInfo elements
    # Also check ThreeShapeOrderNo as project name instead of (sometimes missing)
    # patient names.
    order_node = root.find('ns:Command/ns:AddOrderRequest/ns:Order', namespaces=nsmap)

    if order_node is None:
        raise ThreeOXParserError("Missing element: Order")

    restoration_order_node = order_node.find('ns:InnerOrder/ns:RestorationOrder', namespaces=nsmap)

    if restoration_order_node is None:
        raise ThreeOXParserError("Missing element: RestorationOrder")

    order_info_node = restoration_order_node.find('ns:OrderInfo', namespaces=nsmap)

    if order_info_node is None:
        raise ThreeOXParserError("Missing element: OrderInfo")

    patient_first_name = None
    patient_last_name = None

    patient_last_name_node = order_info_node.find('ns:Patient_LastName', namespaces=nsmap)

    if patient_last_name_node is not None:
        patient_last_name = patient_last_name_node.text

    patient_first_name_node = order_info_node.find('ns:Patient_FirstName', namespaces=nsmap)

    if patient_first_name_node is not None:
        patient_first_name = patient_first_name_node.text

    project = '_'.join(filter(None, [patient_last_name, patient_first_name]))

    if len(project) == 0:
        ts_order_number_node = order_info_node.find('ns:ThreeShapeOrderNo', namespaces=nsmap)

        if ts_order_number_node is None:
            raise ThreeOXParserError("Can't determine project number. Patient_FirstName, Patient_LastName and ThreeShapeOrderNo missing")

        project = ts_order_number_node.text

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

    comments_node = order_node.find('ns:Comments', namespaces=nsmap)

    if comments_node is None:
        raise ThreeOXParserError("Missing element: Comments")

    comments = comments_node.text if comments_node.text else ''

    element_nodes = restoration_order_node.findall('ns:ModelElements/ns:ModelElement',
            namespaces=nsmap)

    scan_files = []
    elements = []
    models = []

    for element_node in element_nodes:
        material = None
        color = None
        height = None
        type = None

        type_node = element_node.find('ns:LinkTypeClass', namespaces=nsmap)
        if type_node is not None:
            type = type_node.text

        material_node = element_node.find("ns:Material", namespaces=nsmap)

        if material_node is not None:
            material = material_node.get("displayName")

        color_node = element_node.find("ns:Color", namespaces=nsmap)

        if color_node is not None:
            color = color_node.get("displayName")

        height_node = element_node.find('ns:ModelHeight', namespaces=nsmap)

        if height_node is not None and height_node.text is not None:
            height = float(height_node.text)

        cam_file_nodes = element_node.findall('ns:CAMFiles/ns:CAMFile', namespaces=nsmap)
        file_name = None

        implant_root = None
        for x in cam_file_nodes:
            path = x.get('path')
            
            if '.stl' in path and not re.search(r'Die_[0-9]{1,2}', path):
                file_name = path.split('\\')[-1]
            elif '.stl' in path and re.search(r'Die_[0-9]{1,2}', path):
                model_path = path.split('\\')[-1]
                model_material = None
                model_color = None
                model_height = 0

                model_fdi = unn_to_fdi(int(model_path.split('_')[-1].split('.')[0]))

                model_tooth = Tooth(fdi=model_fdi, color=None, implant_type=None, implant_system=None, type='modeldie', is_anatomical=False)

                model = Element(file_name=model_path, material=model_material, color=model_color, height=model_height, type='modeldie', teeth=[model_tooth], is_screw_retained=False)
                models.append(model)
            elif '.xml' in path and 'ImplantDirectionPosition' in path:
                implant_file_name = path.split('\\')[-1]
                implant_file = parse_document(threeshape_file.with_name(implant_file_name).open('rb'))
                implant_root = implant_file.getroot()

        # if we couldn't identify a file name for the element, we'll skip it
        if not file_name:
            continue

        teeth = []

        for tooth_node in element_node.findall('ns:TypeIDs/ns:TypeID', namespaces=nsmap):
            # TODO Verify there is no material in teeth
            unn = tooth_node.get('uNN')
            fdi = unn_to_fdi(int(unn))
            tooth_color = None

            tooth_type = tooth_node.get('typeClass')
            implant_type = tooth_node.get('abutmentKitID')
            implant_system = None

            if implant_root is not None:
                implant_system_node = implant_root.find(f'ToothIndex[@unn="{unn}"]/Kit/Name')
                if implant_system_node is not None:
                    implant_system = implant_system_node.get('value')

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

        for scan_node in element_node.findall('ns:ScanFiles/ns:ScanFile', namespaces=nsmap):
            scan_file_name = scan_node.get('path').split('\\')[-1]
            scan_type = scan_node.get('fileType')
            scan_file = ScanFile(file_name=scan_file_name, type=scan_type)
            if scan_file not in scan_files:
                scan_files.append(scan_file)

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

    # patient_id
    patient = Patient(id=None, last_name=patient_last_name, first_name=patient_first_name)

    # unavailable information
    # practice_id
    return Order(format=FORMAT_NAME, project=project, patient=patient, comment=comments,
        practice_id=None, elements=elements, models=models, scan_files=scan_files, loaded_scene_file=None)
