Source: UI.js

import Scene, {
    inputShapes,
    inputCoords,
    Colours,
    shape_obj,
    shape_placed_obj
} from "../js/scene.js"
import Sol_Scene, {
    sol_inputShapes,
    sol_inputCoords,
    sol_Colours
} from "../js/sol_scene.js"

import Pyramid from '../js/pyramid.js'
import {
    convert_to_pyramid_layers
} from "./ConvertSolutionFormat.js";
import {
    generate_headers,
    populate_problem_matrix3D,
    reduce_problem_matrix
} from "./GenerateProblemMatrix.js";
import {
    create_dicts
} from "./CreateObjects.js";
import {
    solve
} from "./Solver.js";
import {
    shapeStore
} from "./Shapes3D.js";
import resetFirstPlacementCoord from "../js/scene.js";



window.onload = function() {
    const image_names = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'];

    const imageIds = [
        "shape-1", "shape-2", "shape-3", "shape-4", "shape-5", "shape-6",
        "shape-7", "shape-8", "shape-9", "shape-10", "shape-11", "shape-12"
    ];


    let currentIndex = 0;
    let currentAlphabetIndex = 0;
    let currentImageName = "A"
    currentImage.className = currentImageName

    /**
     * Updates the alphabet container with the current alphabet index.
     */
    function updateAlphabet() {
        const alphabetContainer = document.getElementById('currentAlphabet');
        alphabetContainer.textContent = image_names[currentAlphabetIndex];
    }

    /**
     * Updates the image source, name, class, and rotation angle of the current image.
     */
    function updateImage() {
        currentImage.src = `/static/polysphere3D_app/img/shapes/${imageIds[currentIndex]}.png`;
        currentImageName = image_names[currentIndex]
        currentImage.className = currentImageName
    }

    /**
     * Moves to the previous image in the sequence.
     */
    function previousImage() {
        currentIndex = (currentIndex - 1 + imageIds.length) % imageIds.length;
        updateImage();
        currentAlphabetIndex = (currentAlphabetIndex - 1 + image_names.length) % image_names.length;
        updateAlphabet();
    }

    /**
     * Advances to the next image and updates the UI accordingly.
     */
    function nextImage() {
        currentIndex = (currentIndex + 1) % imageIds.length;
        updateImage();
        currentAlphabetIndex = (currentAlphabetIndex + 1) % image_names.length;
        updateAlphabet();
    }

    const previousImageButton = document.getElementById('previousImageButton');
    previousImageButton.addEventListener('click', previousImage);
    const nextImageButton = document.getElementById('nextImageButton');
    nextImageButton.addEventListener('click', nextImage);


}
/**
 * Represents a worker object.
 * @type {Pyramid}
 */
let worker = new Pyramid(5, 1);
/**
 * Represents a sol_worker object.
 * @type {Pyramid}
 */
let sol_worker = new Pyramid(5, 1);

let scene = new Scene();
let sol_scene = new Sol_Scene();
const FPS = 30;
let uiTimer = null;
let visibilityStates = [true, true, true, true, true];

/**
 * Creates a timer that repeatedly calls the specified function at a given frame rate.
 * @param {Function} func - The function to be called repeatedly by the timer.
 */
function createTimer(func) {
    if (uiTimer) {
        clearInterval(uiTimer);
        uiTimer = null;
    }

    uiTimer = setInterval(() => {
        func();
    }, 1000 / FPS);
}


/**
 * Renders the pyramid by iterating through the layers of spheres and updating their positions and colors.
 */
function renderPyramid() {
    for (let i = 0; i < worker.layers.length; i++) {
        const spheres = worker.layers[i].matrix;
        for (let x = 0; x < worker.layers[i].size; x++) {
            for (let y = 0; y < worker.layers[i].size; y++) {
                let pos = spheres[x][y].pos;
                let color = spheres[x][y].color;

                if (!spheres[x][y].userData) {
                    spheres[x][y].userData = scene.createSphere(
                        pos[0],
                        pos[1],
                        pos[2],
                        color,
                        worker.radius()
                    );
                    scene.add(spheres[x][y].userData);
                } else {
                    spheres[x][y].userData.material.color.set(color);
                    spheres[x][y].userData.material.specular.set(color);
                }
            }
        }
    }
}

