import numpy as np
import matplotlib.pyplot as plt
from matplotlib.image import imread

def section_4_1():
    print("### 4-1. 벡터(Vector) ###")
    # 벡터 생성
    v1 = np.array([1, 2, 3])
    v2 = np.array([4, 5, 6])
    print(f"v1 = {v1}")
    print(f"v2 = {v2}")

    # 벡터의 크기(norm) 계산
    magnitude_v1 = np.linalg.norm(v1)
    print(f"v1의 크기: {magnitude_v1:.2f}")

    # 벡터 덧셈과 뺄셈
    v_sum = v1 + v2
    v_diff = v1 - v2
    print(f"벡터 덧셈: {v_sum}")
    print(f"벡터 뺄셈: {v_diff}")

    # 스칼라 곱
    scalar = 2
    v_scaled = scalar * v1
    print(f"{scalar} * v1 = {v_scaled}")

    # 내적(Dot Product)
    dot_product = np.dot(v1, v2)
    print(f"v1 · v2 = {dot_product}")

    # 외적(Cross Product) - 3차원 벡터에만 적용 가능
    cross_product = np.cross(v1, v2)
    print(f"v1 × v2 = {cross_product}")
    print("-" * 30)

def section_4_2():
    print("### 4-2. 행렬(Matrix) ###")
    # 행렬 생성
    A = np.array([[1, 2], [3, 4]])
    B = np.array([[5, 6], [7, 8]])

    print(f"행렬 A:\n{A}")
    print(f"행렬 B:\n{B}")

    # 행렬 덧셈과 뺄셈
    C = A + B
    D = A - B
    print(f"A + B =\n{C}")
    print(f"A - B =\n{D}")

    # 행렬 곱셈
    E = np.matmul(A, B)  # 또는 A @ B (Python 3.5 이상)
    print(f"A × B =\n{E}")

    # 전치 행렬(Transpose)
    A_T = A.T
    print(f"A의 전치 행렬:\n{A_T}")

    # 행렬식(Determinant)
    det_A = np.linalg.det(A)
    print(f"A의 행렬식: {det_A}")

    # 역행렬(Inverse Matrix)
    inv_A = np.linalg.inv(A)
    print(f"A의 역행렬:\n{inv_A}")

    # 역행렬 검증: A × A^(-1) = I (단위 행렬)
    I = np.matmul(A, inv_A)
    print(f"A × A^(-1) =\n{np.round(I, decimals=10)}")  # 부동소수점 오차 처리
    print("-" * 30)

def section_5_1():
    print("### 5-1. 고유값(Eigenvalue)과 고유벡터(Eigenvector) ###")
    # 예제 행렬
    A = np.array([[4, -2], [1, 1]])
    print(f"행렬 A:\n{A}")

    # 고유값과 고유벡터 계산
    eigenvalues, eigenvectors = np.linalg.eig(A)

    print(f"고유값: {eigenvalues}")
    print(f"고유벡터:\n{eigenvectors}")

    # 검증: A × v = λ × v
    for i in range(len(eigenvalues)):
        v = eigenvectors[:, i]
        Av = np.matmul(A, v)
        lambda_v = eigenvalues[i] * v
        print(f"\n고유값 {eigenvalues[i]:.2f}에 대한 검증:")
        print(f"A × v = {Av}")
        print(f"λ × v = {lambda_v}")
    print("-" * 30)

def section_5_2():
    print("### 5-2. 특이값 분해(SVD) ###")
    # 예제 행렬 (비정방행렬)
    B = np.array([[3, 2, 2], [2, 3, -2]])
    print(f"행렬 B:\n{B}")

    # SVD 계산
    U, Sigma, VT = np.linalg.svd(B)

    print(f"U 행렬:\n{U}")
    print(f"특이값: {Sigma}")
    print(f"V^T 행렬:\n{VT}")

    # 원래 행렬 복원
    # Sigma는 대각 요소만 반환하므로, 대각 행렬로 변환해야 함
    Sigma_matrix = np.zeros(B.shape)
    for i in range(min(B.shape)):
        Sigma_matrix[i, i] = Sigma[i]

    B_reconstructed = U @ Sigma_matrix @ VT
    print(f"\n복원된 행렬:\n{np.round(B_reconstructed, decimals=10)}")
    print("-" * 30)

def section_6_1():
    print("### 6-1. 선형 회귀와 행렬 연산 ###")
    # 데이터 생성
    np.random.seed(42)
    X = 2 * np.random.rand(100, 1)
    y = 4 + 3 * X + np.random.randn(100, 1)

    # X에 상수항(1)을 추가
    X_b = np.c_[np.ones((100, 1)), X]

    # 정규 방정식으로 파라미터 계산
    theta_best = np.linalg.inv(X_b.T @ X_b) @ X_b.T @ y

    print(f"계산된 파라미터: {theta_best.flatten()}")
    print(f"실제 파라미터: [4, 3]")

    # 예측
    X_new = np.array([[0], [2]])
    X_new_b = np.c_[np.ones((2, 1)), X_new]
    y_predict = X_new_b @ theta_best

    print(f"x=0, x=2에 대한 예측값: {y_predict.flatten()}")
    print("-" * 30)

