1document.addEventListener('DOMContentLoaded', function () {
2    // Function to initialize fuzzy search for a given input or textarea element
3    function initializeFuzzySearch(element) {
4        if (!element) {
5            console.error('Element not found!');
6            return;
7        }
8
9        let resultsDiv = null;
10        let currentIndex = -1;
11        let lastPhrase = '';
12        let fuse = null;
13        let pagesCache = null;
14        let searchTimeout = null;
15
16        // Fetch pages and initialize Fuse.js
17        fetch(DOKU_BASE + 'lib/exe/ajax.php?call=fuzzysearch_pages', {
18            method: 'GET',
19            credentials: 'same-origin'
20        })
21        .then(response => {
22            if (!response.ok) throw new Error('Failed to fetch pages');
23            return response.json();
24        })
25        .then(pages => {
26            pagesCache = pages;
27            fuse = new Fuse(pagesCache, {
28                keys: ['title'],
29                threshold: 0.4,
30                includeScore: true,
31                maxPatternLength: 32,
32                minMatchCharLength: 2
33            });
34        })
35        .catch(error => console.error('Initial fetch error:', error));
36
37        // Function to handle search logic
38        function handleInputChange() {
39            if (searchTimeout) clearTimeout(searchTimeout);
40
41            searchTimeout = setTimeout(() => {
42                const cursorPos = element.selectionStart || element.value.length;
43                const text = element.value;
44                const match = text.substring(0, cursorPos).match(/\[\[([^\[\]]+)\]\]$/);
45                if (!match) {
46                    if (resultsDiv) {
47                        resultsDiv.remove();
48                        resultsDiv = null;
49                    }
50                    return;
51                }
52
53                const phrase = match[1].trim();
54                if (!phrase || phrase === lastPhrase) {
55                    return;
56                }
57                lastPhrase = phrase;
58
59                if (!fuse) {
60                    console.error('Fuse not initialized yet');
61                    return;
62                }
63
64                const results = fuse.search(phrase, { limit: 10 });
65                displayResults(results, phrase, cursorPos);
66            }, 300); // Debounce delay
67        }
68
69        // Add event listeners
70        element.addEventListener('keyup', function (e) {
71            if (e.key === ']') {
72                handleInputChange();
73            }
74        });
75
76        element.addEventListener('input', handleInputChange);
77
78        element.addEventListener('compositionend', handleInputChange);
79
80        // Display results function
81        function displayResults(results, phrase, cursorPos) {
82            if (resultsDiv) {
83                resultsDiv.remove();
84            }
85            if (results.length === 0) {
86                return;
87            }
88
89            resultsDiv = document.createElement('div');
90            resultsDiv.id = 'fuzzysearch-editor-results';
91            resultsDiv.style.position = 'absolute';
92            resultsDiv.style.background = 'white';
93            resultsDiv.style.border = '1px solid #ccc';
94            resultsDiv.style.padding = '5px';
95            resultsDiv.style.zIndex = '1000';
96            resultsDiv.style.maxHeight = '200px';
97            resultsDiv.style.overflowY = 'auto';
98
99            const elementRect = element.getBoundingClientRect();
100            const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
101            const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
102            const coords = getCaretCoordinates(element, cursorPos);
103
104            const cursorTop = elementRect.top + scrollTop + coords.top;
105            const cursorLeft = elementRect.left + scrollLeft + coords.left;
106
107            resultsDiv.style.left = cursorLeft + 'px';
108            let desiredTop = cursorTop;
109
110            results.forEach((result, index) => {
111                const page = result.item;
112                const div = document.createElement('div');
113                div.textContent = page.title;
114                div.dataset.index = index;
115                div.dataset.id = page.id;
116                div.style.cursor = 'pointer';
117                div.style.padding = '2px 5px';
118                div.addEventListener('mouseover', () => highlightResult(index));
119                div.addEventListener('click', () => selectResult(page.id, phrase));
120                resultsDiv.appendChild(div);
121            });
122
123            document.body.appendChild(resultsDiv);
124            const dropdownHeight = resultsDiv.offsetHeight;
125            const elementBottom = elementRect.bottom + scrollTop;
126
127            if (desiredTop + dropdownHeight > elementBottom) {
128                desiredTop = elementBottom - dropdownHeight;
129            }
130
131            const viewportHeight = window.innerHeight;
132            const maxTop = scrollTop + viewportHeight - dropdownHeight;
133            if (desiredTop > maxTop) {
134                desiredTop = maxTop;
135            }
136
137            const elementTop = elementRect.top + scrollTop;
138            if (desiredTop < elementTop) {
139                desiredTop = elementTop;
140            }
141
142            resultsDiv.style.top = desiredTop + 'px';
143
144            currentIndex = 0;
145            highlightResult(currentIndex);
146        }
147
148        function highlightResult(index) {
149            if (!resultsDiv) return;
150            const items = resultsDiv.children;
151            for (let i = 0; i < items.length; i++) {
152                items[i].style.background = i === index ? '#ddd' : 'white';
153            }
154            currentIndex = index;
155            if (items[index]) {
156                items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
157            }
158        }
159
160        function selectResult(pageId, phrase) {
161            const text = element.value;
162            const newLink = `[[${pageId}|${phrase}]]`;
163            const start = text.lastIndexOf(`[[${phrase}]]`);
164            element.value = text.substring(0, start) + newLink + text.substring(start + phrase.length + 4);
165            if (resultsDiv) {
166                resultsDiv.remove();
167                resultsDiv = null;
168            }
169            lastPhrase = '';
170            element.focus();
171        }
172
173        element.addEventListener('keydown', function (e) {
174            if (!resultsDiv || resultsDiv.children.length === 0) return;
175
176            if (e.key === 'ArrowDown') {
177                e.preventDefault();
178                if (currentIndex < resultsDiv.children.length - 1) {
179                    currentIndex++;
180                    highlightResult(currentIndex);
181                }
182            } else if (e.key === 'ArrowUp') {
183                e.preventDefault();
184                if (currentIndex > 0) {
185                    currentIndex--;
186                    highlightResult(currentIndex);
187                }
188            } else if (e.key === 'Space' || e.key === 'Enter') {
189                e.preventDefault();
190                if (currentIndex >= 0) {
191                    const selected = resultsDiv.children[currentIndex];
192                    selectResult(selected.dataset.id, lastPhrase);
193                }
194            } else if (e.key === 'Escape') {
195                if (resultsDiv) {
196                    resultsDiv.remove();
197                    resultsDiv = null;
198                    lastPhrase = '';
199                }
200            }
201        });
202
203        document.addEventListener('click', function (e) {
204            if (resultsDiv && !resultsDiv.contains(e.target) && e.target !== element) {
205                resultsDiv.remove();
206                resultsDiv = null;
207                lastPhrase = '';
208            }
209        });
210
211        function getCaretCoordinates(element, position) {
212            const isTextarea = element.tagName.toLowerCase() === 'textarea';
213            const text = element.value.substring(0, position);
214            const font = window.getComputedStyle(element).font;
215            const canvas = document.createElement('canvas');
216            const context = canvas.getContext('2d');
217            context.font = font;
218
219            if (isTextarea) {
220                const lines = text.split('\n');
221                const lastLine = lines[lines.length - 1];
222                const width = context.measureText(lastLine).width;
223                const lineHeight = parseInt(font);
224                const top = (lines.length - 1) * lineHeight;
225                return { top: top, left: width };
226            } else {
227                // For input type="text", no line breaks, just measure the text width
228                const width = context.measureText(text).width;
229                const lineHeight = parseInt(font);
230                return { top: 0, left: width };
231            }
232        }
233    }
234
235    // Target the main wiki editor textarea
236    const wikiTextarea = document.querySelector('textarea[name="wikitext"]');
237    if (wikiTextarea) {
238        initializeFuzzySearch(wikiTextarea);
239    }
240
241    // Target Bureaucracy form textareas and textboxes
242    const bureaucracyElements = document.querySelectorAll('.bureaucracy__plugin textarea, .bureaucracy__plugin input[type="text"]');
243    bureaucracyElements.forEach(element => {
244        initializeFuzzySearch(element);
245    });
246
247    // Use MutationObserver to catch dynamically added Bureaucracy form elements
248    const observer = new MutationObserver((mutations) => {
249        mutations.forEach((mutation) => {
250            const newElements = mutation.target.querySelectorAll('.bureaucracy__plugin textarea, .bureaucracy__plugin input[type="text"]');
251            newElements.forEach(element => {
252                if (!element.dataset.fuzzyInitialized) {
253                    initializeFuzzySearch(element);
254                    element.dataset.fuzzyInitialized = 'true'; // Mark as initialized
255                }
256            });
257        });
258    });
259
260    observer.observe(document.body, {
261        childList: true,
262        subtree: true
263    });
264});