
// --- Listeners

// Make the body editable
document.body.setAttribute('contenteditable', 'true');

// Update the cursor position when the selection changes.
document.addEventListener('selectionchange', updateCursorPosition);

document.addEventListener('selectionchange', (e) => {
    let href = linkForSelection();
    window.webkit.messageHandlers.selectedLinkDidChange.postMessage({ link: href });
});

// The selection doesn't change until after the context menu appears, so update the existence
// of a link here too.
document.addEventListener('contextmenu', (e) => {
    let href = linkForElement(e.target);
    window.webkit.messageHandlers.selectedLinkDidChange.postMessage({ link: href });
});


// Update the cursor position *again* whenever an input event fires. This works around a case where
// inputs that result in inserting a new node before the current selection wouldn't trigger a
// 'selectionchange' event. Eg. Move the cursor to the start of a line that already has some text
// and press Enter.
//
// Repeated events are filtered in JavaScript so the app-layer doesn't have to deal with this.
document.body.addEventListener('input', updateCursorPosition);

// Post the updated content after every input event.
document.body.addEventListener('input', updateContent);

// Strips any pasted imgs before inserting into the document.
document.body.addEventListener('paste', handlePaste);

document.body.addEventListener('focus', function (e) {
    updateFocus(true);
});

document.body.addEventListener('blur', function (e) {
    updateFocus(false);
});

// Checks for types we handle ourselves (files and images).
Object.defineProperty(DataTransfer.prototype, 'hasHandledPasteType', {
    get() {
        return this.types.some((x) => x === 'Files' || x.startsWith('image/'));
    }
});

document.body.addEventListener('dragover', (e) => {
    if (e.dataTransfer.hasHandledPasteType) {
        e.dataTransfer.dropEffect = 'copy';
    }
});

document.body.addEventListener('drop', (e) => {
    if (e.dataTransfer.hasHandledPasteType) {
        // Pass the drop occurrence to the app.
        handlePaste(e, true);
        e.preventDefault();
    }
});

// ---

// Track last cursor event's values for filtering repeated ranges.
var lastCursorTopOffset = 0;
var lastCursorBottomOffset = 0;
var lastPageOffset = 0;

// Reports the location of the cursor/caret via the cursorPositionChanged message.
function updateCursorPosition() {
    var selection = window.getSelection();
    if (selection.rangeCount == 0) {
        return false;
    }
    
    var topOffset, bottomOffset;
    
    var range = selection.getRangeAt(0);
    
    if (range.startContainer.nodeType == Node.TEXT_NODE) {
        // Typical case - when cursor is in a text node.
        //
        // Here, getClientRects() accurately returns the range of the cursor.
        //
        var rangeRects = range.getClientRects();
        
        if (rangeRects.length > 0) {
            var rect = rangeRects[0];
            topOffset = rect.top;
            bottomOffset = rect.bottom;
        }
    } else if (range.startContainer.nodeType == Node.ELEMENT_NODE) {
        // Special case to handle new lines, initial cursor position, etc.
        //
        // Cursor is within another node, generally before text has been entered. Here, use the
        // start container's top as an anchor, and estimate the bottom based on the current font
        // size.
        //
        var startRects = range.startContainer.getClientRects();
        if (startRects.length > 0) {
            topOffset = startRects[0].top;
            
            var fontSizeString = window.getComputedStyle(range.startContainer).fontSize; // eg: "17px"
            var estCursorHeight = Number(fontSizeString.match(/\d+/)[0]); // eg. 17
            bottomOffset = topOffset + estCursorHeight;
        }
    }
    
    if (topOffset === undefined || bottomOffset === undefined) {
        debugLog("Top and/or bottom inset for current selection is undefined.");
        return;
    }
    
    // Avoid sending duplicate updates across the bridge.
    if (topOffset == lastCursorTopOffset && bottomOffset == lastCursorBottomOffset && lastPageOffset == window.pageYOffset) {
        return;
    }
    
    lastCursorTopOffset = topOffset;
    lastCursorBottomOffset = bottomOffset;
    lastPageOffset = window.pageYOffset
    
    window.webkit.messageHandlers.cursorPositionChanged.postMessage({
        top: topOffset,
        bottom: bottomOffset,
        viewportOffset: window.pageYOffset
    });
}

// Reports that the content has changed via the contentDidChange message.
function updateContent() {
    window.webkit.messageHandlers.contentDidChange.postMessage({
        content: document.body.innerHTML
    });
}

