// Find below all the functions used in code.js

/**
 * This function retrieves the current date and time, formats it into 
 * the YYYY-MM-DDTHH:MM:SS.LLLZ, and returns it.
 * 
 * Format:
 * - Date: YYYY-MM-DD (Year-Month-Day)
 * - Time: HH:MM:SS.LLL (Hour:Minute:Second.Millisecond, 24-hour format)
 * 
 * Usage: Call this function whenever you want to log the current timestamp.
 * 
 * @returns 2024-10-03T14:25:07.000Z
 */
function logCurrentDateTime() {
    const now = new Date();

    // Format date as YYYY-MM-DD
    const date = now.getFullYear() + '-' +
        String(now.getMonth() + 1).padStart(2, '0') + '-' +
        String(now.getDate()).padStart(2, '0');

    // Format time as HH:MM:SS
    const time = String(now.getHours()).padStart(2, '0') + ':' +
        String(now.getMinutes()).padStart(2, '0') + ':' +
        String(now.getSeconds()).padStart(2, '0') + '.' +
        String(now.getMilliseconds()).padStart(2, '0');

    // Combine date and time
    const formattedDateTime = `${date} ${time}`;

    return (formattedDateTime);
};

/**
 * Rounds the number to three decimal places
 * 
 * @param {Number} num e.g., 7.77777
 * @returns 7.778
 */
function round3deci(num) {
    return (Math.round((num + Number.EPSILON) * 1000) / 1000);
};

/**
 * Compare the target array and the array of letters to create an array of 1 (present) and 0 (absent)
 * 
 * @param {Array} target, array of characters representing targets
 * @param {Array} array, array of strings representing the displayed letters
 * @returns an array of 0 (absence of target in the string) and 1 (presence of target in the string)
 */
function targetPresent(target, array) {
    let ret = [];
    array.forEach(function (item, index) {
        if (item.indexOf(target[index]) > -1)
            ret.push(1);
        else
            ret.push(0);
    });
    return (ret);
}

/**
 * Generates the `target_main`, `trial_type`, and `array_main` arrays based on the specified rules.
 *
 * @param {string[]} letter_array - An array of available letters to use.
 * @param {number} n - The length of `target_main`, `trial_type`, and `array_main`.
 * @returns {{target_main: string[], trial_type: number[], array_main: string[]}} - The generated arrays.
 */
function generateArrays(letter_array, n) {
    /**
     * Shuffles an array in place.
     * @param {Array} array - The array to shuffle.
     */
    const shuffleArray = (array) => {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
    };

    // Generate target_main with length n, cycling through letter_array as needed
    const target_main = [];
    let remaining_chars = letter_array.slice(0);
    shuffleArray(remaining_chars);
    while (target_main.length < n) {
        if (remaining_chars.length === 0) {
            remaining_chars = letter_array.slice(0); // Refill and shuffle when exhausted
            shuffleArray(remaining_chars);
        }
        target_main.push(remaining_chars.pop());
    }

    // Generate trial_type ensuring no more than 4 consecutive 0s or 1s
    const trial_type = [];
    for (let i = 0; i < n; i++) {
        let value;
        do {
            value = Math.floor(Math.random() * 2); // Randomly choose 0 or 1
        } while (
            i >= 4 &&
            trial_type.slice(i - 4, i).every(v => v === value) // Ensure no more than 4 consecutive
        );
        trial_type.push(value);
    }

    const array_main = [];
    for (let i = 0; i < n; i++) {
        let options = letter_array.filter(char => char !== target_main[i]);
        shuffleArray(options);

        let selected;
        if (trial_type[i] === 0) {
            selected = options.slice(0, 4); // 4 random letters excluding target_main[i]
        } else {
            selected = options.slice(0, 3).concat(target_main[i]); // 3 random + target_main[i]
            shuffleArray(selected);
        }

        // Ensure no duplicates in the same position between consecutive elements
        let attempt = 0;
        while (
            i > 0 &&
            (selected.some((char, index) => char === array_main[i - 1][index]) ||
                selected.indexOf(target_main[i]) === array_main[i - 1].indexOf(target_main[i - 1]))
        ) {
            shuffleArray(options);
            if (trial_type[i] === 0) {
                selected = options.slice(0, 4);
            } else {
                selected = options.slice(0, 3).concat(target_main[i]);
                shuffleArray(selected);
            }

            attempt++;
            if (attempt > 10) break; // Exit loop after 10 attempts to prevent infinite loop
        }

        array_main.push(selected.join(''));
    }

    return { target_main, trial_type, array_main };
}

