#define GLM_ENABLE_EXPERIMENTAL
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/quaternion.hpp>
#include <iostream>
#include <fstream>
#include <string>
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

// Buffers and Shader Program
GLuint VBO, VAO, EBO, shaderProgram;

// Arcball Variables
glm::vec3 lastPos;
glm::vec3 currPos;
bool isDragging = false;
bool isRotating = false; // Rotation 상태
glm::vec2 lastCursorPos;    // 이전 마우스 커서 위치
glm::mat4 translationMatrix = glm::mat4(1.0f);
glm::quat rotationQuat = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
float zoomLevel = 3.0f; // 초기 줌 레벨

GLuint textures[6]; // 6개의 텍스처 저장

// ReadFile
std::string ReadFile(const char* filePath) {
    std::ifstream file(filePath, std::ios::in);
    if (!file.is_open()) {
        std::cerr << "Failed to open file: " << filePath << std::endl;
        return "";
    }

    std::string content, line;
    while (std::getline(file, line)) {
        content += line + "\\n";
    }
    file.close();
    return content;
}

// CreateShaderProgram 
GLuint CreateShaderProgram(const char* vertexPath, const char* fragmentPath) {
    std::string vsCode = ReadFile(vertexPath);
    std::string fsCode = ReadFile(fragmentPath);

    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    const char* vsSource = vsCode.c_str();
    glShaderSource(vertexShader, 1, &vsSource, NULL);
    glCompileShader(vertexShader);

    GLint success;
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        char infoLog[512];
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cerr << "Error compiling vertex shader: " << infoLog << std::endl;
    }

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    const char* fsSource = fsCode.c_str();
    glShaderSource(fragmentShader, 1, &fsSource, NULL);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        char infoLog[512];
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cerr << "Error compiling fragment shader: " << infoLog << std::endl;
    }

    GLuint program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    glLinkProgram(program);

    glGetProgramiv(program, GL_LINK_STATUS, &success);
    if (!success) {
        char infoLog[512];
        glGetProgramInfoLog(program, 512, NULL, infoLog);
        std::cerr << "Error linking program: " << infoLog << std::endl;
    }

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return program;
}

// LoadTexture 
GLuint LoadTexture(const char* filename) {
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    int width, height, nrChannels;
    unsigned char* data = stbi_load(filename, &width, &height, &nrChannels, 0);
    if (data) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
        std::cout << "Texture loaded: " << filename << " (" << width << "x" << height << ")" << std::endl;
    }
    else {
        std::cerr << "Failed to load texture: " << filename << std::endl;
    }
    stbi_image_free(data);

    return texture;
}

// CreateCube 
void CreateCube() {
    GLfloat vertices[] = {
        // Positions          // Texture Coords
        -1.0f, -1.0f,  1.0f,  0.0f, 0.0f,
         1.0f, -1.0f,  1.0f,  1.0f, 0.0f,
         1.0f,  1.0f,  1.0f,  1.0f, 1.0f,
        -1.0f,  1.0f,  1.0f,  0.0f, 1.0f,
        -1.0f, -1.0f, -1.0f,  0.0f, 0.0f,
        -1.0f,  1.0f, -1.0f,  0.0f, 1.0f,
         1.0f,  1.0f, -1.0f,  1.0f, 1.0f,
         1.0f, -1.0f, -1.0f,  1.0f, 0.0f,
    };

    GLuint indices[] = {
        0, 1, 2, 2, 3, 0,
        4, 5, 6, 6, 7, 4,
        4, 5, 3, 3, 0, 4,
        1, 2, 6, 6, 7, 1,
        3, 2, 6, 6, 5, 3,
        4, 7, 1, 1, 0, 4,
    };

    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glGenBuffers(1, &EBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    std::cout << "Cube created." << std::endl;
}

// Scroll Callback (Zoom 구현)
void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
    zoomLevel -= (float)yoffset * 0.1f;
    zoomLevel = glm::clamp(zoomLevel, 1.0f, 10.0f); // 줌 레벨 제한
}

