import logging
from itertools import chain, count
from typing import Union

import numpy as np

from ada.concepts.containers import Sections
from ada.concepts.levels import FEM
from ada.concepts.structural import Section
from ada.core.utils import roundoff, unit_vector, vector_length
from ada.fem import Csys, Elem, FemSection, FemSet
from ada.fem.containers import FemSections
from ada.fem.formats.utils import str_to_int
from ada.fem.shapes import ElemShape, ElemType
from ada.materials import Material
from ada.sections import GeneralProperties

from . import cards


def get_sections(bulk_str, fem: FEM, mass_elem, spring_elem) -> FemSections:
    # Section Names
    sect_names = {sec_id: name for sec_id, name in map(get_section_names, cards.re_sectnames.finditer(bulk_str))}
    # Local Coordinate Systems
    lcsysd = {transno: vec for transno, vec in map(get_lcsys, cards.re_lcsys.finditer(bulk_str))}
    # Hinges
    hinges = {fixno: values for fixno, values in map(get_hinges, cards.re_belfix.finditer(bulk_str))}
    # Thickness'
    thick = {geono: t for geono, t in map(get_thicknesses, cards.re_thick.finditer(bulk_str))}
    # Eccentricities
    ecc = {eccno: values for eccno, values in map(get_eccentricities, cards.re_geccen.finditer(bulk_str))}

    list_of_sections = chain(
        (get_isection(m, sect_names, fem) for m in cards.re_giorh.finditer(bulk_str)),
        (get_box_section(m, sect_names, fem) for m in cards.re_gbox.finditer(bulk_str)),
        (get_tubular_section(m, sect_names, fem) for m in cards.re_gpipe.finditer(bulk_str)),
        (get_flatbar(m, sect_names, fem) for m in cards.re_gbarm.finditer(bulk_str)),
    )

    fem.parent._sections = Sections(list_of_sections, parent=fem.parent)
    [add_general_sections(m, fem) for m in cards.re_gbeamg.finditer(bulk_str)]

    geom = count(1)
    total_geo = count(1)
    res = (
        get_femsecs(m, total_geo, geom, lcsysd, hinges, ecc, thick, fem, mass_elem, spring_elem)
        for m in cards.re_gelref1.finditer(bulk_str)
    )
    sections = filter(lambda x: type(x) is FemSection, res)

    fem_sections = FemSections(sections, fem_obj=fem)
    logging.info(f"Successfully imported {next(geom) - 1} FEM sections out of {next(total_geo) - 1}")
    return fem_sections


def get_isection(match, sect_names, fem) -> Section:
    d = match.groupdict()
    sec_id = str_to_int(d["geono"])
    name = sect_names[sec_id]
    return Section(
        name=name,
        sec_id=sec_id,
        sec_type=Section.TYPES.IPROFILE,
        h=roundoff(d["hz"]),
        t_w=roundoff(d["ty"]),
        w_top=roundoff(d["bt"]),
        w_btn=roundoff(d["bb"]),
        t_ftop=roundoff(d["tt"]),
        t_fbtn=roundoff(d["tb"]),
        genprops=GeneralProperties(Sfy=float(d["sfy"]), Sfz=float(d["sfz"])),
        parent=fem.parent,
    )


def get_box_section(match, sect_names, fem) -> Section:
    d = match.groupdict()
    sec_id = str_to_int(d["geono"])
    return Section(
        name=sect_names[sec_id],
        sec_id=sec_id,
        sec_type=Section.TYPES.BOX,
        h=roundoff(d["hz"]),
        w_top=roundoff(d["by"]),
        w_btn=roundoff(d["by"]),
        t_w=roundoff(d["ty"]),
        t_ftop=roundoff(d["tt"]),
        t_fbtn=roundoff(d["tb"]),
        genprops=GeneralProperties(Sfy=float(d["sfy"]), Sfz=float(d["sfz"])),
        parent=fem.parent,
    )


def get_flatbar(match, sect_names, fem) -> Section:
    d = match.groupdict()
    sec_id = str_to_int(d["geono"])
    return Section(
        name=sect_names[sec_id],
        sec_id=sec_id,
        sec_type=Section.TYPES.FLATBAR,
        h=roundoff(d["hz"]),
        w_top=roundoff(d["bt"]),
        w_btn=roundoff(d["bb"]),
        genprops=GeneralProperties(Sfy=float(d["sfy"]), Sfz=float(d["sfz"])),
        parent=fem.parent,
    )


