import numpy as np
from itertools import product

# Parameters
learning_rate = 0.1
max_iterations = 1000

# Activation function with dynamic thresholding
def activation_function(weighted_sum, dynamic_threshold):
    return 1 if weighted_sum > dynamic_threshold else 0

# Inputs for the two-variable logic tables
inputs = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])

# Generate all 16 possible truth tables for 2 inputs
all_truth_tables = list(product([0, 1], repeat=4))  # 16 combinations of binary outputs

# Train the network for each truth table
for truth_table_idx, targets in enumerate(all_truth_tables):
    print(f"\nTraining for Truth Table {truth_table_idx + 1}: {targets}")

    # Initialize weights
    weights_input_to_hidden = np.random.rand(3)  # Weights from N1, N2, bias → N3
    weights_hidden_to_output = np.random.rand(2)  # Weights from N3, bias → N4
    weights_input_to_output = np.random.rand(3)  # Weights from N1, N2, bias → N4

    epoch = 0
    network_trained = False

    while epoch < max_iterations:
        epoch += 1
        all_correct = True  # Flag to track if all outputs are correct

        for input_vector, target in zip(inputs, targets):
            N1, N2 = input_vector  # Input neurons
            bias = 1  # Bias input

            # Forward pass
            N3_input = np.dot(np.append(input_vector, bias), weights_input_to_hidden)  # Weighted sum for N3
            N3 = activation_function(N3_input, dynamic_threshold=0.5)  # Output of N3 (hidden neuron)

            N4_input = (N3 * weights_hidden_to_output[0] +  # From hidden neuron
                        bias * weights_hidden_to_output[1] +  # Bias contribution
                        np.dot(np.append(input_vector, bias), weights_input_to_output))  # Direct connections
            N4 = activation_function(N4_input, dynamic_threshold=0.5)  # Output of N4 (final output)

            # Error calculation
            error = target - N4

            # Backpropagation and weight updates
            if error != 0:
                all_correct = False

                # Update weights for N3 → N4
                weights_hidden_to_output[0] += learning_rate * error * N3
                weights_hidden_to_output[1] += learning_rate * error * bias

                # Update weights for N1, N2, bias → N4
                weights_input_to_output += learning_rate * error * np.append(input_vector, bias)

                # Update weights for N1, N2, bias → N3
                weights_input_to_hidden += learning_rate * error * np.append(input_vector, bias)

        if all_correct:
            network_trained = True
            break  # Stop training if all outputs are correct

    # Print results for the truth table
    if network_trained:
        print(f"The network learned the truth table correctly after {epoch} iterations.")
    else:
        print(f"The network failed to learn the truth table after {epoch} iterations.")

    # Test the trained network
    print("\nTesting the trained network:")
    for input_vector, target in zip(inputs, targets):
        N1, N2 = input_vector
        bias = 1

        # Forward pass
        N3_input = np.dot(np.append(input_vector, bias), weights_input_to_hidden)
        N3 = activation_function(N3_input, dynamic_threshold=0.5)

        N4_input = (N3 * weights_hidden_to_output[0] +
                    bias * weights_hidden_to_output[1] +
                    np.dot(np.append(input_vector, bias), weights_input_to_output))
        N4 = activation_function(N4_input, dynamic_threshold=0.5)

        print(f"Input: {input_vector}, Target: {target}, Output: {N4}")
    print("-----------------------------------------------------------")