def section_6_2():
    print("### 6-2. 주성분 분석(PCA)과 고유값 분해 ###")
    # 데이터 생성
    np.random.seed(42)
    mean = [0, 0]
    cov = [[5, 4], [4, 5]]  # 공분산 행렬
    data = np.random.multivariate_normal(mean, cov, 200)

    # 데이터 중앙 정렬
    X = data - data.mean(axis=0)

    # 공분산 행렬 계산
    cov_matrix = np.cov(X.T)
    print(f"공분산 행렬:\n{cov_matrix}")

    # 고유값과 고유벡터 계산
    eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)

    # 고유값 기준으로 고유벡터 정렬
    idx = eigenvalues.argsort()[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]

    print(f"고유값: {eigenvalues}")
    print(f"고유벡터:\n{eigenvectors}")

    # 주성분으로 데이터 투영 (차원 축소)
    n_components = 1  # 첫 번째 주성분만 사용
    X_pca = X @ eigenvectors[:, :n_components]

    # 원래 차원으로 복원
    X_recovered = X_pca @ eigenvectors[:, :n_components].T

    print(f"원본 데이터 차원: {X.shape}")
    print(f"축소된 데이터 차원: {X_pca.shape}")
    print(f"복원된 데이터 차원: {X_recovered.shape}")
    print("-" * 30)

def section_6_3():
    print("### 6-3. 신경망과 행렬 연산 ###")
    def sigmoid(x):
        return 1 / (1 + np.exp(-x))

    # 입력 데이터
    X = np.array([[0.1, 0.2, 0.3]])

    # 첫 번째 층 가중치와 편향
    W1 = np.array([[0.1, 0.2, 0.3],
                   [0.4, 0.5, 0.6],
                   [0.7, 0.8, 0.9]])
    b1 = np.array([0.1, 0.2, 0.3])

    # 두 번째 층 가중치와 편향
    W2 = np.array([[0.1, 0.2, 0.3],
                   [0.4, 0.5, 0.6]])
    b2 = np.array([0.1, 0.2])

    # 순전파 계산
    z1 = X @ W1 + b1
    a1 = sigmoid(z1)
    z2 = a1 @ W2 + b2
    a2 = sigmoid(z2)

    print(f"입력: {X}")
    print(f"첫 번째 층 출력: {a1}")
    print(f"최종 출력: {a2}")
    print("-" * 30)

def exercise_1():
    print("### 연습문제 1: 이미지 압축 (SVD 활용) ###")
    try:
        # 이미지 로드 (그레이스케일로 변환)
        # 실습을 위해 'image.jpg' 파일을 소스코드와 같은 디렉토리에 위치시켜주세요.
        img = imread('ai_lecture/source_code/image.jpg')
        if len(img.shape) == 3:  # 컬러 이미지인 경우
            img = np.mean(img, axis=2)  # 그레이스케일로 변환

        plt.figure(figsize=(12, 6))
        plt.subplot(1, 3, 1)
        plt.imshow(img, cmap='gray')
        plt.title('Original Image')
        plt.axis('off')

        # SVD 계산
        U, Sigma, VT = np.linalg.svd(img)

        # 다양한 수의 특이값으로 이미지 재구성
        k = 50  # 사용할 특이값 수
        reconstructed = U[:, :k] @ np.diag(Sigma[:k]) @ VT[:k, :]

        plt.subplot(1, 3, 2)
        plt.imshow(reconstructed, cmap='gray')
        plt.title(f'Reconstructed (k={k})')
        plt.axis('off')

        # 잔차(Residual) 이미지
        residual = img - reconstructed
        plt.subplot(1, 3, 3)
        plt.imshow(residual, cmap='gray')
        plt.title('Residual (Error)')
        plt.axis('off')
        
        plt.tight_layout()
        plt.show()

        # 압축률과 오차 계산
        original_size = img.shape[0] * img.shape[1]
        compressed_size = k * (img.shape[0] + img.shape[1] + 1)
        compression_ratio = original_size / compressed_size
        error = np.linalg.norm(img - reconstructed) / np.linalg.norm(img)

        print(f"압축률: {compression_ratio:.2f}x")
        print(f"상대 오차: {error:.4f}")

    except FileNotFoundError:
        print("\n오류: `ai_lecture/source_code/image.jpg` 파일을 찾을 수 없습니다.")
        print("실습을 진행하려면 이미지를 해당 경로에 추가해주세요.")
    print("-" * 30)

def exercise_2():
    print("### 연습문제 2: 선형 시스템 해결 ###")
    # 계수 행렬 A와 상수 벡터 b 정의
    A = np.array([[2, 1, -1],
                  [-3, -1, 2],
                  [-2, 1, 2]])
    b = np.array([8, -11, -3])

    # 선형 시스템 해결
    x = np.linalg.solve(A, b)

    print(f"해: x = {x[0]}, y = {x[1]}, z = {x[2]}")

    # 검증
    verification = A @ x
    print(f"A @ x = {verification}")
    print(f"b = {b}")
    print(f"검증 (A@x 와 b가 거의 같은가?): {np.allclose(A @ x, b)}")
    print("-" * 30)

if __name__ == '__main__':
    section_4_1()
    section_4_2()
    section_5_1()
    section_5_2()
    section_6_1()
    section_6_2()
    section_6_3()
    exercise_1()
    exercise_2() 