/**
 * Created by Samuel Gratzl on 15.10.2015.
 */
import * as C from 'phovea_core/src/index';
import * as cmode from './mode';
import * as d3 from 'd3';
import * as marked from 'marked';
import { defaultSelectionType } from 'phovea_core/src/idtype';
import * as player from './player';
import { resolveImmediately } from 'phovea_core/src';
var modeFeatures = {
    isEditable: function () { return cmode.getMode().authoring > 0.8; }
};
/**
 * place where the annotation is attached to an anchor
 */
var EAnchorDirection;
(function (EAnchorDirection) {
    EAnchorDirection[EAnchorDirection["EAST"] = 0] = "EAST";
    EAnchorDirection[EAnchorDirection["NORTH"] = 1] = "NORTH";
    EAnchorDirection[EAnchorDirection["WEST"] = 2] = "WEST";
    EAnchorDirection[EAnchorDirection["SOUTH"] = 3] = "SOUTH";
    EAnchorDirection[EAnchorDirection["NORTH_EAST"] = 4] = "NORTH_EAST";
    EAnchorDirection[EAnchorDirection["NORTH_WEST"] = 5] = "NORTH_WEST";
    EAnchorDirection[EAnchorDirection["SOUTH_EAST"] = 6] = "SOUTH_EAST";
    EAnchorDirection[EAnchorDirection["SOUTH_WEST"] = 7] = "SOUTH_WEST";
    EAnchorDirection[EAnchorDirection["CENTER"] = 8] = "CENTER";
})(EAnchorDirection || (EAnchorDirection = {}));
var anchor2string = (function () {
    var r = [];
    r[EAnchorDirection.EAST] = 'e';
    r[EAnchorDirection.NORTH] = 'n';
    r[EAnchorDirection.WEST] = 'w';
    r[EAnchorDirection.SOUTH] = 's';
    r[EAnchorDirection.NORTH_EAST] = 'ne';
    r[EAnchorDirection.NORTH_WEST] = 'nw';
    r[EAnchorDirection.SOUTH_EAST] = 'se';
    r[EAnchorDirection.SOUTH_WEST] = 'sw';
    r[EAnchorDirection.CENTER] = 'c';
    return r;
})();
var string2anchor = {
    e: EAnchorDirection.EAST,
    n: EAnchorDirection.NORTH,
    w: EAnchorDirection.WEST,
    s: EAnchorDirection.SOUTH,
    ne: EAnchorDirection.NORTH_EAST,
    nw: EAnchorDirection.NORTH_WEST,
    se: EAnchorDirection.SOUTH_EAST,
    sw: EAnchorDirection.SOUTH_WEST,
    c: EAnchorDirection.CENTER
};
/**
 * An anchor is a flexible position at a DOM element
 */
