# ©2025, ANSYS Inc. Unauthorized use, distribution or duplication is prohibited.

from dash_extensions.enrich import Output, State, Trigger, dash_table, dcc, html
import pandas as pd

from ansys.saf.glow.client import DashClient, callback
from ansys.solutions.bolt_app.datamodel.shared.models import ActorAndHid, DesignFields
from ansys.solutions.bolt_app.datamodel.shared.websocket_streams import WebsocketStreamListeners
from ansys.solutions.bolt_app.solution.data_access import DataAccess
from ansys.solutions.bolt_app.solution.definition import Bolt_AppSolution
from ansys.solutions.bolt_app.ui.utils.images import Image

# &nbsp is used to add a space in the markdown text

text = """
This app is used to calculate the allowable shear and tension forces for a bolt. The shear and tension forces are
calculated based on the bolt class, the bolt size, and a safety factor.

The app also reports the ultimate stress of the bolt material, based on the bolt class. The ultimate stress is the
maximum stress that a material can withstand before failure. The app uses the following table to calculate the
tension and shear forces:

&nbsp &nbsp &nbsp $F_{tension} = \\frac{A_s  f_{ub}}{SF}$

&nbsp &nbsp &nbsp $F_{shear} = \\frac{A_s  f_{ub}}{SF}$

Where:

| &nbsp                             | &nbsp                               |
| ----------------------------------|-------------------------------------|
| &nbsp &nbsp &nbsp $F_{tension}$   | &nbsp Allowable tension force       |
| &nbsp &nbsp &nbsp $F_{shear}$     | &nbsp Allowable shear force         |
| &nbsp &nbsp &nbsp $A_s$           | &nbsp Bolt cross-sectional area     |
| &nbsp &nbsp &nbsp $f_{ub}$        | &nbsp Ultimate tensile strength     |
| &nbsp &nbsp &nbsp $SF$            | &nbsp Safety factor                 |

The cross-sectional area of the bolt is calculated based on the bolt size. The app uses the following
formula to calculate the cross-sectional area:


&nbsp &nbsp &nbsp $A_s = π d^2 / 4$


Where:

&nbsp &nbsp &nbsp $A_s$ = Bolt cross-sectional area

&nbsp &nbsp &nbsp $d$ = Bolt diameter

The diameter of the bolt is calculated based on the bolt size. The app uses the following formula to calculate the
diameter:

| &nbsp &nbsp &nbsp Bolt Size | &nbsp Diameter (mm) |
| ----------------------------|---------------------|
| &nbsp &nbsp &nbsp M6        | &nbsp 6             |
| &nbsp &nbsp &nbsp M8        | &nbsp 8             |
| &nbsp &nbsp &nbsp M10       | &nbsp 10            |
| &nbsp &nbsp &nbsp M12       | &nbsp 12            |
| &nbsp &nbsp &nbsp M16       | &nbsp 16            |
| &nbsp &nbsp &nbsp M20       | &nbsp 20            |
| &nbsp &nbsp &nbsp M24       | &nbsp 24            |
"""

UNITS = {
    "A_s": "m²",
    "F_shear": "N",
    "F_tension": "N",
    "f_ub": "MPa",
    "pitch": "mm",
}

FORCE_RESPONSES = ["F_shear", "F_tension"]
STRESS_RESPONSES = ["f_ub"]

image = Image.BOLT.get_div(style={"width": "50%", "float": "right"})

right_content = [
    html.H4("About"),
    html.P([image, dcc.Markdown(text, mathjax=True)]),
]

style_cell_conditional = [
    {"if": {"column_id": "Name"}, "width": "20%", "textAlign": "left"},
    {"if": {"column_id": "Units"}, "width": "20%", "textAlign": "left"},
]

PLACEHOLDER_DATA_COLUMNS = [{"name": "", "id": ""}]
PLACEHOLDER_DATA = [{"": "No data available"}]

