import sympy as sy
from pprint import pprint
from uuid import UUID, uuid4
from math import pi
from pprint import pprint
from mpl_toolkits.mplot3d import Axes3D
from typing import Optional
from colorama import Fore, Back, Style
import matplotlib
import thermo
import sympy as sy
from uuid import UUID, uuid4

import networkx as nx
import matplotlib.pyplot as plt

from .decorators import timer
from conduitforge.components import (
    Component,
    MajorLossComponent,
    MinorLossComponent,
    Port,
    Pipe,
    T_connector,
)

from conduitforge.mosaic import Mosaic
from conduitforge.current import Current
from conduitforge.pump import Pump


matplotlib.use("Qt5Agg")


def reynolds(r_n=None, rho=None, char_length=None, abs_viscosity=None, vel=None):
    # Criação dos symbolos do Symbolic Python
    # Absolute viscosity é a viscosidade Mi
    (
        reynolds_number,
        rho_fluid,
        characteristic_length,
        absolute_viscosity,
        kinematic_viscosity,
        velocity,
    ) = sy.symbols(
        "reynolds_number rho_fluid characteristic_length absolute_viscosity kinematic_viscosity velocity"
    )

    # Equação para calcular o numero de reynolds
    Re = sy.Eq(
        reynolds_number,
        (rho_fluid * velocity * characteristic_length) / absolute_viscosity,
    )

    # Definição da viscosidade cinemmática
    k_viscosity = sy.Eq(
        (absolute_viscosity / rho_fluid, kinematic_viscosity), kinematic_viscosity
    )

    print(f"\nBefore the values list")
    values = [
        (reynolds_number, r_n),
        (rho_fluid, rho),
        (characteristic_length, char_length),
        (absolute_viscosity, abs_viscosity),
        (velocity, vel),
        # (kinematic_viscosity , sy.solve(k_viscosity.subs(values)))
    ]

    print(f"\nAfter the values list, before the return")

    return sy.solve(Re.subs(values))

    """
    if absolute_viscosity == None and kinematic_viscosity == None:
        #this part is used when the user needs to calculate thee absolute viscosity just from the reynolds equation
        absolute_viscosity = sy.solve(Re.subs(values), absolute_viscosity)

    if absolute_viscosity == None and kinematic_viscosity != None and rho_fluid != None:
        print(f'\ndentro do if da eq da viscosidade absoluta')
        absolute_viscosity = sy.solve(k_viscosity.subs(values), absolute_viscosity)

    if kinematic_viscosity == None and absolute_viscosity != None and rho_fluid != None:
        print(f'\ndentro do if da eq da viscosidade cinematica')
        kinematic_viscosity = sy.solve(k_viscosity.subs(values), kinematic_viscosity)
    
    print(f'\nAqui começa a parte depois dos if\'s da viscosidade')
    if reynolds_number == None and rho_fluid != None and characteristic_length != None and absolute_viscosity != None and velocity != None:
        reynolds = sy.solve(Re.subs(values),reynolds_number)
        return reynolds
    
    if rho_fluid == None and reynolds_number != None and characteristic_length != None and absolute_viscosity != None and velocity != None:
        rho = sy.solve(Re.subs(values),rho_fluid)
        return rho
    
    if characteristic_length == None and rho_fluid != None and reynolds_number != None and absolute_viscosity != None and velocity != None:
        length = sy.solve(Re.subs(values),characteristic_length)
        return length
    
    print(f'\n Depois desse pedaço começa o if para calcular a viscosidade absoluta')
    if absolute_viscosity == None and rho_fluid != None and characteristic_length != None and reynolds_number != None and velocity != None:
        print(f'\nEssa parte ja pe dentro do if da viscosidade absoluta')
        abs_viscosity = sy.solve(Re.subs(values),k_viscosity)
        return abs_viscosity
    
    if velocity == None and rho_fluid != None and characteristic_length != None and absolute_viscosity != None and reynolds_number != None:
        local_velocity = sy.solve(Re.subs(values),velocity)
        return local_velocity
    """


# Absolute Viscosity calculation
# The function should return the variable that is not declared by the user
# a_vis = reynolds(r_n=1, rho = 2, char_length=3, vel = 4)

# print(f'\n{a_vis}')


def sympy_solve_test():
    A, B, C, D, E = sy.symbols("A, B, C, D, E")

    equation = sy.Eq(A + B, C + D - E)
    # equation.
    something = equation.evalf(subs={B: 2, C: 3, D: 4, E: 5})

    """
    if {B:2, C:3, D:4, E:5}, A should be A=0
    """
    solve_something = sy.solve(something, A, dict=False)
    solution: dict = sy.solve(equation, A, dict=True)

    a = solution
    print(f"\n{a = }")
    print(f"\n{something = }")
    print(f"\n{solve_something = }")