/**
 * Compute stimulus width in px from the expected width in Visual Angle on screen, the pixel per degree ratio and the eyes-screen distance. Note : your screen pixel is not the same PHYSICAL size than your neighbour's pixel.
 * 
 * @param {Number} stim_width_va 
 * @param {Number} pxperdeg 
 * @returns {Number} Stimulus width in PX.
 */
function vaToPx(stim_width_va, pxperdeg) {
    const stim_width_px = stim_width_va * pxperdeg;
    return (stim_width_px);
}

/**
 * Compute stimulus width in cm from the expected width in Visual Angle on screen and distance in cm
 * 
 * @param {Number} stim_width_va 
 * @param {Number} distance_cm 
 * @returns {Number} Stimulus width in cm.
 */
function vaToCm(stim_width_va, distance_cm) {
    return (2 * distance_cm * Math.tan(stim_width_va * Math.PI / 360));
}

/**
 * _letter_width_px getter
 * The adjustment constant has been manually calculated between an img (BLAST stim) at the right cm width vs. the '#' char with the same px width than the img. 8.6/5.2 ~= 1.66
 * 
 * @returns _letter_width_va in px
 */
function getLetterWidthPx(jp, p) {
    const adjustment = 1.66;
    const px2deg = jp.data.get().last(1).values()[0]._px2deg;
    return (vaToPx(p.letter_width_va, px2deg) * adjustment);
}

/**
 * Returns mask 1
 * 
 * @returns a string that should display mask 1
 */
function getMask1(jp, p) {
    return (`<div style="position: fixed; top: calc(50%); left: calc(50%); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">#</div>`);
}

/**
 * Returns mask 5
 * @param {int} flag 0 = black, 1 = green, 2 = red
 * 
 * @returns a string that should display mask 5
 */
function getMask5(jp, p, flag) {
    let color = flag == 0 ? `Black` : flag == 1 ? `MediumSeaGreen` : `Tomato`;
    return (`
      <span style="color:` + color + `;"> 
      <div style="position: fixed; top: calc(50% - ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); left: calc(50% - ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">#</div>
      <div style="position: fixed; top: calc(50% - ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); left: calc(50% + ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">#</div>
      <div style="position: fixed; top: calc(50% + ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); left: calc(50% - ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">#</div>
      <div style="position: fixed; top: calc(50% + ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); left: calc(50% + ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">#</div>
      <div style="position: fixed; top: calc(50%); left: calc(50%); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">#</div>
      </span>
    `);
}

/**
 * _letter_dist_from_center_px getter
 * 
 * @returns letter_dist_from_center_va in px
 */
function getLetterDistFromCenterPx(jp, p) {
    const px2deg = jp.data.get().last(1).values()[0]._px2deg;
    return (vaToPx(p.letter_dist_from_center_va, px2deg));
}

/**
 * Creates HTML template for array
 * 
 * @param  {String} str String containing the four letters
 * @param  {Number} dist_from_center_px Distance center-letter in pixel
 * @param  {Number} letter_width_px Letter width in pixel
 * @returns Four <div> containing each letter from str at font_size: letter_width_px and at a distance of dist_from_center_px from the center of the screen
 */
