/* Copyright 2012 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        define('pdfjs-web/ui_utils', ['exports', 'pdfjs-web/pdfjs'], factory);
    } else if (typeof exports !== 'undefined') {
        factory(exports, require('./pdfjs.js'));
    } else {
        factory((root.pdfjsWebUIUtils = {}), root.pdfjsWebPDFJS);
    }
}(this, function(exports, pdfjsLib) {

    var CSS_UNITS = 96.0 / 72.0;
    var DEFAULT_SCALE_VALUE = 'auto';
    var DEFAULT_SCALE = 1.0;
    var UNKNOWN_SCALE = 0;
    var MAX_AUTO_SCALE = 1.25;
    var SCROLLBAR_PADDING = 40;
    var VERTICAL_PADDING = 5;

    var mozL10n = document.mozL10n || document.webL10n;

    var PDFJS = pdfjsLib.PDFJS;

    /**
     * Disables fullscreen support, and by extension Presentation Mode,
     * in browsers which support the fullscreen API.
     * @var {boolean}
     */
    PDFJS.disableFullscreen = (PDFJS.disableFullscreen === undefined ?
        false : PDFJS.disableFullscreen);

    /**
     * Enables CSS only zooming.
     * @var {boolean}
     */
    PDFJS.useOnlyCssZoom = (PDFJS.useOnlyCssZoom === undefined ?
        false : PDFJS.useOnlyCssZoom);

    /**
     * The maximum supported canvas size in total pixels e.g. width * height.
     * The default value is 4096 * 4096. Use -1 for no limit.
     * @var {number}
     */
    PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ?
        16777216 : PDFJS.maxCanvasPixels);

    /**
     * Disables saving of the last position of the viewed PDF.
     * @var {boolean}
     */
    PDFJS.disableHistory = (PDFJS.disableHistory === undefined ?
        false : PDFJS.disableHistory);

    /**
     * Disables creation of the text layer that used for text selection and search.
     * @var {boolean}
     */
    PDFJS.disableTextLayer = (PDFJS.disableTextLayer === undefined ?
        false : PDFJS.disableTextLayer);

    /**
     * Disables maintaining the current position in the document when zooming.
     */
    PDFJS.ignoreCurrentPositionOnZoom = (PDFJS.ignoreCurrentPositionOnZoom ===
    undefined ? false : PDFJS.ignoreCurrentPositionOnZoom);

//#if !(FIREFOX || MOZCENTRAL)
    /**
     * Interface locale settings.
     * @var {string}
     */
    PDFJS.locale = (PDFJS.locale === undefined ? navigator.language : PDFJS.locale);

