var HL = HL || {};

/** TODO
 * touch events
 * responsiveness - pens etc.
 * keyboard control - shortcut selecting pens; up/down (w/s) line, use closest bbox left?
 */

/** DEV
 * simulate cursor - CSS animation with pseudo after on focus?
 */

HL.activity = (function(utils) {
    var Pen = function Pen(cfg) {
        Pen.index = ++Pen.index || 0;

        this.el = null;
        this.index = Pen.index;
        this.colour = '';
        this.icon = document.createElement('img');
        this.label = {
            el: document.createElement('span'),
            text: ''
        };
        this.pressed = false;

        OU.utils.setCfg(this, cfg);
        //OU.utils.addClass(this.el, 'pen btn-no-style');

        this.el = OU.buttons.create({
            type: 'pen-' + this.colour,
            label: this.colour + ' highlighter pen',
            classes: ['pen btn-no-style'],
            //html: '<img alt="' + this.colour + ' highlighter pen" src="img/' + this.colour + '1.jpg" />',
            toggle: true
        });

        this.icon.alt = this.colour + ' highlighter pen';
        this.icon.src = OU.utils.rootPath + 'img/' + this.colour + '-pen.svg';
        this.el.appendChild(this.icon);

        //if (this.label.text) {
            this.setLabel(this.label.text);
            this.el.appendChild(this.label.el);
        //}

        this.el.setAttribute('data-pen-colour', this.colour);
    };

    Pen.prototype = {
        select: function() {
            this.el.setAttribute('aria-pressed', 'true');
            OU.utils.addClass(this.el, 'pen-selected');
        },

        deselect: function() {
            this.el.setAttribute('aria-pressed', 'false');
            OU.utils.removeClass(this.el, 'pen-selected');
        },

        setLabel: function(str) {
            this.label.el.textContent = str;
            this.label.text = str;
        },

        getLabel: function() {
            return this.label.el.textContent;
        }
    };

    var HighLighter = function(parent) {
        HighLighter.index = ++HighLighter.index || 0;

        // TODO remove nested objects and set default cfg as prototype 
        // const cfg = Object.create({}, this.defaultCfg);
        this.cfg = {
            type: '',
            lang: 'en',
            pens: {},
            dragging: {
                enabled: true,
                selection: true
            },
            spaces: {
                selectable: true,
                mark: false
            },
            punctuation: {
                selectable: true,
                mark: false
            },
            splitWord: false,
            background: true, // TODO this should default to false but will break back compatibility
            content: '',
            answer: {
                overlay: false
            },
            images: {
                dir: 'img/',
                lang: '',
                placeholder: '[[image]]', //TODO set this up - inserted in order listed in files, distractors first?
                files: [],
                alt: []
            },
            css: null,
            buttons: {
                enabled: ['reveal', 'reset']
            },
            ready: null
        };

        this.colours = {
            yellow: '#',
            green: '#',
            pink: '#',
            blue: '#',
            orange: '#',
            red: '#',
            aqua: '#',
            purple: '#',
            slate: '#',
            brown: '#'
        };

        this.id = 'hl-' + HighLighter.index;
        this.enabled = true;
        this.pens = [];
        this.action = {
            mouse: {
                inProgress: false
            },
            touch: {
                inProgress: false,
                last: null
            },
            drag: {
                inProgress: false,
                apply: false,
                start: null, //first highlighted in drag (mousedown)
                first: null, //first in highlighted block
                last: null //last in highlighted block
            },
            key: {
                inProgress: false
            },
            auto: {
                //apply: true
            },
            apply: true //apply/remove highlight
        };
        this.history = [];
        this.loader = OU.loader.construct();
        this.processes = ['load'];
        this.data = null;
        this.loaded = false;

        // dom
        this.parent = parent || document.body;
        this.container = OU.utils.createElement('div', 'highlight-activity no-select', this.id);
        this.content = OU.utils.createElement('div', 'hl-content');
        this.question = OU.utils.createElement('div', 'hl-question');
        this.answer = OU.utils.createElement('div', 'hl-answer');
        this.spans = [];
        this.buttons = null;
    };

    HighLighter.prototype = {

        /**
         * load images and saved data if save button present
         */
        load: function (data) {
            var height = window.innerHeight;
            var rootPath = OU.utils.rootPath;
            var css = VLE.serverversion ? ['css/style.min.css'] : [
                rootPath + 'css/general.css',
                rootPath + 'css/demo.css',
                rootPath + 'css/main.css',
                rootPath + 'css/hl-main.css'
            ];
            // TODO remove all this hacky back compatibility code in next version (aka complete rewrite)...
            var images = (typeof data.images === 'object') ? (Array.isArray(data.images) ? data.images : data.images.files) : [];
            var colours = Object.keys(this.colours);

            console.log(data);

            // needs to be in DOM prior to updateContent (I think, why?)
            if (!this.parent.contains(this.container)) {
                this.parent.appendChild(this.container);
            }

            // TODO split this? I'm not sure the data file should be loaded here...
            // move to activity level? how to handle pen images etc. - should be usuable outside of activity
            this.loader.load({
                container: this.container,
                // dir: 'img/',
                dir: {
                    // TODO this needs looking at...
                    image:  OU.utils.rootPath + (data.images ? data.images.dir || 'img/' : 'img/')
                },
                load: {
                    css: css,
                    file: data.file,
                    folder: data.folder,
                    // TODO use single SVG loaded as file, clone object and change class for each colour
                    // in the meantime, load all pens here (may be loading the data file so don't know which pens are in use yet)
                    // image: Object.keys(data.pens).reduce(function(prev, curr) {
                    image: colours.reduce(function(prev, curr) {
                        return prev.concat([curr + '-pen.svg']);
                    }, ['undo.svg']).concat(images)
                },
                callback: function (result) {
                    console.log(result);

                    if (Array.isArray(result.file)) {
                        data = result.file[0];
                    }
                    if (result.image.length > colours.length + 1) {
                        // when loaded from a folder images will be pushed to the end so 
                        // mirror order above and correct it here...
                        data.images.files = result.image.slice(colours.length + 1);

                        if (!VLE.serverversion) {
                            // quick workaround to allow images to be loaded from local JSON file
                            data.images.dir = data.images.dir || 'img/';
                        }
                    }
                    this.setCfg(data);
                    this.loadData();
                    this.dequeue('load');
                }.bind(this)
            });

            // make sure spinner can be seen
            if (height === 0) {
                OU.utils.resizeIFrame();
            }
        },

        loadData: function() {
            if (this.cfg.buttons.enabled.indexOf('save') === -1) return;

            console.log('retreive data................');

            this.processes.push('data');

            OU.saver.retrieve({
                local: true,
                names: this.getSaveObject(),
                callback: function (data) {
                    console.log(data.attempt);
                    if (data.attempt) {
                        this.data = JSON.parse(data.attempt);
                    }
                    this.dequeue('data');
                }.bind(this)
            });
        },

        /**
         * tracks loading state and calls init when loaded
         * @param process - process to be dequeued
         */
        dequeue: function (process) {
            var index = this.processes.indexOf(process);

            if (index > -1) {
                this.processes.splice(index, 1);
            }

            if (this.processes.length === 0) {
                this.init();
            }
        },

        setCfg: function (cfg) {
            console.log(cfg);

            // back compatibility
            if (Array.isArray(cfg.images)) {
                cfg.images = {
                    files: cfg.images
                };
            }

            OU.utils.extendDeep(this.cfg, cfg);
        },

        init: function () {
            this.addCSS();
            this.createPens();
            this.initButtons();
            this.initContent();
            this.updateContent();
            this.setEventHandlers();
            this.selectPen();
            this.populate();
            this.initResize();
            this.ready();
        },

        addCSS: function () {
            var css = this.cfg.css;
            var el, ss;
            console.log(this.cfg);

            if (this.cfg.background === true) {
                OU.utils.addClass(this.container, 'apply-background');
            }

            if (typeof css !== 'object' || Array.isArray(css)) {
                return;
            }

            el = document.head.appendChild(document.createElement('style'));
            // ss = document.head.styleSheets[document.head.styleSheets.length - 1];
            ss = el.sheet;

            OU.utils.forIn(css, function (rules, selector) {
                rules = Array.isArray(rules) ? rules : [rules];

                rules.forEach(function (rule) {
                    ss.insertRule('#' + this.id + ' ' + selector + '{' + rule + '}', ss.cssRules.length);
                }.bind(this));
            }.bind(this));
        },

        createPens: function() {
            var pen;

            this.pens.group = OU.utils.createElement('div', 'pen-group hl-controls');

            OU.utils.forIn(this.cfg.pens, function(data, key) {
                pen = new Pen({
                    colour: key,
                    label: {
                        text: data.label
                    }
                });

                this.pens.push(pen);
                this.pens[key] = pen;
                this.pens.group.insertBefore(pen.el, this.pens.group.children[data.index]);
            }.bind(this));

            this.pens.undo = this.pens.group.insertBefore(OU.buttons.create({
                type: 'undo',
                label: 'undo',
                html: '<img src="' + OU.utils.rootPath +'img/undo.svg" alt="undo icon" />',
                classes: ['undo btn-no-style']
            }), this.pens.group.firstChild);

            this.container.appendChild(this.pens.group);
        },

        /**
         * handle creation/initialisation of the main content, including question and answer
         */
        initContent: function () {
            this.cfg.content = this.replaceImagePlaceholders(this.cfg.content);
            this.content.lang = this.cfg.lang;
            this.content.setAttribute('role', 'application');
            this.content.setAttribute('aria-live', 'polite');
            this.content.insertBefore(this.question, this.buttons.group);
            this.container.appendChild(this.content);
        },

        replaceImagePlaceholders: function (str) {
            console.log(this.cfg.images);
            var re = new RegExp(OU.utils.escapeRegExp(this.cfg.images.placeholder), 'g'),
                src, alt;

            return str.replace(re, function () {
                this.cfg.images.applied = ++this.cfg.images.applied || 0;

                src =  this.cfg.images.dir + this.cfg.images.files[this.cfg.images.applied].src;
                alt = this.cfg.images.alt[this.cfg.images.applied] || '';

                console.log(this.cfg.images.files);

                return '<img src="' + OU.utils.rootPath + src + '" alt="' + alt + '" lang="' + this.cfg.images.lang + '" />';
            }.bind(this));
        },

        /**
         * update content byg calling necessary functions
         */
        updateContent: function () {
            this.setContent();
            this.processContent();
            this.setSpanData();
            this.initAnswer();
            this.addARIA(this.answer);
            this.initSpans();
        },

        /**
         * set content of this.question from this.cfg.content
         */
        setContent: function() {
            this.question.textContent = '';
            this.question.insertAdjacentHTML('beforeend', this.cfg.content);
        },

        /**
         * span content as per this.cfg.type (character, word etc.)
         */
        processContent: function() {
            if (utils.process[this.cfg.type]) {
                utils.process.run(this.question, this);
            }
        },

        /**
         * create answer by cloning the question (highlights visible until initSpans),
         * updating class and inserting/appending to DOM depending on answer.overlay.
         */
        initAnswer: function() {
            this.answer = this.question.cloneNode(true);
            this.answer.className = 'hl-answer';

            if (this.cfg.answer.overlay) {
                this.content.insertBefore(this.answer, this.buttons.group);
            }
            else {
                this.content.appendChild(this.answer);
            }
        },

        /**
         * set ARIA labels at the start and end of each highlighted block
         * @param {Element} el - element to be ARIA'd
         */
        addARIA: function(el) {
            var blocks = utils.getBlocks(this.spans),
                answer = (el == this.answer),
                spans, first, last;

            if (answer) {
                spans = OU.utils.toArray(el.querySelectorAll('span[data-hl]'));
            }

            OU.utils.forIn(blocks, function(array, colour) {
                array.forEach(function(block) {
                    first = (answer) ? spans[block[0].index] : block[0].el;
                    last = (answer) ? spans[block[block.length - 1].index] : block[block.length - 1].el;
                    first.setAttribute('aria-label', colour + ' highlight begin');
                    last.insertAdjacentHTML('afterEnd', '<span aria-label="' + colour + ' highlight end"></span>');
                });
            });
        },

        /**
         * pull the relevant data for each span from the dom and store in this.spans
         */
        setSpanData: function() {
            var colour;

            this.spans = OU.utils.toArray(this.question.querySelectorAll('span[data-hl]')).map(function(el, i) {
                colour = this.getHighlight(el);

                return {
                    el: el,
                    index: i,
                    colour: colour,
                    answer: colour,
                    space: OU.utils.hasClass(el, 'space'),
                    punctuation: OU.utils.hasClass(el, 'punctuation'),
                    history: [],
                    apply: true
                };
            }.bind(this));
        },

        /**
         * ready the spans for the activity by setting necessary attributes
         * and removing any colour
         */
        initSpans: function() {
            this.spans.forEach(function(data) {
                data.el.setAttribute('data-hl', data.index);
                data.el.tabIndex = 0;

                if (data.colour) {
                    this.removeHighlight(data);
                }
            }.bind(this));
        },

        /**
         * create buttons (reveal, reset etc.)
         */
        initButtons: function() {
            if (!this.cfg.answer.overlay) {
                OU.utils.extendDeep(this.cfg.buttons, {
                    data: {
                        reveal: {
                            html: 'Reveal answer'
                        }
                    }
                });
            }

            this.buttons = OU.buttons.construct(this.cfg.buttons);

            OU.utils.forIn(this.buttons.buttons, function(btn) {
                OU.utils.addClass(btn.el, 'osep');
            });

            this.content.appendChild(this.buttons.group);
        },

        initResize: function() {
            var width = window.innerWidth;

            window.top.addEventListener('resize', function () {
                if (window.innerWidth !== width) {
                    OU.utils.resizeIFrame();
                    width = window.innerWidth;
                }
            }.bind(this));
        },

        populate: function () {
            if (this.data) {
                this.spans.forEach(function (data, i) {
                    if (this.data[i] && data.colour != this.data[i]) {
                        this.applyHighlight(data, this.data[i]);
                    }
                }.bind(this));
            }
        },

        ready: function () {
            this.loaded = true;
            this.container.style.visibility = 'visible';
            OU.utils.triggerCustomEvent(this.container, 'activity-loaded');
            OU.utils.resizeIFrame();
        },

        /**
         * return highlight colour of el
         * @param {Element} el
         * @returns {string}
         */
        getHighlight: function(el) {
            var colour = null;

            if (el.nodeType == 1) {
                this.pens.some(function(pen) {
                    if (OU.utils.hasClass(el, pen.colour)) {
                        colour = pen.colour;
                        return true;
                    }
                }.bind(this));
            }

            return colour;
        },

        startHighlight: function(e, data) {
            this.action.apply = (!data.colour || this.pens.selected && data.colour != this.pens.selected.colour);

            switch (e.type) {
                case 'mousedown':
                    this.action.mouse.inProgress = true;

                    if (!this.action.apply && e.shiftKey) { //if false check shift key
                        this.action.apply = true;
                    }

                    if (this.cfg.dragging.enabled) {
                        this.startDragHighlight(data);
                    }
                    break;
                case 'keydown':
                    this.action.key.inProgress = true;
                    break;
                case 'touchstart':
                    this.action.touch.inProgress = true;

                    if (this.cfg.dragging.enabled) {
                        this.startDragHighlight(data);
                    }
            }

            this.addHistory();
        },

        stopHighlight: function(e) {
            switch (e.type) {
                case 'mouseup':
                    this.action.mouse.inProgress = false;

                    if (this.cfg.dragging.enabled) {
                        this.stopDragHighlight();
                    }
                    break;
                case 'keyup':
                    this.action.key.inProgress = false;
                    break;
                case 'touchend':
                    this.action.touch.inProgress = false;

                    if (this.cfg.dragging.enabled) {
                        this.stopDragHighlight();
                    }
            }

            //delete if nothing highlighted
            if (this.history.current && this.history.current.length === 0) {
                this.deleteHistory();
            }

            this.trigger('highlight-' + (this.action.apply ? 'applied' : 'removed'), {
                data: this.history.current,
                apply: this.action.apply
            });

            console.log(this.history.current);
        },

        startDragHighlight: function(data) {
            this.action.drag.apply = this.action.apply; //store this as handleAutoHighlight toggles this.action.apply
            this.action.drag.inProgress = true;
            this.action.drag.first = data;
            this.action.drag.start = data;
            this.action.drag.last = data;
            this.setDragEventHandlers();
        },

        stopDragHighlight: function() {
            this.action.apply = this.action.drag.apply; //reapply correct state

            if (this.action.drag.last) {
                this.action.drag.last.el.focus();
            }

            this.action.drag.inProgress = false;
            this.action.drag.first = null;
            this.action.drag.start = null;
            this.action.drag.last = null;
            this.removeDragEventHandlers();
        },

        /**
         *
         * @param data
         * @returns {boolean}
         */
        isHighlightable: function(data) {
            return data &&
                (!data.space || this.cfg.spaces.selectable) &&
                (!data.punctuation || this.cfg.punctuation.selectable);
        },

        /**
         * common highlight function
         * @param {object} data
         */
        handleHighlight: function(data) {
            if (!this.enabled || !this.isHighlightable(data)) {
                return;
            }

            var colour = this.pens.selected ? this.pens.selected.colour : null;

            //TODO add history at remove and add
            this.addHistory(data);
            this.removeHighlight(data);

            if (this.action.apply) {
                this.applyHighlight(data, colour);
            }

            data.apply = this.action.apply;

            this.trigger('highlight-updated', {
                target: data,
                apply: this.action.apply
            });
        },

        /**
         * auto apply/remove highlight for elements missed during drag
         * @param data - data of element that received mouseover
         */
        handleAutoHighlight: function(data) {
            var start = this.action.drag.start.index,
                first = this.action.drag.first.index,
                last = this.action.drag.last.index,
                before = this.spans.slice(first, start),
                after = this.spans.slice(start + 1, last + 1),
                apply = this.action.apply,
                array;

            switch (true) {
                case data.index > start: //forward
                    if (data.index > last) { //apply
                        if (before.length) {
                            this.action.apply = !this.action.drag.apply;
                            this.action.drag.first = this.action.drag.start;
                            this.bulkHighlight(before);
                        }

                        array = this.spans.slice(last + 1, data.index + 1);
                        this.action.apply = this.action.drag.apply;
                    }
                    else { //remove
                        array = this.spans.slice(data.index + 1, last + 1);
                        this.action.apply = !this.action.drag.apply;
                    }
                    this.action.drag.last = data;
                    break;
                case data.index < start: //backward
                    if (data.index < first) { //apply
                        if (after.length) {
                            this.action.apply = !this.action.drag.apply;
                            this.action.drag.last = this.action.drag.start;
                            this.bulkHighlight(after);
                        }

                        array = this.spans.slice(data.index, first);
                        this.action.apply = this.action.drag.apply;
                    }
                    else { //remove
                        array = this.spans.slice(first, data.index);
                        this.action.apply = !this.action.drag.apply;
                    }
                    this.action.drag.first = data;
                    break;
                default: //current el same as start
                    array = before.concat(after);
                    this.action.apply = !this.action.drag.apply;
                    this.action.drag.first = data;
                    this.action.drag.last = data;
            }

            this.bulkHighlight(array);
            this.action.apply = apply; //restore to initial value
        },

        bulkHighlight: function(array) {
            array.forEach(function(data) {
                this.handleHighlight(data);
            }.bind(this));
        },

        applyHighlight: function(data, colour) {
            if (colour) {
                OU.utils.addClass(data.el, colour);
                data.colour = colour;
            }
        },

        removeHighlight: function(data) {
            if (data.colour) {
                OU.utils.removeClass(data.el, data.colour);
                data.colour = null;
            }
        },

        highlightNext: function(data) {
            var next = this.getNextSpan(data);

            if (next) {
                this.handleHighlight(next);
                next.el.focus();
            }
        },

        highlightPrev: function(data) {
            var prev = this.getPrevSpan(data);

            if (prev) {
                this.handleHighlight(prev);
                prev.el.focus();
            }
        },

        undoHighlight: function() {
            if (!this.history.current) {
                return;
            }

            var colour;

            this.history.current.reverse().forEach(function(data) {
                colour = data.history[data.history.length - 1];

                if (data.colour) {
                    this.removeHighlight(data);
                }

                if (colour) {
                    this.applyHighlight(data, colour);
                }
            }.bind(this));

            this.trigger('highlight-undone', {data: this.history.current});
            this.deleteHistory();
        },

        /**
         * add el to current history or create new if omitted
         * @param {Element} [data] - an element to be added to current history
         * @param {string} [data.colour]
         * @param {string} [data.history]
         */
        addHistory: function(data) {
            var index;

            if (data) {
                index = this.history.current.indexOf(data);

                if (index > -1) {
                    if (data.history[data.history.length - 1] === null) {
                        this.history.current.splice(index, 1);
                        data.history.pop();
                    }
                }
                else {
                    data.history.push(data.colour);
                    this.history.current.push(data);
                }
            }
            else {
                this.history.push([]);
                this.setCurrentHistory();
                this.history.current.apply = this.action.apply;
            }
        },

        deleteHistory: function() {
            if (this.history.length) {
                this.history.pop().forEach(function(data) {
                    data.history.pop();
                });
            }

            this.setCurrentHistory();
        },

        setCurrentHistory: function() {
            this.history.current = this.history[this.history.length - 1] || null;
        },

        getSpanData: function(el) {
            return el && el.getAttribute('data-hl') && this.spans[el.getAttribute('data-hl')];
        },

        getNextSpan: function(data) {
            return this.spans[data.index + 1];
        },

        getPrevSpan: function(data) {
            return this.spans[data.index - 1];
        },

        selectPen: function(selected) {
            selected = selected || this.pens[0];

            var detail = {
                prev: this.pens.selected,
                current: selected
            };

            this.pens.selected = selected;

            this.pens.forEach(function(pen) {
                if (pen == selected) {
                    pen.select();
                }
                else {
                    pen.deselect();
                }
            }.bind(this));

            OU.utils.triggerCustomEvent(this.container, 'pen-selected', detail);
        },

        getPensInUse: function() {
            return this.pens.reduce(function(prev, pen) {
                if (this.content.querySelector('.' + pen.colour)) {
                    prev.push(pen);
                }
                return prev;
            }.bind(this), []);
        },

        reveal: function(reveal) {
            console.log(reveal);

            if (reveal) {
                this.answer.style.display = 'block';

                if (this.cfg.answer.overlay) {
                    this.question.style.display = 'none';
                }
                else {
                    this.disable();
                    VLE.resize_iframe();
                }
            }
            else {
                this.answer.style.display = 'none';

                if (this.cfg.answer.overlay) {
                    this.question.style.display = 'block';
                }
                else {
                    this.enable();
                    VLE.resize_iframe();
                }
            }
        },

        save: function() {
            if (!this.enabled) {
                return;
            }

            OU.saver.save({
                local: true,
                values: this.getSaveObject(),
                callback: function() {

                }
            });
        },

        getSaveObject: function() {
            return {
                attempt: JSON.stringify(this.spans.map(function(data) {
                    return data.colour || 0;
                }))
            };
        },

        disable: function() {
            this.enabled = !this.enabled;
        },

        enable: function() {
            this.enabled = !this.enabled;
        },

        reset: function() {
            while (this.history.length) {
                this.undoHighlight();
            }

            //reset loaded data where no history
            if (this.data) {
                this.spans.forEach(function(data) {
                    this.removeHighlight(data);
                }.bind(this));

                this.data = null; //good idea?
            }

            if (this.answer.style.display === 'block') {
                this.reveal();
            }
        },

        setEventHandlers: function() {
            //buttons
            this.container.addEventListener('click', this);
            this.container.addEventListener('reveal', this);
            this.container.addEventListener('reset', this);
            this.container.addEventListener('save', this);

            //start highlight
            this.container.addEventListener('keydown', this);
            this.question.addEventListener('mousedown', this);
            this.question.addEventListener('touchstart', this);

            //end highlight
            this.container.addEventListener('keyup', this);
            window.addEventListener('mouseup', this);
            window.addEventListener('touchend', this);
            window.top.addEventListener('mouseup', this);
            window.top.addEventListener('touchend', this);
        },

        setDragEventHandlers: function() {
            this.question.addEventListener('mouseover', this);
            this.question.addEventListener('touchmove', this);
        },

        removeDragEventHandlers: function() {
            this.question.removeEventListener('mouseover', this);
            this.question.removeEventListener('touchmove', this);
        },

        getSpanFromTarget: function (target) {
            while (target) {
                if (utils.isHighlightable(target)) {
                    break;
                }
                target = target.parentNode;
            }

            return target;
        },

        handleEvent: function(e) {
            var target = e.target,
                data;

            if (['mousedown', 'mouseover', 'touchstart', 'touchmove'].indexOf(e.type) > -1) {
                if (e.type == 'touchmove') {
                    target = document.elementFromPoint(e.targetTouches[0].clientX, e.targetTouches[0].clientY); //TODO check support for clientX/Y

                    if (target == this.action.touch.last) {
                        return;
                    }

                    this.action.touch.last = target;
                }

                target = this.getSpanFromTarget(target);
                data = this.getSpanData(target);

                if (!data) {
                    return;
                }
            }

            if (['touchstart', 'touchmove'].indexOf(e.type) > -1) {
                e.preventDefault(); //prevents mousemove from being triggered
            }

            switch (e.type) {
                case 'mousedown':
                case 'touchstart':
                    this.startHighlight(e, data);
                    this.handleHighlight(data);
                    break;
                case 'mouseover':
                case 'touchmove':
                    e.preventDefault();

                    if (this.action.drag.inProgress) {
                        if (this.cfg.dragging.selection) {
                            this.handleAutoHighlight(data);
                        }
                        else {
                            this.handleHighlight(data);
                        }
                    }
                    break;
                case 'mouseup':
                    if (this.action.mouse.inProgress) {
                        this.stopHighlight(e);
                    }
                    break;
                case 'touchend':
                    if (this.action.touch.inProgress) {
                        this.stopHighlight(e);
                    }
                    break;
                case 'click':
                    this.handleClickEvent(e);
                    break;
                case 'reveal':
                    this.reveal(this.cfg.answer.overlay ? e.detail.pressed : true);
                    break;
                case 'reset':
                    this.reset();
                    break;
                case 'save':
                    this.save();
                    break;
                case 'keydown':
                    this.handleKeyDownEvent(e);
                    break;
                case 'keyup':
                    this.handleKeyUpEvent(e);
            }
        },

        handleClickEvent: function(e) {
            var target = e.target;

            while (target != this.container) {
                if (OU.utils.hasClass(target, 'pen')) {
                    this.selectPen(this.pens[target.getAttribute('data-pen-colour')]);
                    return;
                }
                if (OU.utils.hasClass(target, 'undo')) {
                    this.undoHighlight();
                    return;
                }

                target = target.parentNode;
            }
        },

        handleKeyDownEvent: function(e) {
            var data;

            if ([13, 32, 65, 68].indexOf(e.keyCode) > -1) {
                if (!utils.isHighlightable(e.target)) {
                    return;
                }

                e.preventDefault();
                data = this.getSpanData(e.target);

                if (!this.action.key.inProgress) {
                    this.startHighlight(e, data);
                }

                this.handleHighlight(data);
            }

            switch (e.keyCode) {
                case 65: //a
                    this.highlightPrev(data);
                    break;
                case 68: //d
                    this.highlightNext(data);
                    break;
                case 90:
                    if (e.ctrlKey) {
                        this.undoHighlight();
                    }
            }
        },

        handleKeyUpEvent: function(e) {
            switch (e.which || e.keyCode) {
                case 13:
                case 32:
                case 65: //a
                case 68: //d
                    this.stopHighlight(e);
            }
        },

        trigger: function(type, detail, bubbles) {
            OU.utils.triggerCustomEvent(this.container, type, detail, (bubbles !== false));
        }
    };

    var construct = function(cfg) {
        return new HighLighter(cfg);
    };

    return {
        construct: construct
    };
})(HL.utils);