function getTemplateArray(str, jp, p) {
    if (str.length != 4) {
        throw new Error('Array parameter is not an array of length 4!');
    }
    return (
        `
      <div style="position: fixed; top: calc(50% - ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); left: calc(50% - ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">` + str[0] + `</div>
      <div style="position: fixed; top: calc(50% - ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); left: calc(50% + ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">` + str[1] + `</div>
      <div style="position: fixed; top: calc(50% + ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); left: calc(50% - ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">` + str[2] + `</div>
      <div style="position: fixed; top: calc(50% + ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); left: calc(50% + ` + getLetterDistFromCenterPx(jp, p) / Math.sqrt(2) + `px); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">` + str[3] + `</div>
      <div style="position: fixed; top: calc(50%); left: calc(50%); transform: translate(-50%, -50%); font-size: ` + getLetterWidthPx(jp, p) + `px; text-align: center">#</div>
      `
    )
}

/**
 * 
 * @param {int} present 1 (target present in letters) or 0 (target absent in letters)
 * @returns The keypress corresponding to the correct response
 */
function expectedResponse(present, p) {
    return (present ? p.response_key.present : p.response_key.absent)
}

/**
 * Creates a list of objects for timeline_variables
 * 
 * @param  {Array} target Array of target
 * @param  {Array} arr Array of letters
 * @param  {Array} present Array of 1 (target present in letters) and 0 (target absent in letters)
 * @returns object of length arr.length() containing the timeline_variables
 */
function createBlastTimelineVariables(target, arr, present, jp, p) {
    let ret = [];
    const px2deg = jp.data.get().last(1).values()[0].px2deg;
    for (let i = 0; i < arr.length; i++) {
        ret[i] = {
            stimulus_target: `<div style="position: fixed; top: calc(50%); left: calc(50%); transform: translate(-50%, -50%); font-size:` + getLetterWidthPx(jp, p) + `px; text-align: center">` + target[i] + `</div>`,
            stimulus_array: getTemplateArray(
                arr[i],
                jp,
                p
            ),
            target: target[i],
            array_letters: arr[i],
            present: present[i],
            expected_response: expectedResponse(present[i], p),
            mask1: getMask1(jp, p),
            mask5: getMask5(jp, p, 0),
            mask5g: getMask5(jp, p, 1),
            mask5r: getMask5(jp, p, 2),
        };
    };
    return ret;
}

/**
 * Returns buffer time based on correct or incorrect response
 * 
 * @param {int} correct 1 (trial is correct), 0 (trial is incorrect)
 * @returns if 1, buffer_time_correct, else, buffer_time_incorrect
 */
function postRespTrialDuration(correct, p) {
    return (correct ? p.buffer_time_correct : p.buffer_time_incorrect);
}

/**
 *Creates a list of objects for timeline
 * 
 * @returns timeline
 */