//#endif

    /**
     * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
     * @return {Object} The object with horizontal (sx) and vertical (sy)
     scales. The scaled property is set to false if scaling is
     not required, true otherwise.
     */
    function getOutputScale(ctx) {
        var devicePixelRatio = window.devicePixelRatio || 1;
        var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
            ctx.mozBackingStorePixelRatio ||
            ctx.msBackingStorePixelRatio ||
            ctx.oBackingStorePixelRatio ||
            ctx.backingStorePixelRatio || 1;
        var pixelRatio = devicePixelRatio / backingStoreRatio;
        return {
            sx     : pixelRatio,
            sy     : pixelRatio,
            scaled : pixelRatio !== 1
        };
    }

    /**
     * Scrolls specified element into view of its parent.
     * @param {Object} element - The element to be visible.
     * @param {Object} spot - An object with optional top and left properties,
     *   specifying the offset from the top left edge.
     * @param {boolean} skipOverflowHiddenElements - Ignore elements that have
     *   the CSS rule `overflow: hidden;` set. The default is false.
     */
    function scrollIntoView(element, spot, skipOverflowHiddenElements) {
        // Assuming offsetParent is available (it's not available when viewer is in
        // hidden iframe or object). We have to scroll: if the offsetParent is not set
        // producing the error. See also animationStartedClosure.
        var parent = element.offsetParent;
        if (!parent) {
            console.error('offsetParent is not set -- cannot scroll');
            return;
        }
        var checkOverflow = skipOverflowHiddenElements || false;
        var offsetY = element.offsetTop + element.clientTop;
        var offsetX = element.offsetLeft + element.clientLeft;
        while (parent.clientHeight === parent.scrollHeight ||
        (checkOverflow && getComputedStyle(parent).overflow === 'hidden')) {
            if (parent.dataset._scaleY) {
                offsetY /= parent.dataset._scaleY;
                offsetX /= parent.dataset._scaleX;
            }
            offsetY += parent.offsetTop;
            offsetX += parent.offsetLeft;
            parent = parent.offsetParent;
            if (!parent) {
                return; // no need to scroll
            }
        }
        if (spot) {
            if (spot.top !== undefined) {
                offsetY += spot.top;
            }
            if (spot.left !== undefined) {
                offsetX += spot.left;
                parent.scrollLeft = offsetX;
            }
        }
        parent.scrollTop = offsetY;
    }

    /**
     * Helper function to start monitoring the scroll event and converting them into
     * DRM friendly one: with scroll debounce and scroll direction.
     */
    function watchScroll(viewAreaElement, callback) {
        var debounceScroll = function debounceScroll(evt) {
            if (rAF) {
                return;
            }
            // schedule an invocation of scroll for next animation frame.
            rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
                rAF = null;

                var currentY = viewAreaElement.scrollTop;
                var lastY = state.lastY;
                if (currentY !== lastY) {
                    state.down = currentY > lastY;
                }
                state.lastY = currentY;
                callback(state);
            });
        };

        var state = {
            down          : true,
            lastY         : viewAreaElement.scrollTop,
            _eventHandler : debounceScroll
        };

        var rAF = null;
        viewAreaElement.addEventListener('scroll', debounceScroll, true);
        return state;
    }

    /**
     * Helper function to parse query string (e.g. ?param1=value&parm2=...).
     */
    function parseQueryString(query) {
        var parts = query.split('&');
        var params = {};
        for (var i = 0, ii = parts.length; i < ii; ++i) {
            var param = parts[i].split('=');
            var key = param[0].toLowerCase();
            var value = param.length > 1 ? param[1] : null;
            params[decodeURIComponent(key)] = decodeURIComponent(value);
        }
        return params;
    }

    /**
     * Use binary search to find the index of the first item in a given array which
     * passes a given condition. The items are expected to be sorted in the sense
     * that if the condition is true for one item in the array, then it is also true
     * for all following items.
     *
     * @returns {Number} Index of the first array element to pass the test,
     *                   or |items.length| if no such element exists.
     */
    function binarySearchFirstItem(items, condition) {
        var minIndex = 0;
        var maxIndex = items.length - 1;

        if (items.length === 0 || !condition(items[maxIndex])) {
            return items.length;
        }
        if (condition(items[minIndex])) {
            return minIndex;
        }

        while (minIndex < maxIndex) {
            var currentIndex = (minIndex + maxIndex) >> 1;
            var currentItem = items[currentIndex];
            if (condition(currentItem)) {
                maxIndex = currentIndex;
            } else {
                minIndex = currentIndex + 1;
            }
        }
        return minIndex;
        /* === maxIndex */
    }

    /**
     *  Approximates float number as a fraction using Farey sequence (max order
     *  of 8).
     *  @param {number} x - Positive float number.
     *  @returns {Array} Estimated fraction: the first array item is a numerator,
     *                   the second one is a denominator.
     */
    function approximateFraction(x) {
        // Fast paths for int numbers or their inversions.
        if (Math.floor(x) === x) {
            return [x, 1];
        }
        var xinv = 1 / x;
        var limit = 8;
        if (xinv > limit) {
            return [1, limit];
        } else if (Math.floor(xinv) === xinv) {
            return [1, xinv];
        }

        var x_ = x > 1 ? xinv : x;
        // a/b and c/d are neighbours in Farey sequence.
        var a = 0, b = 1, c = 1, d = 1;
        // Limiting search to order 8.
        while (true) {
            // Generating next term in sequence (order of q).
            var p = a + c, q = b + d;
            if (q > limit) {
                break;
            }
            if (x_ <= p / q) {
                c = p;
                d = q;
            } else {
                a = p;
                b = q;
            }
        }
        // Select closest of the neighbours to x.
        if (x_ - a / b < c / d - x_) {
            return x_ === x ? [a, b] : [b, a];
        } else {
            return x_ === x ? [c, d] : [d, c];
        }
    }

    function roundToDivide(x, div) {
        var r = x % div;
        return r === 0 ? x : Math.round(x - r + div);
    }

    /**
     * Generic helper to find out what elements are visible within a scroll pane.
     */
    function getVisibleElements(scrollEl, views, sortByVisibility) {
        var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
        var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;

        function isElementBottomBelowViewTop(view) {
            var element = view.div;
            var elementBottom =
                    element.offsetTop + element.clientTop + element.clientHeight;
            return elementBottom > top;
        }

        var visible = [], view, element;
        var currentHeight, viewHeight, hiddenHeight, percentHeight;
        var currentWidth, viewWidth;
        var firstVisibleElementInd = (views.length === 0) ? 0 :
            binarySearchFirstItem(views, isElementBottomBelowViewTop);

        for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) {
            view = views[i];
            element = view.div;
            currentHeight = element.offsetTop + element.clientTop;
            viewHeight = element.clientHeight;

            if (currentHeight > bottom) {
                break;
            }

            currentWidth = element.offsetLeft + element.clientLeft;
            viewWidth = element.clientWidth;
            if (currentWidth + viewWidth < left || currentWidth > right) {
                continue;
            }
            hiddenHeight = Math.max(0, top - currentHeight) +
                Math.max(0, currentHeight + viewHeight - bottom);
            percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;

            visible.push({
                id      : view.id,
                x       : currentWidth,
                y       : currentHeight,
                view    : view,
                percent : percentHeight
            });
        }

        var first = visible[0];
        var last = visible[visible.length - 1];

        if (sortByVisibility) {
            visible.sort(function(a, b) {
                var pc = a.percent - b.percent;
                if (Math.abs(pc) > 0.001) {
                    return -pc;
                }
                return a.id - b.id; // ensure stability
            });
        }
        return {first : first, last : last, views : visible};
    }

    /**
     * Event handler to suppress context menu.
     */
    function noContextMenuHandler(e) {
        e.preventDefault();
    }

    /**
     * Returns the filename or guessed filename from the url (see issue 3455).
     * url {String} The original PDF location.
     * @return {String} Guessed PDF file name.
     */
    function getPDFFileNameFromURL(url) {
        var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
        //            SCHEME      HOST         1.PATH  2.QUERY   3.REF
        // Pattern to get last matching NAME.pdf
        var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
        var splitURI = reURI.exec(url);
        var suggestedFilename = reFilename.exec(splitURI[1]) ||
            reFilename.exec(splitURI[2]) ||
            reFilename.exec(splitURI[3]);
        if (suggestedFilename) {
            suggestedFilename = suggestedFilename[0];
            if (suggestedFilename.indexOf('%') !== -1) {
                // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
                try {
                    suggestedFilename =
                        reFilename.exec(decodeURIComponent(suggestedFilename))[0];
                } catch (e) { // Possible (extremely rare) errors:
                    // URIError "Malformed URI", e.g. for "%AA.pdf"
                    // TypeError "null has no properties", e.g. for "%2F.pdf"
                }
            }
        }
        return suggestedFilename || 'document.pdf';
    }

    /**
     * Simple event bus for an application. Listeners are attached using the
     * `on` and `off` methods. To raise an event, the `dispatch` method shall be
     * used.
     */
    var EventBus = (function EventBusClosure() {
        function EventBus() {
            this._listeners = Object.create(null);
        }

        EventBus.prototype = {
            on       : function EventBus_on(eventName, listener) {
                var eventListeners = this._listeners[eventName];
                if (!eventListeners) {
                    eventListeners = [];
                    this._listeners[eventName] = eventListeners;
                }
                eventListeners.push(listener);
            },
            off      : function EventBus_on(eventName, listener) {
                var eventListeners = this._listeners[eventName];
                var i;
                if (!eventListeners || ((i = eventListeners.indexOf(listener)) < 0)) {
                    return;
                }
                eventListeners.splice(i, 1);
            },
            dispatch : function EventBus_dispath(eventName) {
                var eventListeners = this._listeners[eventName];
                if (!eventListeners || eventListeners.length === 0) {
                    return;
                }
                // Passing all arguments after the eventName to the listeners.
                var args = Array.prototype.slice.call(arguments, 1);
                // Making copy of the listeners array in case if it will be modified
                // during dispatch.
                eventListeners.slice(0).forEach(function(listener) {
                    listener.apply(null, args);
                });
            }
        };
        return EventBus;
    })();

    var ProgressBar = (function ProgressBarClosure() {

        function clamp(v, min, max) {
            return Math.min(Math.max(v, min), max);
        }

        function ProgressBar(id, opts) {
            this.visible = true;

            // Fetch the sub-elements for later.
            this.div = document.querySelector(id + ' .progress');

            // Get the loading bar element, so it can be resized to fit the viewer.
            this.bar = this.div.parentNode;

            // Get options, with sensible defaults.
            this.height = opts.height || 100;
            this.width = opts.width || 100;
            this.units = opts.units || '%';

            // Initialize heights.
            this.div.style.height = this.height + this.units;
            this.percent = 0;
        }

        ProgressBar.prototype = {

            updateBar : function ProgressBar_updateBar() {
                if (this._indeterminate) {
                    this.div.classList.add('indeterminate');
                    this.div.style.width = this.width + this.units;
                    return;
                }

                this.div.classList.remove('indeterminate');
                var progressSize = this.width * this._percent / 100;
                this.div.style.width = progressSize + this.units;
            },

            get percent() {
                return this._percent;
            },

            set percent(val) {
                this._indeterminate = isNaN(val);
                this._percent = clamp(val, 0, 100);
                this.updateBar();
            },

            setWidth : function ProgressBar_setWidth(viewer) {
                if (viewer) {
                    var container = viewer.parentNode;
                    var scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
                    if (scrollbarWidth > 0) {
                        this.bar.setAttribute('style', 'width: calc(100% - ' +
                            scrollbarWidth + 'px);');
                    }
                }
            },

            hide : function ProgressBar_hide() {
                if (!this.visible) {
                    return;
                }
                this.visible = false;
                this.bar.classList.add('hidden');
                document.body.classList.remove('loadingInProgress');
            },

            show : function ProgressBar_show() {
                if (this.visible) {
                    return;
                }
                this.visible = true;
                document.body.classList.add('loadingInProgress');
                this.bar.classList.remove('hidden');
            }
        };

        return ProgressBar;
    })();

    exports.CSS_UNITS = CSS_UNITS;
    exports.DEFAULT_SCALE_VALUE = DEFAULT_SCALE_VALUE;
    exports.DEFAULT_SCALE = DEFAULT_SCALE;
    exports.UNKNOWN_SCALE = UNKNOWN_SCALE;
    exports.MAX_AUTO_SCALE = MAX_AUTO_SCALE;
    exports.SCROLLBAR_PADDING = SCROLLBAR_PADDING;
    exports.VERTICAL_PADDING = VERTICAL_PADDING;
    exports.mozL10n = mozL10n;
    exports.EventBus = EventBus;
    exports.ProgressBar = ProgressBar;
    exports.getPDFFileNameFromURL = getPDFFileNameFromURL;
    exports.noContextMenuHandler = noContextMenuHandler;
    exports.parseQueryString = parseQueryString;
    exports.getVisibleElements = getVisibleElements;
    exports.roundToDivide = roundToDivide;
    exports.approximateFraction = approximateFraction;
    exports.getOutputScale = getOutputScale;
    exports.scrollIntoView = scrollIntoView;
    exports.watchScroll = watchScroll;
    exports.binarySearchFirstItem = binarySearchFirstItem;
}));
