
from typing import Dict, List, Tuple

DEFAULT_COLOR = (255 << 24) + (150 << 16) + (150 << 8) + 150


def find_list_of_display_obj(obj) -> List[Tuple["Base", "Base"]]:
    """Get displayable object."""
    
    list_of_display_obj_colors: List = []

    # find displayValue if available
    displayValue = obj
    if hasattr(obj, 'displayValue'):
        displayValue = getattr(obj, 'displayValue')
    elif hasattr(obj, '@displayValue'):
        displayValue = getattr(obj, '@displayValue')
    # return List of displayValues
    if not isinstance(displayValue, List):
        displayValue = [displayValue]
    
    # for Features, return original convertible object and a first item from displayValue
    if obj.speckle_type.endswith("Feature"):
        if len(displayValue)==0:
            return ([(obj, obj)])
        else:
            return([(obj, displayValue[0])])
    
    separated_display_values: List[Tuple] = separate_display_vals(displayValue)
    for item, item_original in separated_display_values:
        if item is None:
            continue
                
        # read displayObj Colors directly from the obj itself, unless its GisFeature or Revit Element: then keep reading from displayValue
        if obj.speckle_type.endswith("Feature") or "BuiltElements.Revit" in obj.speckle_type:
            displayValForColor = item_original
        else:
            displayValForColor = obj

        list_of_display_obj_colors.append((item, displayValForColor))

    return list_of_display_obj_colors


def separate_display_vals(displayValue: List) -> List[Tuple["Base"]]:
    """Return multiple split geometries."""

    from specklepy.objects.geometry import Mesh

    display_objs = []
    
    for i, item in enumerate(displayValue):
        if isinstance(item, Mesh):
            count = 0
            all_count = len(item.faces)

            sub_meshes = []
            for _ in item.faces:
                if count < all_count:
                    faces = []
                    verts = []
                    colors = []

                    vert_num = item.faces[count]
                    if vert_num == 0:
                        vert_num = 3
                    elif vert_num == 1:
                        vert_num = 4

                    faces.append(vert_num)
                    faces.extend([ x for x in list(range(vert_num))])

                    try:
                        for ind in range(vert_num):
                            face_vert_index = count+1+ind
                            #print(face_vert_index)
                            vert_index = item.faces[face_vert_index]

                            new_vert = item.vertices[3*vert_index : 3*vert_index + 3]
                            verts.extend(new_vert)

                            if isinstance(item.colors, List) and len(item.colors) > vert_index:
                                color = item.colors[vert_index]
                                colors.append(color)
                        
                        count += vert_num+1
                        if len(colors)>0:
                            mesh = Mesh.create(faces= faces, vertices=verts, colors=colors)
                        else:
                            mesh = Mesh.create(faces= faces, vertices=verts)
                        
                        sub_meshes.append((mesh, item))
                    
                    except IndexError: # corrupted mesh, drop altogether
                        sub_meshes = []
                        break
                    
            display_objs.extend(sub_meshes)

        elif item is not None:
            display_objs.append((item, item))

    return display_objs

def isDisplayable(obj: "Base") -> bool:

    if is_primitive(obj):
        return True
    
    if obj.speckle_type.endswith("Feature"):
        return True
    
    displayValue = None
    if hasattr(obj, 'displayValue'):
        displayValue = getattr(obj, 'displayValue')
    elif hasattr(obj, '@displayValue'):
        displayValue = getattr(obj, '@displayValue')
    
    # merge to sigle object, if List
    if isinstance(displayValue, List):
        return True
    
    return False


def find_display_obj(obj) -> Tuple["Base", "Base"]:
    """Get displayable object."""

    displayValObj = obj
    displayValForColor = obj

    # find displayValue if available
    displayValue = obj
    if hasattr(obj, 'displayValue'):
        displayValue = getattr(obj, 'displayValue')
    elif hasattr(obj, '@displayValue'):
        displayValue = getattr(obj, '@displayValue')
    # merge to sigle object, if List
    if isinstance(displayValue, List):
        displayValue = get_single_display_object(displayValue)
    
    # read displayObj Colors directly from the obj itself, unless its GisFeature or Revit Element: then keep reading from displayValue
    if not obj.speckle_type.endswith("Feature") and "BuiltElements.Revit" not in obj.speckle_type:
        displayValForColor = obj
    else:
        displayValForColor = displayValue

    # return convertible types as is
    if is_convertible(obj):
        displayValObj = obj
    else:
        displayValObj = displayValue

    return displayValObj, displayValForColor

def is_convertible(obj) -> bool:
    """Check if the object can be converted directly."""
    
    from specklepy.objects.geometry import Base, Point, Line, Polyline, Arc, Circle, Curve, Polycurve, Mesh, Brep

    if ( (isinstance(obj, Base) and obj.speckle_type.endswith("Feature")) or
    isinstance(obj, Point) or
    isinstance(obj, Line) or
    isinstance(obj, Polyline) or
    isinstance(obj, Arc) or
    isinstance(obj, Circle) or
    isinstance(obj, Curve) or
    isinstance(obj, Polycurve) or
    isinstance(obj, Mesh) or
    isinstance(obj, Brep)):
        return True
    return False