def summation_test():
    # Define a variável simbólica
    p = sy.symbols("p")

    # Inicializa a expressão de somatório como zero
    summation_expr = 0

    # Define o número de termos que você quer somar
    num_terms = 3

    # Loop para adicionar termos à expressão do somatório
    for i in range(1, num_terms + 1):
        # Define o novo termo a ser adicionado (por exemplo, i*n)
        new_term = p * (1 + 2 + 3)

        # Atualiza a expressão do somatório
        summation_expr += new_term

    number = 5
    other_number = 7
    some_number = 10

    # Imprime a expressão final do somatório
    total_energy_eq = sy.Eq(number, other_number - summation_expr - some_number)

    p_value = sy.solve(total_energy_eq, p, dict=False)
    print(f"\n{summation_expr = }")
    print(f"\n{total_energy_eq = }")
    print(f"\n{p_value = }")


# summation_test()

import numpy as np


def numpy_test():
    # Parâmetros da equação de Colebrook
    epsilon = 0.0001  # Rugosidade absoluta do tubo (m)
    D = 0.1  # Diâmetro do tubo (m)
    Re = 1e5  # Número de Reynolds

    # Estimativa inicial para f
    f_guess = 0.02

    # Tolerância e número máximo de iterações
    tolerance = 1e-6
    max_iterations = 100

    # Iteração simples
    for i in range(max_iterations):
        f_new = 1 / (
            -2 * np.log10((epsilon / (3.7 * D)) + (2.51 / (Re * np.sqrt(f_guess)))) ** 2
        )
        if abs(f_new - f_guess) < tolerance:
            break
        f_guess = f_new

    # Imprime a solução final
    print(f_guess)


def symbols_generalization_test():
    """
    This function will be used to test if i can input generalyzed variables and variables names that are generated by
    objects inside the Conduit Forge library
    """

    def str_generator(str_quantity: int):
        str_list: list = []
        for item in range(str_quantity):
            new_item = uuid4()
            str_list.append(new_item)

        return str_list
        ...

    names_list = str_generator(str_quantity=3)

    for id in names_list:
        str_name = str(id)
        id = sy.symbols(str_name)

        print(f"\n{str_name = }")
        print(f"\n{id = }")

    some_equation = sy.Eq()


def get_variables_from_sympy_equations() -> None:
    x, y, z = sy.symbols("x y z")

    eq = sy.Eq(z**2, x**2 + y**2)

    pprint(f"{eq.free_symbols = }")

    print(f"\n{eq.free_symbols}\n")

    eq.fre


def equality_between_equations_test():
    """
    This test is meant to verify how well and if the different methods os comparing equations work.

    There is the possiblity thattwo different equation objects are mathematically the same relation,
    that may or may not be in different algebraic forms.

    This test was created to prevent that, inside a dictionary or inside a list, two different
    sy.Equations (that represent the same mathematical equation, or relation) exist, and both could
    even exist linked to different UUID's or ULIDS. In tha case that both exist linked to different
    memory addresses and/or different ID's, this could lead to serious problems in the step of
    solving the system of equations of both mass and energy.

    """

    x, y, z, u, v, w, i, j, k = sy.symbols("x y z u v w i j k")

    eq_1 = sy.Eq(x * y, u - 2)
    eq_2 = sy.Eq(x, (u - 2) / y)

    print(f"\n{(sy.simplify(eq_1.lhs - eq_2.lhs) == 0) = }\n")
    print(f"\n{(sy.simplify(eq_1.rhs - eq_2.rhs) == 0) = }\n")

    # print(f"\n{sy.simplify_logic(eq_1) = }\n")
    # print(f"\n{sy.simplify_logic(eq_2) = }\n")

    # print(f"\n{(sy.simplify_logic(eq_1) == sy.simplify_logic(eq_2)) = }\n")

    print(
        f"\n{(sy.simplify(eq_1.lhs - eq_1.rhs) == sy.simplify(eq_2.lhs - eq_2.rhs)) = }\n"
    )

    both_sides_difference_1 = eq_1.lhs - eq_1.rhs

    print(f"\n{both_sides_difference_1 = }\n")
    print(f"\n{sy.simplify(both_sides_difference_1) = }\n")

    both_sides_difference_2 = eq_2.lhs - eq_2.rhs

    print(f"\n{both_sides_difference_2 = }\n")
    print(f"\n{sy.simplify(eq_2.lhs - eq_2.rhs) = }\n")

    eq_1_equal_to_zero = sy.Eq(eq_1.lhs - eq_1.rhs, 0)
    eq_2_equal_to_zero = sy.Eq(eq_2.lhs - eq_2.rhs, 0)

    print(f"\n{eq_1_equal_to_zero = }\n")
    print(f"\n{eq_2_equal_to_zero = }\n")

    print(f"\n{ eq_1_equal_to_zero.lhs - eq_2_equal_to_zero.lhs = }\n")

    print(f"\n{sy.expand(eq_1) = }\n")
    print(f"\n{sy.expand(eq_2) = }\n")

    print(f"\n{sy.expand(eq_1.lhs - eq_1.rhs) = }\n")
    print(f"\n{sy.expand(eq_2.lhs - eq_2.rhs) = }\n")

    print(
        f"\n{(sy.expand(eq_1.lhs - eq_1.rhs) == sy.expand(eq_2.lhs - eq_2.rhs)) = }\n"
    )

    print(f"\n{sy.trigsimp(eq_1.lhs - eq_1.rhs) = }\n")
    print(f"\n{sy.trigsimp(eq_2.lhs - eq_2.rhs) = }\n")

    print(
        f"\n{(sy.trigsimp(eq_1.lhs - eq_1.rhs) == sy.trigsimp(eq_2.lhs - eq_2.rhs)) = }\n"
    )

    # Resolver eq_1 isolando x

    eq_1_solved = sy.solve(eq_1, x)

    print(f"\n{eq_1_solved = }\n")

    print(f"\n{eq_2.rhs = }\n")

    # Verificar se o lado direito da eq_2 está dentro da solução
    equivalencia_algebrica = eq_2.rhs in eq_1_solved

    print(f"\n{equivalencia_algebrica = }\n")

    # Substituir x na eq_1 usando eq_2

    eq_1_substituida = eq_1.subs(x, eq_2.rhs)

    print(f"\n{(eq_1.subs(x, eq_2.rhs)) = }\n")

    # Simplificar e verificar se a igualdade ainda é válida
    equivalencia_numerica = (
        sy.simplify(eq_1_substituida.lhs - eq_1_substituida.rhs) == 0
    )

    print(f"\n{equivalencia_numerica = }\n")  # True ✅