/**
 * Renders the solution pyramid by updating the positions and colors of the spheres in the scene.
 */
function sol_renderPyramid() {
    for (let i = 0; i < sol_worker.layers.length; i++) {
        const spheres = sol_worker.layers[i].matrix;
        for (let x = 0; x < sol_worker.layers[i].size; x++) {
            for (let y = 0; y < sol_worker.layers[i].size; y++) {
                let pos = spheres[x][y].pos;
                let color = spheres[x][y].color;

                if (!spheres[x][y].userData) {
                    spheres[x][y].userData = sol_scene.createSphere(
                        pos[0],
                        pos[1],
                        pos[2],
                        color,
                        sol_worker.radius()
                    );
                    sol_scene.add(spheres[x][y].userData);
                } else {
                    spheres[x][y].userData.material.color.set(color);
                    spheres[x][y].userData.material.specular.set(color);
                }
            }
        }
    }
}

/**
 * Updates the visibility of a layer and its spheres.
 * @param {number} idx - The index of the layer.
 * @param {boolean} v - The new visibility state.
 */
function layerVisible(idx, v) {
    // Updates the visibilityStates to match change
    visibilityStates[idx - 1] = v
    let layer = worker.getLayer(idx);
    const spheres = layer.matrix;
    for (let x = 0; x < layer.size; x++) {
        for (let y = 0; y < layer.size; y++) {
            if (spheres[x][y].userData) {
                spheres[x][y].userData.visible = v;
                spheres[x][y].visible = v;
                spheres[x][y].userData.needsUpdate = true;
            }
        }
    }
}

/**
 * Updates the visibility of a layer in the solution pyramid object.
 * 
 * @param {number} idx - The index of the layer.
 * @param {boolean} v - The new visibility state of the layer.
 */
function sol_layerVisible(idx, v) {
    // Updates the visibilityStates to match change
    visibilityStates[idx - 1] = v
    let layer = sol_worker.getLayer(idx);
    const spheres = layer.matrix;
    for (let x = 0; x < layer.size; x++) {
        for (let y = 0; y < layer.size; y++) {
            if (spheres[x][y].userData) {
                spheres[x][y].userData.visible = v;
                spheres[x][y].visible = v;
                spheres[x][y].userData.needsUpdate = true;
            }
        }
    }
}

let input;
let input_shapes;
let input_squares;
let problem_mat;
let problem_def;
let headers;
let dicts;

const canvas = document.getElementById('panel');
const sol_canvas = document.getElementById('c');
const NextButton = document.getElementById('onNextButtonClick');
const PrevButton = document.getElementById('onPrevButtonClick');
const ClearButton = document.getElementById('onClearButtonClick');
const StopButton = document.getElementById('onStopButtonClick');
const scount = document.getElementById('solutionCount');
const solveButton = document.getElementById('onSolveButtonClick');
solveButton.addEventListener('click', onSolveButton);
NextButton.addEventListener('click', onNextButton);
PrevButton.addEventListener('click', onPrevButton);

ClearButton.addEventListener('click', onClearButton);
StopButton.addEventListener('click', onStopButton);

const layerCheckboxes = [];
const sol_layerCheckboxes = [];
const toggle = document.getElementById('toggleButton');
const toggleDiv = document.getElementById('SolContainer');
const layer_pyramid = document.getElementById('Levels');

toggleButton.addEventListener('click', function() {
    // Prevent the default form submission behavior
    event.preventDefault();

    // Toggle the display property of the div
    if (toggleDiv.style.display === 'none' || toggleDiv.style.display === '') {
        toggleDiv.style.display = 'flex';
        toggleButton.textContent = 'Hide';
        layer_pyramid.style.display = 'flex';

    } else {
        toggleDiv.style.display = 'none';
        layer_pyramid.style.display = 'none';
        toggleButton.textContent = 'Show Solutions';

    }
});