parameter_table = dash_table.DataTable(
    id="parameters_table", data=PLACEHOLDER_DATA, style_cell_conditional=style_cell_conditional
)
responses_table = dash_table.DataTable(
    id="responses_table", data=PLACEHOLDER_DATA, style_cell_conditional=style_cell_conditional
)

GRAPH_LAYOUT = {"plot_bgcolor": "rgba(0,0,0,0)", "paper_bgcolor": "rgba(0,0,0,0)"}
GRAPH_STYLE = {"width": "100%"}
HIDDEN_GRAPH_STYLE = {**GRAPH_STYLE, "display": "none"}

parameter_graph = dcc.Graph(id="parameter-graph", style=HIDDEN_GRAPH_STYLE, responsive=True)
response_graph = dcc.Graph(id="responses-graph", style=HIDDEN_GRAPH_STYLE, responsive=True)

graph_holder = html.Div(
    [parameter_graph, response_graph], style={"display": "flex", "flex-direction": "row", "size": "100%"}
)

left_content = [
    html.H4("Data Tables"),
    parameter_table,
    html.Br(),
    responses_table,
    html.Br(),
    html.H4("Data Graphs"),
    graph_holder,
]

left_side = html.Div(left_content, style={"display": "flex", "flex-direction": "column", "width": "45%"})
divider = html.Div(style={"display": "flex", "flex-direction": "column", "width": "5%"})
right_side = html.Div(right_content, style={"display": "flex", "flex-direction": "column", "width": "45%"})

layout = html.Div([left_side, divider, right_side], style={"display": "flex", "flex-direction": "row"})


@callback(
    Output("parameters_table", "data"),
    Output("parameters_table", "columns"),
    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
    State("url", "pathname"),
)
def update_parameter_table(pathname):
    project = DashClient[Bolt_AppSolution].get_project(pathname)
    data_access = DataAccess(project)
    osl_project_tree = data_access.get_project_tree()

    if osl_project_tree.is_uninitialized:
        return PLACEHOLDER_DATA, PLACEHOLDER_DATA_COLUMNS

    actors = osl_project_tree.find_nodes_by_name("Sensitivity")

    if not actors:
        return PLACEHOLDER_DATA, PLACEHOLDER_DATA_COLUMNS

    actor = actors[0]
    hids = data_access.monitoring_data.get_actor_states_ids(actor.uid)

    if not hids.states_ids:
        # No data available yet
        return PLACEHOLDER_DATA, PLACEHOLDER_DATA_COLUMNS

    actor_and_hid = ActorAndHid(actor_uid=actor.uid, hid=hids.states_ids[0])
    design_points = data_access.monitoring_data.get_design_points(actor_and_hid)
    parameter_data = design_points.to_dict_by_states(DesignFields.PARAMETERS)
    parameter_frame = pd.DataFrame(parameter_data)
    return parameter_frame.to_dict("records"), [{"name": i, "id": i} for i in parameter_frame.columns]


@callback(
    Output("responses_table", "data"),
    Output("responses_table", "columns"),
    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
    State("url", "pathname"),
)
def update_response_table(pathname):
    project = DashClient[Bolt_AppSolution].get_project(pathname)
    data_access = DataAccess(project)
    osl_project_tree = data_access.get_project_tree()

    if osl_project_tree.is_uninitialized:
        return PLACEHOLDER_DATA, PLACEHOLDER_DATA_COLUMNS

    actors = osl_project_tree.find_nodes_by_name("Sensitivity")

    if not actors:
        return PLACEHOLDER_DATA, PLACEHOLDER_DATA_COLUMNS

    actor = actors[0]
    hids = data_access.monitoring_data.get_actor_states_ids(actor.uid)

    if not hids.states_ids:
        # No data available yet
        return PLACEHOLDER_DATA, PLACEHOLDER_DATA_COLUMNS

    actor_and_hid = ActorAndHid(actor_uid=actor.uid, hid=hids.states_ids[0])
    design_points = data_access.monitoring_data.get_design_points(actor_and_hid)

    response_names = design_points.get_field_names(DesignFields.RESPONSES)
    response_data = design_points.to_dict_by_states(DesignFields.RESPONSES)
    response_frame = pd.DataFrame(response_data)
    # Insert the units of measure
    response_frame.insert(1, "Units", [UNITS.get(field_name, "") for field_name in response_names])
    return response_frame.to_dict("records"), [{"name": i, "id": i} for i in response_frame.columns]


