define(function(require, module, exports) {
    main.consumes = ["Plugin", "UndoManager", "util", "error_handler"];
    main.provides = ["Document"];
    return main;

    function main(options, imports, register) {
        var Plugin = imports.Plugin;
        var util = imports.util;
        var UndoManager = imports.UndoManager;
        var reportError = imports.error_handler.reportError;
        
        function Document(options) {
            var plugin = new Plugin("Ajax.org", main.consumes);
            var undoManager;
            var emit = plugin.getEmitter();
            
            if (options && options.undoManager instanceof Plugin) {
                undoManager = options.undoManager;
                delete options.undoManager;
            }
            else
                undoManager = new UndoManager();
            
            var sessions = {};
            var value = options && options.value || "";
            var changed = false;
            var meta = {};
            var hasValue = options && (typeof options.value === "string");
            
            var tab, lastState, title, tooltip, editor, recentValue, ready;
            
            plugin.on("newListener", function(type, listener) {
                if (type == "state.set" && lastState) {
                    listener({
                        doc: plugin, 
                        state: lastState
                    });
                }
            });
            
            // Listen to changes and detect when the value of the editor
            // is different from what is on disk
            function initUndo() {
                undoManager.on("change", function(e) {
                    var c = !undoManager.isAtBookmark();
                    if (changed !== c) {
                        changed = c;
                        emit("changed", { changed: c });
                    }
                });
            }
            initUndo();
            
            /***** Methods *****/
            
            function setBookmarkedValue(value, cleansed) {
                if (plugin.value == value) {
                    recentValue = value;
                    undoManager.bookmark();
                    return;
                }
                var state = getState();
                
                // Record value (which should add an undo stack item)
                plugin.value = value;
                
                // Bookmark the undo manager
                undoManager.bookmark();
                
                // Update state
                delete state.changed;
                delete state.value;
                delete state.meta;
                state.undoManager = undoManager.getState();
                
                if (cleansed && editor && state[editor.type])
                    state[editor.type].cleansed = true;
                
                // Set new state (preserving original state)
                if (emit("mergeState") !== false)
                    setState(state);
            }
            
            function getState(filter) {
                // Editor is not inited yet, so we keep the state set until 
                // editor is loaded
                var state = !editor && lastState
                    ? state = util.extend({}, lastState)
                    : {};
                
                state.changed = changed;
                state.meta = {};
                state.filter = filter || false;
                
                for (var prop in meta) {
                    if (prop.charAt(0) != "$")
                        state.meta[prop] = meta[prop];
                }
                
                if (title)
                    state.title = title;
                if (tooltip)
                    state.tooltip = tooltip;
                
                if (!filter) {
                    state.value = plugin.value;
                    state.undoManager = undoManager.getState();
                }
                else {
                    delete state.value;
                    delete state.undoManager;
                }
                
                if (editor)
                    state[editor.type] = editor.getState(plugin, filter);
                
                var event = { doc: plugin, state: state };
                emit("getState", event);
                
                lastState = event.state;
                
                return event.state;
            }
            
            function setState(state) {
                if (state.meta) {
                    for (var prop in meta) {
                        if (prop.charAt(0) == "$")
                            state.meta[prop] = meta[prop];
                    }
                    meta = state.meta;
                }
                if (state.title) plugin.title = state.title;
                if (state.tooltip) plugin.tooltip = state.tooltip;
                
                if (typeof state.value == "string")   
                    plugin.value = state.value;
                
                if (state.undoManager && !(state.undoManager instanceof Plugin))
                    undoManager.setState(state.undoManager);
                
                if (state.changed && state.changed !== changed) {
                    changed = state.changed;
                    emit("changed", { changed: changed });
                }
                    
                if (editor && state[editor.type])
                    editor.setState(plugin, state[editor.type]);
                
                var event = { doc: plugin, state: state };
                emit("setState", event);
                
                lastState = event.state;
            }
            
            function getSession(type) {
                var ed = editor || tab && tab.editor;
                if (ed && !type) type = ed.type;
                if (!type) return;
                
                if (sessions[type])
                    return sessions[type];
                
                var session = sessions[type] = new Plugin();
                session.load(null, "session");
                return session;
            }
            
            function progress(options) {
                emit("progress", options);
            }
            
            function clone() {
                var state = this.getState();
                state.undoManager = undoManager;
                var newdoc = new Document(state);
                emit("clone", { doc: newdoc });
                newdoc.meta.cloned = true;
                return newdoc;
            }
            
            function canUnload() {
                return emit("canUnload");
            }
            
            /***** Lifecycle *****/
            
            plugin.on("unload", function() {
                changed = false;
                
                // Unload the undo manager
                undoManager.unload();
                
                // Unload the sessions
                for (var type in sessions)
                    sessions[type].unload();
            });
            
            /**
             * Session Class for Cloud9 Editors. Each document that is loaded into an
             * editor has one session object per editor. A session object is retrieved by
             * calling {@link Document#getSession} and is specifically tied to an editor
             * type. The session is used to store state and functions on by the editor.
             * 
             * The session relates to other objects as such:
             * 
             * * {@link Pane} - Represent a single pane, housing multiple tabs
             *   * {@link Tab} - A single tab (button) in a pane
             *     * {@link Editor} - The editor responsible for displaying the file in the tab
             *     * {@link Document} - The representation of a file in the tab
             *       * **Session - The session information of the editor**
             *       * {@link UndoManager} - The object that manages the undo stack for this document
             * 
             * Panes can live in certain areas of Cloud9. By default these areas are:
             * 
             * * {@link panes}      - The main area where editor panes are displayed
             * * {@link console}    - The console in the bottom of the screen
             * 
             * Tabs are managed by the {@link tabManager}. The default way to
             * open a new file in an editor uses the tabManager:
             * 
             *     tabManager.openFile("/file.js", true, function(err, tab) {
             *         var session = tab.document.getSession();
             *         var aceSession = session.session;
             *     });
             * 
             * Generally the state and functions stored on the session object are private
             * to the editor and do not concern other plugins. However one can imagine that
             * plugins that work on editors make use of these properties. For instance the
             * {@link ace.status.Statusbar} plugin uses the information on the state object from the
             * ace editor to display state information like the line number and language 
             * mode.
             * 
             * When a tab is moved from one pane to another, the document is unloaded from
             * one editor and then loaded into another one. It is up to the editor to 
             * decide how to handle the stored state on the session object. Often the 
             * editor will remove only some of that state and reuse other state in the 
             * new editor. For instance, the {@link ace.Ace Ace} editor stores the ace session 
             * object on the Cloud9 session object. 
             * It will only do this once and will use the same ace session object in 
             * different ace editors, throughout the lifetime of the session object.
             * 
             * This example shows how an editor could deal with a session. For more
             * information see the source code of the {@link texteditor.TextEditor TextEditor} plugin.
             * 
             *     plugin.on("documentLoad", function(e) {
             *         var doc = e.doc;
             *         var session = doc.getSession();
             *         
             *         if (!session.id) {
             *             session.id = Math.random();
             *             session.iframe = document.createElement("iframe");
             *         }
             *         
             *         // Container would be a div created in the {@link Editor#draw} event.
             *         container.appendChild(session.iframe);
             *     });
             * 
             * @class Session
             * @extends Plugin
             */
            /**
             * Document Class for Cloud9 Editors. Each file that is opened
             * in an editor has a document object that contains it's value and 
             * state. 
             * 
             * The document relates to other objects as such:
             * 
             * * {@link Pane} - Represent a single pane, housing multiple tabs
             *   * {@link Tab} - A single tab (button) in a pane
             *     * {@link Editor} - The editor responsible for displaying the file in the tab
             *     * **Document - The representation of a file in the tab**
             *       * {@link Session} - The session information of the editor
             *       * {@link UndoManager} - The object that manages the undo stack for this document
             * 
             * Panes can live in certain areas of Cloud9. By default these areas are:
             * 
             * * {@link panes}      - The main area where editor panes are displayed
             * * {@link console}    - The console in the bottom of the screen
             * 
             * Tabs are managed by the {@link tabManager}. The default way to
             * open a new file in an editor uses the tabManager:
             * 
             *     tabManager.openFile("/file.js", true, function(err, tab) {
             *         var doc = tab.document;
             *         console.log("The value is: ", doc.value);
             *     });
             * 
             * For editors that do not allow a user to edit files
             * (e.g. terminal), a document object will still be created.
             */
            plugin.freezePublicAPI({
                /**
                 * the tab this document is attached to
                 * @property {Tab} tab
                 */
                get tab() { return tab; },
                set tab(v) { tab = v; },
                
                /**
                 * the meta information for this document. Use this object to
                 * store any additional information you'd like to store. If you
                 * are storing state information that should not be kept between
                 * reloads of the IDE, prefix the items with a "$" character.
                 * 
                 * Example:
                 * 
                 *     // This document has a black bird
                 *     var doc = tabs.focussedTab.document;
                 *     doc.meta.blackBird = true;
                 * 
                 * Example (state is not preserved):
                 * 
                 *     // This document is being processed
                 *     var doc = tabs.focussedTab.document;
                 *     doc.meta.$processing = true;
                 * 
                 * @property meta
                 */
                get meta() { return meta; },
                
                /**
                 * The last state that was set into the document.
                 * @property lastState
                 * @private
                 */
                get lastState() { return lastState; },
                
                /**
                 * the editor this document is attached to
                 * @property {Editor} editor
                 */
                get editor() { return editor; },
                set editor(v) { 
                    editor = v;
                    emit("setEditor", { editor: v });
                },
                /**
                 * Whether the document is fully loaded
                 * @property {Boolean} ready
                 */
                get ready() { return ready; },
                set ready(v) {
                    // try to find out why is this called twice
                    var e = new Error("Setting ready on ready document");
                    if (ready)
                        reportError(e, { ready: ready });
                    ready = e.stack || true;
                    emit.sticky("ready", { doc: plugin });
                },
                /**
                 * The tooltip displayed when hovering over the tab button
                 * @property {String} tooltip
                 */
                get tooltip() { return tooltip; },
                set tooltip(v) { 
                    tooltip = v; 
                    emit("setTooltip", { tooltip: v });
                },
                /**
                 * The title of the document (displayed as caption of the tab button)
                 * @property {String} title
                 */
                get title() { return title; },
                set title(v) { 
                    title = v; 
                    emit("setTitle", { title: v });
                },
                /**
                 * Sets or retrieves the serialized value of this document.
                 * Setting this property will not change the undo stack. Set
                 * this property only to initialize the document or to reset
                 * the value of this document. Requesting the value of this
                 * document will cause it to serialize it's full state.
                 * @property {String} value
                 */
                get value() {
                    var calculated = emit("getValue", { value: recentValue || value });
                    if (typeof calculated != "string")
                        calculated = value;
                    
                    if (undoManager.isAtBookmark())
                        recentValue = calculated;
                        
                    return calculated;
                },
                set value(v) { 
                    value = recentValue = v;
                    emit("setValue", { value: v });
                    hasValue = true;
                },
                /**
                 * The last serialized value that was either set or retrieved.
                 * The `recentValue` is only updated during a get when the 
                 * document is saved (undoManager.isAtBookmark() == true).
                 * @property {String} recentValue
                 * @readonly
                 */
                get recentValue() { return recentValue; },
                /**
                 * Specifies whether the document on this tab is changed
                 * @property {Boolean} changed
                 */
                get changed() { return changed; },
                /**
                 * The object managing all the changes to the document
                 * @property {UndoManager} undoManager
                 */
                get undoManager() { return undoManager; },
                set undoManager(newUndo) { 
                    undoManager.unload();
                    undoManager = newUndo; 
                    initUndo();
                },
                
                _events: [
                    /**
                     * Fires when the state is retrieved
                     * @event getState
                     * @param {Object}   e
                     * @param {Document} e.doc    the document for which the state is retrieved
                     * @param {Object}   e.state  the state object to extend with information
                     */
                    "getState",
                    /**
                     * Fires when the state is set
                     * @event setState
                     * @param {Object}   e
                     * @param {Document} e.doc    the document for which the state is set
                     * @param {Object}   e.state  the state object
                     */
                    "setState",
                    /**
                     * Fires when the value of this document is retrieved.
                     * When implementing an editor, use this event to return
                     * the current value of this document in the event handler.
                     * 
                     * Example:
                     * 
                     *     doc.on("getValue", function(e) { 
                     *         return "The current value of this editor"
                     *     }, doc.getSession());
                     * 
                     * @event getValue
                     * @param {Object} e
                     * @param {String} e.value  the last cached value
                     */
                    "getValue",
                    /**
                     * Fires when the value of this document is set
                     * When implementing an editor, use this event to set
                     * the new value of this document in the event handler.
                     * 
                     * Example:
                     * 
                     *     doc.on("setValue", function(e) { 
                     *         myDiv.textContent = e.value
                     *     }, doc.getSession());
                     * 
                     * @event setValue
                     * @param {Object} e
                     * @param {String} e.value  the value that is being set
                     */
                    "setValue",
                    /**
                     * @ignore
                     */
                    "mergeState",
                    /**
                     * Fires when the title of this document is set
                     * @event setTitle
                     * @param {Object} e
                     * @param {String} e.value  the title that is being set
                     */
                    "setTitle",
                    /**
                     * Fires when the tooltip of this document is set
                     * @event setTooltip
                     * @param {Object} e
                     * @param {String} e.value  the tooltip that is being set
                     */
                    "setTooltip",
                    /**
                     * Fires when the editor of this document is set
                     * @event setEditor
                     * @param {Object} e
                     * @param {String} e.value  the editor that is being set
                     */
                    "setEditor",
                    /**
                     * Fires when the change state of the document has changed
                     * @event changed
                     * @param {Object} e
                     * @param {String} e.changed  whether the value is changed
                     */
                    "changed",
                    /**
                     * Fires when there is progress information related to this
                     * file. This can be either downloading or uploading.
                     * @event progress
                     * @param {Object}  e
                     * @param {Number}  e.loaded    the number of bytes that have been downloaded/uploaded.
                     * @param {Number}  e.total     the total number of bytes for this file.
                     * @param {Boolean} e.complete  whether the download has completed.
                     * @param {Boolean} e.upload    whether this is an upload (instead of a download).
                     **/
                    "progress",
                    /**
                     * Fires when this document is cloned
                     * @event cone
                     * @param {Object}   e
                     * @param {Document} e.doc    the cloned document
                     **/
                    "clone",
                    /**
                     * @event canUnload
                     */
                    "canUnload"
                ],
                
                /**
                 * Retrieves the session object where editors can store state
                 */
                getSession: getSession,
                
                /**
                 * Sets the value of the document and bookmarks it in the 
                 * undoManager. This method also preserves the complete state
                 * of the editor while transitioning to the new value.
                 * @param {String} value  The new value of the document
                 */
                setBookmarkedValue: setBookmarkedValue,
                
                /**
                 * Retrieves the state of the document. The state object will
                 * have a section for each editor that it is loaded in. These
                 * sections have the name of the editor. For instance if the
                 * editor is ace, the returned object will look something like
                 * this:
                 * 
                 *     {
                 *         ace : {
                 *             scrollTop : 100,
                 *             ...
                 *         }
                 *     }
                 * 
                 * @return {Object}
                 * @return {Boolean} return.changed      Specifying whether the document state has been saved.
                 * @return {Object}  return.meta         Any metadata set on this document (except items starting with a "$").
                 * @return {String}  return.title        The title displayed in the tab, displaying this document.
                 * @return {String}  return.tooltip      The tooltip displayed in the tab, displaying this document
                 * @return {String}  return.value        The value of the document.
                 * @return {Object}  return.undoManager  The result of {@link UndoManager#getState}.
                 */
                getState: getState,
                
                /**
                 * Sets the state of the document
                 * @param {Object}  state              the state of the document. This can also include sections for editors. See {@link Document#method-getState}.
                 * @param {Boolean} state.changed      Specifying whether the document state has been saved.
                 * @param {Object}  state.meta         Any metadata set on this document (except items starting with a "$").
                 * @param {String}  state.title        The title displayed in the tab, displaying this document.
                 * @param {String}  state.tooltip      The tooltip displayed in the tab, displaying this document
                 * @param {String}  state.value        The value of the document.
                 * @param {Object}  state.undoManager  The result of {@link UndoManager#getState}.
                 */
                setState: setState,
                
                /**
                 * Set the progress indicator for this document
                 * @param {Object}  options 
                 * @param {Number}  options.loaded    the number of bytes that have been downloaded/uploaded.
                 * @param {Number}  options.total     the total number of bytes for this file.
                 * @param {Boolean} options.complete  whether the download has completed.
                 * @param {Boolean} options.upload    whether this is an upload (instead of a download).
                 */
                progress: progress,
                
                /**
                 * Clones this document
                 */
                clone: clone,
                
                /**
                 * Whether this document is ready to be unloaded
                 */
                canUnload: canUnload,
                
                /**
                 * Returns whether the document has it's value set.
                 * @return {Boolean} 
                 */
                hasValue: function() { return !!hasValue; },
            });
            
            if (options)
                setState(options);
            
            plugin.load(null, "document");
            
            return plugin;
        }
        
        /***** Register and define API *****/
        
        register(null, {
            Document: Document
        });
    }
});