// Mouse Button Callback
void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
    if (button == GLFW_MOUSE_BUTTON_LEFT) {
        if (action == GLFW_PRESS) {
            isDragging = true;

            double x, y;
            glfwGetCursorPos(window, &x, &y);
            lastCursorPos = glm::vec2(x, y);
        }
        else if (action == GLFW_RELEASE) {
            isDragging = false;
        }
    }

    if (button == GLFW_MOUSE_BUTTON_RIGHT) {
        if (action == GLFW_PRESS) {
            isRotating = true;

            double x, y;
            glfwGetCursorPos(window, &x, &y);
            lastCursorPos = glm::vec2(x, y);
        }
        else if (action == GLFW_RELEASE) {
            isRotating = false;
        }
    }
}
// Cursor Position Callback
void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
    glm::vec2 currentPos = glm::vec2(xpos, ypos);
    glm::vec2 delta = currentPos - lastCursorPos;

    if (isDragging) {
        translationMatrix = glm::translate(translationMatrix, glm::vec3(delta.x * 0.01f, -delta.y * 0.01f, 0.0f));
        lastCursorPos = currentPos;
    }

    if (isRotating) {
        float angle = glm::length(delta) * 0.01f;
        glm::vec3 axis = glm::vec3(-delta.y, delta.x, 0.0f);
        if (glm::length(axis) > 0.0001f) {
            axis = glm::normalize(axis);
            rotationQuat = glm::rotate(rotationQuat, angle, axis);
        }
        lastCursorPos = currentPos;
    }
}

int main() {
    if (!glfwInit()) return -1;

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "Arcball Interaction", NULL, NULL);
    if (!window) return -1;

    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK) return -1;

    // Set up cube and shaders
    shaderProgram = CreateShaderProgram("shader.vert", "shader.frag");
    CreateCube();

    textures[0] = LoadTexture("C:/Users/kshyu/OpenGLBasic/OpenGLVS/OpenGLVS/texture/front.jpg");
    textures[1] = LoadTexture("C:/Users/kshyu/OpenGLBasic/OpenGLVS/OpenGLVS/texture/back.jpg");
    textures[2] = LoadTexture("C:/Users/kshyu/OpenGLBasic/OpenGLVS/OpenGLVS/texture/left.jpg");
    textures[3] = LoadTexture("C:/Users/kshyu/OpenGLBasic/OpenGLVS/OpenGLVS/texture/right.jpg");
    textures[4] = LoadTexture("C:/Users/kshyu/OpenGLBasic/OpenGLVS/OpenGLVS/texture/top.jpg");
    textures[5] = LoadTexture("C:/Users/kshyu/OpenGLBasic/OpenGLVS/OpenGLVS/texture/bottom.jpg");

    glEnable(GL_DEPTH_TEST);

    // Register callbacks
    glfwSetScrollCallback(window, ScrollCallback);
    glfwSetMouseButtonCallback(window, MouseButtonCallback);
    glfwSetCursorPosCallback(window, CursorPosCallback);

    while (!glfwWindowShouldClose(window)) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(shaderProgram);

        glm::mat4 model = glm::mat4(1.0f);
        model = translationMatrix * glm::toMat4(rotationQuat) * model;

        glm::mat4 view = glm::lookAt(
            glm::vec3(3.0f, 3.0f, zoomLevel),
            glm::vec3(0.0f, 0.0f, 0.0f),
            glm::vec3(0.0f, 1.0f, 0.0f)
        );
        glm::mat4 projection = glm::perspective(
            glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f
        );
        glm::mat4 mvp = projection * view * model;

        GLuint mvpLoc = glGetUniformLocation(shaderProgram, "MVP");
        glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, glm::value_ptr(mvp));

        for (int i = 0; i < 6; i++) {
            glUniform1i(glGetUniformLocation(shaderProgram, "faceIndex"), i);

            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, textures[i]);
            glUniform1i(glGetUniformLocation(shaderProgram, "textures[0]"), 0);

            glBindVertexArray(VAO);
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)(i * 6 * sizeof(GLuint)));
        }

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

shader.frag

#version 460

in vec2 TexCoords;   // Vertex Shader에서 전달받은 텍스처 좌표
out vec4 FragColor;  // 최종 픽셀 색상 출력

uniform sampler2D textures[6]; // 6개의 텍스처 샘플러
uniform int faceIndex;         // 현재 렌더링 중인 면의 인덱스

void main() {
    FragColor = texture(textures[faceIndex], TexCoords); // 현재 면에 해당하는 텍스처 샘플링
}

shader.vert

#version 460

layout(location = 0) in vec3 pos;         // 정점 위치
layout(location = 1) in vec2 texCoord;    // 텍스처 좌표

out vec2 TexCoords; // 텍스처 좌표를 Fragment Shader로 전달

uniform mat4 MVP; // 모델-뷰-투영 행렬

void main() {
    gl_Position = MVP * vec4(pos, 1.0); // 모델-뷰-투영 행렬을 적용하여 정점 위치 계산
    TexCoords = texCoord;              // 텍스처 좌표를 그대로 전달
}