def re_f_q_arrangement_test():
    """
    Esse método está sendo criado para verificar se o sympy é capaz de resolver
    um sistema de equações de escoamento nas quais f é definido como uma função
    em partes, delimitada para diferentes valores de Re. Nesse sistema, Re é
    declarado como uma função de Q. Dessa forma, f é uma função em partes, que
    será resolvida normalmente com o sistema de equações.

    Se esse teste funcionar, eu poderia tentar usar esse arranjo em toda a
    livraria. Mas, antes disso, eu preciso verificar se esse arranjo é estável,
    e se ele poderia provocar algum erro, ja que é possível que alguma das
    iterações do nsolve passem por valores de Re que estejam na faixa de
    2300 < Re < 4000 , e como f não estã definido nessa faixa, o algoritmo pode
    quebrar, e simplesmente não funcionar. E esse pode ser um problema que talvez
    só apareça para sistemas específicos e, por isso, esse pode ser um problema
    sério: por que ele é imprevisível. Como este problema é imprevisível,
    eu não sei qual é a região de Q ou Re em todo o um sistema de tubulações na
    qual eu posso ter certeza que o erro não vai acontecer.

    Para resolver isso, eu poderia colocar uma limitação, ou recomendação, para
    que os usuarios sempre setassem a pressão ou a vazão inicial de uma dada
    corrente de entrada, acima de um valor de piso, por segurança;
    """

    from sympy import symbols, pi, Piecewise, solve, Eq, simplify

    # Constantes do sistema
    D_val = 0.05  # Diâmetro em m = 5 cm
    L_val = 4.0  # Comprimento em m
    mu_val = 8.00e-4  # 8.55e-4 Viscosidade dinâmica em Pa.s
    rho_val = 995.7  # Densidade em kg/m³
    P1_val = 300000  # Pressão de entrada em Pa
    P2_val = 101325  # Pressão de saída em Pa
    g = 9.81

    # Variáveis simbólicas
    Q = symbols("Q", real=True, positive=True)  # Vazão volumétrica
    D, L, mu, rho = symbols("D L mu rho")
    Re = (4 * rho * Q) / (pi * D * mu)

    # Fator de atrito por partes
    f = Piecewise(
        (64 / Re, Re < 2300),
        (0.3164 / Re**0.25, Re > 4000),
        (0.02, True),  # valor intermediário arbitrário # *(None, True)
    )

    # Velocidade média
    A = pi * (D / 2) ** 2
    v = Q / A

    # Equação de Darcy-Weisbach para perda de carga em termos de ΔP
    delta_P = f * (L / D) * (rho * v**2 / 2)

    # * Hl = f * (L / D) * (v**2 / 2)

    # Equação que iguala ΔP calculada à diferença de pressão real
    eq = Eq(delta_P, P1_val - P2_val)

    # * eq = Eq(Hl, (P1_val - P2_val) / rho)

    # Substituir valores numéricos
    eq_num = eq.subs({D: D_val, L: L_val, mu: mu_val, rho: rho_val})

    # Resolver numericamente (símbolos usados, mas agora é quase numérico)
    from sympy import nsolve

    # Chute inicial para Q
    Q_guess = 0.01

    Q_sol = nsolve(eq_num, Q, Q_guess)

    print(f"\nVazão volumétrica Q = {Q_sol:.6f} m³/s\n")


def second_re_f_q_arrangement_test():
    """
    Eu pensei em testar o sistema super_simples com esse arranjo, ja que eu
    tenho todas as equações desse sistema anotadas no papel.

    Esse sistema é aquele que tem só uma corrente e um tubo
    """
    ...


