(function(window, document) {
    var state = {
        ready: false,
        loading: false,
        dictionary: null,
        customWords: {},
        ignoredByField: new Map(),
        pendingByField: new Map(),
        activeContainer: null,
        packageUrl: '',
        addWordUrl: '',
        customWordsUrl: '',
        tenant: '',
        canAddWords: false
    };

    function fieldKey(field) {
        return [
            field.name,
            field.id,
            field.getAttribute('data-name'),
            field.getAttribute('data-field'),
            field.getAttribute('data-key'),
            field.getAttribute('placeholder'),
            field.getAttribute('aria-label')
        ].join(' ').toLowerCase();
    }

    function shouldSkipField(field) {
        if (!field || field.disabled || field.readOnly) {
            return true;
        }

        var tagName = (field.tagName || '').toLowerCase();
        var type = (field.getAttribute('type') || '').toLowerCase();

        if (field.classList && field.classList.contains('report-image-desc')) {
            return true;
        }

        if (field.closest && field.closest('.report-image-card, .report-image-handler-slot, .report-image-dropzone')) {
            return true;
        }

        if (tagName === 'input' && ['hidden', 'number', 'date', 'time', 'checkbox', 'radio', 'file', 'range', 'color', 'button', 'submit', 'password', 'email'].indexOf(type) !== -1) {
            return true;
        }

        if (tagName === 'textarea' || field.isContentEditable) {
            return false;
        }

        var key = fieldKey(field);
        return /(plant\s*(number|no|id|ref)|serial|postcode|post\s*code|swl|barcode|itemnumber|item\s*number|repnumber|rep\s*number|model|date|time|frequency|clientnumber|locnumber|locationnumber|asset\s*tag|tag\s*number|certificate|cert\s*number|reference|ref\b|image|photo|picture|filename|file\s*name)/.test(key);
    }

    function fieldLabel(field) {
        var id = field.id || '';
        var label = id ? document.querySelector('label[for="' + id.replace(/"/g, '\\"') + '"]') : null;

        if (!label) {
            var group = field.closest ? field.closest('.form-group, .col-12, .col-md-12, .col-md-6, .col-md-4, .col-md-3') : null;
            label = group ? group.querySelector('label') : null;
        }

        return label ? label.textContent.replace(/\s+/g, ' ').trim() : (field.name || field.id || 'Report field');
    }

    function normaliseWord(word) {
        return word.replace(/[’]/g, "'").replace(/^'+|'+$/g, '');
    }

    function capitaliseLineStarts(value) {
        return value.replace(/(^|[\r\n])([ \t]*)([a-z])/g, function(match, lineStart, spacing, letter) {
            return lineStart + spacing + letter.toUpperCase();
        });
    }

    function bindCapitalisation(field) {
        if (field.enrepSpellcheckCapBound || (field.tagName || '').toLowerCase() !== 'textarea') {
            return;
        }

        field.enrepSpellcheckCapBound = true;
        field.addEventListener('input', function() {
            var corrected = capitaliseLineStarts(field.value);
            if (corrected === field.value) {
                return;
            }

            var start = field.selectionStart;
            var end = field.selectionEnd;
            field.value = corrected;
            field.setSelectionRange(start, end);
        });
    }

    function isSkippableWord(word) {
        if (!word || word.length < 3) {
            return true;
        }

        if (/\d/.test(word) || /_/.test(word)) {
            return true;
        }

        if (/^[A-Z]{2,}$/.test(word)) {
            return true;
        }

        return false;
    }

    function isIgnoredForField(field, word) {
        var ignored = state.ignoredByField.get(field);
        return !!(ignored && ignored[word.toLowerCase()]);
    }

    function isCorrect(word, field) {
        var key = word.toLowerCase();
        if (state.customWords[key] || isIgnoredForField(field, word)) {
            return true;
        }

        try {
            return state.dictionary && state.dictionary.check(word);
        } catch (e) {
            return true;
        }
    }

    function wordsFromText(text) {
        var words = [];
        var seen = {};
        var regex = /[A-Za-z][A-Za-z'’-]*/g;
        var match;

        while ((match = regex.exec(text || '')) !== null) {
            var word = normaliseWord(match[0]);
            var key = word.toLowerCase();

            if (seen[key] || isSkippableWord(word)) {
                continue;
            }

            seen[key] = true;
            words.push(word);
        }

        return words;
    }

    function checkField(field, includeSuggestions) {
        if (shouldSkipField(field)) {
            clearFieldStatus(field);
            return [];
        }

        var issues = wordsFromText(field.value || field.textContent || '').filter(function(word) {
            return !isCorrect(word, field);
        }).map(function(word) {
            var suggestions = [];

            if (includeSuggestions) {
                try {
                    suggestions = state.dictionary.suggest(word, 4);
                } catch (e) {
                    suggestions = [];
                }
            }

            return {
                word: word,
                suggestions: suggestions
            };
        });

        updateFieldStatus(field, issues);
        return issues;
    }

    function clearFieldStatus(field) {
        field.classList.remove('enrep-spellcheck-warning');
        field.removeAttribute('data-enrep-spellcheck-count');
        var anchor = field.enrepSpellcheckAnchor;
        if (anchor && anchor.parentNode) {
            anchor.parentNode.removeChild(anchor);
        }
        field.enrepSpellcheckBadge = null;
        field.enrepSpellcheckAnchor = null;
        state.pendingByField.delete(field);
    }

    function updateFieldStatus(field, issues) {
        if (!issues.length) {
            clearFieldStatus(field);
            return;
        }

        field.classList.add('enrep-spellcheck-warning');
        field.setAttribute('data-enrep-spellcheck-count', issues.length);
        state.pendingByField.set(field, issues);

        if (!field.enrepSpellcheckBadge) {
            var anchor = document.createElement('span');
            anchor.className = 'enrep-spellcheck-anchor';

            var badge = document.createElement('button');
            badge.type = 'button';
            badge.className = 'enrep-spellcheck-badge';
            badge.innerHTML = '<i class="fad fa-spell-check"></i><span></span>';
            badge.addEventListener('click', function() {
                var issues = checkField(field, true);
                if (!issues.length) {
                    return;
                }

                showReviewModal([{
                    field: field,
                    label: fieldLabel(field),
                    issues: issues
                }]);
            });

            var target = field.parentNode && field.parentNode.classList && field.parentNode.classList.contains('d-flex') ? field.parentNode : field;
            anchor.appendChild(badge);
            target.parentNode.insertBefore(anchor, target.nextSibling);
            field.enrepSpellcheckBadge = badge;
            field.enrepSpellcheckAnchor = anchor;
        }

        field.enrepSpellcheckBadge.querySelector('span').textContent = issues.length + ' spelling ' + (issues.length === 1 ? 'query' : 'queries');
    }

    function scanContainer(container, includeSuggestions) {
        var root = container || state.activeContainer || document;
        var fields = root.querySelectorAll('textarea, input[type="text"], input:not([type]), [contenteditable="true"]');
        var results = [];

        Array.prototype.forEach.call(fields, function(field) {
            field.setAttribute('spellcheck', 'false');
            field.setAttribute('autocomplete', field.getAttribute('autocomplete') || 'off');
            bindCapitalisation(field);

            var issues = checkField(field, includeSuggestions);
            if (issues.length) {
                results.push({
                    field: field,
                    label: fieldLabel(field),
                    issues: issues
                });
            }
        });

        return results;
    }

    function debounce(fn, wait) {
        var timer;
        return function() {
            var args = arguments;
            var self = this;
            clearTimeout(timer);
            timer = setTimeout(function() {
                fn.apply(self, args);
            }, wait);
        };
    }

    function bindFields(container) {
        var root = container || document;
        var fields = root.querySelectorAll('textarea, input[type="text"], input:not([type]), [contenteditable="true"]');

        Array.prototype.forEach.call(fields, function(field) {
            if (field.enrepSpellcheckBound) {
                return;
            }

            field.enrepSpellcheckBound = true;
            var debounced = debounce(function() {
                checkField(field, false);
            }, 1500);

            field.addEventListener('input', debounced);
            field.addEventListener('blur', function() {
                checkField(field, false);
            });
        });
    }

    function escapeHtml(value) {
        return String(value).replace(/[&<>"']/g, function(ch) {
            return ({
                '&': '&amp;',
                '<': '&lt;',
                '>': '&gt;',
                '"': '&quot;',
                "'": '&#039;'
            })[ch];
        });
    }

    function reviewHtml(results, canContinueSave) {
        var rows = results.map(function(result, fieldIndex) {
            var issueRows = result.issues.map(function(issue) {
                var suggestions = issue.suggestions.length ? issue.suggestions.map(function(suggestion) {
                    return '<button type="button" class="enrep-spellcheck-suggestion" data-enrep-spellcheck-action="replace" data-field-index="' + fieldIndex + '" data-word="' + encodeURIComponent(issue.word) + '" data-suggestion="' + encodeURIComponent(suggestion) + '">' + escapeHtml(suggestion) + '</button>';
                }).join('') : '<span class="text-muted">No suggestions</span>';

                return '<div class="enrep-spellcheck-word-row">' +
                    '<div class="enrep-spellcheck-word">' + escapeHtml(issue.word) + '</div>' +
                    '<div class="enrep-spellcheck-suggestions">' + suggestions + '</div>' +
                    '<div class="enrep-spellcheck-actions">' +
                    '<button type="button" class="btn btn-sm btn-outline-secondary" data-enrep-spellcheck-action="ignore" data-field-index="' + fieldIndex + '" data-word="' + encodeURIComponent(issue.word) + '">Ignore</button>' +
                    (state.canAddWords ? '<button type="button" class="btn btn-sm btn-outline-primary" data-enrep-spellcheck-action="add-word" data-field-index="' + fieldIndex + '" data-word="' + encodeURIComponent(issue.word) + '">Add to dictionary</button>' : '') +
                    '</div>' +
                    '</div>';
            }).join('');

            return '<div class="enrep-spellcheck-field">' +
                '<div class="enrep-spellcheck-field-title">' + escapeHtml(result.label) + '</div>' +
                issueRows +
                '</div>';
        }).join('');

        return '<div class="enrep-spellcheck-modal">' +
            '<p class="mb-3">Please fix, ignore, or add each spelling query before saving this report.</p>' +
            rows +
            '<div class="text-end mt-3">' +
            '<button type="button" class="btn btn-secondary me-2" onclick="killModal(\'spellcheckBox\')">Return to report</button>' +
            (canContinueSave ? '<button type="button" class="btn btn-primary" data-enrep-spellcheck-action="continue">Continue save</button>' : '') +
            '</div>' +
            '</div>';
    }

    function resolvedReviewHtml(canContinueSave) {
        return '<div class="enrep-spellcheck-modal">' +
            '<div class="note note-success mb-3"><strong>Spelling review:</strong> All spelling queries in this review have been handled.</div>' +
            '<div class="text-end mt-3">' +
            '<button type="button" class="btn btn-secondary me-2" onclick="killModal(\'spellcheckBox\')">Return to report</button>' +
            (canContinueSave ? '<button type="button" class="btn btn-primary" data-enrep-spellcheck-action="continue">Continue save</button>' : '') +
            '</div>' +
            '</div>';
    }

    function reviewedFields() {
        return (state.lastReviewResults || []).map(function(result) {
            return result.field;
        }).filter(function(field, index, fields) {
            return field && fields.indexOf(field) === index;
        });
    }

    function refreshReviewModal() {
        var fields = reviewedFields();
        var results = [];

        fields.forEach(function(field) {
            var issues = checkField(field, true);
            if (issues.length) {
                results.push({
                    field: field,
                    label: fieldLabel(field),
                    issues: issues
                });
            }
        });

        state.lastReviewResults = results;

        var body = document.querySelector('#spellcheckBox .modal-body');
        if (!body) {
            return;
        }

        if (results.length) {
            body.innerHTML = reviewHtml(results, !!state.pendingSaveButton);
            return;
        }

        if (state.pendingSaveButton) {
            body.innerHTML = resolvedReviewHtml(true);
            return;
        }

        if (window.killModal) {
            killModal('spellcheckBox');
        }
    }

    function showReviewModal(results, saveButton) {
        state.lastReviewResults = results;
        state.pendingSaveButton = saveButton || null;

        if (window.loadModalContent) {
            loadModalContent(reviewHtml(results, !!saveButton), 'spellcheckBox', 'modal-xl', 1099, 'true', 'Spelling Review', '');
        } else {
            if (window.showResponse) {
                showResponse('warning', 'Please review spelling queries before saving.');
            }
            console.warn('EnRep spelling queries', results);
        }
    }

    function bindSaveButtons(container) {
        var root = container || document;
        var buttons = root.querySelectorAll('#btn_save, #btn_later');

        Array.prototype.forEach.call(buttons, function(button) {
            if (button.enrepSpellcheckSaveBound) {
                return;
            }

            button.enrepSpellcheckSaveBound = true;
            button.addEventListener('click', function(event) {
                if (button.dataset.enrepSpellcheckBypass === '1' || !state.ready) {
                    button.dataset.enrepSpellcheckBypass = '';
                    return;
                }

                var results = scanContainer(state.activeContainer || container, true);
                if (!results.length) {
                    return;
                }

                event.preventDefault();
                event.stopImmediatePropagation();
                showReviewModal(results, button);
            }, true);
        });
    }

    function loadCustomWords() {
        return fetch(state.customWordsUrl + (state.customWordsUrl.indexOf('?') === -1 ? '?' : '&') + '_=' + Date.now(), {
            cache: 'no-store'
        })
            .then(function(response) {
                return response.ok ? response.text() : '';
            })
            .then(function(text) {
                text.split(/\r?\n/).forEach(function(word) {
                    word = normaliseWord(word.trim());
                    if (word && word.charAt(0) !== '#') {
                        state.customWords[word.toLowerCase()] = true;
                    }
                });
            })
            .catch(function() {});
    }

    function loadDictionary(done) {
        if (state.ready || state.loading) {
            if (done) {
                done();
            }
            return;
        }

        state.loading = true;
        loadCustomWords().then(function() {
            state.dictionary = new Typo('en_GB', false, false, {
                dictionaryPath: state.packageUrl + '/dictionaries',
                asyncLoad: true,
                loadedCallback: function() {
                    state.ready = true;
                    state.loading = false;
                    if (done) {
                        done();
                    }
                }
            });
        });
    }

    function observe(container) {
        var root = container || document.body;
        if (!root || root.enrepSpellcheckObserved || !window.MutationObserver) {
            return;
        }

        root.enrepSpellcheckObserved = true;
        new MutationObserver(function(mutations) {
            var changed = mutations.some(function(mutation) {
                return Array.prototype.some.call(mutation.addedNodes, function(node) {
                    return node.nodeType === 1 && (
                        node.matches('textarea, input, [contenteditable="true"]') ||
                        node.querySelector('textarea, input, [contenteditable="true"]')
                    );
                });
            });

            if (changed) {
                bindFields(root);
            }
        }).observe(root, {
            childList: true,
            subtree: true
        });
    }

    function bindReviewActions() {
        if (document.enrepSpellcheckReviewActionsBound) {
            return;
        }

        document.enrepSpellcheckReviewActionsBound = true;
        document.addEventListener('click', function(event) {
            var button = event.target.closest ? event.target.closest('[data-enrep-spellcheck-action]') : null;
            if (!button) {
                return;
            }

            var action = button.getAttribute('data-enrep-spellcheck-action');
            if (action === 'ignore') {
                window.EnrepInHouseSpellcheck.ignoreWord(
                    parseInt(button.getAttribute('data-field-index'), 10),
                    decodeURIComponent(button.getAttribute('data-word') || '')
                );
            }
            if (action === 'replace') {
                window.EnrepInHouseSpellcheck.replaceWord(
                    parseInt(button.getAttribute('data-field-index'), 10),
                    decodeURIComponent(button.getAttribute('data-word') || ''),
                    decodeURIComponent(button.getAttribute('data-suggestion') || '')
                );
            }
            if (action === 'add-word') {
                window.EnrepInHouseSpellcheck.addWord(
                    parseInt(button.getAttribute('data-field-index'), 10),
                    decodeURIComponent(button.getAttribute('data-word') || '')
                );
            }
            if (action === 'continue') {
                window.EnrepInHouseSpellcheck.continueSave();
            }
        });
    }

    function init(container, options) {
        state.activeContainer = container || document;
        state.packageUrl = options && options.packageUrl ? options.packageUrl : (window.ROOT_URL || '') + '/packages/spellcheck';
        state.addWordUrl = options && options.addWordUrl ? options.addWordUrl : (window.ROOT_URL || '') + '/includes/dictionary/dictionary_actions.php';
        state.customWordsUrl = options && options.customWordsUrl ? options.customWordsUrl : state.addWordUrl + '?action=custom_words&tenant=' + encodeURIComponent(options && options.tenant ? options.tenant : (window.TENANT_URL || ''));
        state.tenant = options && options.tenant ? options.tenant : (window.TENANT_URL || '');
        state.canAddWords = !!(options && options.canAddWords);
        bindReviewActions();

        loadDictionary(function() {
            bindFields(state.activeContainer);
            bindSaveButtons(state.activeContainer);
            observe(state.activeContainer);
        });
    }

    window.EnrepInHouseSpellcheck = {
        init: init,
        ignoreWord: function(fieldIndex, word) {
            var result = state.lastReviewResults && state.lastReviewResults[fieldIndex];
            if (!result || !result.field) {
                return;
            }

            var ignored = state.ignoredByField.get(result.field) || {};
            ignored[normaliseWord(word).toLowerCase()] = true;
            state.ignoredByField.set(result.field, ignored);
            refreshReviewModal();
        },
        replaceWord: function(fieldIndex, oldWord, newWord) {
            var result = state.lastReviewResults && state.lastReviewResults[fieldIndex];
            if (!result || !result.field) {
                return;
            }

            var regex = new RegExp('\\b' + oldWord.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
            result.field.value = result.field.value.replace(regex, newWord);
            refreshReviewModal();
        },
        addWord: function(fieldIndex, word) {
            word = normaliseWord(word);

            if (!state.canAddWords) {
                if (window.showResponse) {
                    showResponse('danger', 'Only administrators can add dictionary words');
                }
                return;
            }

            fetch(state.addWordUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                body: new URLSearchParams({
                    action: 'add',
                    word: word,
                    tenant: state.tenant
                })
            })
                .then(function(response) {
                    return response.json().then(function(data) {
                        return {
                            ok: response.ok,
                            data: data
                        };
                    });
                })
                .then(function(result) {
                    if (!result.ok || result.data.status !== 'success') {
                        throw new Error(result.data.message || 'Unable to add dictionary word');
                    }

                    state.customWords[word.toLowerCase()] = true;
                    if (window.showResponse) {
                        showResponse('success', 'Dictionary word added');
                    }

                    refreshReviewModal();
                })
                .catch(function(error) {
                    if (window.showResponse) {
                        showResponse('danger', error.message || 'Unable to add dictionary word');
                    }
                });
        },
        continueSave: function() {
            var button = state.pendingSaveButton;
            if (window.killModal) {
                killModal('spellcheckBox');
            }
            if (button) {
                button.dataset.enrepSpellcheckBypass = '1';
                button.click();
            }
        }
    };
})(window, document);
