'use strict'

const Settings = require('./settings').Settings
    , Transliterator = require('./translit').Transliterator
    , browserapi = require('./browserapi')


class AwesomeTimer {
    constructor(callback) {
        this.callback = callback
    }

    handler() {
        this.clear()
        this.callback()
    }

    asap() {
        if (!this.tid) {
            this.tid = requestAnimationFrame(() => { this.handler() })
        }
    }

    clear() {
        cancelAnimationFrame(this.tid)
        this.tid = undefined
    }
}


class DomObserver {

    constructor(callback) {
        this.callback = callback || (() => {})
        this._delayed = true
        this.includeMatching = /[абвгґдеєжзиіїйклмнопрстуфхцчшщьюя]/i
        this.excludeTags = new Set(['head', 'link', 'meta', 'script', 'style'])
        this.excludeClass = 'notranslit'
        this.changedNodes = new Set()
        this.visited = new Set()
        this.observers = []
        this.timer = new AwesomeTimer(() => { this.processChanges() })
    }

    get delayed() {
        return this._delayed
    }

    set delayed(value) {
        this._delayed = value
        if (!value && this.changedNodes.size) {
            this.timer.asap()
        }
    }

    processChanges() {
        const changes = this.changedNodes
        this.changedNodes = new Set()

        const filtered = new Set()

        function has_class_or_ancestor(el, classname) {
            if (!classname) {
                return false
            }
            for (; el && el.nodeType !== Node.DOCUMENT_NODE; el = el.parentElement) {
                if (el.nodeType === Node.ELEMENT_NODE && el.classList.contains(classname)) {
                    return true
                }
            }
            return false
        }

        changes.forEach((node) => {
            if (node.nodeType === Node.TEXT_NODE) {
                if (!node.parentElement.isContentEditable &&
                    !has_class_or_ancestor(node, this.excludeClass) &&
                    this.includeMatching.test(node.data)) {

                    filtered.add(node)
                }
            }
            else if (node.nodeType === Node.ELEMENT_NODE &&
                !node.isContentEditable &&
                !this.excludeTags.has(node.localName) &&
                !has_class_or_ancestor(node, this.excludeClass)) {

                const it = document.createNodeIterator(node,
                    NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT,
                    (node) => {
                        if (has_class_or_ancestor(node, this.excludeClass)) {
                            return NodeFilter.FILTER_REJECT
                        }

                        return (node.nodeType === Node.TEXT_NODE) ?
                            (this.includeMatching.test(node.data) ?
                            NodeFilter.FILTER_ACCEPT :
                            NodeFilter.FILTER_REJECT)
                            :
                            (this.excludeTags.has(node.localName) ?
                            NodeFilter.FILTER_REJECT :
                            NodeFilter.FILTER_SKIP)
                    })

                let child;
                while (child = it.nextNode()) {
                    filtered.add(child)
                }
            }
        })

        this.visited = filtered
        this.callback(filtered)
    }

    add(node) {
        if (!this.visited.has(node)) {
            this.changedNodes.add(node)
        }
    }

    observe(node) {
        const handleMutations = (records) => {
            records.forEach((record) => {
                this.add(record.target)
            })

            if (!this.delayed) {
                this.timer.asap()
            }
        }

        const observer = new MutationObserver(handleMutations)
        this.observers.push(observer)

        observer.observe(node, {
            subtree: true,
            childList: true,
            characterData: true,
        })
    }

    disconnect() {
        this.observers.forEach((observer) => {
            observer.disconnect()
        })
        this.observers = []
    }
}


class Controller {
    constructor() {
        this.settings = new Settings(browserapi.storage, () => { this._check_enabled(false) })
        this._check_enabled(true)
    }

    _check_enabled(delayed) {
        const url = document.URL
        if (this.settings.enabled_for_url(url)) {
            const table = this.settings.selected_translit_table || {rules: {}}
            this.start(delayed, table.rules)
        }
        else {
            this.stop()
        }
    }

    start(delayed, rules) {
        if (!this.observer) {
            this.observer = new DomObserver()
            this.observer.observe(document.documentElement)
        }

        if (!delayed) {
            const translit = new Transliterator(rules)
            this.observer.callback = (nodes) => translit.processTextNodes(nodes)
        }

        this.observer.delayed = delayed
    }

    stop() {
        if (this.observer) {
            this.observer.disconnect()
            this.observer = undefined
        }
    }
}


const ctl = new Controller()

@@ -1364,32 +1364,108 @@
 ered = new Set()
+%0A        const excludeMemo = new Set()%0A        const includeMemo = new Set()
 %0A%0A        functi
@@ -1489,18 +1489,21 @@
 ncestor(
-el
+child
 , classn
@@ -1574,32 +1574,33 @@
 e%0A            %7D%0A
+%0A
             for 
@@ -1600,16 +1600,30 @@
    for (
+let el = child
 ; el && 
@@ -1679,24 +1679,299 @@
 tElement) %7B%0A
+                if (excludeMemo.has(el)) %7B%0A                    excludeMemo.add(child)%0A                    return false%0A                %7D%0A%0A                if (includeMemo.has(el)) %7B%0A                    includeMemo.add(child)%0A                    return true%0A                %7D%0A%0A
             
@@ -2011,20 +2011,44 @@
 ENT_NODE
- && 
+) %7B%0A                    if (
 el.class
@@ -2071,24 +2071,119 @@
 assname)) %7B%0A
+                        includeMemo.add(el)%0A                        includeMemo.add(child)%0A    
             
@@ -2210,34 +2210,41 @@
                 
-%7D%0A
+    %7D%0A   
             %7D%0A  
@@ -2231,33 +2231,84 @@
 %0A               
-%7D
+ %7D%0A            %7D%0A%0A            excludeMemo.add(child)
 %0A            ret

improve perf