var Anchor = (function () {
    function Anchor(elem, anchor, lazy) {
        if (lazy === void 0) { lazy = false; }
        this.elem = elem;
        this.anchor = anchor;
        /**
         * position of the anchor
         * @type {null}
         * @private
         */
        this._pos = null;
        if (!lazy) {
            this._pos = this.compute();
        }
    }
    /**
     * returns the square distance of this anchor point to the given point
     * @param pos the current point
     * @returns {number} the square distance
     */
    Anchor.prototype.distance = function (pos) {
        var p = this.pos;
        var dx = pos[0] - p[0];
        var dy = pos[1] - p[1];
        return dx * dx + dy * dy;
    };
    Object.defineProperty(Anchor.prototype, "pos", {
        get: function () {
            return this._pos !== null ? this._pos : this.compute();
        },
        enumerable: true,
        configurable: true
    });
    /**
     * checks wether the anchor position has changed and updates its position accordingly
     * @returns {boolean} true if there was a change
     */
    Anchor.prototype.checkForPositionChange = function () {
        var old = this.pos;
        var newValue = this._pos = this.compute();
        return Math.abs(old[0] - newValue[0]) > 1 || Math.abs(old[1] - newValue[1]) > 1;
    };
    Anchor.prototype.compute = function () {
        //start with the bounds
        var o = C.bounds(this.elem);
        //add offset
        o.x += window.pageXOffset;
        o.y += window.pageYOffset;
        //select direction
        switch (this.anchor) {
            case EAnchorDirection.EAST:
                return [o.x, o.y + o.h / 2 - 3];
            case EAnchorDirection.NORTH:
                return [o.x + o.w / 2 - 3, o.y];
            case EAnchorDirection.WEST:
                return [o.x + o.w - 5, o.y + o.h / 2 - 3];
            case EAnchorDirection.SOUTH:
                return [o.x + o.w / 2 - 3, o.y + o.h - 5];
            case EAnchorDirection.NORTH_EAST:
                return [o.x, o.y];
            case EAnchorDirection.NORTH_WEST:
                return [o.x + o.w - 6, o.y];
            case EAnchorDirection.SOUTH_EAST:
                return [o.x, o.y + o.h - 5];
            case EAnchorDirection.SOUTH_WEST:
                return [o.x + o.w - 5, o.y + o.h - 5];
            default:
                return [o.x + o.w / 2 - 3, o.y + o.h / 2 - 3];
        }
    };
    /**
     * returns the ids identifying this anchor based on the data-anchor DOM attribute
     * @returns {string}
     */
    Anchor.prototype.toString = function () {
        return this.elem.getAttribute('data-anchor') + '@' + anchor2string[this.anchor];
    };
    /**
     * converts the given encoded anchor string to its DOM element
     * @param s
     * @returns {Anchor}
     */
    Anchor.fromString = function (s, lazy) {
        if (lazy === void 0) { lazy = true; }
        var parts = s.split('@');
        var elem = document.querySelector('*[data-anchor="' + parts[0] + '"]');
        var anchor = string2anchor[parts[1]];
        return new Anchor(elem, anchor, lazy);
    };
    return Anchor;
}());
var AnchorWatcher = (function () {
    function AnchorWatcher() {
        this.anchors = [];
        this.intervall = -1;
    }
    AnchorWatcher.prototype.add = function (anchor, callback) {
        this.anchors.push({ anchor: Anchor.fromString(anchor, false), callback: callback });
        if (this.intervall < 0) {
            this.watch();
        }
    };
    AnchorWatcher.prototype.check = function () {
        this.anchors.forEach(function (entry) {
            if (entry.anchor.checkForPositionChange()) {
                entry.callback();
            }
        });
    };
    AnchorWatcher.prototype.watch = function () {
        this.intervall = setInterval(this.check.bind(this), AnchorWatcher.UPDATE_INTERVALL);
    };
    AnchorWatcher.prototype.clear = function () {
        if (this.intervall >= 0) {
            clearInterval(this.intervall);
            this.intervall = -1;
        }
        this.anchors = [];
    };
    return AnchorWatcher;
}());
AnchorWatcher.UPDATE_INTERVALL = 100;
var Renderer = (function () {
    function Renderer($main, graph, options) {
        if (options === void 0) { options = {}; }
        var _this = this;
        this.$main = $main;
        this.graph = graph;
        this.options = {
            /**
             * should animations for the annotations being used
             */
            animation: true,
            /**
             * animation duration
             */
            duration: 100,
            /**
             * should text annotations being rendered as markdown
             */
            markdown: true,
            /**
             * render a subtitle annotation containing the predefined content
             */
            renderSubtitle: false,
            /**
             * subtitle content
             */
            subtitlePattern: '${description}'
        };
        this.prev = resolveImmediately(null);
        this.l = function (event, state, type, op, extras) { return _this.render(state, extras.withTransition !== false); };
        this.updateAnnotations = function () { return _this.renderAnnotationsImpl(_this.act); };
        this.rerender = function () { return _this.render(_this.act, true, true); };
        this.act = null;
        this.renderer = this.rendererImpl.bind(this);
        this.anchorWatcher = new AnchorWatcher();
        C.mixin(this.options, options);
        //update during slide change
        this.graph.on('select_slide_' + defaultSelectionType, this.l);
        //and mode change
        cmode.on('modeChanged', this.rerender);
        C.onDOMNodeRemoved($main.node(), this.destroy.bind(this));
    }
    /**
     * renders the given text by replacing variables and rendering markdown, then return the HTML code to display
     *
     * variables: usage: `${variable_name}`
     *  * name ... current slide name
     *  * description ... current slide description
     *  * duration ... current slide duration
     *  * slide_number ... current slide Index
     *  * state_name ... if the slide is associated with a state: its name
     *  * state_notes ... if the slide is associated with a state: its notes
     *  * action_name ... if the state was created by some action, its name
     *  * action_category ... the category of the action (data, vis,...)
     *  * action_operation ... the operation (create, update, delete)
     *  * action_user ... the user performed the operation
     *  * action_ts .. the date when the action was executed
     *
     * @param d the text to render
     * @returns {String}
     */
    Renderer.prototype.rendererImpl = function (d) {
        if (modeFeatures.isEditable() && d.length === 0) {
            //return placeholder
            return "<i class=\"placeholder\">Enter Text by Clicking (MarkDown supported)</i>";
        }
        //replace variables within the text
        if (this.act) {
            //vars contains all possible variables
            var vars = {
                name: this.act.name,
                description: this.act.description,
                duration: this.act.duration,
                slide_number: this.act.slideIndex
            };
            var s = this.act.state;
            if (s) {
                vars.state_name = s.name;
                vars.state_notes = s.getAttr('notes');
                var a = s.creator;
                if (a) {
                    var aa = a.meta;
                    vars.action_name = aa.name;
                    vars.action_category = aa.category;
                    vars.action_operation = aa.operation;
                    vars.action_user = aa.user;
                    vars.action_ts = new Date(aa.timestamp);
                }
            }
            d = this.replaceVariables(d, vars);
        }
        return (this.options.markdown ? marked(d) : d);
    };
    Renderer.prototype.replaceVariables = function (d, vars) {
        return d.replace(/\$\{([^}]+)\}/gi, function (match, variable) {
            var r = vars[variable];
            if (r) {
                return r;
            }
            return '${' + variable + '}?';
        });
    };
    Renderer.prototype.destroy = function () {
        this.graph.off('select_slide_' + defaultSelectionType, this.l);
        cmode.off('modeChanged', this.rerender);
    };
    Renderer.prototype.render = function (state, withTransition, waitBetweenTakeDown) {
        var _this = this;
        if (withTransition === void 0) { withTransition = true; }
        if (waitBetweenTakeDown === void 0) { waitBetweenTakeDown = false; }
        if (this.act) {
            //disable annotation update listener
            this.act.off('push-annotations,attr-name,attr-duration', this.updateAnnotations);
        }
        //create full chain
        this.prev = this.prev.then(function () {
            //hide old annotations
            var takedown = _this.hideOld();
            _this.act = state;
            if (!state) {
                return takedown;
            }
            //listen to annotation changes
            _this.act.on('push-annotations,attr-name,attr-duration', _this.updateAnnotations);
            //no annotations should be shown
            if (cmode.getMode().exploration > 0.8) {
                return takedown;
            }
            //wait 1sec till the previous annotations are removed
            return takedown.then(function () { return C.resolveIn(waitBetweenTakeDown ? 1000 : 0); }).then(function () {
                var next = resolveImmediately(null);
                if (state.isTextOnly) {
                    next = _this.renderText(state);
                }
                else {
                    //jump to next state
                    next = _this.graph.jumpTo(state.state, state.transition <= 0 || !withTransition ? player.MIN_TRANSITION : state.transition * player.FACTOR);
                }
                //wait till next is done before rendering annotations
                return next.then(function () {
                    var all = [_this.renderAnnotations(state)];
                    if (_this.options.renderSubtitle) {
                        all.push(_this.renderSubtitle(state));
                    }
                    return Promise.all(all);
                });
            });
        });
        return this.prev;
    };
    /**
     * renders anchor hits
     * @param bounds the parent bounds where the anchors are rendered into
     */
    Renderer.prototype.renderAnchors = function (bounds) {
        var mainNode = this.$main.node();
        var anchorElements = [].slice.apply(mainNode.querySelectorAll('*[data-anchor]'));
        if (mainNode.getAttribute('data-anchor') != null) {
            anchorElements.push(mainNode);
        }
        var anchors = [];
        //create anchors
        anchorElements.forEach(function (a) {
            var b = C.bounds(a);
            if (b.w * b.h < 50 * 50) {
                anchors.push(new Anchor(a, EAnchorDirection.CENTER));
            }
            else {
                anchors.push.apply(anchors, Object.keys(string2anchor).map(function (s) { return new Anchor(a, string2anchor[s]); }));
            }
        });
        var $anchors = this.$main.selectAll('div.annotation-anchor').data(anchors);
        $anchors.enter().append('div').classed('annotation-anchor', true);
        $anchors.style({
            display: null,
            left: function (d) { return (d.pos[0] + -bounds.x) + 'px'; },
            top: function (d) { return (d.pos[1] - bounds.y) + 'px'; },
        });
        $anchors.exit().remove();
    };
    /**
     * updates all anchors by computing the one with the minimal distance and highlight it
     * @param pos current position
     * @param bounds bounds of the parent element containing the anchors
     * @returns {any}
     */
    Renderer.prototype.updateAnchor = function (pos, bounds) {
        var $anchors = this.$main.selectAll('div.annotation-anchor');
        if ($anchors.empty()) {
            return null;
        }
        var abspos = [pos[0] + bounds.x, pos[1] + bounds.y];
        var minV = Number.POSITIVE_INFINITY, minAnchor = null;
        $anchors.each(function (d) {
            var distance = d.distance(abspos);
            if (distance < minV) {
                minAnchor = d;
                minV = distance;
            }
        });
        $anchors.classed('closest', function (d, i) { return d === minAnchor; });
        if (minAnchor) {
            return {
                anchor: minAnchor.toString(),
                offset: [abspos[0] - minAnchor.pos[0], abspos[1] - minAnchor.pos[1]]
            };
        }
        //no anchor relative version
        return [pos[0] * 100 / bounds.w, pos[1] * 100 / bounds.h];
    };
    Renderer.prototype.removeAnchors = function () {
        this.$main.selectAll('div.annotation-anchor').style('display', 'none').remove();
    };
    Renderer.prototype.renderAnnotationsImpl = function (state) {
        var _this = this;
        var that = this;
        var editable = modeFeatures.isEditable() && state != null;
        var $anns = this.$main.selectAll('div.annotation').data(state ? state.annotations : [], function (d, i) { return state.id + '@' + d.type + i; });
        var $annsEnter = $anns.enter().append('div')
            .attr('class', function (d) { return d.type + '-annotation annotation'; });
        var bounds = C.bounds(this.$main.node());
        function updatePos(d) {
            var elem = this;
            var p = d.pos;
            if (Array.isArray(p)) {
                elem.style.left = p[0] + '%';
                elem.style.top = p[1] + '%';
            }
            else {
                var anchor = Anchor.fromString(p.anchor);
                var base = anchor.pos;
                elem.style.left = (base[0] - bounds.x) + p.offset[0] + 'px';
                elem.style.top = (base[1] - bounds.y) + p.offset[1] + 'px';
            }
        }
        this.anchorWatcher.clear();
        function watchAnchor(d) {
            var _this = this;
            var p = d.pos;
            if (!Array.isArray(p)) {
                that.anchorWatcher.add(p.anchor, function () { return updatePos.call(_this, d); });
            }
        }
        function updateSize(d) {
            var elem = this;
            var p = d.pos2;
            if (p) {
                var base = Anchor.fromString(p.anchor).pos;
                var pos = Anchor.fromString(d.pos.anchor).pos;
                elem.style.width = (base[0] + p.offset[0] - pos[0] - d.pos.offset[0]) + 'px';
                elem.style.height = (base[1] + p.offset[1] - pos[1] - d.pos.offset[1]) + 'px';
            }
            else {
                var size = d.size;
                elem.style.width = size[0] + '%';
                elem.style.height = size[1] + '%';
            }
        }
        function watchSizeAnchor(d) {
            var _this = this;
            var p = d.pos2;
            if (p) {
                that.anchorWatcher.add(p.anchor, function () { return updateSize.call(_this, d); });
            }
        }
        //move
        $annsEnter.append('button').attr('tabindex', -1).attr('class', 'btn btn-default btn-xs fa fa-arrows').call(d3.behavior.drag()
            .on('dragstart', function (d, i) {
            that.renderAnchors(bounds);
        })
            .on('dragend', that.removeAnchors.bind(that))
            .on('drag', function (d, i) {
            var mouse = d3.mouse(this.parentNode.parentNode);
            d.pos = that.updateAnchor(mouse, bounds);
            state.updateAnnotation(d);
            d3.select(this.parentNode).each(updatePos);
        }));
        //remove
        $annsEnter.append('button').attr('tabindex', -1).attr('class', 'btn btn-default btn-xs fa fa-times')
            .on('click', function (d, i) {
            d3.select(this.parentNode).remove();
            state.removeAnnotationElem(d);
            d3.event.preventDefault();
        });
        //Text
        $anns.filter(function (d) { return d.type === 'text' || !d.hasOwnProperty('type'); }).call(function ($texts, $textsEnter) {
            var onEdit = function (d, i) {
                var $elem = d3.select(this);
                if (!d3.select(this.parentNode).classed('editable')) {
                    return;
                }
                $elem.on('click', null);
                $elem.append('textarea').property('value', d.text).on('blur', function () {
                    d.text = this.value;
                    state.updateAnnotation(d);
                    //update value and enable edit click handler again
                    $elem.html(that.renderer(this.value)).on('click', onEdit);
                });
            };
            $textsEnter.append('div').classed('text', true).on('click', onEdit);
            $texts.select('div.text').html(function (d) { return _this.renderer(d.text); }).style({
                width: function (d) { return d.size ? d.size[0] + 'px' : null; },
                height: function (d) { return d.size ? d.size[1] + 'px' : null; },
            }).each(function (d) {
                if (d.styles) {
                    d3.select(this).style(d.styles);
                }
            });
        }, $annsEnter.filter(function (d) { return d.type === 'text' || !d.hasOwnProperty('type'); }));
        //Arrow
        $anns.filter(function (d) { return d.type === 'arrow'; }).call(function ($arrows, $arrowsEnter) {
            var $svgEnter = $arrowsEnter.insert('svg', ':first-child').attr({
                width: function (d) { return 30 + Math.abs(d.at[0]); },
                height: function (d) { return 30 + Math.abs(d.at[1]); }
            }).style({
                left: function (d) { return (-15 + Math.min(0, d.at[0])) + 'px'; },
                top: function (d) { return (-15 + Math.min(0, d.at[1])) + 'px'; }
            });
            $svgEnter.append('defs').append('marker').attr({
                id: function (d, i) { return 'clue_text_arrow_marker' + i; },
                viewBox: '0 0 10 10',
                refX: 6,
                refY: 5,
                markerWidth: 4,
                markerHeight: 3,
                markerUnits: 'strokeWidth',
                orient: 'auto'
            }).append('path').attr('d', 'M 0 0 L 10 5 L 0 10 z');
            $svgEnter.append('g').append('line').classed('arrow', true).attr({
                'marker-end': function (d, i) { return 'url(#clue_text_arrow_marker' + i + ')'; }
            });
            var $svg = $arrows.select('svg');
            function updateShift() {
                $svg.attr({
                    width: function (d) { return 30 + Math.abs(d.at[0]); },
                    height: function (d) { return 30 + Math.abs(d.at[1]); } //TODO yminmax[1] - yminmax[0],
                }).style({
                    left: function (d) { return (-15 + Math.min(0, d.at[0])) + 'px'; },
                    top: function (d) { return (-15 + Math.min(0, d.at[1])) + 'px'; }
                });
                $svg.select('g').attr('transform', function (d) { return "translate(" + (-Math.min(0, d.at[0]) + 15) + "," + (-Math.min(0, d.at[1]) + 15) + ")"; });
            }
            $svgEnter.select('g').append('circle').classed('anchor', true).attr('r', 5);
            $svg.select('circle').style({
                cx: function (d) { return d.at[0]; },
                cy: function (d) { return d.at[1]; }
            }).call(d3.behavior.drag()
                .on('drag', function (d, i) {
                var e = d3.event;
                d.at = [e.x, e.y];
                state.updateAnnotation(d);
                d3.select(this).style({
                    cx: d.at[0],
                    cy: d.at[1]
                });
                $svg.select('line[data-index="' + i + '"]').attr({
                    x2: d.at[0],
                    y2: d.at[1]
                });
                updateShift();
            }));
            updateShift();
            $svg.select('line').attr({
                'data-index': function (d, i) { return i; },
                x2: function (d) { return d.at[0]; },
                y2: function (d) { return d.at[1]; }
            }).each(function (d) {
                if (d.styles) {
                    d3.select(this).style(d.styles);
                }
            });
        }, $annsEnter.filter(function (d) { return d.type === 'arrow'; }));
        //FRAME
        $anns.filter(function (d) { return d.type === 'frame'; }).call(function ($frames, $framesEnter) {
            $frames.each(function (d) {
                updateSize.call(this, d);
                watchSizeAnchor.call(this, d);
                if (d.styles) {
                    d3.select(this).style(d.styles);
                }
            });
            //resize
            $framesEnter.append('button').attr('tabindex', -1).attr('class', 'btn btn-default btn-xs fa fa-expand fa-flip-horizontal')
                .call(d3.behavior.drag()
                .on('dragstart', function (d, i) {
                that.renderAnchors(bounds);
            })
                .on('dragend', that.removeAnchors.bind(that))
                .on('drag', function (d, i) {
                var mouse = d3.mouse(this.parentNode.parentNode);
                d.pos2 = that.updateAnchor(mouse, bounds);
                state.updateAnnotation(d);
                d3.select(this.parentNode).each(updateSize);
            }));
        }, $annsEnter.filter(function (d) { return d.type === 'frame'; }));
        $anns.each(updatePos).each(watchAnchor).classed('editable', editable);
        $anns.exit().remove();
        return $anns;
    };
    Renderer.prototype.renderAnnotations = function (state) {
        var _this = this;
        return new Promise(function (resolve) {
            var $anns = _this.renderAnnotationsImpl(state);
            if (_this.options.animation && !$anns.empty() && _this.options.duration > 0) {
                $anns.style('opacity', 0).transition().duration(_this.options.duration).style('opacity', 1);
                C.resolveIn(_this.options.duration + 10).then(function () { return resolve($anns.node()); });
            }
            else {
                $anns.style('opacity', 1);
                resolve($anns.node());
            }
        });
    };
    Renderer.prototype.hideOld = function () {
        var _this = this;
        return new Promise(function (resolve) {
            _this.anchorWatcher.clear();
            var $div = _this.$main.classed('hide-all-non-annotations', false).selectAll('div.annotation, div.text-overlay, div.add-text-annotation, div.subtitle-annotation');
            if (_this.options.animation && !$div.empty() && _this.options.duration > 0) {
                $div.transition().duration(_this.options.duration).style('opacity', 0).remove();
                C.resolveIn(_this.options.duration + 10).then(function () { return resolve(); });
            }
            else {
                $div.remove();
                resolve();
            }
        });
    };
    Renderer.prototype.renderSubtitle = function (overlay) {
        var _this = this;
        return new Promise(function (resolve) {
            _this.$main.append('div').attr('class', 'subtitle-annotation').html(_this.renderer(_this.options.subtitlePattern));
            resolve(_this.$main.node());
        });
    };
    Renderer.prototype.renderText = function (overlay) {
        var _this = this;
        var t = overlay.transition * player.FACTOR;
        return C.resolveIn(t).then(function () {
            _this.$main.classed('hide-all-non-annotations', true);
            return _this.$main.node();
        });
    };
    return Renderer;
}());
export { Renderer };
export function create(main, graph) {
    var instance = new Renderer(d3.select(main), graph);
    return {
        render: instance.render.bind(instance)
    };
}
//# sourceMappingURL=annotation.js.map