import streamlit as st
import matplotlib.pyplot as plt
import random
from mpl_toolkits.mplot3d import Axes3D
import plotly.graph_objects as go
import pandas as pd

# Se debe tener instalado plotly, streamlit y matplotlib
st.set_page_config(layout="centered", page_title="AG for TSP 3D", page_icon="馃К")


# Funci贸n para generar una poblaci贸n inicial aleatoria
def generar_poblacion(num_individuos, num_ciudades):
    poblacion = []
    for _ in range(num_individuos):
        individuo = list(range(num_ciudades))
        random.shuffle(individuo)
        poblacion.append(individuo)
    return poblacion


# Funci贸n para evaluar la aptitud de un individuo (distancia total del recorrido)
def calcular_aptitud(individuo, distancias, coordenadas):
    distancia_total = 0

    # Verificar si todas las coordenadas de inicio son cero
    coordenadas_inicio_cero = all(coord == 0 for coord in coordenadas[0])

    if not coordenadas_inicio_cero:
        for i in range(len(individuo) - 1):
            ciudad_actual = individuo[i]
            siguiente_ciudad = individuo[i + 1]
            distancia_total += distancias[ciudad_actual][siguiente_ciudad]
        distancia_total += distancias[individuo[-1]][individuo[0]]

    return distancia_total


# Funci贸n para seleccionar individuos para la reproducci贸n (torneo binario)
def seleccion_torneo(poblacion, distancias, coordenadas):
    seleccionados = []
    for _ in range(len(poblacion)):
        torneo = random.sample(poblacion, 2)
        aptitud_torneo = [
            calcular_aptitud(individuo, distancias, coordenadas) for individuo in torneo
        ]
        seleccionado = torneo[aptitud_torneo.index(min(aptitud_torneo))]
        seleccionados.append(seleccionado)
    return seleccionados


# Funci贸n para realizar el cruce de dos padres para producir un hijo
def cruzar(padre1, padre2):
    punto_cruce = random.randint(0, len(padre1) - 1)
    hijo = padre1[:punto_cruce] + [
        gen for gen in padre2 if gen not in padre1[:punto_cruce]
    ]
    return hijo


# Funci贸n para aplicar mutaciones en la poblaci贸n
def mutar(individuo, probabilidad_mutacion):
    if random.random() < probabilidad_mutacion:
        indices = random.sample(range(len(individuo)), 2)
        individuo[indices[0]], individuo[indices[1]] = (
            individuo[indices[1]],
            individuo[indices[0]],
        )
    return individuo


# Funci贸n para generar distancias aleatorias entre ciudades y sus coordenadas tridimensionales
def generar_distancias(num_ciudades):
    distancias = [[0] * num_ciudades for _ in range(num_ciudades)]
    # Verificar si todas las coordenadas de inicio son cero
    coordenadas_inicio_cero = all(coord == 0 for coord in coordenadas[0])

    if not coordenadas_inicio_cero:
        for i in range(num_ciudades):
            for j in range(i + 1, num_ciudades):
                distancias[i][j] = distancias[j][i] = (
                    sum((x - y) ** 2 for x, y in zip(coordenadas[i], coordenadas[j]))
                    ** 0.5
                )

    return distancias