def third_re_f_q_arrangement_test():
    """
    In this test, the Re_f_Q arrangement will be applied to a more complex
    system, with some branching of the pipeline, to evaluate if this approach
    stays viable for bigger mosaics.

    the piping system is as follows:

                   ^
                   |
    ca --> p1 --> t1 -->

    """

    from sympy import symbols, pi, Piecewise, solve, Eq, simplify

    # Constantes do sistema
    D_val = 0.05  # Diâmetro em m = 5 cm
    L_p1_val = 4.0  # Comprimento em m
    mu_val = 8.00e-4  # 8.55e-4 Viscosidade dinâmica em Pa.s
    rho_val = 995.7  # Densidade em kg/m³
    P_ca_val = 300000  # Pressão de entrada em Pa
    P_out_val = 101325  # Pressão de saída em Pa
    g = 9.81

    # Variáveis simbólicas
    Q = symbols("Q", real=True, positive=True)  # Vazão volumétrica
    D, L, mu, rho = symbols("D L mu rho")
    Re = (4 * rho * Q) / (pi * D * mu)

    # Fator de atrito por partes
    f = Piecewise(
        (64 / Re, Re < 2300),
        (0.3164 / Re**0.25, Re > 4000),
        (0.02, True),  # valor intermediário arbitrário # *(None, True)
    )

    # Velocidade média
    A = pi * (D / 2) ** 2
    v = Q / A

    # Equação de Darcy-Weisbach para perda de carga em termos de ΔP
    delta_P = f * (L / D) * (rho * v**2 / 2)

    # * Hl = f * (L / D) * (v**2 / 2)

    # //energy_eq
    # Equação que iguala ΔP calculada à diferença de pressão real
    eq = Eq(delta_P, P1_val - P2_val)
    eq_from_p1_1_to_t1_1
    eq_from_p1_1_to_t1_2

    # //mass_eq
    mass_cons_ca_p1_0
    mass_cons_p1_1_to_p1_1
    mass_cons_t1

    # * eq = Eq(Hl, (P1_val - P2_val) / rho)

    # Substituir valores numéricos
    eq_num = eq.subs({D: D_val, L: L_val, mu: mu_val, rho: rho_val})

    # Resolver numericamente (símbolos usados, mas agora é quase numérico)
    from sympy import nsolve

    # Chute inicial para Q, em todos os pontos dentro da rede de tubulação
    Q_guess = 0.01

    # Chute inicial para P, em todos os pontos dentro da rede de tubulação
    P_guess = 100_000

    Q_sol = nsolve(eq_num, Q, Q_guess)

    print(f"\nVazão volumétrica Q = {Q_sol:.6f} m³/s\n")