def is_primitive(obj) -> bool:
    """Check if the object can be converted directly."""
    
    from specklepy.objects.geometry import Polyline, Point, Line, Arc, Circle, Curve, Polycurve, Mesh, Brep

    if (
        isinstance(obj, Point) or
        isinstance(obj, Line) or
        isinstance(obj, Polyline) or
        isinstance(obj, Arc) or
        isinstance(obj, Circle) or
        isinstance(obj, Curve) or
        isinstance(obj, Mesh) 
        ):
            return True
    return False

def get_single_display_object(displayValForColor: List) -> "Base":
    """Get a merged Mesh or a first item from displayValue list."""

    from specklepy.objects.geometry import Mesh
    
    faces = []
    verts = []
    colors = []
    for i, item in enumerate(displayValForColor):
        if isinstance(item, Mesh):
            start_vert_count = int(len(verts)/3)

            # only add colors if existing and incoming colors are valid (same length as vertices)
            if len(colors) == start_vert_count and isinstance(item.colors, List) and len(item.colors)== int(len(item.vertices)/3)>0:
                colors.extend(item.colors)
            else:
                colors = []

            verts.extend(item.vertices)

            count = 0
            for _ in item.faces:
                try:
                    vert_num = item.faces[count]
                    faces.append(vert_num)
                    faces.extend([ x+start_vert_count for x in item.faces[count+1 : count+1+vert_num]])
                    count += vert_num+1
                except IndexError:
                    break
        elif item is not None:
            return item

    mesh = Mesh.create(faces= faces, vertices=verts, colors=colors)

    if isinstance(displayValForColor, List) and len(displayValForColor)>0:
        for prop in displayValForColor[0].get_member_names():
            if prop not in ["colors", "vertices", "faces"]:
                mesh[prop] = getattr(displayValForColor[0], prop)

    displayValForColor = mesh
    return displayValForColor
    
def get_display_units(context_list: List["TraversalContext"]) -> None | str:
    """Get units from either of displayable objects."""

    from specklepy.objects.geometry import Base

    displayUnits = None

    for item in context_list:
        if hasattr(item.current, "displayValue"):
            try:
                displayVal = item.current["displayValue"]
            except:
                displayVal = item.current.displayValue
            if isinstance(displayVal, list) and len(displayVal)>0:
                displayUnits = displayVal[0].units
                break
            elif isinstance(displayVal, Base):
                displayUnits = item.current.units
                break
        else:
            if item.current.units is not None:
                displayUnits = item.current.units
                break

    return displayUnits

def get_material_color_proxies(root_obj) -> Dict:
    """Get colors and object IDs using ColorProxies and renderMaterialProxies."""
    obj_colors = {}

    # first, get colors
    try:
        colorProxies = root_obj["colorProxies"]
        if isinstance(colorProxies, List):
            for proxy in colorProxies:
                color = proxy.value
                a, r, g, b = get_r_g_b(color)
                color = f'rgba({r},{g},{b},{a})'

                for obj in proxy.objects:
                    obj_colors[obj] = color
    except:
        pass

    # overwrite with materials if available
    try:
        materialProxies = root_obj["renderMaterialProxies"]
        if isinstance(materialProxies, List):
            for proxy in materialProxies:
                material = proxy.value
                color = material['diffuse']
                opacity = material['opacity']
                
                a, r, g, b = get_r_g_b(color)
                if opacity is not None and isinstance(opacity, float):
                    a_test = int(255* opacity)
                    if 0 <= a_test <= 255:
                        a = a_test
                color = f'rgba({r},{g},{b},{a})'

                for obj in proxy.objects:
                    obj_colors[obj] = color

    except:
        pass


    return obj_colors

def set_default_color(context_list: List["TraversalContext"]) -> None:
    """Get and set the default color."""

    from specklepy.objects.GIS.layers import VectorLayer

    global DEFAULT_COLOR
    DEFAULT_COLOR = (255 << 24) + (150 << 16) + (150 << 8) + 150

    for item in context_list:
        # for GIS-commits, use default blue color
        if isinstance(item.current, VectorLayer) or (item.parent is not None and isinstance(item.parent.current, VectorLayer)):
            DEFAULT_COLOR = (255 << 24) + (10 << 16) + (132 << 8) + 255 # speckle blue, speckle_blue
            break

def getAllParents(tc: "TraversalContext"):

    all_tc = [tc]
    while True:
        try:
            parent = tc.parent
            if parent:
                all_tc.append(parent)
                tc = parent
            else:
                break
        except:
            break

    return all_tc