function handlePaste(event, isDrag) {
    let transfer = isDrag ? event.dataTransfer : event.clipboardData;
    
    if (transfer.types.includes('text/html')) {
        return;
    }
    
    if (isDrag) {
        // Selection is updated here instaed of in pastedFile() so inline images are inserted in the
        // correct order.
        var dropRange = document.caretRangeFromPoint(event.clientX, event.clientY);

        let selection = window.getSelection();
        selection.empty();
        selection.addRange(dropRange);
    } else {
        dropRange = null;
    }
    
    if (transfer.hasHandledPasteType) {
        // Make sure updateContent() isn't called until all inline images have been added, otherwise all
        // but the first will be deleted. The Real Fix would be to make the relevant Swift signals and
        // functions take multiple URLs, but that's far more work.
        pastedFile("Pasted", isDrag);
        event.preventDefault();
        
        return;
    }
}

function updateFocus(isFocused) {
    window.webkit.messageHandlers.focusChanged.postMessage({isFocused: isFocused});
}

// Sends a message via the debugLog handler.
function debugLog(msg) {
    window.webkit.messageHandlers.debugLog.postMessage({message: msg});
}

function pastedFile(msg, isDrag) {
    window.webkit.messageHandlers.pastedFile.postMessage({message: msg, isDrag: isDrag});
}

// --- Signatures

// Public:

function updateSignature(newSignatureHTML) {
    var signatureDiv = document.getElementById('dl-mail-signature');
    
    // Create signature div if it doesn't exist yet.
    if (signatureDiv == null) {
        signatureDiv = createSignatureDiv();
    }
    
    // Detect case when the user has edited the signature div.
    if (lastSignatureHTML != null && !signaturesAreEquivalent(signatureDiv, lastSignatureHTML)) {
        moveCurrentSignatureIntoBody();
    }
    
    signatureDiv.innerHTML = newSignatureHTML;
    
    // Store inner HTML to compare next time signature content changes.
    lastSignatureHTML = signatureDiv.innerHTML;
    
    // Notify that the body content has changed
    updateContent();
}

// Private:

var lastSignatureHTML = null;

// Compare a signature div with the saved signature HTML, treating
// <img src="cid:foo"> as equivalent to <img src="cid:foo">
function signaturesAreEquivalent(signatureDiv, lastSignatureHTML) {
    let lastSignatureWrapper = document.createElement('div');
    lastSignatureWrapper.innerHTML = lastSignatureHTML;
    
    let signatureCopy = signatureDiv.cloneNode(true);
    signatureCopy.removeAttribute('id');
    
    function removeAutoScale(image) {
        image.classList.remove('dl-auto-scale');
        
        if (image.classList.length == 0) {
            image.removeAttribute('class');
        }
    }
    
    let images = signatureCopy.querySelectorAll('img');
    
    for (let image of images) {
        // Treat an image with src="cid:foo" as equivalent to an image with dl-cid="foo".
        let cid = image.getAttribute('dl-cid');
        
        if (cid) {
            // Setting this will cause a load error in the browser console; this is seemingly unavoidable.
            image.src = `cid:${cid}`;
            image.removeAttribute('dl-cid');
            image.removeAttribute('class');
        }
        
        removeAutoScale(image);
    }
    
    let lastImages = lastSignatureWrapper.querySelectorAll('img');
    
    for (let image of lastImages) {
        removeAutoScale(image);
    }
    
    return signatureCopy.isEqualNode(lastSignatureWrapper);
}

function createSignatureDiv() {
    // Append the signature to the end of the body.
    document.body.appendChild(document.createElement('br'));
    document.body.appendChild(document.createElement('br'));
    signatureDiv = document.createElement('div');
    signatureDiv.setAttribute('id', 'dl-mail-signature');
    document.body.appendChild(signatureDiv);
    return signatureDiv;
}

// Clones the current signature, moving it into the body above the 'main' signature div, then
// clears the content of the main one.
function moveCurrentSignatureIntoBody() {
    var signatureDiv = document.getElementById('dl-mail-signature');
    if (signatureDiv == null) return;
    
    var oldSignatureDiv = signatureDiv.cloneNode(true);
    oldSignatureDiv.removeAttribute('id');
    document.body.insertBefore(oldSignatureDiv, signatureDiv);
    document.body.insertBefore(document.createElement('br'), signatureDiv);
    
    signatureDiv.innerHTML = '';
}

function resizeWindow() {
    // Not necessary for now in the compose view
}

function insertInlineImages(cids) {
    let html = cids.map((cid) => `<img src="cid:${cid}" class="dl-auto-scale">`).join('')
    
    // Replace the current selection with the images.
    // Using execCommand() allows the user to undo.
    document.execCommand('insertHTML', false, html);
}

function removeInlineImage(cid) {
    var img = document.querySelector('[dl-cid*="' + cid + '"]');
    
    if (img) {
        img.remove();
        updateContent();
    }
}

function linkForElement(element) {
    let link = element.closest('a[href]');
    return link?.href;
}

function linkForSelection() {
    let sel = window.getSelection();
    
    if (sel.rangeCount == 0)
        return null;
    
    let range = sel.getRangeAt(0);
    let elem = range.commonAncestorContainer.parentElement;
    return linkForElement(elem);
}