for (let i = 1; i <= 5; i++) {
    const checkbox = document.getElementById('l' + i);
    const sol_checkbox = document.getElementById('ls' + i);
    checkbox.addEventListener('change', (event) => {
        layerVisible(i, event.target.checked);
    });
    sol_checkbox.addEventListener('change', (event) => {
        sol_layerVisible(i, event.target.checked);
    });
    const label = document.getElementById('l' + i + 'sLabel');
    const sol_label = document.getElementById('ls' + i + 'Label');
    layerCheckboxes.push(checkbox, label);
    sol_layerCheckboxes.push(sol_checkbox, sol_label);
}



const state = createState();

/**
 * Creates a new state object.
 * @returns {Object} The newly created state object.
 */
function createState() {
    return {
        stopExecution: false,
        solutionCount: 0,
        solutions: [],

    };
}

/**
 * Handles the event when the solve button is clicked.
 */
function onSolveButton() {
    state.solutions = []
    var allSolutions = [];
    let solutionCount = 0;
    let solutions = [];

    const startTime = performance.now();

    const input_shapes = inputShapes.get();
    const input_squares = inputCoords.get();

    // If incorrect number of spheres for shape, abort.
    if (!checkInput(input_shapes, input_squares)) {
        return;
    }


    const problem_mat = populate_problem_matrix3D();
    const problem_def = reduce_problem_matrix(problem_mat, generate_headers(problem_mat), input_shapes, input_squares);
    const updatedProblemMat = problem_def[0];
    const headers = problem_def[1];



    const dicts = create_dicts(updatedProblemMat, headers);



    const ret = solve(dicts[0], dicts[1], [], headers);
    let cnt = 0;

    const uiTimer = createTimer(() => {
        const arr = ret.next().value;
        if (arr == undefined) {
            console.log('done');
            if(cnt < 1){
                scount.textContent = "No solutions found!";
            }
            onStopButton();
            return;
        }

        console.log(arr);

        cnt++;
        scount.textContent = "Number of solutions: " + cnt;
        // Push the current pyramid_layers into the array

        const pyramid_layers = convert_to_pyramid_layers(arr, updatedProblemMat, headers, input_shapes, input_squares);
        console.log("Pyramid layers");
        console.log("arr",arr);
        console.log("updatedProblemMat",updatedProblemMat);
        console.log("headers",headers);
        console.log("input_shapes",input_shapes);
        console.log("input_squares",input_squares);
        state.solutions = [...state.solutions, pyramid_layers];
        allSolutions.push(pyramid_layers); // All solutions
        sol_drawPosition(pyramid_layers);
        const endTime = performance.now();
        const totalTime = ((endTime - startTime) / 1000).toFixed(1); // Convert to seconds and round to 1 decimal place
        document.getElementById('timeTakenLabel').textContent = `Time taken: ${totalTime} seconds`;
    });
}

/**
 * Resets the shape object by updating the shape_obj and shape_placed_obj properties.
 */
function resetShapeObj() {
    for (let shape in shapeStore) {
        shape_obj[shape] = shapeStore[shape].layout.length;
        shape_placed_obj[shape] = 0;
    }
}

/**
 * Clears the UI and resets the state.
 */
function onClearButton() {
    // Reset counts to 0 when clearing
    for (let shape in shape_obj) {
        shape_obj[shape] = 0;
        shape_placed_obj[shape] = 0;
    }

    // Reset the count display
    scount.textContent = "Number of solutions: 0";

    // Clear state solutions
    state.solutions = [];

    // Clear input shapes and coords
    inputShapes.clear();
    inputCoords.clear();

    // Reset the firstPlacementCoord
    new resetFirstPlacementCoord();
    new resetShapeObj();
    // Set pyramid to empty and render empty pyramid
    const empty_position = new Array(5);
    for (let i = 0; i < 5; i++) {
        empty_position[i] = new Array(5 - i);
        empty_position[i].fill(0);
    }
    for (let layer = 0; layer < 5; layer++) {
        for (let row = 0; row < 5 - layer; row++) {
            empty_position[layer][row] = new Array(5 - layer);
            empty_position[layer][row].fill(0);
        }
    }

    // Draw the empty pyramid
    drawPosition(empty_position);
    sol_drawPosition(empty_position);
}