def add_general_sections(match, fem) -> None:
    d = match.groupdict()
    sec_id = str_to_int(d["geono"])
    gen_props = GeneralProperties(
        Ax=roundoff(d["area"], 10),
        Ix=roundoff(d["ix"], 10),
        Iy=roundoff(d["iy"], 10),
        Iz=roundoff(d["iz"], 10),
        Iyz=roundoff(d["iyz"], 10),
        Wxmin=roundoff(d["wxmin"]),
        Wymin=roundoff(d["wymin"]),
        Wzmin=roundoff(d["wzmin"]),
        Shary=roundoff(d["shary"]),
        Sharz=roundoff(d["sharz"]),
        Shceny=roundoff(d["shceny"]),
        Shcenz=roundoff(d["shcenz"]),
        Sy=float(d["sy"]),
        Sz=float(d["sz"]),
    )

    if sec_id in fem.parent.sections.id_map.keys():
        sec = fem.parent.sections.get_by_id(sec_id)
        sec._genprops = gen_props
        gen_props.parent = sec
    else:
        stype = Section.TYPES.GENERAL
        sec = Section(name=f"GB{sec_id}", sec_id=sec_id, sec_type=stype, genprops=gen_props, parent=fem.parent)
        gen_props.parent = sec
        fem.parent.sections.add(sec)


def get_tubular_section(match, sect_names, fem) -> Section:
    d = match.groupdict()
    sec_id = str_to_int(d["geono"])
    if sec_id not in sect_names:
        sec_name = f"TUB{sec_id}"
    else:
        sec_name = sect_names[sec_id]
    t = d["t"] if d["t"] is not None else (d["dy"] - d["di"]) / 2
    return Section(
        name=sec_name,
        sec_id=sec_id,
        sec_type=Section.TYPES.TUBULAR,
        r=roundoff(float(d["dy"]) / 2),
        wt=roundoff(t),
        genprops=GeneralProperties(Sfy=float(d["sfy"]), Sfz=float(d["sfz"])),
        parent=fem.parent,
    )


def read_line_section(elem: Elem, fem: FEM, mat: Material, geono, d, lcsysd, hinges_global, eccentricities):
    transno = str_to_int(d["transno"])
    sec = fem.parent.sections.get_by_id(geono)
    n1, n2 = elem.nodes
    v = n2.p - n1.p
    if vector_length(v) == 0.0:
        xvec = [1, 0, 0]
    else:
        xvec = unit_vector(v)
    zvec = lcsysd[transno]
    crossed = np.cross(zvec, xvec)
    ma = max(abs(crossed))
    yvec = tuple([roundoff(x / ma, 3) for x in crossed])

    fix_data = str_to_int(d["fixno"])
    ecc_data = str_to_int(d["eccno"])

    members = None
    if d["members"] is not None:
        members = [str_to_int(x) for x in d["members"].replace("\n", " ").split()]

    if fix_data == -1:
        add_hinge_prop_to_elem(elem, members, hinges_global, xvec, yvec)

    if ecc_data == -1:
        add_ecc_to_elem(elem, members, eccentricities, fix_data)

    fem_set = FemSet(sec.name, [elem], "elset", metadata=dict(internal=True), parent=fem)
    fem.sets.add(fem_set, append_suffix_on_exist=True)
    fem_sec = FemSection(
        name=sec.name,
        sec_type=ElemType.LINE,
        elset=fem_set,
        section=sec,
        local_z=zvec,
        local_y=yvec,
        material=mat,
        parent=fem,
    )
    return fem_sec


def read_shell_section(elem: Elem, fem: FEM, mat: Material, elno, thicknesses, geono):
    sec_name = f"sh{elno}"
    fem_set = FemSet(sec_name, [elem], "elset", parent=fem, metadata=dict(internal=True))
    fem.sets.add(fem_set)
    fem_sec = FemSection(
        name=sec_name,
        sec_type=ElemType.SHELL,
        thickness=roundoff(thicknesses[geono]),
        elset=fem_set,
        material=mat,
        parent=fem,
    )
    return fem_sec