def visualizar_camino_streamlit(camino, coordenadas, mejor_distancia):
    fig_camino = go.Figure()

    # A帽adir el camino como un trazado 3D interactivo
    x = [coordenadas[i][0] for i in camino]
    y = [coordenadas[i][1] for i in camino]
    z = [coordenadas[i][2] for i in camino]

    # A帽adir el camino como un trazado 3D interactivo con identificadores
    fig_camino.add_trace(
        go.Scatter3d(
            x=x, y=y, z=z, mode="lines+markers", marker=dict(size=5), name="Camino"
        )
    )

    # A帽adir los puntos de inicio y fin con etiquetas
    fig_camino.add_trace(
        go.Scatter3d(
            x=[x[0]],
            y=[y[0]],
            z=[z[0]],
            mode="markers+text",
            marker=dict(color="green", size=10),
            name="Inicio",
            text=[str(camino[0])],
            textposition="top center",
        )
    )

    fig_camino.add_trace(
        go.Scatter3d(
            x=[x[-1]],
            y=[y[-1]],
            z=[z[-1]],
            mode="markers+text",
            marker=dict(color="red", size=10),
            name="Fin",
            text=[str(camino[-1])],
            textposition="top center",
        )
    )

    # A帽adir etiquetas a los puntos intermedios
    for i, (xi, yi, zi) in enumerate(zip(x[1:-1], y[1:-1], z[1:-1])):
        fig_camino.add_trace(
            go.Scatter3d(
                x=[xi],
                y=[yi],
                z=[zi],
                mode="markers+text",
                marker=dict(size=5),
                text=[str(camino[i + 1])],
                textposition="top center",
                showlegend=False,
            )
        )

    # Configuraciones adicionales
    fig_camino.update_layout(
        scene=dict(aspectmode="cube"),
        title=f"Mejor Camino Encontrado\nDistancia: {mejor_distancia:.2f}",
    )

    # Mostrar el gr谩fico interactivo en Streamlit
    st.plotly_chart(fig_camino)


def fitness(distancia, maxima_distancia, tamCromosoma):
    return (
        0
        if distancia * tamCromosoma == 0
        else 1 - ((distancia) / (maxima_distancia * tamCromosoma))
    )


def algoritmo_genetico(
    num_generaciones,
    num_ciudades,
    num_individuos,
    probabilidad_mutacion,
    distancias,
    coordenadas,
    probabilidad_cruce,
):
    poblacion = generar_poblacion(num_individuos, num_ciudades)
    mejor_solucion_historial = []
    mejor_distancia_historial = []
    peor = 0
    fitness_historial = []
    for _ in range(num_generaciones):
        poblacion = sorted(
            poblacion, key=lambda x: calcular_aptitud(x, distancias, coordenadas)
        )
        mejor_individuo = poblacion[0]
        mejor_distancia = calcular_aptitud(mejor_individuo, distancias, coordenadas)
        mejor_solucion_historial.append(mejor_individuo)
        mejor_distancia_historial.append(mejor_distancia)
        seleccionados = seleccion_torneo(poblacion, distancias, coordenadas)
        nueva_poblacion = []
        for i in range(0, len(seleccionados), 2):
            padre1, padre2 = seleccionados[i], seleccionados[i + 1]
            aleatorio_cruce = random.uniform(0, 1)
            if aleatorio_cruce < probabilidad_cruce:
                hijo1 = cruzar(padre1, padre2)
                hijo2 = cruzar(padre2, padre1)
            else:
                hijo1, hijo2 = padre1, padre2
            hijo1 = mutar(hijo1, probabilidad_mutacion)
            hijo2 = mutar(hijo2, probabilidad_mutacion)
            nueva_poblacion.extend([hijo1, hijo2])
        poblacion = nueva_poblacion
        if peor < mejor_distancia:
            peor = mejor_distancia
        fitness_historial.append(
            fitness(
                mejor_distancia,
                peor,
                len(padre1),
            )
        )
    mejor_solucion = poblacion[0]
    mejor_distancia = calcular_aptitud(mejor_solucion, distancias, coordenadas)
    # Visualizar el proceso del algoritmo
    visualizar_proceso_streamlit(
        mejor_distancia_historial, mejor_solucion, coordenadas, mejor_distancia
    )
    # visualizar el fitness
    visualizar_proceso_fitness_streamlit(fitness_historial)
    # Visualizar el mejor camino encontrado
    visualizar_camino_streamlit(mejor_solucion, coordenadas, mejor_distancia)

    return mejor_solucion, mejor_distancia