@callback(
    Output("parameter-graph", "figure"),
    Output("parameter-graph", "style"),
    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
    State("url", "pathname"),
)
def update_stress_graph(pathname):
    project = DashClient[Bolt_AppSolution].get_project(pathname)
    data_access = DataAccess(project)
    osl_project_tree = data_access.get_project_tree()

    if osl_project_tree.is_uninitialized:
        return {}, HIDDEN_GRAPH_STYLE

    actors = osl_project_tree.find_nodes_by_name("Sensitivity")

    if not actors:
        return {}, HIDDEN_GRAPH_STYLE

    actor = actors[0]
    hids = data_access.monitoring_data.get_actor_states_ids(actor.uid)

    if not hids.states_ids:
        # No data available yet
        return {}, HIDDEN_GRAPH_STYLE

    actor_and_hid = ActorAndHid(actor_uid=actor.uid, hid=hids.states_ids[0])
    design_points = data_access.monitoring_data.get_design_points(actor_and_hid)
    data = design_points.to_dict_by_keys(DesignFields.RESPONSES)
    states = design_points.get_states()
    data = [
        {
            "x": states,
            "y": data[field_name],
            "type": "bar",
            "name": field_name,
        }
        for field_name in STRESS_RESPONSES
    ]
    layout = {
        **GRAPH_LAYOUT,
        "title": "Stresses",
        "xaxis": {"title": "Design", "tickmode": "array", "tickvals": states, "ticktext": states},
        "yaxis": {"title": "Stress (MPa)"},
    }
    figure = {"data": data, "layout": layout}
    return figure, GRAPH_STYLE


@callback(
    Output("responses-graph", "figure"),
    Output("responses-graph", "style"),
    Trigger(WebsocketStreamListeners.ACTOR_DATA.value, "message"),
    State("url", "pathname"),
)
def update_responses_graph(pathname):
    project = DashClient[Bolt_AppSolution].get_project(pathname)
    data_access = DataAccess(project)
    osl_project_tree = data_access.get_project_tree()

    if osl_project_tree.is_uninitialized:
        return {}, HIDDEN_GRAPH_STYLE

    actors = osl_project_tree.find_nodes_by_name("Sensitivity")

    if not actors:
        return {}, HIDDEN_GRAPH_STYLE

    actor = actors[0]
    hids = data_access.monitoring_data.get_actor_states_ids(actor.uid)

    if not hids.states_ids:
        # No data available yet
        return {}, HIDDEN_GRAPH_STYLE

    actor_and_hid = ActorAndHid(actor_uid=actor.uid, hid=hids.states_ids[0])
    design_points = data_access.monitoring_data.get_design_points(actor_and_hid)
    data = design_points.to_dict_by_keys(DesignFields.RESPONSES)
    states = design_points.get_states()
    data = [
        {
            "x": states,
            "y": data[field_name],
            "type": "bar",
            "name": field_name,
        }
        for field_name in FORCE_RESPONSES
    ]
    layout = {
        **GRAPH_LAYOUT,
        "title": "Forces",
        "xaxis": {"title": "Design", "tickmode": "array", "tickvals": states, "ticktext": states},
        "yaxis": {"title": "Force (N)"},
    }
    figure = {"data": data, "layout": layout}
    return figure, GRAPH_STYLE