def get_femsecs(match, total_geo, curr_geom_num, lcsysd, hinges_global, ecc, thicknesses, fem, mass_elem, spring_elem):
    next(total_geo)
    d = match.groupdict()
    geono = str_to_int(d["geono"])
    elno = str_to_int(d["elno"])
    matno = str_to_int(d["matno"])

    # Go no further if element has no fem section
    if elno in spring_elem.keys():
        next(curr_geom_num)
        spring_elem[elno]["section_data"] = d
        return None

    if elno in mass_elem.keys():
        next(curr_geom_num)
        mass_elem[elno]["section_data"] = d
        return None

    elem = fem.elements.from_id(elno)
    mat = fem.parent.materials.get_by_id(matno)
    if elem.type in ElemShape.TYPES.lines:
        next(curr_geom_num)
        return read_line_section(elem, fem, mat, geono, d, lcsysd, hinges_global, ecc)
    elif elem.type in ElemShape.TYPES.shell:
        next(curr_geom_num)
        return read_shell_section(elem, fem, mat, elno, thicknesses, geono)
    else:
        raise ValueError("Section not added to conversion")


def get_thicknesses(match):
    d = match.groupdict()
    sec_id = str_to_int(d["geono"])
    t = d["th"]
    return sec_id, t


def get_hinges(match):
    d = match.groupdict()
    fixno = str_to_int(d["fixno"])
    opt = str_to_int(d["opt"])
    trano = str_to_int(d["trano"])
    a1 = str_to_int(d["a1"])
    a2 = str_to_int(d["a2"])
    a3 = str_to_int(d["a3"])
    a4 = str_to_int(d["a4"])
    a5 = str_to_int(d["a5"])
    try:
        a6 = str_to_int(d["a6"])
    except BaseException as e:
        logging.debug(e)
        a6 = 0
        pass
    return fixno, (opt, trano, a1, a2, a3, a4, a5, a6)


def get_eccentricities(match):
    d = match.groupdict()
    eccno = str_to_int(d["eccno"])
    ex = float(d["ex"])
    ey = float(d["ey"])
    ez = float(d["ez"])
    return eccno, (ex, ey, ez)


def get_section_names(m):
    d = m.groupdict()
    return str_to_int(d["geono"]), d["set_name"].strip()


def add_hinge_prop_to_elem(elem: Elem, members, hinges_global, xvec, yvec) -> None:
    """Add hinge property to element from sesam FEM file"""
    from ada.fem.elements import Hinge, HingeProp

    if len(elem.nodes) > 2:
        raise ValueError("This algorithm was not designed for more than 2 noded elements")

    for i, x in enumerate(members):
        if i >= len(elem.nodes):
            break
        if x == 0:
            continue
        if x not in hinges_global.keys():
            raise ValueError("fixno not found!")
        opt, trano, a1, a2, a3, a4, a5, a6 = hinges_global[x]
        n = elem.nodes[i]
        if trano > 0:
            csys = None
        else:
            csys = Csys(
                f"el{elem.id}_hinge{i + 1}_csys",
                coords=([unit_vector(xvec) + n.p, unit_vector(yvec) + n.p, n.p]),
                parent=elem.parent,
            )
        dofs_origin = [1, 2, 3, 4, 5, 6]
        dofs = [int(x) for x, i in zip(dofs_origin, (a1, a2, a3, a4, a5, a6)) if int(i) != 0]
        end = Hinge(retained_dofs=dofs, csys=csys, fem_node=n)
        if i == 0:
            elem.hinge_prop = HingeProp(end1=end)
        else:
            elem.hinge_prop = HingeProp(end2=end)


def add_ecc_to_elem(elem: Elem, members, eccentricities, fix_data) -> None:
    """Adds eccentricity to element from sesam FEM file"""
    from ada.fem.elements import Eccentricity, EccPoint

    # To the interpretation here
    start = 0 if fix_data != -1 else len(elem.nodes)
    end = len(elem.nodes) - 1 if fix_data != -1 else 2 * len(elem.nodes)
    end1: Union[None, EccPoint] = None
    end2: Union[None, EccPoint] = None
    for i, x in enumerate(members[start:]):
        if i > end:
            break
        if x == 0:
            continue
        n_offset = elem.nodes[i]
        ecc = eccentricities[x]
        if i == 0:
            end1 = EccPoint(n_offset, ecc)
        if i == end:
            end2 = EccPoint(n_offset, ecc)

    elem.eccentricity = Eccentricity(end1, end2)


def get_lcsys(m):
    d = m.groupdict()
    return str_to_int(d["transno"]), (
        roundoff(d["unix"]),
        roundoff(d["uniy"]),
        roundoff(d["uniz"]),
    )
