
// --- 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', updateTextAttributes);

document.addEventListener('selectionchange', () => delayedFontSize = 0);

// 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);

// Update a delayed font element to the correct size.
document.body.addEventListener('input', setFontSizeFromDelayedFontSize);

// 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);
});

// ---

// 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
    });
}

// MARK: - Formatting

let delayedFontSize = 0;

function setFontSizeFromDelayedFontSize() {
    if (delayedFontSize > 0) {
        updateInsertedFontElement(delayedFontSize);
        delayedFontSize = 0;
    }
}

// The way this is done isn't ideal and should be redone to be more robust, but it does what
// it says on the tin and still allows undo/redo.
function updateInsertedFontElement(size) {
    let fontElements = document.querySelectorAll('font[size="7"]');
    
    for (element of fontElements) {
        element.removeAttribute('size');
        element.style.fontSize = `${size}px`;
    }
    
    updateContent();
}

function setFontSize(size) {
    document.execCommand('fontSize', false, 7);
    
    if (document.getSelection().isCollapsed) {
        delayedFontSize = size;
    } else {
        updateInsertedFontElement(size);
    }
}


// The text-decoration property propagates but doesn't inherit, so won't show up on the
// child element. This also means it can't be overridden by a child element.
function nodeHasUnderline(element) {
    if (element.nodeType != Node.ELEMENT_NODE)
        element = element.parentElement;
    
    while (element) {
        let style = window.getComputedStyle(element, null);
        
        if (style.textDecoration.match(/\bunderline\b/))
            return true;
        
        element = element.parentElement;
    }
    
    return false;
}

function nodeHasStrikethrough(element) {
    if (element.nodeType != Node.ELEMENT_NODE)
        element = element.parentElement;

    while (element) {
        let style = window.getComputedStyle(element, null);
        
        if (style.textDecoration.match(/\bline-through\b/))
            return true;
        
        element = element.parentElement;
    }
    
    return false;
}

Object.defineProperty(Node.prototype, "closestElement", {
    get() {
        return this.nodeType == Node.ELEMENT_NODE ? this : this.parentElement
    }
});

// Assumes a valid font-family attribute returned from getComputedStyle(),
// which wraps multi-word families in double quotes, never single, and always
// separates family names with ", " (comma+space).
function parseCSSFontFamily(fontFamilyString) {
    const len = fontFamilyString.length;
    
    if (len === 0)
        return [];
    
    let currentFamily = '';
    
    let inQuote = false;
    let lastWasBackslash = false;
    
    let result = [];
    
    for (let i = 0; i < len; ++i) {
        const c = fontFamilyString[i];
        
        if (c === ',') {
            if (inQuote) {
                currentFamily += c;
            } else {
                result.push(currentFamily);
                currentFamily = '';
                ++i; // Skip the next character, which will be a space.
            }
        } else if (c === '"') {
            if (inQuote) {
                if (lastWasBackslash)
                    currentFamily += c;
                else
                    inQuote = false;
            } else {
                inQuote = true;
            }
        } else if (c === '\\') {
            if (lastWasBackslash)
                currentFamily += c;
            else
                lastWasBacklash = true;
        } else {
            currentFamily += c;
        }
    }
    
    result.push(currentFamily);
    
    return result;
}

// Reports the current typing attributes, e.g., for the formatting bar.
function updateTextAttributes() {
    let selection = window.getSelection();
    // TODO: Update for multiple ranges?
    
    if (selection.rangeCount == 0)
        return null;
        
    let range = selection.getRangeAt(0);
    
    let commonAncestor = range.commonAncestorContainer;
    
    let walker = document.createTreeWalker(commonAncestor, NodeFilter.SHOW_TEXT);
    
    let startNode = range.startContainer;
    let endNode = range.endContainer;
    
    walker.currentNode = startNode;
    
    let style = window.getComputedStyle(startNode.closestElement, null);
    
    let align = style.textAlign;
    
    let alignFromDirection = style.direction === 'ltr' ? 'left' : 'right';
    
    if (!align || align === 'start')
        align = alignFromDirection;
            
    let result = {
        fontFamily: undefined,
        fontSize: undefined,
        bold: true,
        italic: true,
        underline: true,
        strikethrough: true,
        align: align
    };

    let node = startNode;
    
    const boldThreshold = 600; // From WebKit
    
    while (node) {
        if (node === startNode && startNode !== endNode && range.startOffset === node.textContent.length) {
            node = walker.nextNode();
            continue;
        }
        
        let style = window.getComputedStyle(node.closestElement, null);
        let family = style.fontFamily;
        
        if (result.fontFamily === undefined)
            result.fontFamily = parseCSSFontFamily(family);
        else if (parseCSSFontFamily(family).toString() !== result.fontFamily.toString())
            result.fontFamily = null;
        
        let size = parseInt(style.fontSize);
        
        if (result.fontSize === undefined)
            result.fontSize = size;
        else if (size !== result.fontSize)
            result.fontSize = null;
                
        if (result.bold && parseInt(style.fontWeight) < boldThreshold)
            result.bold = false;
        
        // TODO: Use a threshold like WebKit
        if (result.italic && !style.fontStyle.match(/^(?:\bitalic\b|\boblique\b)/))
            result.italic = false;
            
        if (result.underline && !nodeHasUnderline(node))
            result.underline = false;

        if (result.strikethrough && !nodeHasStrikethrough(node))
            result.strikethrough = false;
        
        let align = style.textAlign;
        
        // This currently doesn't correctly handle elements whose directions don't match the body's
        if (!align || align === 'start')
            align = alignFromDirection;
            
        if (result.align !== align)
            result.align = null;
        
        if (node === endNode)
            break;
            
        node = walker.nextNode();
    }
    
    window.webkit.messageHandlers.textAttributes.postMessage(result);
}

function handlePaste(event) {
    // Get the html data - if not html, then returns empty string.
    var htmlData = event.clipboardData.getData('text/html');

    if (htmlData.length > 0) {
        return;
    }

    if (event.clipboardData.files.length > 0) {
        for (let file of event.clipboardData.files) {
            var type = file.type.split('/')[0];
            if (type == "image") {
                pastedFile("Pasted Image");
                event.preventDefault();
            }
        }
    }
}

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) {
    window.webkit.messageHandlers.pastedFile.postMessage({message: msg});
}

// --- 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');
        
    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');
        }
    }
    
    let lastImages = lastSignatureWrapper.querySelectorAll('img');
    
    for (let image of lastImages) {
        image.removeAttribute('class');
    }
    
    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 insertInlineImage(cid) {
    var img = document.createElement('img');
    img.setAttribute('src', 'cid:' + cid)
    img.setAttribute('class', 'dl-auto-scale');

    // Replace the current selection with the image
    const selection = window.getSelection();
    selection.deleteFromDocument();
    selection.getRangeAt(0).insertNode(img);
    selection.collapseToEnd();
    updateContent();
}

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