def assign_color(self: "SpeckleProvider", obj_display_tc: "TraversalContext", props: Dict) -> None:
    """Get and assign color to feature displayProperties."""

    from specklepy.objects.geometry import Mesh, Brep
    
    # initialize Speckle Blue color
    color = DEFAULT_COLOR
    opacity = None

    obj_display = obj_display_tc.current

    try:
        # first, choose if get color from the parent obj or displayValue
        if hasattr(obj_display, 'displayStyle') or hasattr(obj_display, '@displayStyle') or hasattr(obj_display, 'renderMaterial') or hasattr(obj_display, '@renderMaterial'):
            obj_display = obj_display_tc.current
        else:
            # this option will be not very reliable: 
            # there could be different colors for diff displayValues in the list
            if hasattr(obj_display, 'displayValue'):
                try:
                    displayVal = obj_display['displayValue']
                except:
                    displayVal = obj_display.displayValue
                if isinstance(displayVal, list) and len(displayVal)>0:
                    obj_display = displayVal[0]

            elif hasattr(obj_display, '@displayValue') and isinstance(obj_display['@displayValue'], list) and len(obj_display['@displayValue'])>0:
                obj_display = obj_display['@displayValue'][0]

        # prioritize renderMaterials for Meshes & Brep
        if isinstance(obj_display, Mesh) or isinstance(obj_display, Brep): 
            # print(obj_display.get_member_names())
            if hasattr(obj_display, 'renderMaterial'):
                try:
                    renderMaterial = obj_display['renderMaterial']
                except:
                    renderMaterial = obj_display.renderMaterial
                color = renderMaterial['diffuse']
                opacity = renderMaterial['opacity']
            elif hasattr(obj_display, '@renderMaterial'):
                color = obj_display['@renderMaterial']['diffuse']
                opacity = obj_display['@renderMaterial']['opacity']

            elif isinstance(obj_display, Mesh) and isinstance(obj_display.colors, List) and len(obj_display.colors)>1:
                colors_number = 0
                all_colors = []
                for c in obj_display.colors:
                    if c not in all_colors:
                        colors_number += 1
                    all_colors.append(c)

                if colors_number>1:
                    all_a = 0
                    all_r = 0
                    all_g = 0
                    all_b = 0
                    for col in all_colors:
                        a, r, g, b = get_r_g_b(col)
                        all_a += a
                        all_r += r
                        all_g += g
                        all_b += b
                    color = ( 
                        (int(all_a/len(obj_display.colors)) << 24) + (int(all_r/len(obj_display.colors)) << 16) 
                        + (int(all_g/len(obj_display.colors)) << 8) + int(all_b/len(obj_display.colors))
                    )
                else: 
                    color = obj_display.colors[0]
            
            elif hasattr(obj_display, 'displayStyle'):
                color = obj_display['displayStyle']['color']
            elif hasattr(obj_display, '@displayStyle'):
                color = obj_display['@displayStyle']['color']

        elif hasattr(obj_display, 'displayStyle'):
            color = obj_display['displayStyle']['color']
        elif hasattr(obj_display, '@displayStyle'):
            color = obj_display['@displayStyle']['color']
        elif hasattr(obj_display, 'renderMaterial'):
            color = obj_display['renderMaterial']['diffuse']
            opacity = obj_display['renderMaterial']['opacity']
        elif hasattr(obj_display, '@renderMaterial'):
            color = obj_display['@renderMaterial']['diffuse']
            opacity = obj_display['@renderMaterial']['opacity']
    except Exception as e:
        print(e)
    
    a, r, g, b = get_r_g_b(color)
    if opacity is not None and isinstance(opacity, float):
        a_test = int(255* opacity)
        if 0 <= a_test <= 255:
            a = a_test
    # hex_color = '#%02x%02x%02x' % (r, g, b)
    props['color'] = f'rgba({r},{g},{b},{a})'

    # if still not found, check proxies: 
    if color == DEFAULT_COLOR:
        for tc in getAllParents(obj_display_tc):
            
            try:
                color = self.material_color_proxies[tc.current.applicationId]
                props['color'] = color
                return
            except:
                pass

        try:
            color = self.material_color_proxies[obj_display.applicationId]
            props['color'] = color
            return
        except:
            pass

def get_r_g_b(rgb: int) -> Tuple[int, int, int]:
    """Get R, G, B values from int."""

    r = g = b = 0
    a = 255
    try:
        a = (rgb & 0xFF000000) >> 24
        r = (rgb & 0xFF0000) >> 16
        g = (rgb & 0xFF00) >> 8
        b = rgb & 0xFF
    except Exception as e:
        r = g = b = 150
        a = 255
    return a, r, g, b

def assign_display_properties(self: "SpeckleProvider", feature: Dict, f_base: "Base",  obj_display_tc: "TraversalContext") -> None:
    """Assign displayProperties to the feature."""
    
    from specklepy.objects.geometry import Mesh, Brep    

    assign_color(self, obj_display_tc, feature["displayProperties"])
    feature["properties"]["color"] = feature["displayProperties"]["color"]

    # other properties for rendering 
    if isinstance(f_base, Mesh) or isinstance(f_base, Brep):
        feature["displayProperties"]['lineWidth'] = 0.3
    elif "Line" in feature["geometry"]["type"]: 
        feature["displayProperties"]['lineWidth'] = 3
    else: 
        feature["displayProperties"]['lineWidth'] = 1

    # if "Point" in feature["geometry"]["type"]:
    try:
        feature["displayProperties"]["radius"] = feature["properties"]["weight"]
    except:
        feature["displayProperties"]["radius"] = 10
