refiaa / Advanced Twins for University of Tsukuba

// ==UserScript==
// @name         Advanced Twins for University of Tsukuba
// @namespace    https://github.com/refiaa
// @version      240429.1605
// @description  Provide Advanced function for Twins (University of Tsukuba)
// @author       refiaa
// @match        https://twins.tsukuba.ac.jp/campusweb/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    class KeyObserver {
        constructor(targetSelector) {
            this.targetSelector = targetSelector;
            this.currentKey = '';
            this.latestKey = '';
            this.displayElement = null;
            this.logElement = null;
            this.inputField = null;
            this.addButton = null;
            this.targetIframe = null;
            this.init();

            this.initSessionStorage();
            this.logAnalyzer = new LogAnalyzer();
        }

        init() {
            this.findTargetIframe();
            if (this.targetIframe) {
                this.initObserver();
                this.initUIComponents();
            }
        }

        // SESSION STORAGE LOGIC

        initSessionStorage() {
            const logKey = 'flowExecutionLogs';
            if (!sessionStorage.getItem(logKey)) {
                sessionStorage.setItem(logKey, JSON.stringify([]));
            }
        }

        addLog(type, key, error = null) {
            const logKey = 'flowExecutionLogs';
            const currentLogs = JSON.parse(sessionStorage.getItem(logKey));

            const log = {
                type: type,
                key: key,
                timestamp: new Date().toISOString(),
                error: error
            };

            currentLogs.push(log);
            sessionStorage.setItem(logKey, JSON.stringify(currentLogs));
        }

        // END OF SESSION STORAGE LOGIC

        findTargetIframe() {
            const iframes = document.getElementsByTagName('iframe');
            for (const iframe of iframes) {
                if (iframe.src.includes('campussquare.do?_flowId=RSW0001000-flow')) {
                    this.targetIframe = iframe;
                    console.log('Target iframe found:', this.targetIframe);
                    break;
                }
            }
            if (!this.targetIframe) {
                console.warn('Target iframe not found');
            }
        }

        initObserver() {
            if (!this.targetIframe) {
                console.error('Target iframe not found');
                return;
            }

            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const targetNode = targetDocument.querySelector(this.targetSelector);
                if (!targetNode) {
                    console.error('Target element not found in the iframe');
                    return;
                }

                const config = { attributes: true, childList: true, subtree: true };
                const observer = new MutationObserver(mutations => this.handleMutations(mutations));
                observer.observe(targetNode, config);
            } catch (error) {
                console.error('Error accessing iframe:', error);
            }
        }

        handleMutations(mutations) {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList' || mutation.type === 'attributes') {
                    this.handleMutation(mutation);
                }
            });
        }

        handleMutation(mutation) {
            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const target = targetDocument.querySelector('input[name="_flowExecutionKey"]');
                if (target && target.value !== this.currentKey) {
                    this.currentKey = target.value;
                    this.latestKey = this.currentKey;

                    this.addLog('childList', this.currentKey);
                }
            } catch (error) {
                this.addLog('error', 'handleMutation', error.message);
                console.error('Error handling mutation:', error);
            }
        }

        initUIComponents() {
            this.createDisplay();
            this.setupInputField();
            this.setupButtons();
        }

        createDisplay() {
            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const uiPanel = targetDocument.createElement('div');
                uiPanel.id = 'keyObserverUI';
                uiPanel.classList.add('key-observer-panel');

                const targetParagraph = targetDocument.querySelector('table[border="0"][cellspacing="1"][cellpadding="1"] + p');
                if (targetParagraph) {
                    targetParagraph.parentNode.insertBefore(uiPanel, targetParagraph);
                    console.log('UI panel created:', uiPanel);
                } else {
                    console.warn('Target paragraph not found, appending UI panel to body');
                    targetDocument.body.appendChild(uiPanel);
                }

                this.displayElement = uiPanel;
                this.logElement = targetDocument.createElement('div');
                this.logElement.classList.add('key-observer-log');
                this.displayElement.appendChild(this.logElement);
            } catch (error) {
                console.error('Error creating display:', error);
            }
        }

        setupInputField() {
            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                this.inputField = targetDocument.createElement('input');
                this.inputField.id = 'jikanwariCodeInput';
                this.inputField.type = 'text';
                this.inputField.placeholder = '時間割コード';
                this.inputField.classList.add('jikanwari-code-input');
                this.displayElement.appendChild(this.inputField);
            } catch (error) {
                console.error('Error setting up input field:', error);
            }
        }

        setupButtons() {
            const actions = ['Add'];
            actions.forEach(action => this.createButton(action));
        }

        createButton(action) {
            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const button = targetDocument.createElement('button');
                button.textContent = action;
                button.classList.add('key-observer-button');
                button.addEventListener('click', () => this.executeCommand(action));
                this.displayElement.appendChild(button);

                if (action === 'Add') {
                    this.addButton = button;
                }
            } catch (error) {
                console.error('Error creating button:', error);
            }
        }

        executeCommand(action, jikanwariShozokuCode, yobi, jigen, jikanwariCode) {
            if (action === 'Add') {
                jikanwariCode = this.inputField.value.trim();
                if (!jikanwariCode) {
                    alert('時間割コードを入力してください。');
                    return;
                }
            }

            const config = this.getButtonConfig(action, jikanwariShozokuCode, yobi, jigen);

            const finalizeAction = () => {
                this.sendRequest('back', this.getButtonConfig('back'))
                    .then(() => {
                        this.refreshTimetable();
                        if (action === 'Add') {
                            this.clearInputField();
                        }
                    })
                    .catch(error => {
                        console.error('Error during finalization:', error);
                    });
            };

            if (action === 'Add') {
                this.sendRequest('input', { ...config.inputParams, jikanwariCode })
                    .then(() => {
                        this.sendRequest('insert', { nendo: new Date().getFullYear().toString(), jikanwariCode })
                            .then(finalizeAction)
                            .catch(error => {
                                console.error('Error during the insert process:', error);
                                finalizeAction();
                            });
                    })
                    .catch(error => {
                        console.error('Error during the first request in insert with input:', error);
                        finalizeAction();
                    });
            } else if (action === 'delete') {
                this.sendRequest('delete', { ...config, jikanwariCode })
                    .then(() => {
                        this.sendRequest('delete', { _flowExecutionKey: this.latestKey || this.currentKey })
                            .then(finalizeAction)
                            .catch(error => {
                                console.error('Error during the delete process:', error);
                                finalizeAction();
                            });
                    })
                    .catch(error => {
                        console.error('Error during the delete process:', error);
                        finalizeAction();
                    });
            }
        }

        clearInputField() {
            this.inputField.value = '';
        }

        getButtonConfig(action, jikanwariShozokuCode, yobi, jigen) {
            const configs = {
                add: { inputParams: { yobi: 9, jigen: 0 } },
                delete: { nendo: new Date().getFullYear().toString(), jikanwariShozokuCode, yobi, jigen },
                back: {}
            };
            return configs[action] || {};
        }

        sendRequest(eventId, params) {
            const keyToUse = this.latestKey || this.currentKey;
            const requestOptions = {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: this.encodeParams({ ...params, _flowExecutionKey: keyToUse, _eventId: eventId })
            };

            return fetch(`/campusweb/campussquare.do`, requestOptions)
                .then((response) => response.text())
                .then((html) => {
                    this.handleResponse(html, eventId);

                    this.logAnalyzer.analyzeLogs();
                })
                .catch((error) => {
                    console.error('Error with AJAX request:', error);
                });
        }

        handleResponse(html, eventId) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const newKey = doc.querySelector('input[name="_flowExecutionKey"]')?.value;

            if (newKey) {
                this.latestKey = newKey;
                this.addLog(eventId, newKey);
            } else {
                throw new Error('Failed to fetch new key');
            }
        }

        encodeParams(params) {
            return Object.keys(params)
                .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
                .join('&');
        }

        refreshTimetable()   {
            return new Promise((resolve, reject) => {
                const requestOptions = {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: this.encodeParams({ _flowExecutionKey: this.latestKey || this.currentKey })
                };

                fetch(`/campusweb/campussquare.do?_flowId=RSW0001000-flow`, requestOptions)
                    .then(response => response.text())
                    .then(html => {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(html, 'text/html');
                        const newTimetable = doc.querySelector('table.rishu-koma');
                        if (newTimetable) {
                            const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                            const currentTimetable = targetDocument.querySelector('table.rishu-koma');
                            if (currentTimetable) {
                                currentTimetable.parentNode.replaceChild(newTimetable, currentTimetable);
                                resolve();
                            } else {
                                reject('Current timetable not found');
                            }
                        } else {
                            reject('New timetable not found');
                        }
                    })
                    .catch(error => {
                        console.error('Error refreshing timetable:', error);
                        reject(error);
                    });
            });
        }
    }

    class AdvancedSyllabus {
        constructor(keyObserver) {
            this.keyObserver = keyObserver;
            this.targetIframe = keyObserver.targetIframe;
            this.init();
        }

        init() {
            this.injectStyles();
            this.observePageChanges();
        }

        injectStyles() {
            const styleElement = document.createElement('style');
            styleElement.textContent = `
                .syllabus-link {
                    color: blue !important;
                    cursor: pointer !important;
                    text-decoration: underline !important;
                    margin-left: 5px;
                }
                .delete-button {
                    background-color: transparent;
                    font-weight: bold;
                    color: #333;
                    border: none;
                    border-radius: 0;
                    width: 24px;
                    height: 24px;
                    position: absolute;
                    top: 10px;
                    right: 10px;
                    transform: translate(50%, -50%);
                    cursor: pointer;
                    z-index: 1;
                }

                .delete-button:hover {
                    color: #ff0000;
                    text-decoration: none;
                }

                .delete-button::before {
                    content: "科目を削除";
                    visibility: hidden;
                    color: #fff;
                    background-color: #555;
                    padding: 5px 10px;
                    border-radius: 6px;
                    position: absolute;
                    z-index: 1000;
                    left: 120%;
                    top: -75%;
                    transform: translate(-50%, -50%);
                    white-space: nowrap;
                    font-size: 12px;
                    box-shadow: 0px 2px 5px rgba(0,0,0,0.2);
                    transition: visibility 0.2s, opacity 0.2s ease;
                    opacity: 0;
                }

                .delete-button:hover::before {
                    visibility: visible;
                    opacity: 1;
                }
            `;

            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                targetDocument.head.appendChild(styleElement);
            } catch (error) {
                console.error('Error injecting styles:', error);
            }
        }

        observePageChanges() {
            const targetNode = document.body;
            const observerOptions = {
                childList: true,
                subtree: true
            };

            const observer = new MutationObserver(mutationsList => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList') {
                        this.addLinksToTimetable();
                    }
                }
            });

            observer.observe(targetNode, observerOptions);
        }

        addLinksToTimetable() {
            if (!this.targetIframe) {
                console.warn('Target iframe not found, skipping adding links to timetable');
                return;
            }

            try {
                const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document;
                const timetableCells = targetDocument.querySelectorAll('td[bgcolor="#ffcc99"], td[bgcolor="#ffffcc"]');
                timetableCells.forEach(cell => {
                    this.addLinkToCell(cell, false);
                    this.addDeleteButton(cell);
                });

                const concentratedRows = targetDocument.querySelectorAll('table.rishu-etc tr[bgcolor="#ffcc99"]');
                concentratedRows.forEach(row => {
                    const syllabusCell = row.children[4];
                    if (syllabusCell) {
                        this.addLinkToCell(syllabusCell, true);
                        this.addDeleteButton(syllabusCell);
                    }
                });
            } catch (error) {
                console.error('Error adding links to timetable:', error);
            }
        }

        addLinkToCell(cell, isSyllabus) {
            if (cell.querySelector('.syllabus-link')) return;

            let courseCode = '';

            if (isSyllabus) {
                const courseCodeCell = cell.parentElement.querySelector('td:nth-child(3)');
                if (courseCodeCell) {
                    courseCode = courseCodeCell.textContent.trim();
                }
            } else {
                const courseCodeElement = cell.querySelector('td[valign="top"]');
                if (courseCodeElement) {
                    const textContent = courseCodeElement.textContent.trim();
                    const lines = textContent.split('\n');
                    courseCode = lines[0].trim();
                }
            }

            if (!courseCode) return;

            const link = document.createElement('a');
            link.className = 'syllabus-link';
            link.textContent = 'シラバス';
            link.href = `https://kdb.tsukuba.ac.jp/syllabi/${new Date().getFullYear()}/${courseCode}/jpn`;
            link.target = '_blank';
            link.rel = 'noopener noreferrer';
            link.addEventListener('click', function (event) {
                event.preventDefault();
                window.open(this.href, 'syllabusWindow', 'width=800,height=600,resizable=yes,scrollbars=yes');
            });

            const linkContainer = document.createElement('div');
            linkContainer.classList.add('syllabus-link-container');
            linkContainer.appendChild(link);

            if (!cell.querySelector('.syllabus-link-container')) {
                cell.appendChild(linkContainer);
            }
        }

        addDeleteButton(cell) {
            if (cell.querySelector('.delete-button')) return;

            const deleteButton = document.createElement('button');
            deleteButton.classList.add('delete-button');
            deleteButton.innerHTML = '✕';
            deleteButton.addEventListener('click', () => {
                const deleteLink = cell.querySelector('a[onclick^="return DeleteCallA"]');
                if (deleteLink) {
                    const onclickArgs = deleteLink.getAttribute('onclick').match(/'(.*?)'/g);
                    if (onclickArgs && onclickArgs.length === 5) {
                        const [nendo, jikanwariShozokuCode, jikanwariCode, yobi, jigen] = onclickArgs.map(arg => arg.replace(/'/g, ''));
                        const confirmDelete = window.confirm('本当にこの科目を削除しますか?');
                        if (confirmDelete) {
                            this.keyObserver.executeCommand('delete', jikanwariShozokuCode, yobi, jigen, jikanwariCode);
                        }
                    }
                }
            });

            const deleteButtonContainer = document.createElement('div');
            deleteButtonContainer.classList.add('delete-button-container');
            deleteButtonContainer.appendChild(deleteButton);

            cell.style.position = 'relative';
            cell.appendChild(deleteButtonContainer);
        }
    }

    class kdb_Displayer {
        constructor(urls, keyObserver) {
            this.urls = urls;

            this.keyObserver = keyObserver;
            if (!this.keyObserver) {
                console.error('KeyObserver not initialized');
                return;
            }

            this.displayElement = null;
            this.toggleButton = null;
            this.searchContainer = null;
            this.sortContainer = null;
            this.tableContainer = null;
            this.jsonData = null;
            this.indexedData = null;
            this.filteredData = null;
            this.dayOfWeekSelect = null;
            this.periodSelect = null;
            this.currentPage = 1;
            this.pageSize = 20;
            this.currentUrlIndex = 0;

            this.init();
        }

        async init() {
            try {
                const targetIframe = await this.waitForTargetIframe();
                if (targetIframe && this.keyObserver) {
                    const jsonData = await this.fetchJsonData(this.urls[this.currentUrlIndex]);
                    this.createUI();
                    this.displayJsonData(jsonData);
                } else {
                    console.warn('KeyObserver not initialized or target iframe not found');
                }
            } catch (error) {
                console.error('JSON Error', error);
            }
        }

        async waitForTargetIframe(maxRetries = 10, retryDelay = 1000) {
            let retries = 0;
            return new Promise((resolve, reject) => {
                const checkIframe = () => {
                    const targetIframe = document.querySelector('iframe[src*="campussquare.do?_flowId=RSW0001000-flow"]');
                    if (targetIframe) {
                        resolve(targetIframe);
                    } else {
                        retries++;
                        if (retries < maxRetries) {
                            setTimeout(checkIframe, retryDelay);
                        } else {
                            resolve(null);
                        }
                    }
                };
                checkIframe();
            });
        }

        async fetchJsonData(url) {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`HTTP error status: ${response.status}`);
            }
            return await response.json();
        }

        createUI() {
            const existingElement = document.getElementById('kdb_Displayer');
            const existingToggleButton = document.getElementById('jsonToggleButton');

            if (existingElement) {
                this.displayElement = existingElement;
                this.toggleButton = existingToggleButton;
                this.searchContainer = document.getElementById('searchContainer');
                this.sortContainer = document.getElementById('sortContainer');
                this.tableContainer = document.getElementById('tableContainer');
            } else {
                this.displayElement = document.createElement('div');
                this.displayElement.id = 'kdb_Displayer';
                this.displayElement.style.position = 'fixed';
                this.displayElement.style.bottom = '2vh';
                this.displayElement.style.right = '2vw';
                this.displayElement.style.width = '40vw';
                this.displayElement.style.maxHeight = '60vh';
                this.displayElement.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
                this.displayElement.style.color = 'black';
                this.displayElement.style.padding = '1vw';
                this.displayElement.style.zIndex = '9999';
                this.displayElement.style.overflowY = 'auto';
                this.displayElement.style.border = '1px solid #ccc';
                this.displayElement.style.borderRadius = '5px';
                this.displayElement.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.1)';
                this.displayElement.style.display = 'none';

                // Header
                const headerDiv = document.createElement('div');
                headerDiv.style.display = 'flex';
                headerDiv.style.alignItems = 'center';
                headerDiv.style.marginBottom = '10px';

                const mainHeaderText = document.createElement('span');
                mainHeaderText.style.fontWeight = 'bold';
                mainHeaderText.style.fontSize = '25px';
                mainHeaderText.textContent = 'KDB Searcher';

                const subHeaderText = document.createElement('span');
                subHeaderText.style.fontSize = '14px';
                subHeaderText.style.marginLeft = '10px';

                const subHeaderTextNode = document.createTextNode('Source code is available on ');
                subHeaderText.appendChild(subHeaderTextNode);

                const githubLink = document.createElement('a');
                githubLink.href = 'https://github.com/refiaa';
                githubLink.target = '_blank';
                githubLink.textContent = 'Github';

                subHeaderText.appendChild(githubLink);

                headerDiv.appendChild(mainHeaderText);
                headerDiv.appendChild(subHeaderText);
                this.displayElement.appendChild(headerDiv);

                this.toggleButton = document.createElement('button');
                this.toggleButton.id = 'jsonToggleButton';
                this.toggleButton.textContent = 'kdbを開く';
                this.toggleButton.style.position = 'fixed';
                this.toggleButton.style.bottom = '20px';
                this.toggleButton.style.right = '20px';
                this.toggleButton.style.padding = '5px 10px';
                this.toggleButton.style.zIndex = '9999';
                this.toggleButton.style.cursor = 'pointer';

                this.toggleButton.addEventListener('click', this.toggleDisplay.bind(this));

                this.searchContainer = document.createElement('div');
                this.searchContainer.id = 'searchContainer';
                this.sortContainer = document.createElement('div');
                this.sortContainer.id = 'sortContainer';
                this.tableContainer = document.createElement('div');
                this.tableContainer.id = 'tableContainer';

                this.createSearchUI();
                this.createSortUI();
                this.createUrlSwitchUI();

                const spacerDiv1 = document.createElement('div');
                spacerDiv1.style.marginBottom = '15px';
                this.displayElement.appendChild(spacerDiv1);

                this.displayElement.appendChild(this.searchContainer);
                this.displayElement.appendChild(this.sortContainer);
                this.displayElement.appendChild(this.tableContainer);

                // レファレンス
                const descriptionDiv = document.createElement('div');
                descriptionDiv.style.marginTop = '20px';
                descriptionDiv.style.fontSize = '14px';
                descriptionDiv.style.color = '#666';

                const descriptionTextNode1 = document.createTextNode('Using ');
                descriptionDiv.appendChild(descriptionTextNode1);

                const kdbLink = document.createElement('a');
                kdbLink.href = 'https://github.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb';
                kdbLink.target = '_blank';
                kdbLink.textContent = 'alternative-tsukuba-kdb';
                descriptionDiv.appendChild(kdbLink);

                const descriptionTextNode2 = document.createTextNode(' for kdb data.');
                descriptionDiv.appendChild(descriptionTextNode2);

                this.displayElement.appendChild(descriptionDiv);

                document.body.appendChild(this.displayElement);
                document.body.appendChild(this.toggleButton);
            }
        }

        createSearchUI() {
            const searchUIContainer = document.createElement('div');
            searchUIContainer.style.marginBottom = '20px';

            const subjectSearchRow = document.createElement('div');
            subjectSearchRow.classList.add('row')

            const subjectCodeInput = document.createElement('input');
            subjectCodeInput.type = 'text';
            subjectCodeInput.placeholder = '科目番号で検索';
            subjectCodeInput.style.marginRight = '10px';
            subjectCodeInput.addEventListener('input', this.filterData.bind(this));
            subjectSearchRow.appendChild(subjectCodeInput);

            const subjectNameInput = document.createElement('input');
            subjectNameInput.type = 'text';
            subjectNameInput.placeholder = '科目名で検索';
            subjectNameInput.style.marginRight = '10px';
            subjectNameInput.addEventListener('input', this.filterData.bind(this));
            subjectSearchRow.appendChild(subjectNameInput);

            this.searchContainer.appendChild(subjectSearchRow);

            const dayTimeRow = document.createElement('div');
            dayTimeRow.classList.add('row');

            const daysOfWeekOptions = ['月', '火', '水', '木', '金', '土', '日'];
            this.dayOfWeekSelect = document.createElement('select');

            const daysOfWeekPlaceholderOption = document.createElement('option');
            daysOfWeekPlaceholderOption.value = '';
            daysOfWeekPlaceholderOption.textContent = '曜日';
            daysOfWeekPlaceholderOption.disabled = true;
            daysOfWeekPlaceholderOption.selected = true;
            this.dayOfWeekSelect.appendChild(daysOfWeekPlaceholderOption);

            for (const option of daysOfWeekOptions) {
                const daysOfWeekOption = document.createElement('option');
                daysOfWeekOption.value = option;
                daysOfWeekOption.textContent = option;
                this.dayOfWeekSelect.appendChild(daysOfWeekOption);
            }

            this.dayOfWeekSelect.addEventListener('change', this.filterData.bind(this));
            dayTimeRow.appendChild(this.dayOfWeekSelect);

            const periodsOptions = ['1', '2', '3', '4', '5', '6', '7', '8'];
            this.periodSelect = document.createElement('select');

            const periodPlaceholderOption = document.createElement('option');
            periodPlaceholderOption.value = '';
            periodPlaceholderOption.textContent = '時限';
            periodPlaceholderOption.disabled = true;
            periodPlaceholderOption.selected = true;
            this.periodSelect.appendChild(periodPlaceholderOption);

            for (const option of periodsOptions) {
                const periodsOption = document.createElement('option');
                periodsOption.value = option;
                periodsOption.textContent = option;
                this.periodSelect.appendChild(periodsOption);
            }

            this.periodSelect.addEventListener('change', this.filterData.bind(this));
            dayTimeRow.appendChild(this.periodSelect);

            this.searchContainer.appendChild(dayTimeRow);

            const filterRow = document.createElement('div');
            filterRow.classList.add('row');

            const semesterContainer = document.createElement('div');
            const semesterOptions = ['春', '秋', 'A', 'B', 'C'];
            for (const option of semesterOptions) {
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.value = option;
                checkbox.id = `semester-${option}`;
                checkbox.style.marginRight = '5px';
                checkbox.addEventListener('change', this.filterData.bind(this));
                semesterContainer.appendChild(checkbox);

                const label = document.createElement('label');
                label.htmlFor = `semester-${option}`;
                label.textContent = option;
                semesterContainer.appendChild(label);
            }

            filterRow.appendChild(semesterContainer);

            const onlineOfflineContainer = document.createElement('div');
            const onlineOfflineOptions = ['オンライン', '対面'];
            for (const option of onlineOfflineOptions) {
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.value = option;
                checkbox.id = `format-${option}`;
                checkbox.style.marginRight = '5px';
                checkbox.addEventListener('change', this.filterData.bind(this));
                onlineOfflineContainer.appendChild(checkbox);

                const label = document.createElement('label');
                label.htmlFor = `format-${option}`;
                label.textContent = option;
                onlineOfflineContainer.appendChild(label);
            }

            filterRow.appendChild(onlineOfflineContainer);

            const resetButton = document.createElement('button');
            resetButton.textContent = '検索結果をリセット';
            resetButton.style.marginLeft = '80px';
            resetButton.addEventListener('click', this.resetSearchOptions.bind(this));
            onlineOfflineContainer.appendChild(resetButton);

            this.searchContainer.appendChild(filterRow);

            const style = document.createElement('style');
            style.textContent = `.row {margin-bottom: 10px;}`;
            document.head.appendChild(style);
        }

        resetSearchOptions() {
            const subjectCodeInput = this.displayElement.querySelector('input[placeholder="科目番号で検索"]');
            const subjectNameInput = this.displayElement.querySelector('input[placeholder="科目名で検索"]');
            const semesterCheckboxes = this.displayElement.querySelectorAll('input[id^="semester-"]');
            const onlineOfflineCheckboxes = this.displayElement.querySelectorAll('input[id^="format-"]');

            subjectCodeInput.value = '';
            subjectNameInput.value = '';
            this.dayOfWeekSelect.selectedIndex = 0;
            this.periodSelect.selectedIndex = 0;
            semesterCheckboxes.forEach(checkbox => checkbox.checked = false);
            onlineOfflineCheckboxes.forEach(checkbox => checkbox.checked = false);

            this.filterData();
        }

        createSortUI() {
            const sortLabel = document.createElement('label');
            sortLabel.textContent = '並び変え: ';
            this.sortContainer.appendChild(sortLabel);

            const sortSelect = document.createElement('select');
            sortSelect.style.marginRight = '10px';
            const sortOptions = [
                {value: 'subjectCode', label: '科目番号'},
                {value: 'subjectName', label: '科目名'},
            ];
            for (const option of sortOptions) {
                const sortOption = document.createElement('option');
                sortOption.value = option.value;
                sortOption.textContent = option.label;
                sortSelect.appendChild(sortOption);
            }
            sortSelect.addEventListener('change', this.sortData.bind(this));
            this.sortContainer.appendChild(sortSelect);

            const sortOrderSelect = document.createElement('select');
            const sortOrderOptions = [
                {value: 'asc', label: '昇順'},
                {value: 'desc', label: '降順'},
            ];
            for (const option of sortOrderOptions) {
                const sortOrderOption = document.createElement('option');
                sortOrderOption.value = option.value;
                sortOrderOption.textContent = option.label;
                sortOrderSelect.appendChild(sortOrderOption);
            }
            sortOrderSelect.addEventListener('change', this.sortData.bind(this));

            this.sortContainer.style.marginBottom = '15px'
            this.sortContainer.appendChild(sortOrderSelect);
        }

        toggleDisplay() {
            if (this.displayElement.style.display === 'none') {
                this.displayElement.style.display = 'block';
            } else {
                this.displayElement.style.display = 'none';
            }
        }

        createUrlSwitchUI() {
            const urlSwitchContainer = document.createElement('div');
            urlSwitchContainer.style.marginTop = '10px';
            urlSwitchContainer.style.display = 'flex';
            urlSwitchContainer.style.alignItems = 'center';

            const toggleSwitch = document.createElement('div');
            toggleSwitch.style.position = 'relative';
            toggleSwitch.style.display = 'inline-block';
            toggleSwitch.style.width = '40px';
            toggleSwitch.style.height = '20px';
            toggleSwitch.style.borderRadius = '20px';
            toggleSwitch.style.backgroundColor = '#ccc';
            toggleSwitch.style.cursor = 'pointer';
            toggleSwitch.style.transition = 'background-color 0.3s';

            const toggleIndicator = document.createElement('div');
            toggleIndicator.style.position = 'absolute';
            toggleIndicator.style.top = '2px';
            toggleIndicator.style.left = '2px';
            toggleIndicator.style.width = '16px';
            toggleIndicator.style.height = '16px';
            toggleIndicator.style.borderRadius = '50%';
            toggleIndicator.style.backgroundColor = 'white';
            toggleIndicator.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.2)';
            toggleIndicator.style.transition = 'transform 0.3s';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.style.display = 'none';

            checkbox.addEventListener('change', () => {
                this.currentUrlIndex = checkbox.checked ? 1 : 0;

                this.resetSearchOptions();

                this.fetchJsonData(this.urls[this.currentUrlIndex]).then((jsonData) => {
                    this.displayJsonData(jsonData);
                });

                toggleIndicator.style.transform = checkbox.checked ? 'translateX(20px)' : 'translateX(0)';
                toggleSwitch.style.backgroundColor = checkbox.checked ? '#2196F3' : '#ccc';
            });

            toggleSwitch.appendChild(checkbox);
            toggleSwitch.appendChild(toggleIndicator);

            const labelUrl1 = document.createElement('span');
            labelUrl1.textContent = '学類';
            labelUrl1.style.marginRight = '5px';
            labelUrl1.style.fontSize = '14px';

            const labelUrl2 = document.createElement('span');
            labelUrl2.textContent = '大学院';
            labelUrl2.style.marginLeft = '5px';
            labelUrl2.style.fontSize = '14px';

            urlSwitchContainer.appendChild(labelUrl1);
            urlSwitchContainer.appendChild(toggleSwitch);
            urlSwitchContainer.appendChild(labelUrl2);

            toggleSwitch.addEventListener('click', () => {
                checkbox.checked = !checkbox.checked;
                checkbox.dispatchEvent(new Event('change'));
            });

            this.displayElement.appendChild(urlSwitchContainer);
        }

        async displayJsonData(jsonData) {
            this.jsonData = jsonData;
            this.indexData(jsonData.subject);
            this.filteredData = jsonData.subject;
            this.renderTable();
        }

        indexData(data) {
            this.indexedData = {
                subjectCode: {},
                subjectName: {},
                semester: {},
                format: {},
                dayOfWeek: {},
                period: {},
            };

            data.forEach((subject, index) => {
                const subjectCode = subject[0];
                const subjectName = subject[1];
                const semester = subject[5];
                const format = subject[10];
                const timetable = subject[6];
                const dayOfWeek = timetable.slice(0, 1);
                const period = timetable.slice(1);

                if (!this.indexedData.subjectCode[subjectCode]) {
                    this.indexedData.subjectCode[subjectCode] = [];
                }
                this.indexedData.subjectCode[subjectCode].push(index);

                if (!this.indexedData.subjectName[subjectName]) {
                    this.indexedData.subjectName[subjectName] = [];
                }
                this.indexedData.subjectName[subjectName].push(index);

                if (!this.indexedData.semester[semester]) {
                    this.indexedData.semester[semester] = [];
                }
                this.indexedData.semester[semester].push(index);

                if (!this.indexedData.format[format]) {
                    this.indexedData.format[format] = [];
                }
                this.indexedData.format[format].push(index);

                if (!this.indexedData.dayOfWeek[dayOfWeek]) {
                    this.indexedData.dayOfWeek[dayOfWeek] = [];
                }
                this.indexedData.dayOfWeek[dayOfWeek].push(index);

                if (!this.indexedData.period[period]) {
                    this.indexedData.period[period] = [];
                }
                this.indexedData.period[period].push(index);
            });
        }

        renderTable() {
            const tableHeaders = ['  ', '科目番号', '科目名', '単位', '年次', '開講時期', '時間割', '教室', '担当教員', '概要', '備考'];

            let tableHtml = `
            <table style="border-collapse: collapse; width: 100%;">
                <thead>
                    <tr style="background-color: #f2f2f2;">
                        ${tableHeaders.map(header => `<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">${header}</th>`).join('')}
                    </tr>
                </thead>
                <tbody>
        `;

            const startIndex = (this.currentPage - 1) * this.pageSize;
            const endIndex = startIndex + this.pageSize;
            const paginatedData = this.filteredData.slice(startIndex, endIndex);

            if (paginatedData.length === 0) {
                tableHtml += `
                <tr>
                    <td colspan="${tableHeaders.length}" style="text-align: center; padding: 8px;">検索結果がありません</td>
                </tr>
            `;
            } else {
                paginatedData.forEach((subject, index) => {
                    const rowStyle = index % 2 === 0 ? 'background-color: #f9f9f9;' : '';
                    tableHtml += `
                    <tr style="${rowStyle}">
                        <td style="border: 1px solid #ddd; padding: 8px;">
                        <button class="subject-button syllabus" data-subject-code="${subject[0]}" style="width: 80px;">シラバス</button>
                        <button class="subject-button add" data-subject-code="${subject[0]}" style="width: 80px;">科目を追加</button>
                        </td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[0]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[1]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[3]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[4]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[5]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[6]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[7]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[8]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[9]}</td>
                        <td style="border: 1px solid #ddd; padding: 8px;">${subject[10]}</td>
                    </tr>
                `;
                });
            }

            tableHtml += `
                </tbody>
            </table>
        `;

            const totalPages = Math.ceil(this.filteredData.length / this.pageSize);
            const paginationHtml = `
            <div style="margin-top: 10px;">
                <button id="prevPage" style="margin-right: 5px;">前</button>
                <span>ページ ${this.currentPage} / ${totalPages}</span>
                <button id="nextPage" style="margin-left: 5px;">次</button>
            </div>
        `;

            this.tableContainer.innerHTML = tableHtml + paginationHtml;

            const prevPageButton = this.tableContainer.querySelector('#prevPage');
            const nextPageButton = this.tableContainer.querySelector('#nextPage');

            prevPageButton.addEventListener('click', () => {
                if (this.currentPage > 1) {
                    this.currentPage--;
                    this.renderTable();
                }
            });

            nextPageButton.addEventListener('click', () => {
                if (this.currentPage < totalPages) {
                    this.currentPage++;
                    this.renderTable();
                }
            });

            this.addSubjectButtonListeners();
        }

        addSubjectButtonListeners() {
            const addButtons = this.tableContainer.querySelectorAll('.subject-button.add');
            addButtons.forEach((button) => {
                button.addEventListener('click', () => {
                    const subjectCode = button.dataset.subjectCode;
                    const subjectName = button.closest('tr').querySelector('td:nth-child(3)').textContent.trim();

                    const confirmAdd = window.confirm(`科目名 ${subjectName} (科目番号 : ${subjectCode}) を追加しますか?`);
                    if (confirmAdd) {
                        this.handleSubjectButtonClick(subjectCode, subjectName, 'add');
                    }
                });
            });

            const syllabusButtons = this.tableContainer.querySelectorAll('.subject-button.syllabus');
            syllabusButtons.forEach((button) => {
                button.addEventListener('click', () => {
                    const subjectCode = button.dataset.subjectCode;
                    this.handleSubjectButtonClick(subjectCode, '', 'syllabus');
                });
            });
        }

        handleSubjectButtonClick(subjectCode, subjectName, action) {
            if (!this.keyObserver) {
                console.error('KeyObserver or its required properties are not initialized');
                return;
            }

            if (action === 'add') {
                try {
                    this.keyObserver.inputField.value = subjectCode;
                    this.keyObserver.addButton.click();
                } catch (error) {
                    console.error('Error adding subject:', error);
                    alert('科目の追加中にエラーが発生しました。');
                }
            } else if (action === 'syllabus') {
                const syllabusUrl = `https://kdb.tsukuba.ac.jp/syllabi/${new Date().getFullYear()}/${subjectCode}/jpn`;
                window.open(syllabusUrl, '_blank', 'width=800,height=600,resizable=yes,scrollbars=yes');
            }
        }

        filterData() {
            const subjectCodeInput = this.displayElement.querySelector('input[placeholder="科目番号で検索"]');
            const subjectNameInput = this.displayElement.querySelector('input[placeholder="科目名で検索"]');
            const semesterCheckboxes = this.displayElement.querySelectorAll('input[id^="semester-"]:checked');
            const onlineOfflineCheckboxes = this.displayElement.querySelectorAll('input[id^="format-"]:checked');

            const subjectCode = subjectCodeInput.value.toLowerCase();
            const subjectName = subjectNameInput.value.toLowerCase();
            const selectedDayOfWeek = this.dayOfWeekSelect.value;
            const selectedPeriod = this.periodSelect.value;
            const selectedSemesters = Array.from(semesterCheckboxes).map(checkbox => checkbox.value);
            const selectedFormats = Array.from(onlineOfflineCheckboxes).map(checkbox => checkbox.value);

            let filteredIndices = this.getIntersection(
                this.searchIndex(this.indexedData.subjectCode, subjectCode),
                this.searchIndex(this.indexedData.subjectName, subjectName),
                this.filterIndexByIncludingOptions(this.indexedData.dayOfWeek, [selectedDayOfWeek]),
                this.filterIndexByIncludingOptions(this.indexedData.period, [selectedPeriod]),
                this.filterIndexBySemesterModules(this.indexedData.semester, selectedSemesters),
                this.filterIndexByIncludingOptions(this.indexedData.format, selectedFormats)
            );

            this.filteredData = filteredIndices.map(index => this.jsonData.subject[index]);

            this.currentPage = 1;
            this.renderTable();
        }

        searchIndex(index, query) {
            if (query === '') {
                return Object.values(index).flat();
            }
            const matches = Object.entries(index).filter(([key]) => key.toLowerCase().includes(query));
            return matches.map(([_, indices]) => indices).flat();
        }

        /**
         * @param {Object} index - 検索対象のindex
         * @param {string[]} selectedOptions - 選択したoptionsの配列
         * @returns {number[]} - 選択したoptionsを含む項目のindex配列
         */

        filterIndexByIncludingOptions(index, selectedOptions) {
            if (selectedOptions.length === 0) {
                return Object.values(index).flat();
            }
            const matches = Object.entries(index).filter(([key, indices]) => selectedOptions.some(option => key.includes(option)));
            return matches.map(([_, indices]) => indices).flat();
        }

        filterIndexBySemesterModules(index, selectedSemesters) {
            if (selectedSemesters.length === 0) {
                return Object.values(index).flat();
            }

            const matches = Object.entries(index).filter(([key, indices]) => {
                return selectedSemesters.every(semester => key.includes(semester));
            });

            return matches.map(([_, indices]) => indices).flat();
        }

        getIntersection(...arrays) {
            return arrays.reduce((a, b) => a.filter(c => b.includes(c)));
        }

        sortData() {
            const sortSelect = this.displayElement.querySelector('select');
            const sortOrderSelect = sortSelect.nextElementSibling;
            const sortBy = sortSelect.value;
            const sortOrder = sortOrderSelect.value;

            const subjectIndex = sortBy === 'subjectCode' ? 0 : 1;

            this.filteredData.sort((a, b) => {
                const subjectA = a[subjectIndex].toLowerCase();
                const subjectB = b[subjectIndex].toLowerCase();

                if (subjectA < subjectB) return sortOrder === 'asc' ? -1 : 1;
                if (subjectA > subjectB) return sortOrder === 'asc' ? 1 : -1;
                return 0;
            });

            this.currentPage = 1;
            this.renderTable();
        }
    }

    class LogAnalyzer {
        constructor() {
            this.logKey = 'flowExecutionLogs';
        }

        getLogs() {
            const logs = sessionStorage.getItem(this.logKey);
            return logs ? JSON.parse(logs) : [];
        }

        sortLogsByTimestamp(logs) {
            return logs.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
        }

        cleanUpLogs() {
            const logs = this.getLogs();
            const maxLogs = 10;
            const minLogsToKeep = 5;

            if (logs.length > maxLogs) {
                const sortedLogs = this.sortLogsByTimestamp(logs);
                const logsToKeep = sortedLogs.slice(-minLogsToKeep);

                sessionStorage.setItem(this.logKey, JSON.stringify(logsToKeep));
            }
        }

        detectAbnormalPattern(logs) {
            const sortedLogs = this.sortLogsByTimestamp(logs);
            const validPattern = sortedLogs.slice(-3);

            const patternTypes = validPattern.map(log => log.type).join('-');
            return patternTypes === 'input-insert-back';
        }

        analyzeLogs() {
            this.cleanUpLogs();

            const logs = this.getLogs();
            const isAbnormal = this.detectAbnormalPattern(logs);

            if (isAbnormal) {
                alert("科目が追加されませんでした。時限・曜日を確認してください。");
            }
        }
    }

    // kdbっぽいなにか https://github.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb から取ってきています。
    const jsonUrls = [
        'https://raw.githubusercontent.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb/main/src/kdb.json', // 学類
        'https://raw.githubusercontent.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb/main/src/kdb-grad.json' // 大学院
    ];
    new kdb_Displayer(jsonUrls);

    let keyObserver = null;
    let advancedSyllabus = null;

    function initializeEnhancer() {
        const targetIframe = document.querySelector('iframe[src*="campussquare.do?_flowId=RSW0001000-flow"]');
        if (targetIframe) {
            targetIframe.addEventListener('load', function () {
                try {
                    const iframeDocument = targetIframe.contentDocument || targetIframe.contentWindow.document;
                    if (iframeDocument.readyState === 'complete') {
                        keyObserver = new KeyObserver('body');
                        advancedSyllabus = new AdvancedSyllabus(keyObserver);

                        new kdb_Displayer(jsonUrls, keyObserver);
                    } else {
                        setTimeout(initializeEnhancer, 100);
                    }
                } catch (error) {
                    console.error('Error accessing iframe content:', error);
                    setTimeout(initializeEnhancer, 1000);
                }
            });
        } else {
            console.warn('Target iframe not found, retrying in 1 second');
            setTimeout(initializeEnhancer, 1000);
        }
    }

    function observePageChanges() {
        const observerOptions = {
            childList: true,
            subtree: true
        };

        const observer = new MutationObserver(function(mutationsList, observer) {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    const addedIframes = Array.from(mutation.addedNodes).filter(node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'IFRAME');
                    if (addedIframes.length > 0) {
                        console.log('New iframe(s) added:', addedIframes);
                        initializeEnhancer();
                    }
                }
            }
        });

        observer.observe(document.body, observerOptions);
    }
    observePageChanges();

})();