def algoritmo_genetico_early_stopping(
    num_generaciones,
    num_ciudades,
    num_individuos,
    probabilidad_mutacion,
    distancias,
    coordenadas,  # Aseg煤rate de tener este par谩metro
    probabilidad_cruce,
    max_generaciones_sin_mejora,
):
    poblacion = generar_poblacion(num_individuos, num_ciudades)
    mejor_solucion_historial = []
    mejor_distancia_historial = []
    peor = 0
    fitness_historial = []
    generaciones_sin_mejora = 0  # Inicializar contador de generaciones sin mejora

    for _ in range(num_generaciones):
        poblacion = sorted(
            poblacion, key=lambda x: calcular_aptitud(x, distancias, coordenadas)
        )
        mejor_individuo = poblacion[0]

        mejor_distancia = calcular_aptitud(mejor_individuo, distancias, coordenadas)
        # Almacenar el mejor individuo y su distancia en cada generaci贸n
        mejor_solucion_historial.append(mejor_individuo)
        mejor_distancia_historial.append(mejor_distancia)

        seleccionados = seleccion_torneo(
            poblacion, distancias, coordenadas
        )  # Pasa coordenadas aqu铆

        nueva_poblacion = []
        for i in range(0, len(seleccionados), 2):
            padre1, padre2 = seleccionados[i], seleccionados[i + 1]
            aleatorio_local = random.uniform(0, 1)
            if aleatorio_local <= probabilidad_cruce:
                hijo1 = cruzar(padre1, padre2)
                hijo2 = cruzar(padre2, padre1)
            else:
                hijo1, hijo2 = padre1, padre2
            hijo1 = mutar(hijo1, probabilidad_mutacion)
            hijo2 = mutar(hijo2, probabilidad_mutacion)
            nueva_poblacion.extend([hijo1, hijo2])

        poblacion = nueva_poblacion
        if peor < mejor_distancia:
            peor = mejor_distancia
            generaciones_sin_mejora = 0  # Reiniciar contador al mejorar
        else:
            generaciones_sin_mejora += 1

        # Verificar Early Stopping
        if generaciones_sin_mejora >= max_generaciones_sin_mejora:
            st.warning(
                "Early Stopping: Se detuvo el algoritmo debido a falta de mejora."
            )
            break

        fitness_historial.append(fitness(mejor_distancia, peor, len(padre1)))
    mejor_solucion = poblacion[0]
    mejor_distancia = calcular_aptitud(mejor_solucion, distancias, coordenadas)
    # Visualizar el proceso del algoritmo
    visualizar_proceso_streamlit(
        mejor_distancia_historial, mejor_solucion, coordenadas, mejor_distancia
    )
    # visualizar el fitness
    visualizar_proceso_fitness_streamlit(fitness_historial)
    # Visualizar el mejor camino encontrado
    visualizar_camino_streamlit(mejor_solucion, coordenadas, mejor_distancia)

    return mejor_solucion, mejor_distancia


def visualizar_proceso_fitness_streamlit(fitness_arreglo):
    generaciones = list(range(len(fitness_arreglo)))

    # Crear gr谩fico interactivo de evoluci贸n de la distancia
    fig_distancia = go.Figure()
    fig_distancia.add_trace(
        go.Scatter(x=generaciones, y=fitness_arreglo, mode="lines+markers")
    )
    fig_distancia.update_layout(
        title="Evoluci贸n del fitnesss en Cada Generaci贸n",
        xaxis_title="Generaci贸n",
        yaxis_title="fitness",
    )
    st.plotly_chart(fig_distancia)


def visualizar_proceso_streamlit(
    mejor_distancia_historial, mejor_solucion, coordenadas, mejor_distancia
):
    generaciones = list(range(len(mejor_distancia_historial)))

    # Crear gr谩fico interactivo de evoluci贸n de la distancia
    fig_distancia = go.Figure()
    fig_distancia.add_trace(
        go.Scatter(x=generaciones, y=mejor_distancia_historial, mode="lines+markers")
    )
    fig_distancia.update_layout(
        title="Evoluci贸n de la Distancia en Cada Generaci贸n",
        xaxis_title="Generaci贸n",
        yaxis_title="Distancia",
    )
    st.plotly_chart(fig_distancia)