def fourth_re_f_q_arrangement_test():
    """
    Eu oensei em testar o sistema com loop que eu estou usando como teste padrão
    na livraria que é a da forma:

    ca --> t1 --> p4 --> t2 -->
           ^             ^
           |             |
           p1 --> p2 --> p3

    O lado bom é que eu tenho todas as equações desse sistema anotadas no papel
    O problema desse teste é que existem 30 equações.

    Uma coisa boa desse sistema é que eu vou poder testar o chute de pressão
    nele (e isso é algo que eu não consigo testar no sistema supersimplificado,
    ja que a pressão nos dois pontos do sistema é conhecida).

    """
    from math import pi

    # * pi,
    from sympy import symbols, Piecewise, solve, Eq, simplify, nsolve

    # // comprimento dos tubos, medidos em m
    l_p1 = 4
    l_p2 = 4
    l_p3 = 4
    l_p4 = 4

    # // Obs: Os t's não tem medidas, por que eles são considerados pontos no
    # // espaço, e não objetos com comprimento, mas isso não é um problema, ja
    # // que eu só preciso da elevação em cada ponto, e não dos comprimentos

    # // lista de elevacões do sistema, medidas em m (metro)
    z_t1_0 = 0
    z_t1_1 = 0
    z_t1_2 = 0

    z_t2_0 = 0
    z_t2_1 = 0
    z_t2_2 = 0

    z_p1_0 = z_t1_1
    z_p1_1 = -l_p1

    z_p2_0 = z_p1_1
    z_p2_1 = z_p1_1

    z_p3_0 = z_p1_1
    z_p3_1 = z_t2_1 = 0

    z_p4_0 = z_t1_2 = 0
    z_p4_1 = z_t2_0 = 0

    # // Diâmetros das tubulações, medidos em m (metro)
    d_t1_0 = 0.05
    d_t1_1 = 0.05
    d_t1_2 = 0.05

    d_t2_0 = 0.05
    d_t2_1 = 0.05
    d_t2_2 = 0.05

    d_p1_0 = 0.05
    d_p1_1 = 0.05

    d_p2_0 = 0.05
    d_p2_1 = 0.05

    d_p3_0 = 0.05
    d_p3_1 = 0.05

    d_p4_0 = 0.05
    d_p4_1 = 0.05

    # //Declaração de todas as incógnitas do sistema

    # * Q

    ca_q = sy.symbols("ca_q")

    t1_0_q = sy.symbols("t1_0_q")
    t1_1_q = sy.symbols("t1_1_q")
    t1_2_q = sy.symbols("t1_2_q")

    t2_0_q = sy.symbols("t2_0_q")
    t2_1_q = sy.symbols("t2_1_q ")
    t2_2_q = sy.symbols("t2_2_q")

    p1_0_q = sy.symbols("p1_0_q")
    p1_1_q = sy.symbols("p1_1_q")

    p2_0_q = sy.symbols("p2_0_q")
    p2_1_q = sy.symbols("p2_1_q")

    p3_0_q = sy.symbols("p3_0_q")
    p3_1_q = sy.symbols("p3_1_q")

    p4_0_q = sy.symbols("p4_0_q")
    p4_1_q = sy.symbols("p4_1_q")

    # * P

    ca_p = sy.symbols("ca_p")

    t1_0_p = sy.symbols("t1_0_p")
    t1_1_p = sy.symbols("t1_1_p")
    t1_2_p = sy.symbols("t1_2_p")

    t2_0_p = sy.symbols("t2_0_p")
    t2_1_p = sy.symbols("t2_1_p")
    t2_2_p = sy.symbols("t2_2_p")

    p1_0_p = sy.symbols("p1_0_p")
    p1_1_p = sy.symbols("p1_1_p")

    p2_0_p = sy.symbols("p2_0_p")
    p2_1_p = sy.symbols("p2_1_p")

    p3_0_p = sy.symbols("p3_0_p")
    p3_1_p = sy.symbols("p3_1_p")

    p4_0_p = sy.symbols("p4_0_p")
    p4_1_p = sy.symbols("p4_1_p")

    # // Declaração de todos os valores numéricos (os que já são definidos, antes
    # // de resolver o sistema, acho que poderiam ser chamados de condições de
    # // contorno)

    # ca_p_numeric = 300_000  # * Pressão na entrada, medida em Pa
    # t2_2_p_numeric = 0  # * 101325  # * Pressão na saída, medida em Pa

    inital_conditions_eq_1 = sy.Eq(ca_p, 300000)  # * Pressão na entrada, medida em Pa
    inital_conditions_eq_2 = sy.Eq(t2_2_p, 0)  # * Pressão na saída, medida em Pa

    # // Eqs de Massa
    # * Eqs de energia nas juntas
    ma_j_1 = sy.Eq(ca_q, t1_0_q)
    ma_j_2 = sy.Eq(p1_0_q, t1_1_q)
    ma_j_3 = sy.Eq(p2_0_q, p1_1_q)
    ma_j_4 = sy.Eq(p3_0_q, p2_1_q)
    ma_j_5 = sy.Eq(p4_0_q, t1_2_q)
    ma_j_6 = sy.Eq(t2_0_q, p4_1_q)
    ma_j_7 = sy.Eq(t2_1_q, p3_1_q)

    # * Eqs de massa nos comps
    ma_cp_1 = sy.Eq(0, t1_0_q + t1_1_q + t1_2_q)
    ma_cp_2 = sy.Eq(0, t2_0_q + t2_1_q + t2_2_q)
    ma_cp_3 = sy.Eq(0, p1_0_q + p1_1_q)

    ma_cp_4 = sy.Eq(0, p2_0_q + p2_1_q)
    ma_cp_5 = sy.Eq(0, p3_0_q + p3_1_q)
    ma_cp_6 = sy.Eq(0, p4_0_q + p4_1_q)

    g = 9.81  # // Gravity acceleration, in m/s^2
    rho = 997  # //! This value is approximated, Water density, in Kg/m^3re_p1_1 = (p1_1_q)
    mi = 0.0007998  # // Absolute viscosity of water, in Pa*s

    # // Eqs de energia

    # * definição dos diferentes fatores de atrito de Darcy-Weisbach
    # * Esse parâmetro só se aplica a tubos (e não aos ts)

    re_p1_1 = ((4 * rho) / (mi * pi * d_p1_1)) * p1_1_q
    f_laminar_p1_1 = 64 / re_p1_1

    blasius_p1_1 = 0.079 / (re_p1_1 ** (1 / 4))

    f_p1 = sy.Piecewise(
        (f_laminar_p1_1, re_p1_1 < 2300),
        (0.002, sy.And(re_p1_1 > 2300, re_p1_1 <= 4000)),
        (blasius_p1_1, re_p1_1 > 4000),
    )

    re_p2_1 = ((4 * rho) / (mi * pi * d_p2_1)) * p2_1_q
    f_laminar_p2_1 = 64 / re_p2_1

    blasius_p2_1 = 0.079 / (re_p2_1 ** (1 / 4))

    f_p2 = sy.Piecewise(
        (f_laminar_p2_1, re_p2_1 < 2300),
        (0.002, sy.And(re_p2_1 > 2300, re_p2_1 <= 4000)),
        (blasius_p2_1, re_p2_1 > 4000),
    )

    re_p3_1 = ((4 * rho) / (mi * pi * d_p3_1)) * p3_1_q
    f_laminar_p3_1 = 64 / re_p3_1

    blasius_p3_1 = 0.079 / (re_p3_1 ** (1 / 4))

    f_p3 = sy.Piecewise(
        (f_laminar_p3_1, re_p3_1 < 2300),
        (0.002, sy.And(re_p3_1 > 2300, re_p3_1 <= 4000)),
        (blasius_p3_1, re_p3_1 > 4000),
    )

    re_p4_1 = ((4 * rho) / (mi * pi * d_p4_1)) * p4_1_q
    f_laminar_p4_1 = 64 / re_p4_1

    blasius_p4_1 = 0.079 / (re_p4_1 ** (1 / 4))

    f_p4 = sy.Piecewise(
        (f_laminar_p4_1, re_p4_1 < 2300),
        (0.002, sy.And(re_p3_1 > 2300, re_p3_1 <= 4000)),
        (blasius_p4_1, re_p4_1 > 4000),
    )

    # * Termos de perda de carga (hl)

    # * em t1 e t2, eu preciso lembrar de usar a equação com os Ks da perda
    # // Escrever essas funções diretamente como função de q, ja que é nesse formato
    # // que elas seriam usadas para compor a equação de energia dentro da livraria
    # //, SE ESSE ARRANJO FOR USADO NA LIVRARIA
    #! (OBS): Se eu for usar esse arranjo, eu iria precisar fazer com que o
    #! o método de Reynolds retornasse uma equação (ou um lado da equação)
    #! para que essa equação fosse usada em hl. Dessa forma, hl ja viria
    #! automaticamente como uma função de Q, quando ela for passada para a equação
    #! de energia. O lado bom disso é que eu não vou precisar passar f, Re ou hl
    #! como equações individuais para o solver.

    # * The head pressure loss equations for the pipes have the following shape:
    # * hl =  = sy.Eq(self_hb, (((second_port_id) ** 2) ** (1 / 2)) * c_constant)
    # * C constant formula: C = K / ((2*g) * second_port_cross_sect_area)

    k_t1_0_1 = 1
    k_t1_0_2 = 0.2

    a_t1_1 = (pi / 4) * (d_t1_1**2)
    c_t1_0_1 = k_t1_0_1 / ((2 * g) * a_t1_1)

    a_t1_2 = (pi / 4) * (d_t1_2**2)
    c_t1_0_2 = k_t1_0_2 / ((2 * g) * a_t1_2)

    hl_t1_0_1 = (
        (t1_1_q**2) ** (1 / 2)
    ) * c_t1_0_1  # * sy.Basic(((t1_1_q**2) ** (1 / 2)) * c_t1_0_1)

    hl_t1_0_2 = (
        (t1_2_q**2) ** (1 / 2)
    ) * c_t1_0_2  # * sy.Basic(((t1_2_q**2) ** (1 / 2)) * c_t1_0_2)

    k_t2_0_1 = 1
    k_t2_0_2 = 0.2

    a_t2_1 = (pi / 4) * (d_t2_1**2)
    c_t2_0_1 = k_t2_0_1 / ((2 * g) * a_t2_1)

    a_t2_2 = (pi / 4) * (d_t1_2**2)
    c_t2_0_2 = k_t2_0_2 / ((2 * g) * a_t2_2)

    hl_t2_0_1 = (
        (t2_1_q**2) ** (1 / 2)
    ) * c_t2_0_1  # * sy.Basic(((t2_1_q**2) ** (1 / 2)) * c_t2_0_1)

    hl_t2_0_2 = (
        (t2_2_q**2) ** (1 / 2)
    ) * c_t2_0_2  # * sy.Basic(((t2_2_q**2) ** (1 / 2)) * c_t2_0_2)

    # * The head pressure loss equations for the pipes have the following shape: hl = B_constant * Q * abs(Q)

    hl_p1 = (
        ((f_p1 * l_p1 * 8) / ((d_p1_1**5) * (pi**2) * g))
        * p1_1_q
        * ((p1_1_q**2) ** (1 / 2))
    )
    hl_p2 = (
        ((f_p2 * l_p2 * 8) / ((d_p2_1**5) * (pi**2) * g))
        * p2_1_q
        * ((p2_1_q**2) ** (1 / 2))
    )
    hl_p3 = (
        ((f_p3 * l_p3 * 8) / ((d_p3_1**5) * (pi**2) * g))
        * p3_1_q
        * ((p3_1_q**2) ** (1 / 2))
    )
    hl_p4 = (
        ((f_p4 * l_p4 * 8) / ((d_p4_1**5) * (pi**2) * g))
        * p4_1_q
        * ((p4_1_q**2) ** (1 / 2))
    )

    # * E_constant for all Pipes and T_connectors

    p1_0_e = 16 / ((2 * g) * (pi**2) * (d_p1_0**4))
    p1_1_e = 16 / ((2 * g) * (pi**2) * (d_p1_1**4))

    p2_0_e = 16 / ((2 * g) * (pi**2) * (d_p2_0**4))
    p2_1_e = 16 / ((2 * g) * (pi**2) * (d_p2_1**4))

    p3_0_e = 16 / ((2 * g) * (pi**2) * (d_p3_0**4))
    p3_1_e = 16 / ((2 * g) * (pi**2) * (d_p3_1**4))

    p4_0_e = 16 / ((2 * g) * (pi**2) * (d_p4_0**4))
    p4_1_e = 16 / ((2 * g) * (pi**2) * (d_p4_1**4))

    t1_0_e = 16 / ((2 * g) * (pi**2) * (d_t1_0**4))
    t1_1_e = 16 / ((2 * g) * (pi**2) * (d_t1_1**4))
    t1_2_e = 16 / ((2 * g) * (pi**2) * (d_t1_2**4))

    t2_0_e = 16 / ((2 * g) * (pi**2) * (d_t2_0**4))
    t2_1_e = 16 / ((2 * g) * (pi**2) * (d_t2_1**4))
    t2_2_e = 16 / ((2 * g) * (pi**2) * (d_t2_2**4))

    # * Eqs de energia nas juntas
    # // Talvez eu possa usar uma simplificação para facilitar o meu trabalho
    # // Como todos os tubos tem o mesmo diametro (em todo e qualquer ponto
    # // dentro do sistema de tubulação montado), eu posso escrever essas eqs
    # // só em termos de P, ja que o E é função da geometria, e Q em duas portas
    # // de uma junta (entre dois components diferentes) não tem variação de vazão

    en_j_1 = sy.Eq(ca_p, t1_0_p)
    en_j_2 = sy.Eq(p1_0_p, t1_1_p)
    en_j_3 = sy.Eq(p2_0_p, p1_1_p)
    en_j_4 = sy.Eq(p2_1_p, p3_0_p)
    en_j_5 = sy.Eq(t1_2_p, p4_0_p)
    en_j_6 = sy.Eq(p1_1_p, t2_0_p)
    en_j_7 = sy.Eq(p3_1_p, t2_1_p)

    # * Eqs de energia nos comps
    #! Pegar só o lado direito das equações de hl (por que é só o lado direito
    #! que eu preciso)
    # // valores do chute inicial (p/ todas as variáveis declaradas)

    en_cp_1 = sy.Eq(
        ((p1_0_p) / (rho * g)) + (p1_0_e * (p1_0_q**2)) + z_p1_0,
        ((p1_1_p) / (rho * g)) + (p1_1_e * (p1_1_q**2)) + z_p1_1 + hl_p1,
    )

    en_cp_2 = sy.Eq(
        ((p2_0_p) / (rho * g)) + (p2_0_e * (p2_0_q**2)) + z_p2_0,
        ((p2_1_p) / (rho * g)) + (p2_1_e * (p2_1_q**2)) + z_p2_1 + hl_p2,
    )

    en_cp_3 = sy.Eq(
        ((p3_0_p) / (rho * g)) + (p3_0_e * (p3_0_q**2)) + z_p3_0,
        ((p3_1_p) / (rho * g)) + (p3_1_e * (p3_1_q**2)) + z_p3_1 + hl_p3,
    )

    en_cp_4 = sy.Eq(
        ((p4_0_p) / (rho * g)) + (p4_0_e * (p4_0_q**2)) + z_p4_0,
        ((p4_1_p) / (rho * g)) + (p4_1_e * (p4_1_q**2)) + z_p4_1 + hl_p4,
    )

    en_cp_5 = sy.Eq(
        ((t1_0_p) / (rho * g)) + (t1_0_e * (t1_0_q**2)) + z_t1_0,
        ((t1_1_p) / (rho * g)) + (t1_1_e * (t1_1_q**2)) + z_t1_1 + hl_t1_0_1,
    )

    en_cp_6 = sy.Eq(
        ((t1_0_p) / (rho * g)) + (t1_0_e * (t1_0_q**2)) + z_t1_0,
        ((t1_2_p) / (rho * g)) + (t1_2_e * (t1_2_q**2)) + z_t1_2 + hl_t1_0_2,
    )

    en_cp_7 = sy.Eq(
        ((t2_0_p) / (rho * g)) + (t2_0_e * (t2_0_q**2)) + z_t2_0,
        ((t2_1_p) / (rho * g)) + (t2_1_e * (t2_1_q**2)) + z_t2_1 + hl_t2_0_1,
    )

    en_cp_8 = sy.Eq(
        ((t2_0_p) / (rho * g)) + (t2_0_e * (t2_0_q**2)) + z_t2_0,
        ((t2_2_p) / (rho * g)) + (t2_2_e * (t2_2_q**2)) + z_t2_2 + hl_t2_0_2,
    )

    # // I forgot what i had to do on this part, so i'll just raise this error
    # // and than i can remember myself from the future to serach what i have to
    # // do on this part

    all_vars: tuple = (
        ca_q,  # 1
        t1_0_q,  # 2
        t1_1_q,  # 3
        t1_2_q,  # 4
        t2_0_q,  # 5
        t2_1_q,  # 6
        t2_2_q,  # 7
        p1_0_q,  # 8
        p1_1_q,  # 9
        p2_0_q,  # 10
        p2_1_q,  # 11
        p3_0_q,  # 12
        p3_1_q,  # 13
        p4_0_q,  # 14
        p4_1_q,  # 15
        ca_p,  # 16
        t1_0_p,  # 17
        t1_1_p,  # 18
        t1_2_p,  # 19
        t2_0_p,  # 20
        t2_1_p,  # 21
        t2_2_p,  # 22
        p1_0_p,  # 23
        p1_1_p,  # 24
        p2_0_p,  # 25
        p2_1_p,  # 26
        p3_0_p,  # 27
        p3_1_p,  # 28
        p4_0_p,  # 29
        p4_1_p,  # 30
    )

    all_eqs: tuple = (
        inital_conditions_eq_1,
        inital_conditions_eq_2,
        ma_j_1,
        ma_j_2,
        ma_j_3,
        ma_j_4,
        ma_j_5,
        ma_j_6,
        ma_j_7,
        ma_cp_1,
        ma_cp_2,
        ma_cp_3,
        ma_cp_4,
        ma_cp_5,
        ma_cp_6,
        en_j_1,
        en_j_2,
        en_j_3,
        en_j_4,
        en_j_5,
        en_j_6,
        en_j_7,
        en_cp_1,
        en_cp_2,
        en_cp_3,
        en_cp_4,
        en_cp_5,
        en_cp_6,
        en_cp_7,
        en_cp_8,
    )

    print(f"\n{len(all_eqs) = }\n")
    print(f"\n{len(all_vars) = }\n")

    sistem_is_defined_and_only_1_solution = len(all_eqs) == len(all_vars)

    assert sistem_is_defined_and_only_1_solution is True

    print(f"\n{sistem_is_defined_and_only_1_solution = }\n")

    all_initial_guesses: list = [None for i in range(len(all_vars))]

    Q_initial_guess: int | float = 5  # * in m^3/s
    P_initial_guess: int | float = 200_000  # * in Pa

    for var_index, var in enumerate(all_vars):
        if var_index < 15:
            print(f"\n{var = }{Q_initial_guess}")
            all_initial_guesses[var_index] = Q_initial_guess

        elif var_index >= 15:
            print(f"\n{var = }{P_initial_guess}")
            all_initial_guesses[var_index] = P_initial_guess
        else:
            print(f"\n{var = }\n")
            print(f"\n{var_index = }\n")
            raise IndexError()

    # // Sanity check
    assert len(all_initial_guesses) == len(all_vars)
    for value in all_initial_guesses:
        assert value is not None

    solved_system = sy.nsolve(all_eqs, all_vars, all_initial_guesses)

    print(f"\n{solved_system = }\n")

    # // In this system, there is only one inlet and one outlet.
    # // Because of that, ca_q_solved and t2_2_q should be numerically equal
    # // if this criteria isn't satisfyedm the system is being solved with a numerical error

    ca_q_index_inside_solved_system: int = (
        0  # * ca_q é a primeira variável da lista all_vars
    )
    t2_2_q_index: int = 6  # * t2_2_q é a tereceira variável da lista all_vars

    ca_q_solved: float | int | list = solved_system[ca_q_index_inside_solved_system]
    t2_2_q_solved: float | int | list = solved_system[t2_2_q_index]

    print(f"\n{ca_q_solved = }\n")
    print(f"\n{t2_2_q_solved = }\n")

    formated_solved_system: dict = dict(zip(all_vars, solved_system))

    pprint(f"{formated_solved_system = }")

    # * delta_q_in_out = ca_q_solved - t2_2_q_solved

    # * print(f"\n{delta_q_in_out = }\n")