/**
 * Draws the position on the pyramid.
 * 
 * @param {Array<Array<Array<string>>>} position - The position to be drawn.
 */
function drawPosition(position) {

    for (let layer = 0; layer < position.length; layer++) {
        for (let i = 0; i < position[layer].length; i++) {
            for (let j = 0; j < position[layer].length; j++) {
                if (["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"].indexOf(position[layer][i][j]) !== -1) {
                    // Set to shape colour
                    worker.getLayer(5 - layer).set(i, j, Colours[position[layer][i][j]]);

                } else {
                    // Set to black to indicate empty
                    worker.getLayer(5 - layer).set(i, j, 0x999999);

                }
            }
        }
    }
    renderPyramid();
}

/**
 * Draws the position on the solution pyramid.
 * 
 * @param {Array<Array<Array<string>>>} position - The position to be drawn on the pyramid.
 * @returns {void}
 */
function sol_drawPosition(position) {

    for (let layer = 0; layer < position.length; layer++) {
        for (let i = 0; i < position[layer].length; i++) {
            for (let j = 0; j < position[layer].length; j++) {
                if (["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"].indexOf(position[layer][i][j]) !== -1) {
                    // Set to shape colour
                    sol_worker.getLayer(5 - layer).set(i, j, sol_Colours[position[layer][i][j]]);

                } else {
                    // Set to black to indicate empty
                   sol_worker.getLayer(5 - layer).set(i, j, 0x999999);

                }
            }
        }
    }
    sol_renderPyramid();
}

/**
 * Checks if the number of spheres for each shape matches the number of coordinates provided.
 * @param {string[]} shapes - An array of shape names.
 * @param {number[][]} coords - An array of coordinate arrays, where each array represents the coordinates for a shape.
 * @returns {boolean} - Returns true if the number of spheres for each shape matches the number of coordinates, otherwise false.
 */
function checkInput(shapes, coords) {
    console.log("Shapes:", shapes, "Coords:", coords);
    for (let i = 0; i < shapes.length; i++) {
        if (shapeStore[shapes[i]].layout.length !== coords[i].length) {
            // Wrong number of spheres for shape, abort.
            return false;
        }
    }
    return true;
}

let currentSolutionIndex = 0;  // Initialize the index at the beginning

/**
 * Handles the click event of the next button.
 * Pops a solution from the state's solutions array and calls sol_drawPosition to draw it.
 */
function onNextButton() {
    console.log("Clicked next");
    const solutions = state.solutions;

    if (currentSolutionIndex < solutions.length - 1) {
        currentSolutionIndex++;
        sol_drawPosition(solutions[currentSolutionIndex]);
    }
}

/**
 * Handles the click event of the "Prev" button.
 */
function onPrevButton() {
    console.log("Clicked Prev");
    const solutions = state.solutions;

    if (currentSolutionIndex > 0) {
        currentSolutionIndex--;
        sol_drawPosition(solutions[currentSolutionIndex]);
    }
}

/**
 * Stops the execution and clears the interval timer.
 */
function onStopButton() {
    let stopExecution = true;
    clearInterval(uiTimer);
    uiTimer = null;
}

/**
 * Initializes the component and sets up the scene and pyramid rendering.
 */
function componentDidMount() {
    scene.init(panel);
    sol_scene.sol_init(c);
    renderPyramid();
    sol_renderPyramid();
}

/**
 * Cleans up resources before the component is unmounted.
 */
function componentWillUnmount() {
    scene.dispose();
    sol_scene.dispose();
}

scene.init(panel);
sol_scene.sol_init(c);
renderPyramid();
sol_renderPyramid();

export {
    worker,
    sol_worker
};
window.worker = worker;
window.sol_worker = sol_worker;