# Funci贸n para generar distancias aleatorias entre ciudades y sus coordenadas tridimensionales
def generar_distancias_coordenadas(num_ciudades):
    distancias = [[0] * num_ciudades for _ in range(num_ciudades)]
    coordenadas = [
        (random.uniform(0, 100), random.uniform(0, 100), random.uniform(0, 100))
        for _ in range(num_ciudades)
    ]

    for i in range(num_ciudades):
        for j in range(i + 1, num_ciudades):
            distancias[i][j] = distancias[j][i] = (
                sum((x - y) ** 2 for x, y in zip(coordenadas[i], coordenadas[j])) ** 0.5
            )

    return distancias, coordenadas


if __name__ == "__main__":
    st.title("Algoritmo Gen茅tico para el Problema del Viajante")
    st.sidebar.header("Configuraci贸n")
    manual_input = st.sidebar.checkbox("Ingresar datos manualmente")

    # Ingresar datos manualmente o generarlos aleatoriamente (por defecto), adem谩s de generar las distancias
    if manual_input:
        st.sidebar.subheader("Ingresar coordenadas manualmente:")
        num_ciudades = st.sidebar.number_input(
            "N煤mero de Ciudades", min_value=5, max_value=100, value=10, step=5
        )

        coordenadas = []
        for i in range(num_ciudades):
            st.sidebar.subheader(f"Coordenadas Ciudad {i + 1}")
            x = st.sidebar.number_input(
                f"X Ciudad {i + 1}:", value=0.0, key=f"x_{i}", step=1.0
            )
            y = st.sidebar.number_input(
                f"Y Ciudad {i + 1}:", value=0.0, key=f"y_{i}", step=1.0
            )
            z = st.sidebar.number_input(
                f"Z Ciudad {i + 1}:", value=0.0, key=f"z_{i}", step=1.0
            )
            coordenadas.append((x, y, z))

        data = pd.DataFrame(coordenadas, columns=["X", "Y", "Z"])
        distancias = generar_distancias(num_ciudades)
    else:
        num_ciudades = st.sidebar.number_input(
            "N煤mero de Ciudades", min_value=5, max_value=100, value=10, step=5
        )

        coordenadas = generar_distancias_coordenadas(num_ciudades)[1]
        distancias = generar_distancias(num_ciudades)

    # Mostrar datos en la interfaz
    st.write(
        "Puntos en el espacio tridimensional (X, Y, Z), donde cada punto es una ciudad"
    )
    st.dataframe(
        data=pd.DataFrame(coordenadas, columns=["X", "Y", "Z"]),
        use_container_width=True,
    )

    # Configuraciones adicionales
    num_generaciones = st.sidebar.number_input(
        "N煤mero de Generaciones", min_value=10, max_value=1000, value=50
    )
    num_individuos = st.sidebar.slider(
        "Tama帽o de la Poblaci贸n ($par$)", min_value=10, max_value=100, value=50, step=2
    )
    probabilidad_mutacion = st.sidebar.slider(
        "Probabilidad de Mutaci贸n", min_value=0.01, max_value=0.1, value=0.01
    )
    probabilidad_cruce = st.sidebar.slider(
        "Probabilidad de cruce", min_value=0.9, max_value=0.95, value=0.01, step=0.01
    )

    checkbox = st.sidebar.checkbox("Early Stopping")
    if checkbox:
        max_generaciones_sin_mejora = st.sidebar.number_input(
            "Limite de generaciones sin mejora",
            min_value=5,
            max_value=1000,
            value=20,
            step=5,
        )
        mejor_solucion, mejor_distancia = algoritmo_genetico_early_stopping(
            num_generaciones,
            num_ciudades,
            num_individuos,
            probabilidad_mutacion,
            distancias,
            coordenadas,
            probabilidad_cruce,
            max_generaciones_sin_mejora,
        )

    else:
        mejor_solucion, mejor_distancia = algoritmo_genetico(
            num_generaciones,
            num_ciudades,
            num_individuos,
            probabilidad_mutacion,
            distancias,
            coordenadas,
            probabilidad_cruce,
        )

    # Mostrar resultados
    st.success(f"Mejor soluci贸n encontrada: {mejor_solucion}")
    st.success(f"Mejor distancia encontrada: {mejor_distancia:.2f}")