function createBlastTimeline(block, jp, p) {
    return ([
        // Target (one char)
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: jp.timelineVariable('stimulus_target'),
            choices: `NO_KEYS`,
            trial_duration: p.pres_time_target,
            data: {
                trial_duration: p.pres_time_target,
                trial_name: "target",
                real_trial_index: '',  // see on_finish()
                block: block
            },
            on_finish: function (data) {
                data.real_trial_index = jp.data.get().last(2).values()[0].real_trial_index ? jp.data.get().last(2).values()[0].real_trial_index + 1 : 1;
            }
        },
        // Mask (the '#')
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: jp.timelineVariable('mask1'),
            choices: `NO_KEYS`,
            trial_duration: p.pres_time_mask1,
            data: {
                trial_duration: p.pres_time_mask1,
                trial_name: "mask1",
                real_trial_index: '',
                block: block
            },
            on_finish: function (data) {
                data.real_trial_index = jp.data.get().last(2).values()[0].real_trial_index;
            },
        },
        // Array (four letters with # at center)
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: jp.timelineVariable('stimulus_array'),
            choices: [p.response_key.present, p.response_key.absent],
            trial_duration: p.pres_time_array,
            data: {
                trial_duration: p.pres_time_array,
                trial_name: "array",
                real_trial_index: '',
                block: block,
                array_letters: jp.timelineVariable("array_letters"),
                target: jp.timelineVariable("target"),
                present: jp.timelineVariable("present"),
                expected_response: jp.timelineVariable("expected_response")
            },
            on_finish: function (data) {
                data.real_trial_index = jp.data.get().last(2).values()[0].real_trial_index;
                data.correct = +jp.pluginAPI.compareKeys(data.response, data.expected_response);
                // compute trial_accuracy (whether hit, fa, cr, miss)
                if (data.correct) // if response == expected response, Hit
                    data.trial_accuracy = "Hit"
                else if (!data.rt) // if no answer, Miss
                    data.trial_accuracy = "Miss"
                else
                    data.trial_accuracy = "False Alarm" // if response != expected response, FA
                // data.trial_accuracy =
                //   +(data.present && data.correct) == 1 ? "Hit" : // is hit ? true = "Hit" otherwise
                //     +(!data.present && !data.correct) == 1 ? "False Alarm" : // is fa ? true = "False Alarm" otherwise
                //       +(!data.present && data.correct) == 1 ? "Correct Rejection" : // is cr ? true = "False Alarm" otherwise
                //         +(data.present && !data.correct) == 1 ? "Miss" : null; // is miss ? true = "Miss" otherwise null
                data.trial_duration = !data.rt ? p.pres_time_array : data.rt;
                // Automatic fullscreen if pp quitted fullscreen mode
                let docElm = document.documentElement;
                fullscreencheck: if (data.trial_duration == p.pres_time_array)
                    break fullscreencheck;
                else if (docElm.requestFullscreen) {
                    docElm.requestFullscreen();
                }
                else if (docElm.mozRequestFullScreen) {
                    docElm.mozRequestFullScreen();
                }
                else if (docElm.webkitRequestFullScreen) {
                    docElm.webkitRequestFullScreen();
                }
                else if (docElm.msRequestFullscreen) {
                    docElm.msRequestFullscreen();
                }
            }
        },
        // Mask5 or Array mask ('# # # # #')
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: function () {
                if (block == "main")
                    return (jp.timelineVariable('mask5'));
                // if practice, red if no answer or incorrect, green if correct
                if (jp.data.get().last(1).values()[0].rt == p.pres_time_array)
                    return (jp.timelineVariable('mask5r'));
                if (jp.data.get().last(1).values()[0].correct == 0)
                    return (jp.timelineVariable('mask5r'));
                if (jp.data.get().last(1).values()[0].correct == 1)
                    return (jp.timelineVariable('mask5g'));
            },
            choices: `NO_KEYS`,
            trial_duration: p.pres_time_mask5,
            data: {
                trial_duration: "",  // see on_finish()
                trial_name: "mask5",
                real_trial_index: '',
                block: block,
            },
            // on begin get correct here and put trial duration 
            on_finish: function (data) {
                data.real_trial_index = jp.data.get().last(2).values()[0].real_trial_index;
                data.trial_duration = p.pres_time_mask5;
            }
        },
        // Mask1
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: jp.timelineVariable('mask1'),
            choices: `NO_KEYS`,
            trial_duration: function () {
                return (postRespTrialDuration(jp.data.get().last(2).values()[0].correct, p))
            },
            data: {
                trial_duration: "",  // see on_finish()
                trial_name: "mask1",
                real_trial_index: '',
                block: block,
            },
            // on begin get correct here and put trial duration 
            on_finish: function (data) {
                data.real_trial_index = jp.data.get().last(2).values()[0].real_trial_index;
                data.trial_duration = postRespTrialDuration(jp.data.get().last(3).values()[0].correct, p);
            }
        },
    ])
}
