/* 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/pdf_history', ['exports', 'pdfjs-web/dom_events'],
            factory);
    } else if (typeof exports !== 'undefined') {
        factory(exports, require('./dom_events.js'));
    } else {
        factory((root.pdfjsWebPDFHistory = {}), root.pdfjsWebDOMEvents);
    }
}(this, function(exports, domEvents) {

    function PDFHistory(options) {
        this.linkService = options.linkService;
        this.eventBus = options.eventBus || domEvents.getGlobalEventBus();

        this.initialized = false;
        this.initialDestination = null;
        this.initialBookmark = null;
    }

    PDFHistory.prototype = {
        /**
         * @param {string} fingerprint
         */
        initialize : function pdfHistoryInitialize(fingerprint) {
            this.initialized = true;
            this.reInitialized = false;
            this.allowHashChange = true;
            this.historyUnlocked = true;
            this.isViewerInPresentationMode = false;

            this.previousHash = window.location.hash.substring(1);
            this.currentBookmark = '';
            this.currentPage = 0;
            this.updatePreviousBookmark = false;
            this.previousBookmark = '';
            this.previousPage = 0;
            this.nextHashParam = '';

            this.fingerprint = fingerprint;
            this.currentUid = this.uid = 0;
            this.current = {};

            var state = window.history.state;
            if (this._isStateObjectDefined(state)) {
                // This corresponds to navigating back to the document
                // from another page in the browser history.
                if (state.target.dest) {
                    this.initialDestination = state.target.dest;
                } else {
                    this.initialBookmark = state.target.hash;
                }
                this.currentUid = state.uid;
                this.uid = state.uid + 1;
                this.current = state.target;
            } else {
                // This corresponds to the loading of a new document.
                if (state && state.fingerprint &&
                    this.fingerprint !== state.fingerprint) {
                    // Reinitialize the browsing history when a new document
                    // is opened in the web viewer.
                    this.reInitialized = true;
                }
                this._pushOrReplaceState({fingerprint : this.fingerprint}, true);
            }

            var self = this;
            window.addEventListener('popstate', function pdfHistoryPopstate(evt) {
                if (!self.historyUnlocked) {
                    return;
                }
                if (evt.state) {
                    // Move back/forward in the history.
                    self._goTo(evt.state);
                    return;
                }

                // If the state is not set, then the user tried to navigate to a
                // different hash by manually editing the URL and pressing Enter, or by
                // clicking on an in-page link (e.g. the "current view" link).
                // Save the current view state to the browser history.

                // Note: In Firefox, history.null could also be null after an in-page
                // navigation to the same URL, and without dispatching the popstate
                // event: https://bugzilla.mozilla.org/show_bug.cgi?id=1183881

                if (self.uid === 0) {
                    // Replace the previous state if it was not explicitly set.
                    var previousParams = (self.previousHash && self.currentBookmark &&
                        self.previousHash !== self.currentBookmark) ?
                        {hash : self.currentBookmark, page : self.currentPage} :
                        {page : 1};
                    replacePreviousHistoryState(previousParams, function() {
                        updateHistoryWithCurrentHash();
                    });
                } else {
                    updateHistoryWithCurrentHash();
                }
            }, false);

            function updateHistoryWithCurrentHash() {
                self.previousHash = window.location.hash.slice(1);
                self._pushToHistory({hash : self.previousHash}, false, true);
                self._updatePreviousBookmark();
            }

            function replacePreviousHistoryState(params, callback) {
                // To modify the previous history entry, the following happens:
                // 1. history.back()
                // 2. _pushToHistory, which calls history.replaceState( ... )
                // 3. history.forward()
                // Because a navigation via the history API does not immediately update
                // the history state, the popstate event is used for synchronization.
                self.historyUnlocked = false;

                // Suppress the hashchange event to avoid side effects caused by
                // navigating back and forward.
                self.allowHashChange = false;
                window.addEventListener('popstate', rewriteHistoryAfterBack);
                history.back();

                function rewriteHistoryAfterBack() {
                    window.removeEventListener('popstate', rewriteHistoryAfterBack);
                    window.addEventListener('popstate', rewriteHistoryAfterForward);
                    self._pushToHistory(params, false, true);
                    history.forward();
                }

                function rewriteHistoryAfterForward() {
                    window.removeEventListener('popstate', rewriteHistoryAfterForward);
                    self.allowHashChange = true;
                    self.historyUnlocked = true;
                    callback();
                }
            }

            function pdfHistoryBeforeUnload() {
                var previousParams = self._getPreviousParams(null, true);
                if (previousParams) {
                    var replacePrevious = (!self.current.dest &&
                        self.current.hash !== self.previousHash);
                    self._pushToHistory(previousParams, false, replacePrevious);
                    self._updatePreviousBookmark();
                }
                // Remove the event listener when navigating away from the document,
                // since 'beforeunload' prevents Firefox from caching the document.
                window.removeEventListener('beforeunload', pdfHistoryBeforeUnload,
                    false);
            }

            window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);

            window.addEventListener('pageshow', function pdfHistoryPageShow(evt) {
                // If the entire viewer (including the PDF file) is cached in
                // the browser, we need to reattach the 'beforeunload' event listener
                // since the 'DOMContentLoaded' event is not fired on 'pageshow'.
                window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
            }, false);

            self.eventBus.on('presentationmodechanged', function(e) {
                self.isViewerInPresentationMode = e.active;
            });
        },

        clearHistoryState : function pdfHistory_clearHistoryState() {
            this._pushOrReplaceState(null, true);
        },

        _isStateObjectDefined : function pdfHistory_isStateObjectDefined(state) {
            return (state && state.uid >= 0 &&
                state.fingerprint && this.fingerprint === state.fingerprint &&
                state.target && state.target.hash) ? true : false;
        },

        _pushOrReplaceState : function pdfHistory_pushOrReplaceState(stateObj,
                                                                     replace) {
//#if CHROME
            // history.state.chromecomState is managed by chromecom.js.
            if (window.history.state && 'chromecomState' in window.history.state) {
                stateObj = stateObj || {};
                stateObj.chromecomState = window.history.state.chromecomState;
            }
//#endif
            if (replace) {
//#if (GENERIC || CHROME)
                window.history.replaceState(stateObj, '', document.URL);
//#else
//    window.history.replaceState(stateObj, '');
//#endif
            } else {
//#if (GENERIC || CHROME)
                window.history.pushState(stateObj, '', document.URL);
//#else
//    window.history.pushState(stateObj, '');
//#endif
//#if CHROME
//    if (top === window) {
//      chrome.runtime.sendMessage('showPageAction');
//    }
//#endif
            }
        },

        get isHashChangeUnlocked() {
            if (!this.initialized) {
                return true;
            }
            return this.allowHashChange;
        },

        _updatePreviousBookmark : function pdfHistory_updatePreviousBookmark() {
            if (this.updatePreviousBookmark &&
                this.currentBookmark && this.currentPage) {
                this.previousBookmark = this.currentBookmark;
                this.previousPage = this.currentPage;
                this.updatePreviousBookmark = false;
            }
        },

        updateCurrentBookmark : function pdfHistoryUpdateCurrentBookmark(bookmark,
                                                                         pageNum) {
            if (this.initialized) {
                this.currentBookmark = bookmark.substring(1);
                this.currentPage = pageNum | 0;
                this._updatePreviousBookmark();
            }
        },

        updateNextHashParam : function pdfHistoryUpdateNextHashParam(param) {
            if (this.initialized) {
                this.nextHashParam = param;
            }
        },

        push : function pdfHistoryPush(params, isInitialBookmark) {
            if (!(this.initialized && this.historyUnlocked)) {
                return;
            }
            if (params.dest && !params.hash) {
                params.hash = (this.current.hash && this.current.dest &&
                    this.current.dest === params.dest) ?
                    this.current.hash :
                    this.linkService.getDestinationHash(params.dest).split('#')[1];
            }
            if (params.page) {
                params.page |= 0;
            }
            if (isInitialBookmark) {
                var target = window.history.state.target;
                if (!target) {
                    // Invoked when the user specifies an initial bookmark,
                    // thus setting initialBookmark, when the document is loaded.
                    this._pushToHistory(params, false);
                    this.previousHash = window.location.hash.substring(1);
                }
                this.updatePreviousBookmark = this.nextHashParam ? false : true;
                if (target) {
                    // If the current document is reloaded,
                    // avoid creating duplicate entries in the history.
                    this._updatePreviousBookmark();
                }
                return;
            }
            if (this.nextHashParam) {
                if (this.nextHashParam === params.hash) {
                    this.nextHashParam = null;
                    this.updatePreviousBookmark = true;
                    return;
                } else {
                    this.nextHashParam = null;
                }
            }

            if (params.hash) {
                if (this.current.hash) {
                    if (this.current.hash !== params.hash) {
                        this._pushToHistory(params, true);
                    } else {
                        if (!this.current.page && params.page) {
                            this._pushToHistory(params, false, true);
                        }
                        this.updatePreviousBookmark = true;
                    }
                } else {
                    this._pushToHistory(params, true);
                }
            } else if (this.current.page && params.page &&
                this.current.page !== params.page) {
                this._pushToHistory(params, true);
            }
        },

        _getPreviousParams : function pdfHistory_getPreviousParams(onlyCheckPage,
                                                                   beforeUnload) {
            if (!(this.currentBookmark && this.currentPage)) {
                return null;
            } else if (this.updatePreviousBookmark) {
                this.updatePreviousBookmark = false;
            }
            if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) {
                // Prevent the history from getting stuck in the current state,
                // effectively preventing the user from going back/forward in
                // the history.
                //
                // This happens if the current position in the document didn't change
                // when the history was previously updated. The reasons for this are
                // either:
                // 1. The current zoom value is such that the document does not need to,
                //    or cannot, be scrolled to display the destination.
                // 2. The previous destination is broken, and doesn't actally point to a
                //    position within the document.
                //    (This is either due to a bad PDF generator, or the user making a
                //     mistake when entering a destination in the hash parameters.)
                return null;
            }
            if ((!this.current.dest && !onlyCheckPage) || beforeUnload) {
                if (this.previousBookmark === this.currentBookmark) {
                    return null;
                }
            } else if (this.current.page || onlyCheckPage) {
                if (this.previousPage === this.currentPage) {
                    return null;
                }
            } else {
                return null;
            }
            var params = {hash : this.currentBookmark, page : this.currentPage};
            if (this.isViewerInPresentationMode) {
                params.hash = null;
            }
            return params;
        },

        _stateObj : function pdfHistory_stateObj(params) {
            return {fingerprint : this.fingerprint, uid : this.uid, target : params};
        },

        _pushToHistory : function pdfHistory_pushToHistory(params,
                                                           addPrevious, overwrite) {
            if (!this.initialized) {
                return;
            }
            if (!params.hash && params.page) {
                params.hash = ('page=' + params.page);
            }
            if (addPrevious && !overwrite) {
                var previousParams = this._getPreviousParams();
                if (previousParams) {
                    var replacePrevious = (!this.current.dest &&
                        this.current.hash !== this.previousHash);
                    this._pushToHistory(previousParams, false, replacePrevious);
                }
            }
            this._pushOrReplaceState(this._stateObj(params),
                (overwrite || this.uid === 0));
            this.currentUid = this.uid++;
            this.current = params;
            this.updatePreviousBookmark = true;
        },

        _goTo : function pdfHistory_goTo(state) {
            if (!(this.initialized && this.historyUnlocked &&
                this._isStateObjectDefined(state))) {
                return;
            }
            if (!this.reInitialized && state.uid < this.currentUid) {
                var previousParams = this._getPreviousParams(true);
                if (previousParams) {
                    this._pushToHistory(this.current, false);
                    this._pushToHistory(previousParams, false);
                    this.currentUid = state.uid;
                    window.history.back();
                    return;
                }
            }
            this.historyUnlocked = false;

            if (state.target.dest) {
                this.linkService.navigateTo(state.target.dest);
            } else {
                this.linkService.setHash(state.target.hash);
            }
            this.currentUid = state.uid;
            if (state.uid > this.uid) {
                this.uid = state.uid;
            }
            this.current = state.target;
            this.updatePreviousBookmark = true;

            var currentHash = window.location.hash.substring(1);
            if (this.previousHash !== currentHash) {
                this.allowHashChange = false;
            }
            this.previousHash = currentHash;

            this.historyUnlocked = true;
        },

        back : function pdfHistoryBack() {
            this.go(-1);
        },

        forward : function pdfHistoryForward() {
            this.go(1);
        },

        go : function pdfHistoryGo(direction) {
            if (this.initialized && this.historyUnlocked) {
                var state = window.history.state;
                if (direction === -1 && state && state.uid > 0) {
                    window.history.back();
                } else if (direction === 1 && state && state.uid < (this.uid - 1)) {
                    window.history.forward();
                }
            }
        }
    };

    exports.PDFHistory = PDFHistory;
}));