def super_simple_system_simplifyed():
    """
    This system will just be used to assure that the library is also capable of
    handling simple and/or small systems too.

    The system tested is shown below:

    ca -> p1
    """

    # // Essa seção é só para consulta

    pure_water = thermo.Chemical("water", T=303, P=300_000)  # temperature in Kelvin
    ca = Current(composition=pure_water)

    p1 = Pipe(internal_diam=0.05, length=4, orientation=0)

    m1 = p1 + ca

    print(f"\n{ca.id = }")
    print(f"\n{ca.pressure_name = }")

    print(f"\n{p1 = }")
    print(f"\n{p1.component_ports[0].id = }")
    print(f"\n{p1.component_ports[0].local_current.id = }")
    print(f"\n{p1.component_ports[0].local_current.pressure_name = }\n")
    print(f"\n{p1.component_ports[1].id = }")
    print(f"\n{p1.component_ports[1].local_current.id = }")
    print(f"\n{p1.component_ports[1].local_current.pressure_name = }\n")

    m1.create_graph_2()

    print(f"\n{m1.graph.edges = }")

    # // Numerical constants
    p_in = 300_000  # * pressão na entrada do tubo (que é a mesma de Ca), em Pa

    # * pressão na saida do tubo (que é a pressão atmosférica manométrica), em Pa
    p_out = 0

    g = 9.18  # * aceleração da gravidade, em m/s^2
    D = 0.05  # * diametro do tubo, em m
    L = 4  # * comprimento do tubo, em m
    mi = ca.ab_viscosity()  # * viscosidade absoluta, em Pa*s

    # // pi = pi
    rho = ca.density()  # * densidade da agua, em kg/m^3 ou g/m^3

    local_q = sy.symbols("Q")

    Re = ((4 * rho) / (mi * pi * D)) * local_q

    Blasius = (0.079) / (Re ** (1 / 4))

    f_laminar = 64 / Re

    f = sy.Piecewise(
        (f_laminar, Re < 2300),
        (
            0.002,
            sy.And(Re > 2300, Re <= 4000),
        ),
        (Blasius, Re > 4000),
    )

    # * f = sy.Piecewise()

    B_cte = f * ((L * 8) / ((D**5) * (pi**2) * g))

    final_energy_eq = (300_000 / (rho * B_cte), local_q * ((local_q ** (1 / 2)) ** 2))

    q_initial_guess = 20  # * chute inicial, em m^3/s

    solved_eq = sy.nsolve([final_energy_eq], [local_q], [q_initial_guess])

    print(f"\n{solved_eq = }\n")


def main():
    # numpy_test()
    # symbols_generalization_test()
    # get_variables_from_sympy_equations()

    # equality_between_equations_test()

    # re_f_q_arrangement_test()
    # // This was the latest test runned before the actual test
    # //  fourth_re_f_q_arrangement_test()

    super_simple_system_simplifyed()


main()
