// = Apple.com QuickTime Adapter =
//
// This script provides functionality for both creating QuickTime elements
// suitable for embedding within the DOM on demand as well as a controller
// element that wraps much of the QuickTime JS API and adds additional
// functionality.
//
// Note that the **preferred library name is //AC.QuickTime//**.  The previous name,
// //AC.Quicktime// is deprecated.

if (typeof AC === "undefined") {
    AC = {};
}

AC.QuickTime = {

    // Creates a movie just to trigger the native activeX/missing plugin dialog in a browser
    _createNullMovie: function(width, height)
    {
        width = 0;
        height = 0;
        var nullContainer = $(document.createElement('div')),
            //needed in the object to trigger the missing activeX control in IE
            classid = "clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B",
            codebase = 'http://www.apple.com/qtactivex/qtplugin.cab#version=7,3,0,0',
            // triggers the missing plugin for other browsers like firefox
            pluginspage = 'http://www.apple.com/quicktime/download/';

        nullContainer.innerHTML = '<object width="' + width + '" height="' + height + '" classid="' + classid +'" codebase="' + codebase + '"><embed width="' + width + '" height="' + height + '" type="video/quicktime" pluginspage="' + pluginspage + '"></embed></object>';

        return nullContainer;
    },

    // ** {{{ AC.QuickTime.packageMovie(name, fileUrl, options) }}} **
    //
    // Returns a element suitable for appending to the DOM.
    // 
    // {{{name}}}: the name used to identify this media.\\
    // {{{fileURL}}}: the URL of the media file this element should refer to.\\
    // {{{options}}}: an associative array of option key value pairs used to
    // configure the created element.
    // 
    // Required {{{options}}}:
    // * {{{width}}}: The width of the element in pixels. Any integer greater than 0.
    // * {{{height}}} : The height of the element in pixels. Any integer greater than 0.
    //
    // Optional {{{options}}}:
    // * Any valid QuickTime embed parameter
    //
    // === Notifications ===
    // {{{QuickTime:noCompatibleQTAvailable(controller, minVersion)}}}
    //    Fired whenver there is no QuickTime compatible with the minimum version
    //    available.
    packageMovie: function(name, fileUrl, options)
    {
        if (!name || !fileUrl) {
            throw new TypeError('Valid Name and File URL are required arguments.');
        }

        var minVersion = '7',
            downloadNotice = null;
        if (options && options.minVersion) {
            minVersion = options.minVersion;
        }

        // If required QuickTime is not available provide a link to download instead of a movie object
        if (!AC.Detector.isMobile() && !AC.Detector.isValidQTAvailable(minVersion)) {
            downloadNotice = $(document.createElement('a'));
            downloadNotice.addClassName('quicktime-download');

            downloadNotice.setAttribute('href', 'http://www.apple.com/quicktime/download/');
            downloadNotice.innerHTML = options.downloadText || 'Get the latest QuickTime.';

            $$('body')[0].fire("QuickTime:noCompatibleQTAvailable", {controller: this, minVersion: minVersion});

            return downloadNotice;
        }

        if (AC.Detector.isIEStrict()) {
            AC.QuickTime.createEventSource();
        }

        if (options && !!options.factory) {
            return options.factory.create.call(options.factory, name, fileUrl, options);
        } else {
            return AC.QuickTime.Factory.Plugin.create(name, fileUrl, options);
        }
    },

    // Creates a binary behavior object so QuickTime elements in the page can
    // raise DOM Events in IE
    createEventSource: function()
    {
        var source_id = "qt_event_source",
            behavior,
            head;

        // Don't recreate it if we already have one by this id...?
        if (document.getElementById(source_id)) {
            return;
        }

        behavior = document.createElement('object');
        behavior.id = source_id;
        behavior.setAttribute('clsid', 'CB927D12-4FF7-4a9e-A169-56E4B8A75598');

        head = document.getElementsByTagName('head')[0];
        head.appendChild(behavior);
    }
};

// Legacy scripts will be using the old incorrect capitalization
// Don't hold that against them
AC.Quicktime = AC.QuickTime;

// == AC.QuickTime.Factory ==
// 
// A library of factories that know how to create media elements. You can
// actually use a factory directly to create an element. The
// AC.QuickTime.packageMovie function itself actually relies on these
// factories to do any actual element generation.
AC.QuickTime.Factory = {

    // == AC.QuickTime.Factory.Combined ==
    //
    // Factory that creates a video element, and nests the plugin elements
    // inside.
    //
    // Eventually this should be the default in cases where you don't
    // have any reason to care about what you get back.
    Combined: {

        // TODO temporary work around for IE only create the activeX control...
        // be sure to see the controller's _responsiveMediaElement method for further
        // discussion
        create: function(name, fileUrl, options)
        {
            var pluginElement = null,
                videoElement = null;

            pluginElement = AC.QuickTime.Factory.Plugin.create(name, fileUrl, options);
            if (AC.Detector.isIEStrict()) {
                return pluginElement;
            } else {
                videoElement = AC.QuickTime.Factory.Video.create(name, fileUrl, options);
                videoElement.appendChild(pluginElement);
                return videoElement;
            }
        }
    },

    // == AC.QuickTime.Factory.Plugin ==
    // 
    // Factory that creates the plugin elements only. This should be used
    // in cases where you know the video element would be inappropriate
    // such as movies that are simply buttons to launch QuickTime player
    // 
    // It is also the safest factory at the moment until we've tested the
    // video element in enough settings.
    Plugin: {

        create: function(name, fileUrl, options)
        {

            var outerObject = this._createOuterObject(name, fileUrl, options),
                innerObject = null;

            if (!AC.Detector.isIEStrict()) {
                //really imperitive we don't create an inner object for IE
                //this is what causes the N items remaining as it never actually
                //gets requested but IE still decides to report it as
                //needing to be loaded

                innerObject = this._createInnerObject(name, fileUrl, options);
                outerObject.appendChild(innerObject);
                outerObject.inner = innerObject;

            } else {

                // Attach to the binary behavior so we can raise DOM Events in IE
                // TODO just do this in CSS somewhere for all "object" elements
                outerObject.style.behavior = "url(#qt_event_source)";

                if (options.aggressiveCleanup !== false){

                    //knowing it's IE at this point, make sure we clear the movie when the page closes
                    //we also set our reference to null for good measure
                    Event.observe(window, 'unload', function() {
                        try {
                            outerObject.Stop();
                            } catch(e) {}
                            outerObject.style.display = 'none';
                            outerObject = null;

                            //this only masks memory leaks
                            //movie could still be present even if hidden
                            //if the movie remains in the viewport upon visiting other
                            //pages, you have remaining references to the movie somewhere
                            //they need to be removed as early as possible preferably
                            //or at least before you leave if you have no other choice

                            //closing the window that is leaking will throw an error message
                            //but at least due to the hiding it won't completely interrupt
                            //browsing
                    });

                }
            }

            this._configure(innerObject, outerObject, options);

            //force preservation of existing params even if the movie's URL is changed
            //this is the ooopiste of how QT works by default

            this._addParameter(outerObject, 'saveembedtags', true);
            this._addParameter(innerObject, 'saveembedtags', true);

            //instruct quicktime to post dom events
            this._addParameter(outerObject, 'postdomevents', true);
            this._addParameter(innerObject, 'postdomevents', true);

            //Needs to be last so IE sees all the parameters appended to
            //the object prior to loading the activex control
            outerObject.setAttribute('classid',
                'clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B');

            return outerObject;
        },

        _configure: function(innerObject, outerObject, options)
        {
            if (!options) {
                return false;
            }

            var property = null,
                attributeName = null;

            for (property in options) {
                if(options.hasOwnProperty(property)) {

                    attributeName = property.toLowerCase();

                    switch(attributeName) {
                        case('type'):
                        case('src'):
                        case('data'):
                        case('classid'):
                        case('name'):
                        case('id'):
                        case('postdomevents'):
                        case('saveembedtags'):
                        case('factory'):
                        case('aggressiveCleanup'):
                            //do nothing as these shouldn't be overridden or set
                        break;
                        case('class'):
                            Element.addClassName(outerObject, options[property]);
                        break;
                        case('innerId'):
                            if(innerObject) {
                                innerObject.setAttribute('id', options[property]);
                            }
                        break;
                        case('autoplay'):
                            this._addParameter(outerObject, 'autostart', options[property]);
                            this._addParameter(innerObject, 'autostart', options[property]);
                        break;
                        case('width'):
                        case('height'):
                            outerObject.setAttribute(attributeName, options[property]);
                            if(innerObject) {
                                innerObject.setAttribute(attributeName, options[property]);
                            }
                        break;
                        default:
                            this._addParameter(outerObject, attributeName, options[property]);
                            this._addParameter(innerObject, attributeName, options[property]);
                        break;
                    }
                }
            }
        },

        /**
        * Adds an object param tag to the specified parent
        * Note that the attributes are added in this seemingly odd order
        * so they show up in the logical order in the dom
        */
        _addParameter: function(parent, name, value)
        {
            if (!parent) {
                return;
            }

            var param = document.createElement('param');
            param.setAttribute('value', value);
            param.setAttribute('name', name);
            parent.appendChild(param);

            param = null;
        },

        /**
        * Creates the IE friendly outer object
        * NOTE Safari and Opera both seem to be able to use this one as well
        * I'm assuming this is due to some hacking on their part for
        * compatibility
        */
        _createOuterObject: function(name, fileUrl, options)
        {
            var outerObject = document.createElement('object'),
                activexVersion = '7,3,0,0';

            if (AC.Detector.isMobile() && options.posterFrame) {
                this._addParameter(outerObject, 'src', options.posterFrame);
                this._addParameter(outerObject, 'href', fileUrl);
                this._addParameter(outerObject, 'target', 'myself');
            } else {
                this._addParameter(outerObject, 'src', fileUrl);
            }

            outerObject.setAttribute('id', name);

            if (null !== options && (typeof options.codebase !== 'undefined') && '' !== options.codebase) {
                activexVersion = options.codeBase;
            }

            outerObject.setAttribute('codebase',
                'http://www.apple.com/qtactivex/qtplugin.cab#version=' + activexVersion);

            return outerObject;
        },

        /**
        * Creates the more standards compliant object which Firefox and Netscape
        * rely on to load the movie
        */
        _createInnerObject: function(name, fileUrl, options)
        {
            var innerObject = document.createElement('object');

            innerObject.setAttribute('type', 'video/quicktime');
            innerObject.setAttribute('data', fileUrl);
            innerObject.setAttribute('id', name + "Inner");
            innerObject.setAttribute("name", name);

            return innerObject;
        }
    },

    // == AC.QuickTime.Factory.Video ==
    // 
    // Factory that creates the video element only. For use in cases where 
    // there's no need for the plugin elements to be generated at all.
    // 
    // Should not be used until the video element is tested enough for
    // use to trust it.
    Video: {

        create: function(name, fileUrl, options)
        {
            var video = document.createElement('video');

            video.setAttribute('id', name);
            video.setAttribute('src', fileUrl);

            this._configure(video, options);

            return video;
        },

        _configure: function(video, options)
        {
            if(!options) {
                return false;
            }

            var property = null,
                attributeName = null;

            for (property in options) {
                if(options.hasOwnProperty(property)) {

                    attributeName = property.toLowerCase();

                    switch(attributeName) {
                        case('type'):
                        case('src'):
                        case('data'):
                        case('classid'):
                        case('name'):
                        case('id'):
                        case('postdomevents'):
                        case('saveembedtags'):
                        case('factory'):
                        case('aggressiveCleanup'):
                        case('innerId'):
                        case('cache'):
                        case('wmode'):
                        case('aggressivecleanup'):
                        case('showlogo'):
                            //do nothing as these shouldn't be overridden or set
                        break;
                        case('class'):
                            Element.addClassName(video, options[property]);
                        break;
                        case('controller'):
                            if (options[property]) {
                                video.setAttribute("controls", "controls");
                            }
                        break;
                        case('autoplay'):
                        case('autostart'):
                            if (options[property]) {
                                video.setAttribute("autoplay", "autoplay");
                            }
                        break;
                        default:
                            video.setAttribute(attributeName, options[property]);
                        break;
                    }
                }
            }
        }

    },

    // TODO implement an audio element factory
    Audio: {},

    // TODO implement the a null movie factory, really just enough of a movie
    // to trigger a missing plugin notice. This is already hardcoded above.
    Null: {}

};


// == AC.QuickTime.States ==
// 
// A library of states that an AC.QuickTime.Controller can adopt for
// monitoring and detaching from attached media.
AC.QuickTime.States = {

    // === AC.QuickTime.States.Polling ===
    // 
    // A state the controller can adopt to rely primarily on polling the
    // attached media for changes.
    // 
    // This is currently the most robust and recommended state.
    Polling: {

        isAvailable: function(movie)
        {
            return true;
        },

        toString: function()
        {
            return "Polling State";
        },

        detachFromMovie: Prototype.emptyFunction,

        reset: Prototype.emptyFunction,

        _monitor: function()
        {
            // Important we cache this before we check the status
            // we always want to make sure didBecomePlayable fires before didBegin
            var isPlayingNow = this.isPlaying(),
                status = null;

            if (!this._acknowledgedPlayable) {
                status = this.GetPluginStatus();
                if ("Playable" === status || "Complete" === status) {
                    this._didBecomePlayable();
                }
            } else {

                // don't change states if we're jogging the video
                if (!this.isJogging) {

                    //is the movie playing right now but it wasn't before?
                    if (isPlayingNow && !this.playing) {
                        this._didStart();
                    } else if (!isPlayingNow && this.playing) {
                        this._didStop();
                    }
                }
            }

            // Update the controller to reflect the state of the movie
            this.updateController();

            if(this.movie !== null) {
                this.movieWatcher = setTimeout(AC.Quicktime.States.Polling._monitor.bind(this), this._monitorDelay);
            }
        },

        monitorMovie: function()
        {
            this.movieWatcher = setTimeout(AC.Quicktime.States.Polling._monitor.bind(this), this._monitorDelay);
        }

    },

    Events: {


        eventNames: {
            plugin: {
                loadstart: "loadstart",
                progress: "qt_progress",
                loadedmetadata: "loadedmetadata",
                loadedfirstframe: "loadedfirstframe",
                load: "load",
                abort: "abort",
                error: "error",
                emptied: "emptied",
                stalled: "stalled",
                play: "qt_play",
                pause: "qt_pause",
                waiting: "waiting",
                seeking: "seeking",
                seeked: "seeked",
                timeupdate: "timeupdate",
                ended: "qt_ended",
                dataunavailable: "dataunavailable",
                canshowcurrentframe: "canshowcurrentframe",
                canplay: "canplay",
                canplaythrough: "qt_canplaythrough",
                ratechange: "ratechange",
                durationchange: "durationchange",
                volumechange: "volumechange"
            },

            video: {
                loadstart: "loadstart",
                progress: "progress",
                loadedmetadata: "loadedmetadata",
                loadedfirstframe: "loadedfirstframe",
                load: "load",
                abort: "abort",
                error: "error",
                emptied: "emptied",
                stalled: "stalled",
                play: "play",
                pause: "pause",
                waiting: "waiting",
                seeking: "seeking",
                seeked: "seeked",
                timeupdate: "timeupdate",
                ended: "ended",
                dataunavailable: "dataunavailable",
                canshowcurrentframe: "canshowcurrentframe",
                canplay: "canplay",
                canplaythrough: "canplaythrough",
                ratechange: "ratechange",
                durationchange: "durationchange",
                volumechange: "volumechange"
            }
        },

        isAvailable: function(movie)
        {
            return AC.Detector.isQTCompatible("7.3");
        },

        toString: function()
        {
            return "DOM Event State";
        },

        detachFromMovie: function()
        {
            var eventNames = AC.QuickTime.States.Events._eventNames(this.movie);

            Event.stopObserving(this.movie, eventNames.canplaythrough, this._didBecomePlayableCallback);
            // Event.stopObserving(this.movie, eventNames.ended, this._didStopCallback);
            Event.stopObserving(this.movie, eventNames.pause, this._didStopCallback);
            Event.stopObserving(this.movie, eventNames.play, this._didStartCallback);
            //TODO Event.stopObserving(this.movie, 'qt_progress', ___) from monitor
            // Event.stopObserving(this.movie, eventNames.pause, this._showPauseControlCallback);
            // Event.stopObserving(this.movie, eventNames.play, this._showPlayControlCallback);
        },

        _eventNames: function(movie)
        {
            var eventNames = movie.tagName.match(/video/i) ? AC.QuickTime.States.Events.eventNames.video : AC.QuickTime.States.Events.eventNames.plugin;
            return eventNames;
        },

        reset: Prototype.emptyFunction,

        _monitor: function()
        {
            var status = null,
                eventNames;
            if (!this._acknowledgedPlayable) {
                status = this.GetPluginStatus();
                if ("Playable" === status || "Complete" === status) {
                    this._didBecomePlayable();

                    // if the movie is already playing as we're attaching
                    // manually fire the _didStart
                    if (this.isPlaying()) {
                        this._didStart();
                    }

                    // and then set up the listener for any subsequent play events
                    eventNames = AC.QuickTime.States.Events._eventNames(this.movie);
                    this._didStartCallback = this._didStart.bind(this);
                    Event.observe(this.movie, eventNames.play, this._didStartCallback);
                }
            }

            this.updateController();

            if(this.movie !== null) {
                this.movieWatcher = setTimeout(AC.Quicktime.States.Events._monitor.bind(this), this._monitorDelay);
            }
        },

        monitorMovie: function()
        {
            if (!this._hasBegunMonitoring) {
                var eventNames = AC.QuickTime.States.Events._eventNames(this.movie);

                // TODO to prevent a disparity between what happens when
                // attaching to a new movie and one already in progress
                // we adopt the same strategy as the polling state
                // and discover the status based upon what we noticed as we
                // monitor the movie
                // this._didBecomePlayableCallback = this._didBecomePlayable.bind(this)
                // Event.observe(this.movie, eventNames.canplaythrough, this._didBecomePlayableCallback);

                this._didStopCallback = this._didStop.bind(this);
                Event.observe(this.movie, eventNames.pause, this._didStopCallback);
                // TODO not sure we need to bother with this considering how we handle
                // stop events causes a net effect of us calling didEnd twice on delegates
                Event.observe(this.movie, eventNames.ended, this._didStopCallback);
            }

            this.movieWatcher = setTimeout(AC.Quicktime.States.Events._monitor.bind(this), this._monitorDelay);
        }
    }
};

// == AC.QuickTime.Controller ==
//
// This is the controller class for attaching to a media element in the
// document. 
// 
// Controllers can exist without being attached to anything, but they're 
// pretty useless. Once attached you can control and observe the media though
// a controller object. 
// 
// The controller abstracts which element you are actually communicating with
// so you're free to simply deal with the controller.
//
// The controller can also be rendered into the document as a CSS-styleable
// playback controller.
// 
// If you run into issues trying to control a movie:
// * Movie needs to be at least 1x1 px to be addressable in firefox
// * Movie needs to be visible on screen to be addressable in firefox
// * If you remove a media element from the DOM, it may not behave expectedly
//   if reinserted.
//
// Note that the **preferred controller name is {{{AC.QuickTime.Controller}}}**.  The previous name,
// {{{AC.QuicktimeController}}} is deprecated.
// 
// ==== Delegate Methods And Notifications ====
//
// Note: This functionality relies on Prototype 1.6 custom events.
// The event source is the body element, though eventually provisions may
// be made to specify another event source.
// 
// All data passed along with the event is included inside the {{{event.memo}}}
// hash. Unless otherwise noted the same parameters passed to the delegate
// methods are passed to the parallel event inside the memo keyed with the
// specified names.
//
// Delegate method and event notification names are the same unless otherwise noted.
// All events are prefixed with "QuickTime:" and need to be observed like any
// other Prototype event.
//
// * {{{didRenderController(controller)}}} Delegate method and event:
//   The controller has rendered itself into the DOM. This *only* fires the
//   render() method is used to insert the controller.
//
// * {{{didAttach(controller)}}} method and event:
//   The controller did attach to a responsive media element.
//
// * {{{didBecomePlayable(controller)}}} method and {{{canplaythrough}}} event:
//   The attached media has loaded enough that it can begin playback
//   and expect to finish.
//
// * {{{didBegin(controller)}}} method and {{{begin}}} event: The attached
//   media has started playing for the first time.
//
// * {{{didStart(controller)}}} method and {{{start}}} event: The attached 
//   media has started playing any time other than the first time.
//
// * {{{didPlayProgress(controller, currentTime, duration)}}} method and
//   event: The playhead has progressed such that the media is showing 
//   another point in its timeline.
//
// * {{{didStartJogging(controller, time)}}} method and event: The user
//   initiated a jog event
//   using the custom controller.
//
// * {{{didStopJogging(controller, time)}}} method and event: The user
//   stopped a jog event
//   using the custom controller.
//
// * {{{didStop(controller)}}} method and {{{stop}}} event:
//   The attached media has stopped, but not ended.
//
// * {{{didEnd(controller)}}} method and {{{end}}} event:
//   The attached media has ended.
//
// * {{{didDetach(controller)}}} method and event:
//   The controller has detached from the media it was attached to.
// 
// * {{{didSetClosedCaptions(controller, enabled)}}} method and event: 
//   The attached media has had its closed captioning enabled or disabled.
// 
AC.QuickTime.Controller = Class.create();

// Legacy scripts will be using the old incorrect capitalization
// Don't hold that against them
AC.QuicktimeController = AC.QuickTime.Controller;
AC.QuickTimeController = AC.QuickTime.Controller;

AC.QuickTime.Controller.prototype = {

    movie: null,
    options: null,

    movieAttacher: null,
    attachDelay: 500,

    movieWatcher: null,
    normalMonitorDelay: 480,
    longMonitorDelay: 4800,
    _monitorDelay: this.normalMonitorDelay,
    _hasBegunMonitoring: false,

    currentTime: 0,
    percentLoaded: 0,
    maxBytesLoaded: 0,
    movieSize: 0,

    allowAttach: true,

    controllerPanel: null,
    currentControl: null,
    playControl: null,
    pauseControl: null,
    slider: null,
    track: null,
    playHead: null,
    loadedProgress: null,
    state: null,

    _closedCaptionsAvailable: false,
    _closedCaptionsEnabled: false,
    _closedCaptionTrackIndex: 4,

    _acknowledgedPlayable: false,
    isJogging: false,
    hardPaused: false,
    duration: 0,
    finished: false,
    playing: false,

    unloader: null,


    // ** {{{ AC.QuickTime.Controller.initialize(movie, options) }}} **
    // 
    // Initializes the receiver with optional movie element and options
    // Specifying a movie starts the attachment and monitoring process
    // Note that in this case it is suggested that you also specify the
    // renderInto option if you want a visible controller so that it can be
    // wired up during attachment.
    // 
    // {{{movie}}}: The element you suspect is linked to a media plugin
    //              or object.\\
    // {{{options}}}: Associative array of options used to configure the
    //              controller.
    // 
    // 
    // Note that often you'll want to render the controller into
    // the page long before you even try inserting the movies.
    // In this case you need to:
    // # Init and render the controller with no movie
    // # Build and append the movie when needed
    // # Attach the controller to the inserted movie
    initialize: function(movie, options)
    {
        this._eventSource = $$('body')[0];

        this.options = options || {};

        if (this.options.delegate) {
            this.setDelegate(this.options.delegate);
        }

        if (this.options.renderInto) {
            this.render(this.options.renderInto);
        }

        this.attachToMovie(movie, options);
    },

    // ** {{{ AC.QuickTime.Controller.setDelegate(value) }}} **
    // 
    // Sets the delegate of the receiver.
    // 
    // {{{value}}}: the delegate
    setDelegate: function(value)
    {
        this.delegate = value;
    },

    // ** {{{ AC.QuickTime.Controller.setState(state, movie) }}} **
    // 
    // Sets the state of the receiver to the state specified. If none is
    // specified a state is chosen based upon which one is appropriate for
    // the given movie.
    // 
    // {{{state}}}: The AC.QuickTime.States Class to use as the state of 
    //               this controller.\\
    // {{{movie}}}: The media element to use to help determine which state
    //              to use if none is provided.
    // 
    // //FIXME there's no way to not specify state given method signature//
    setState: function(state, movie)
    {
        if (typeof(state) !== 'undefined') {
            this.state = state;
        } else {
            this.state = AC.Quicktime.States.Polling;
        }
    },

    // ** {{{ AC.QuickTime.Controller.attachToMovie(movie, options) }}} **
    // 
    // Attaches the receiver to the specified movie with specified options.
    attachToMovie: function(movie, options)
    {
        if(!movie || !this.allowAttach) {
            return;
        }

        if (!$(movie)) {
            throw 'Movie has to be appended to document prior to attaching to with a controller.';
        }

        if (this.movie) {
            this.detachFromMovie();
        }

        // While I could initialize these when the controller is initialized
        // I'm only doing so when you actually attach to a movie.
        // While unattached, there really is no concept of tracktypes or
        // tracknames so I can use that case as a chance to warn that there
        // is no movie attached
        // TODO arguably the same could be said for any plugin method called
        // prior to attaching to a movie.
        this._trackTypes = [];
        this._trackNames = [];
        this._chapterNames = [];

        clearInterval(this.movieAttacher);

        if (typeof options !== 'undefined') {
            this.options = options;
        }

        //if not scheduled for detaching on unload, schedule detachment
        if (!this.unloader) {
            this.unloader = this.detachFromMovie.bind(this);
            Event.observe(window, 'unload', this.unloader);
        }

        this._startLoadingIndicator(movie);

        this.movieAttacher = setInterval(this._attach.bind(this, movie), this.attachDelay);

        movie = null;
    },

    // Internal attachment callback
    _attach: function(movie)
    {
        if(!this.allowAttach) {
            return;
        }

        this.movie = this._responsiveMediaElement(movie);

        if (this.movie) {
            clearInterval(this.movieAttacher);

            this.setState(this.options.state);
            this.movieIsVideo = !!this.movie.tagName.match(/video/i);

            if (this.delegate && typeof this.delegate.didAttach === "function") {
                this.delegate.didAttach(this);
            }

            this._eventSource.fire("QuickTime:didAttach", {controller: this});

            this.monitorMovie();
        }

        movie = null;
    },

    // Add the "loading" class name to the movie container and the controller panel
    _startLoadingIndicator: function(movie)
    {
        this._movieContainer = movie.parentNode;
        Element.addClassName(this._movieContainer, 'movie-loading');
        if (this.controllerPanel) {
            Element.addClassName(this.controllerPanel, 'movie-loading');
        }
    },

    // Remove the "loading" class name from the movie container and the controller panel
    _stopLoadingIndicator: function()
    {
        if (this._movieContainer) {
            Element.removeClassName(this._movieContainer, 'movie-loading');
            this._movieContainer = null;
        }

        if (this.controllerPanel) {
            Element.removeClassName(this.controllerPanel, 'movie-loading');
        }
    },

    // Returns the node that appears to be a responsive media element
    // within the supplied nodetree. 
    // This search favors video elements, then object, and finally embeds
    _responsiveMediaElement: function(elem)
    {
        try {
            if (!elem) {
                return null;
            } else if (elem.play || elem.Play) {
                return elem;
            } else {
                return this._responsiveMediaElement(elem.down('video, object, embed'));
            }
        } catch (e) {
            // IE does not like simply looking for the properties
            // it complains about the properites not existing on the element
            // regardless of how you check for it elem.play or elem["play"]
            // although it seems to have no problem calling functions,
            // so we call an idempotent one and use that for checking if
            // we've found a responsiveMedia element in IE
            if (elem.GetPluginStatus()) {
                return elem;
            } else {
                return this._responsiveMediaElement(elem.down('video, object, embed'));
            }
            return null;
        }
    },

    // ** {{{ AC.QuickTime.Controller.detachFromMovie()) }}} **
    // 
    // Detaches the receiver from any connected media element, resetting the
    // receiver in the process.
    detachFromMovie: function()
    {
        this.allowAttach = false;

        if (this.state) {
            this.state.detachFromMovie.call(this);
        }

        clearInterval(this.movieAttacher);
        clearTimeout(this.movieWatcher);

        this.movie = null;

        this.reset();

        // Reset empties these arrays but doesn't null them.
        // When detaching, however, we need need actually null them out as we
        // know there is no movie attached at this point.
        this._trackTypes = null;
        this._trackNames = null;
        this._chapterNames = null;

        //if we've manually detached, there's no need to do it on unload
        Event.stopObserving(window, 'unload', this.unloader);
        this.unloader = null;

        this.allowAttach = true;

        if (this.delegate && typeof this.delegate.didDetach === "function") {
            this.delegate.didDetach(this);
        }

        this._eventSource.fire("QuickTime:didDetach", {controller: this});
    },

    // ** {{{ AC.QuickTime.Controller.monitorMovie()) }}} **
    // 
    // Set the receiver to begin monitoring the attached media element for
    // interesting events such as begin, start, and stop.
    // 
    // If a controller is rendered into the document this also begins updating
    // the controller's user interface.
    monitorMovie: function()
    {
        if (!this.movie) {
            throw new Error("Cannot begin monitoring until attached to a movie");
        }

        this.state.monitorMovie.call(this);

        if(this.controllerPanel !== null && !this._hasBegunMonitoring) {

            this.slider = new Control.Slider(this.playHead, this.track, {
                onSlide: function(value) {

                    if (isNaN(value)) {
                        return;
                    }

                    this.trackProgress.style.width = this.slider.translateToPx(value);


                    // If we're not jogging yet, start jogging
                    if (!this.isJogging) {
                        this._didStartJogging();
                    }

                    this.SetTime(value * this.GetDuration());

                }.bind(this),

                onChange: function(value) {

                    if(isNaN(value)) {
                        return;
                    }

                    // If the controller is jogging, it's stopped now as it registers the change
                    if (this.isJogging) {
                        this._didStopJogging();
                    }

                    this.trackProgress.style.width = this.slider.translateToPx(value);

                }.bind(this)
            });

            //This was showing up a lot in profiling and was pretty much constantly setting and removing
            //a selected class on the slider handle. Which I couldn't care less about
            //There's probably a better way to handle this but this at least stops a lot of "unnecessary" dom class changes
            this.slider.updateStyles = Prototype.emptyFunction;

        }

        this._hasBegunMonitoring = true;
    },

    // Notifies the receiver that its attached movie became playable
    _didBecomePlayable: function()
    {
        this._stopLoadingIndicator();

        // Make sure the controller loaded progress is correct in this case
        this.updateController();

        if (this._acknowledgedPlayable) {
            return;
        }

        this._acknowledgedPlayable = true;

        // Try to find the closed caption trackCount
        // (tragically the index starts at 1, not 0)
        var trackCount = this.GetTrackCount(),
            i;
        for (i = 1; i <= trackCount; i++) {
            if ('Closed Caption' === this.GetTrackType(i)) {
                this._closedCaptionTrackIndex = i;
                this._setClosedCaptionsAvailable(true);
            }
        }

        // LEGACY callback options are deprecated
        if (this.options.onMoviePlayable && typeof this.options.onMoviePlayable === "function") {
            this.options.onMoviePlayable();
        }

        if (this.delegate && typeof this.delegate.didBecomePlayable === "function") {
            this.delegate.didBecomePlayable(this);
        }

        this._eventSource.fire("QuickTime:canplaythrough", {controller: this});
    },

    // The number of times the attached movie has played/unpaused
    playedCount: 0,

    // Notifies the receiver that its attached movie started playing
    _didStart: function()
    {
        if (this.wasJogging) {
            // We wan't to ignore the didStart when we first notice the movie
            // is playing again after a jog, since it wasn't an explicit play, 
            // but a resume after jogging
            this.wasJogging = false;
            return;
        }

        this.playing = true;

        // Movie started playing, show the pause control
        if (this.controllerPanel) {
            this.controllerPanel.replaceChild(this.pauseControl, this.currentControl);
            this.currentControl = this.pauseControl;
        }

        // LEGACY onStart callback
        if (typeof this.options.onMovieStart === "function") {
            this.options.onMovieStart();
        }

        if (0 === this.playedCount) {
            if (this.delegate && typeof this.delegate.didBegin === "function") {
                this.delegate.didBegin(this);
            }

            this._eventSource.fire("QuickTime:begin", {controller: this});

        } else {
            if (this.delegate && typeof this.delegate.didStart === "function") {
                this.delegate.didStart(this);
            }

            this._eventSource.fire("QuickTime:start", {controller: this});
        }
        this.playedCount += 1;
    },

    // Notifies the receiver that its attached movie paused
    // If the movie has finished this will rely on _didEnd functionality
    _didStop: function()
    {
        if (this.isJogging) {
            return;
        }

        this.playing = false;

        // Movie stopped playing, show the play control
        if (this.controllerPanel) {
            this.controllerPanel.replaceChild(this.playControl, this.currentControl);
            this.currentControl = this.playControl;
        }

        // LEGACY onStop callback
        if (typeof this.options.onMovieStop === "function") {
            this.options.onMovieStop();
        }

        var time = this.GetTime(),
            duration = this.GetDuration();

        if (time >= duration) {

            // The movie ended, fire this one last time so the
            // current time matches the duration on any time displays
            // or anything people were running expecting the time to match
            // at the end of the movie
            this._didPlayProgress(duration, duration);

            if (this.options.onMovieFinished && typeof this.options.onMovieFinished === "function") {
                this.options.onMovieFinished();
            }

            if (this.delegate && typeof this.delegate.didEnd === "function") {
                this.delegate.didEnd(this);
            }

            this._eventSource.fire("QuickTime:end", {controller: this});

        } else {
            if (this.delegate && typeof this.delegate.didStop === "function") {
                this.delegate.didStop(this);
            }

            this._eventSource.fire("QuickTime:stop", {controller: this});
        }
    },

    // ** {{{ AC.QuickTime.Controller.reset()) }}} **
    // 
    // Resets the receiver such that the receiver appears to have never
    // attached to a media element. This *does not* detach the controller
    // from any media element it is currently attached to.
    // 
    // See {{{AC.QuickTime.Controller.detachFromMovie()}}} for actually
    // detaching from any attached media.
    reset: function()
    {
        if (this.state) {
            this.state.reset.call(this);
        }

        this.duration = 0;
        this.movieSize = 0;
        this.maxBytesLoaded = 0;
        this.percentLoaded = 0;
        this.movieIsVideo = false;
        this._acknowledgedPlayable = false;
        this.playedCount = 0;
        this._hasBegunMonitoring = false;
        this._monitorDelay = this.normalMonitorDelay;

        // When resetting, clear out cached track names and types
        // don't null them though as we may still be attached to a movie
        // these get nulled on detachment only
        this._trackCount = NaN;
        this._trackTypes = [];
        this._trackNames = [];

        this._chapterCount = NaN;
        this._chapterNames = [];

        this.setClosedCaptionsEnabled(false);
        this._setClosedCaptionsAvailable(false);

        delete this.timeScale;

        if (this.slider) {
            this.slider.setValue(0);
            this.slider.trackLength = this.slider.maximumOffset() - this.slider.minimumOffset();
        }

        this._stopLoadingIndicator();

        if (this.loadedProgress) {
            this.loadedProgress.style.width = "0px";
        }
    },

    // ** {{{ AC.QuickTime.Controller.render(containerId)) }}} **
    // 
    // Returns the controller as an interactive user interface that can 
    // be inserted into the DOM.
    // 
    // If a media element is attached, the controller begins monitoring it.
    // 
    // {{{containerId}}}: Optional element to insert the controller node into.
    //                    If not used, the returned node can be appended later.
    render: function(containerId)
    {
        if (typeof containerId !== "undefined" && !$(containerId)) {
            throw new Error("Specified container ID, '" + containerId + "' not found in DOM");
        }

        this.controllerPanel = $(document.createElement('div'));
        Element.addClassName(this.controllerPanel, 'ACQuicktimeController');

        //TODO encapsulate button creation
        this.playControl = document.createElement('div');
        Element.addClassName(this.playControl, 'control');
        Element.addClassName(this.playControl, 'play');
        this.playControl.innerHTML = 'Play';
        this.playControl.onclick = this.Play.bind(this);

        this.pauseControl = document.createElement('div');
        Element.addClassName(this.pauseControl, 'control');
        Element.addClassName(this.pauseControl, 'pause');
        this.pauseControl.innerHTML = 'Pause';
        this.pauseControl.onclick = this.Stop.bind(this);

        var playing = false;

        if(null !== this.movie) {
            playing = this.GetAutoPlay();
        }

        this.currentControl = (playing) ? this.pauseControl : this.playControl;
        this.controllerPanel.appendChild(this.currentControl);

        this.sliderPanel = document.createElement('div');
        Element.addClassName(this.sliderPanel, 'sliderPanel');

        this.track = document.createElement('div');
        Element.addClassName(this.track, 'track');
        this.sliderPanel.appendChild(this.track);

        this.loadedProgress = document.createElement('div');
        Element.addClassName(this.loadedProgress, 'loadedProgress');
        this.track.appendChild(this.loadedProgress);

        this.trackProgress = document.createElement('div');
        Element.addClassName(this.trackProgress, 'trackProgress');
        this.track.appendChild(this.trackProgress);

        this.playHead = document.createElement('div');
        Element.addClassName(this.playHead, 'playHead');
        this.track.appendChild(this.playHead);

        this.controllerPanel.appendChild(this.sliderPanel);

        this.timeDisplay = document.createElement('div');
        Element.addClassName(this.timeDisplay, 'timeDisplay');

        this.controllerPanel.appendChild(this.timeDisplay);

        if (containerId) {
            $(containerId).appendChild(this.controllerPanel);
            this.trackWidth = Element.getDimensions(this.track).width;

            if (this.delegate && typeof this.delegate.didRenderController === "function") {
                this.delegate.didRenderController(this);
            }

            this._eventSource.fire("QuickTime:didRenderController", {controller: this});

            if (this.movie) {
                this.monitorMovie();
            }
        }

        return this.controllerPanel;
    },

    // ** {{{ AC.QuickTime.Controller.setTimeDisplayString(display)) }}} **
    // 
    // Sets the text visible within the controller's time display output.
    // 
    //  {{{display}}}: The string to display.
    // 
    // //TODO (completed on a branch) allow passing in dom elements
    setTimeDisplayString: function(display)
    {
        if(this.timeDisplay) this.timeDisplay.innerHTML = display;
    },

    // TODO should not be public
    updateController: function(loaded)
    {
        if (!this.controllerPanel) {
            return;
        }

        this._updateControllerLoadedProgress();

        // Ensure we don't stop updating while jogging
        // Also make sure we update if we're at the end of the movie, even if we're no long playing
        if (this.isJogging || this.isPlaying()) {

            var oldTime = this.currentTime,
                newTime = this.GetTime(),
                duration = this.GetDuration();

            if (!isNaN(oldTime) && !isNaN(newTime) && oldTime !== newTime) {

                // if we're jogging the slider is already getting updated
                if (!this.isJogging) {
                    this.slider.setValue(newTime/duration);
                }

                this._didPlayProgress(newTime, duration);
            }
        }
    },

    _didPlayProgress: function(newTime, duration)
    {
        if (this.delegate && typeof this.delegate.didPlayProgress === "function") {
            this.delegate.didPlayProgress(this, newTime, duration);
        }

        this._eventSource.fire("QuickTime:didPlayProgress", {controller: this, currentTime: newTime, duration: duration});
    },

    _updateControllerLoadedProgress: function()
    {
        if(this.percentLoaded < 1) {

            var trackWidth = Element.getDimensions(this.track).width,
                loaded = this.GetMaxBytesLoaded()/this.GetMovieSize(),
                progressWidth = 0;

            if(!isNaN(loaded) && 0 !== loaded) {
                this.percentLoaded = loaded;
            }

            progressWidth = trackWidth * this.percentLoaded;
            Element.setStyle(this.loadedProgress, {width: progressWidth + 'px'});
        }
    },

    _didStartJogging: function()
    {
        if (!this.isJogging) {

            // If the movie is playing right now, continue playing
            // after the jog is over
            this.playAfterJog = this.isPlaying();

            // Set the controller to jog mode so the call to stop
            // generates a didStartJogging event and not a 
            // didStop event
            this.isJogging = true;

            // Stop the movie during the jog
            this.Stop();

            var time = this.GetTime();

            if (this.delegate && typeof this.delegate.didStartJogging === "function") {
                this.delegate.didStartJogging(this, time);
            }

            this._eventSource.fire("QuickTime:didStartJogging", {controller: this, time: time});
        }
    },

    _didStopJogging: function()
    {
        this.isJogging = false;

        var time = this.GetTime();

        if (this.delegate && typeof this.delegate.didStopJogging === "function") {
            this.delegate.didStopJogging(this, time);
        }

        this._eventSource.fire("QuickTime:didStopJogging", {controller: this, time: time});

        // Start playing again if necessary,
        // The controller is out of jog mode right now
        // But be sure that the Play generates a 
        // didStartJogging event and not a didStart event
        if (this.playAfterJog) {
            this.wasJogging = true;
            this.Play();
        }
    },

    // ** {{{ AC.QuickTime.Controller.Play()) }}} **
    // 
    // Plays the movie at the default rate, starting from the movie’s 
    // current time.
    Play: function()
    {
        if (null !== this.movie) {
            try {
                if (this.movieIsVideo) {
                    this.movie.play();
                } else {
                    this.movie.Play();
                }
            } catch(e) {}
        }
    },

    // ** {{{ AC.QuickTime.Controller.Stop()) }}} **
    // 
    // Stops the movie without changing the movie’s current time.
    Stop: function()
    {
        if (null !== this.movie) {
            try {
                if (this.movieIsVideo) {
                    this.movie.pause();
                } else {
                    this.movie.Stop();
                }
            } catch(e) {}
        }
    },

    // ** {{{ AC.QuickTime.Controller.Rewind()) }}} **
    // 
    // Sets the current time to the movie’s start time and pauses the movie.
    Rewind: function()
    {
        if (null !== this.movie) {
            this.movie.Stop();
            this.movie.Rewind();
        }
    },

    // ** {{{AC.QuickTime.Controller.Step(count) }}} **
    //
    // Steps the movie forward or backward the specified number of frames
    // from the point at which the command is received. If the movie’s 
    // rate is non-zero, it is paused.
    Step: function(count)
    {
        this.movie.Step(count);
    },

    // ** {{{AC.QuickTime.Controller.ShowDefaultView() }}} **
    //
    // Displays a QuickTime VR movie’s default node, using the default
    // pan angle, tilt angle, and field of view as set by the movie’s author.
    ShowDefaultView: function()
    {
        this.movie.ShowDefaultView();
    },

    // ** {{{AC.QuickTime.Controller.GoPreviousNode() }}} **
    //
    // Returns to the previous node in a QuickTime VR movie (equivalent to 
    // clicking the Back button on the VR movie controller).
    GoPreviousNode: function()
    {
        this.movie.GoPreviousNode();
    },

    // ** {{{AC.QuickTime.Controller.GetQuicktimeVersion() }}} **
    //
    // Returns the version of QuickTime.
    GetQuicktimeVersion: function()
    {
        return this.movie.GetQuickTimeVersion();
    },

    // ** {{{AC.QuickTime.Controller.GetQuicktimeLanguage() }}} **
    //
    // Returns the user’s QuickTime language (set through the plug-in’s 
    // Set Language dialog).
    GetQuicktimeLanguage: function()
    {
        return this.movie.GetQuicktimeLanguage();
    },

    // ** {{{AC.QuickTime.Controller.GetQuicktimeConnectionSpeed() }}} **
    //
    // Returns the connection speed setting from the users 
    // QuickTime preferences.
    GetQuicktimeConnectionSpeed: function()
    {
        return this.movie.GetQuicktimeConnectionSpeed();
    },

    // ** {{{AC.QuickTime.Controller.GetIsQuickTimeRegistered() }}} **
    //
    // Returns true if the user is registered for the Pro version
    // of QuickTime; otherwise returns false.
    GetIsQuickTimeRegistered: function()
    {
        return this.movie.GetIsQuickTimeRegistered();
    },

    // ** {{{AC.QuickTime.Controller.GetComponentVersion() }}} **
    //
    // Returns the version of a specific QuickTime component. The component
    // is specified using a four character string for the type, subtype, and
    // manufacturer. For example, to check the version of Apple’s JPEG
    // graphics importer call GetComponentVersion> 'grip','http://images.apple.com/tw/global/scripts/JPEG','appl').'0'
    // is a wildcard for any field. If the component is not available,
    // 0.0 is returned.
    GetComponentVersion: function()
    {
        return this.movie.GetComponentVersion();
    },

    // ** {{{AC.QuickTime.Controller.GetPluginVersion() }}} **
    //
    // Returns the version of the QuickTime plug-in.
    GetPluginVersion: function()
    {
        return this.movie.GetPluginVersion();
    },

    // ** {{{AC.QuickTime.Controller.ResetPropertiesOnReload() }}} **
    //
    // By default, most movie and plug-in properies are reset when a new
    // movie is loaded. For example, when a new movie loads,
    // the default controller setting is true for a linear movie and false
    // for a VR movie, regardless of the prior setting. If this property
    // is set to false, the new movie inherits the settings in use
    // with the current movie.
    ResetPropertiesOnReload: function()
    {
        this.movie.ResetPropertiesOnReload();
    },

    // ** {{{AC.QuickTime.Controller.GetPluginStatus() }}} **
    //
    // GetPluginStatus returns a string with the status of the current movie.
    // Possible states are:
    // * ”Waiting”—waiting for the movie data stream to begin
    // * ”Loading”—data stream has begun, not able to play/display the
    //   movie yet
    // * ”Playable”—movie is playable, although not all data has been
    //   downloaded
    // * ”Complete”—all data has been downloaded
    // * ”Error: <error number>”—the movie failed with the specified 
    //   error number
    // 
    // * Note: Even though the method is named GetPluginStatus it gets the
    // status of a specific movie, not the status of the plug-in as a
    // whole. If more than one movie is embedded in a document, there can be
    // a different status for each movie. For example, one movie could be
    // playable while another is still loading.
    GetPluginStatus: function()
    {
        if (!this.movieIsVideo) {
            try {
                return this.movie.GetPluginStatus();
            } catch (e) {
                return "Waiting";
            }
        } else {
            // TODO this is one of those rough areas we need to either make
            // behave like the plugin or the video element
            var code = this.movie.readyState;
            return code > 2 ? "Playable" : "Waiting";
        }
    },

    // ** {{{AC.QuickTime.Controller.GetAutoPlay() }}} **
    //
    // Get whether a movie automatically starts playing as
    // soon as it can.
    // 
    GetAutoPlay: function() {
        return this.movie.GetAutoPlay();
    },

    // ** {{{AC.QuickTime.Controller.SetAutoPlay(autoPlay) }}} **
    //
    // Set whether a movie automatically starts playing as
    // soon as it can.
    // 
    // The Set method is roughly equivalent to setting the AUTOPLAY
    // parameter in the <EMBED> tag, but the @HH:MM:SS:FF feature is not
    // yet supported in JavaScript.
    SetAutoPlay: function(autoPlay)
    {
        this.movie.SetAutoPlay(autoPlay);
    },

    // ** {{{AC.QuickTime.Controller.GetControllerVisible() }}} **
    //
    GetControllerVisible: function()
    {
        return this.movie.GetControllerVisible();
    },

    // ** {{{AC.QuickTime.Controller.SetControllerVisible(visible) }}} **
    //
    SetControllerVisible: function(visible)
    {
        this.movie.SetControllerVisible(visible);
    },

    // ** {{{AC.QuickTime.Controller.GetRate() }}} **
    //
    GetRate: function()
    {
        return this.movie.GetRate();
    },

    // ** {{{AC.QuickTime.Controller.SetRate(rate) }}} **
    //
    SetRate: function(rate)
    {
        this.movie.SetRate();
    },

    // ** {{{AC.QuickTime.Controller.GetTime() }}} **
    //
    GetTime: function()
    {

        var actualTime = 0;
        try {
            //IE sometimes throws an error on accessing this property on first load
            if (this.movieIsVideo) {
                actualTime = this.movie.currentTime;
            } else {
                actualTime = this.movie.GetTime();
            }
        } catch (e) {
            //ignore error
        }

        if (0 === actualTime) {
            //if we can't talk ot the plugin directly, estimate what time should be
            actualTime = this.currentTime + this._monitorDelay;
        } else {
            this.currentTime = actualTime;
        }

        return actualTime;
    },

    // ** {{{AC.QuickTime.Controller.SetTime(time) }}} **
    //
    SetTime: function(time)
    {
        try {
            if (this.movieIsVideo) {
                this.movie.currentTime = time;
            } else {
                this.movie.SetTime(time);
            }
        } catch(e) {
            // ignore error
        }
    },

    // ** {{{AC.QuickTime.Controller.GetVolume() }}} **
    //
    GetVolume: function()
    {
        return this.movie.GetVolume();
    },

    // ** {{{AC.QuickTime.Controller.SetVolume(volume) }}} **
    //
    SetVolume: function(volume)
    {
        this.movie.SetVolume(volume);
    },

    // ** {{{AC.QuickTime.Controller.GetMute() }}} **
    //
    GetMute: function()
    {
        return this.movie.GetMute();
    },

    // ** {{{AC.QuickTime.Controller.SetMute(mute) }}} **
    //
    SetMute: function(mute)
    {
        this.movie.SetMute(mute);
        // If the movie is muted, try to enable closed captions
        this.setClosedCaptionsEnabled(mute);
    },

    // ** {{{AC.QuickTime.Controller.GetMovieName() }}} **
    //
    GetMovieName: function()
    {
        return this.movie.GetMovieName();
    },

    // ** {{{AC.QuickTime.Controller.SetMovieName(movieName) }}} **
    //
    SetMovieName: function(movieName)
    {
        this.movie.SetMovieName(movieName);
    },

    // ** {{{AC.QuickTime.Controller.GetMovieID() }}} **
    //
    GetMovieID: function()
    {
        return this.movie.GetMovieID();
    },

    // ** {{{AC.QuickTime.Controller.SetMovieID(movieID) }}} **
    //
    SetMovieID: function(movieID)
    {
        this.movie.SetMovieID(movieID);
    },

    // ** {{{AC.QuickTime.Controller.GetStartTime() }}} **
    //
    GetStartTime: function()
    {
        return this.movie.GetStartTime();
    },

    // ** {{{AC.QuickTime.Controller.SetStartTime(time) }}} **
    //
    SetStartTime: function(time)
    {
        this.movie.SetStartTime(time);
    },

    // ** {{{AC.QuickTime.Controller.GetEndTime() }}} **
    //
    GetEndTime: function()
    {
        return this.movie.GetEndTime();
    },

    // ** {{{AC.QuickTime.Controller.SetEndTime(time) }}} **
    //
    SetEndTime: function(time)
    {
        this.movie.SetEndTime(time);
    },

    // ** {{{AC.QuickTime.Controller.GetBgColor() }}} **
    //
    GetBgColor: function()
    {
        return this.movie.GetBgColor();
    },

    // ** {{{AC.QuickTime.Controller.SetBgColor(color) }}} **
    //
    SetBgColor: function(color)
    {
        this.movie.SetBgColor(color);
    },

    // ** {{{AC.QuickTime.Controller.GetIsLooping() }}} **
    //
    GetIsLooping: function()
    {
        return this.movie.GetIsLooping();
    },

    // ** {{{AC.QuickTime.Controller.SetIsLooping(loop) }}} **
    //
    SetIsLooping: function(loop)
    {
        this.movie.SetIsLooping(loop);
    },

    // ** {{{AC.QuickTime.Controller.GetLoopIsPalindrome() }}} **
    //
    GetLoopIsPalindrome: function()
    {
        return this.movie.GetLoopIsPalindrome();
    },

    // ** {{{AC.QuickTime.Controller.SetLoopIsPalindrome(loop) }}} **
    //
    SetLoopIsPalindrome: function(loop)
    {
        this.movie.SetLoopIsPalindrome(loop);
    },

    // ** {{{AC.QuickTime.Controller.GetPlayEveryFrame() }}} **
    //
    GetPlayEveryFrame: function()
    {
        return this.movie.GetPlayEveryFrame();
    },

    // ** {{{AC.QuickTime.Controller.SetPlayEveryFrame(playAll) }}} **
    //
    SetPlayEveryFrame: function(playAll)
    {
        this.movie.SetPlayEveryFrame(playAll);
    },

    // ** {{{AC.QuickTime.Controller.GetHREF() }}} **
    //
    GetHREF: function()
    {
        return this.movie.GetHREF();
    },

    // ** {{{AC.QuickTime.Controller.SetHREF(url) }}} **
    //
    SetHREF: function(url)
    {
        this.movie.SetHREF(url);
    },

    // ** {{{AC.QuickTime.Controller.GetTarget() }}} **
    //
    GetTarget: function()
    {
        return this.movie.GetTarget();
    },

    // ** {{{AC.QuickTime.Controller.SetTarget(target) }}} **
    //
    SetTarget: function(target)
    {
        this.movie.SetTarget(target);
    },

    // ** {{{AC.QuickTime.Controller.GetQTNEXTUrl() }}} **
    //
    GetQTNEXTUrl: function()
    {
        return this.movie.GetQTNEXTUrl();
    },

    // ** {{{AC.QuickTime.Controller.SetQTNEXTUrl(index, url) }}} **
    //
    SetQTNEXTUrl: function(index, url)
    {
        this.movie.SetQTNEXTUrl(index, url);
    },

    // ** {{{AC.QuickTime.Controller.GetURL() }}} **
    //
    GetURL: function()
    {
        return this.movie.GetURL();
    },

    // ** {{{AC.QuickTime.Controller.SetURL(url) }}} **
    //
    SetURL: function(url)
    {
        if (!!this.movie.tagName.match(/video/i)) {
            this.movie.src = url;
            this.movie.load();
        } else {
            this.movie.SetURL(url);
        }
        //since the movie's changed make sure the controller is reset
        this.reset();
    },

    // ** {{{AC.QuickTime.Controller.GetKioskMode() }}} **
    //
    GetKioskMode: function()
    {
        return this.movie.GetKioskMode();
    },

    // ** {{{AC.QuickTime.Controller.SetKioskMode(kioskMode) }}} **
    //
    SetKioskMode: function(kioskMode)
    {
        this.movie.SetKioskMode(kioskMode);
    },

    // ** {{{AC.QuickTime.Controller.GetDuration() }}} **
    //
    GetDuration: function()
    {
        if (null === this.duration || 0 === this.duration || isNaN(this.duration) || this.duration === Infinity) {
            try {
                if (this.movieIsVideo) {
                    this.duration = this.movie.duration;
                } else {
                    this.duration = this.movie.GetDuration();
                }
            } catch(e) {
                this.duration = 0;
            }
        }
        return this.duration;
    },

    // ** {{{AC.QuickTime.Controller.GetMaxTimeLoaded() }}} **
    //
    GetMaxTimeLoaded: function()
    {
        return this.movie.GetMaxTimeLoaded();
    },

    // ** {{{AC.QuickTime.Controller.GetTimeScale() }}} **
    //
    GetTimeScale: function()
    {
        if (typeof this.timeScale !== 'undefined') {
            return this.timeScale;
        }

        try {
            if (this.movieIsVideo) {
                // FIXME I dunno what the api is to find this out
                this.timeScale = 2997;
            } else {
                this.timeScale = this.movie.GetTimeScale();
            }
        } catch (e) {
            //ignore error
        }

        return this.timeScale;
    },

    // ** {{{AC.QuickTime.Controller.GetMovieSize() }}} **
    //
    GetMovieSize: function()
    {
        if (0 === this.movieSize) {
            try {
                if (this.movieIsVideo) {
                    this.movieSize = this.movie.totalBytes;
                } else {
                    this.movieSize = this.movie.GetMovieSize();
                }
            } catch(e) {
                this.movieSize = 0;
            }
        }
        return this.movieSize;
    },

    // ** {{{AC.QuickTime.Controller.GetMaxBytesLoaded() }}} **
    //
    GetMaxBytesLoaded: function()
    {
        try {
            if (this.movieIsVideo) {
                this.maxBytesLoaded = this.movie.bufferedBytes;
            } else {
                this.maxBytesLoaded = this.movie.GetMaxBytesLoaded();
            }
        } catch(e) {}

        return this.maxBytesLoaded;
    },

    // ** {{{AC.QuickTime.Controller.GetTrackCount() }}} **
    //
    _trackCount: NaN,
    GetTrackCount: function()
    {
        if (!isNaN(this._trackCount)) {
            return this._trackCount;
        }

        try {
            this._trackCount = this.movie.GetTrackCount();
            return this._trackCount;
        } catch(e) {
            return NaN;
        }
    },

    // ** {{{AC.QuickTime.Controller.GetMatrix() }}} **
    //
    GetMatrix: function()
    {
        return this.movie.GetMatrix();
    },

    // ** {{{AC.QuickTime.Controller.SetMatrix(matrix) }}} **
    //
    SetMatrix: function(matrix)
    {
        this.movie.SetMatrix(matrix);
    },

    // ** {{{AC.QuickTime.Controller.GetRectangle() }}} **
    //
    GetRectangle: function()
    {
        return this.movie.GetRectangle();
    },

    // ** {{{AC.QuickTime.Controller.SetRectangle(rect) }}} **
    //
    SetRectangle: function(rect)
    {
        this.movie.SetRectangle(rect);
    },

    // ** {{{AC.QuickTime.Controller.GetLanguage() }}} **
    //
    GetLanguage: function()
    {
        return this.movie.GetLanguage();
    },

    // ** {{{AC.QuickTime.Controller.SetLanguage(language) }}} **
    //
    SetLanguage: function(language) {
        this.movie.SetLanguage(language);
    },

    // ** {{{AC.QuickTime.Controller.GetMIMEType() }}} **
    //
    GetMIMEType: function()
    {
        return this.movie.GetMIMEType();
    },

    // ** {{{AC.QuickTime.Controller.GetUserData(type) }}} **
    //
    GetUserData: function(type)
    {
        return this.movie.GetUserData(type);
    },

    // ** {{{AC.QuickTime.Controller.GetIsVRMovie() }}} **
    //
    GetIsVRMovie: function()
    {
        return this.movie.GetIsVRMovie();
    },

    // ** {{{AC.QuickTime.Controller.GetHotspotUrl(hotspotID) }}} **
    //
    GetHotspotUrl: function(hotspotID)
    {
        return this.movie.GetHotspotUrl(hotspotID);
    },

    // ** {{{AC.QuickTime.Controller.SetHotspotUrl(hotspotID, url) }}} **
    //
    SetHotspotUrl: function(hotspotID, url)
    {
        this.movie.SetHotspotUrl(hotspotID, url);
    },

    // ** {{{AC.QuickTime.Controller.GetHotspotTarget(hotspotID) }}} **
    //
    GetHotspotTarget: function(hotspotID)
    {
        return this.movie.GetHotspotTarget(hotspotID);
    },

    // ** {{{AC.QuickTime.Controller.SetHotspotTarget(hotspotID, target) }}} **
    //
    SetHotspotTarget: function(hotspotID, target)
    {
        this.movie.SetHotspotTarget(hotspotID, target);
    },

    // ** {{{AC.QuickTime.Controller.GetPanAngle() }}} **
    //
    GetPanAngle: function()
    {
        return this.movie.GetPanAngle();
    },

    // ** {{{AC.QuickTime.Controller.SetPanAngle(angle) }}} **
    //
    SetPanAngle: function(angle)
    {
        this.movie.SetPanAngle(angle);
    },

    // ** {{{AC.QuickTime.Controller.GetTiltAngle() }}} **
    //
    GetTiltAngle: function()
    {
        return this.movie.GetTiltAngle();
    },

    // ** {{{AC.QuickTime.Controller.SetTiltAngle(angle) }}} **
    //
    SetTiltAngle: function(angle)
    {
        this.movie.SetTiltAngle(angle);
    },

    // ** {{{AC.QuickTime.Controller.GetFieldOfView() }}} **
    //
    GetFieldOfView: function()
    {
        return this.movie.GetFieldOfView();
    },

    // ** {{{AC.QuickTime.Controller.SetFieldOfView(fov) }}} **
    //
    SetFieldOfView: function(fov)
    {
        this.movie.SetFieldOfView(fov);
    },

    // ** {{{AC.QuickTime.Controller.GetNodeCount() }}} **
    //
    GetNodeCount: function()
    {
        return this.movie.GetNodeCount();
    },

    // ** {{{AC.QuickTime.Controller.SetNodeID(id) }}} **
    //
    SetNodeID: function(id)
    {
        this.movie.SetNodeID(id);
    },

    _trackNames: null,
    // Returns the name of the track at the specified index
    // Note that in QuickTime the track index numbering begins at 1
    GetTrackName: function(index)
    {
        if(!this._trackNames) {
            throw "Need to attach to a movie before getting track names";
        }

        if (this._trackNames[index]) {
            return this._trackNames[index];
        }

        try {
            var trackName = this.movie.GetTrackName(index);
            this._trackNames[index] = trackName;
            return trackName;
        } catch(e) {
            if (!this._trackExistsAtIndex(index)) {
                // index out of bounds, propagate the error
                throw "There is no track at the specified index: " + index;
            } else {
                // plugin error, return sensible unknown
                return 'Unknown';
            }
        }
    },

    _trackTypes: null,
    // Returns the name of the type of the track at the specified index
    // Note that in QuickTime the track index numbering begins at 1
    GetTrackType: function(index)
    {
        if(!this._trackTypes) {
            throw "Need to attach to a movie before getting track types";
        }

        if (this._trackTypes[index]) {
            return this._trackTypes[index];
        }

        try {
            var trackType = this.movie.GetTrackType(index);
            this._trackTypes[index] = trackType;
            return trackType;
        } catch(e) {
            if (!this._trackExistsAtIndex(index)) {
                // index out of bounds, propagate the error
                throw "There is no track at the specified index: " + index;
            } else {
                // plugin error, return sensible unknown
                return 'Unknown';
            }
        }
    },

    // ** {{{AC.QuickTime.Controller.GetTrackEnabled(index)}}} **
    // 
    // Returns whether the track at the specified index is enabled or not
    // Note that in QuickTime the track index numbering begins at 1
    GetTrackEnabled: function(index)
    {
        try {
            return this.movie.GetTrackEnabled(index);
        } catch(e) {
            if (!this._trackExistsAtIndex(index)) {
                // index out of bounds, propagate the error
                throw "There is no track at the specified index: " + index;
            }
            // otherwise ignore the plugin error
        }
    },

    // ** {{{AC.QuickTime.Controller.SetTrackEnabled(index, enabled)}}} **
    //
    // Sets whether the track at the specified index is enabled or not
    // Note that in QuickTime the track index numbering begins at 1
    SetTrackEnabled: function(index, enabled)
    {
        try {
            this.movie.SetTrackEnabled(index, enabled);
        } catch(e) {
            if (!this._trackExistsAtIndex(index)) {
                // index out of bounds, propagate the error
                throw "There is no track at the specified index: " + index;
            }
            // otherwise ignore the plugin error
        }
    },

    // Whether or not we know that a track exists at the specified index
    _trackExistsAtIndex: function(index)
    {
        return !isNaN(this._trackCount) && (index <= this._trackCount) && (index > 0);
    },

    // ** {{{AC.QuickTime.Controller.GetSpriteTrackVariable(trackIndex, variableIndex) }}} **
    //
    GetSpriteTrackVariable: function(trackIndex, variableIndex)
    {
        return this.movie.GetSpriteTrackVariable(trackIndex, variableIndex);
    },

    // ** {{{AC.QuickTime.Controller.SetSpriteTrackVariable(variableIndex, value) }}} **
    //
    SetSpriteTrackVariable: function(variableIndex, value)
    {
        this.movie.SetSpriteTrackVariable(variableIndex, value);
    },

    // ** {{{AC.QuickTime.Controller.GetChapterCount() }}} **
    //
    // Returns the number of chapters in the movie.
    _chapterCount: NaN,
    GetChapterCount: function() {

        if (!isNaN(this._chapterCount)) {
            return this._chapterCount;
        }

        try {
            this._chapterCount = this.movie.GetChapterCount();
            return this._chapterCount;
        } catch(e) {
            return NaN;
        }
    },

    // ** {{{AC.QuickTime.Controller.GetChapterName(index) }}} **
    //
    // Takes a chapter number and returns the chapter name.
    _chapterNames: null,
    GetChapterName: function(index) {

        if(!this._chapterNames) {
            throw "Need to attach to a movie before getting chapter names";
        }

        if (this._chapterNames[index]) {
            return this._chapterNames[index];
        }

        try {
            var chapterName = this.movie.GetChapterName(index);
            this._chapterNames[index] = chapterName;
            return chapterName;
        } catch(e) {
            if (!this._chapterExistsAtIndex(index)) {
                // index out of bounds, propagate the error
                throw "There is no chapter at the specified index: " + index;
            } else {
                // plugin error, return sensible unknown
                return null;
            }
        }
    },

    // ** {{{AC.QuickTime.Controller.GoToChapter(name) }}} **
    //
    // Takes a chapter name and sets the movie's current time to the 
    // beginning of that chapter. 
    // 
    // See also: GetChapterName and GetChapterCount in movie properties.
    GoToChapter: function(name) {
        try {
            this.movie.GoToChapter(name);
            return true;
        } catch(e) {
            return false;
        }
    },

    // Whether or not we know that a chapter exists at the specified index
    _chapterExistsAtIndex: function(index)
    {
        return !isNaN(this._chapterCount) && (index <= this._chapterCount) && (index > 0);
    },

    // ** {{{ AC.QuickTime.Controller.isPlaying() }}} **
    // 
    // Returns whether or not the receiver's attached media is 
    // currently playing.
    isPlaying: function()
    {
        try {
            if (this.movieIsVideo) {
                return !this.movie.paused && !this.movie.ended;
            } else {
                return this.movie.GetRate() !== 0;
            }
        } catch(e) {
            // plugin exception, assume the movie is not playing
            return false;
        }
    },

    // ** {{{ AC.QuickTime.Controller.isFinished() }}} **
    // 
    // Returns whether or not the receiver's attached media is 
    // currently finished.
    isFinished: function()
    {
        try {
            if (this.movieIsVideo) {
                return this.movie.ended;
            }

            var isStopped = this.movie.GetRate() === 0,
                isAtEnd = this.movie.GetTime() === this.GetDuration();

            return isStopped && isAtEnd;
        } catch (e) {
            // Plugin exception, assume that the movie is not finished
            return false;
        }
    },

    // ** {{{AC.QuickTime.Controller.toggle()}}} **
    //
    // Sets the receiver from playing to stopped or stopped to playing.
    toggle: function()
    {
        if (this.isPlaying()) {
            this.Stop();
        } else {
            this.Play();
        }
    },

    // Sets whether the closed captions are available or not
    _setClosedCaptionsAvailable: function(available)
    {
        this._closedCaptionsAvailable = available;

        // TODO since we don't want to have to check how the CC toggle would
        // render in every existing controller on apple.com we're not going to
        // render it until we know the movie has CC
        // This has the unfortunate side effect that the CC toggle will not
        // be in the page, so we can't render a ghosted one or anything
        // but that's the idea. Eventually it may make sense to have it
        // everywhere, but then again this makes some sense as we may not
        // want to highlight the fact that CC is not available on any movies
        // in particular

        if (this.controllerPanel) {
            if (available) {
                // create caption toggle if none already exists
                if (!this.captioningToggle) {
                    this.captioningToggle = document.createElement('div');
                    Element.addClassName(this.captioningToggle, 'captioningToggle');
                    Element.addClassName(this.captioningToggle, 'ccAvailable');
                    this.captioningToggle.innerHTML = 'Closed Captioning';
                    this.captioningToggle.onclick = this.toggleClosedCaptions.bind(this);
                }

                // insert toggle control if not already in document
                if (!this.captioningToggle.parentNode !== this.controllerPanel) {
                    this.controllerPanel.appendChild(this.captioningToggle);
                }

            } else if (!available && this.captioningToggle && this.captioningToggle.parentNode) {
                // Remove from the document if the toggle exists and is in the DOM
                this.captioningToggle.parentNode.removeChild(this.captioningToggle);
            }
        }

    },

    // ** {{{ AC.QuickTime.Controller.toggleClosedCaptions()) }}} **
    // 
    // Toggle the current state of closed captions form on to off, and 
    // vice versa.
    toggleClosedCaptions: function()
    {
        if (!this._closedCaptionsAvailable) {
            return;
        }

        this.setClosedCaptionsEnabled(!this._closedCaptionsEnabled);
    },

    // ** {{{ AC.QuickTime.Controller.setClosedCaptionsEnabled(enabled)) }}} **
    //
    // Sets the enabled status of the receiver's attached media's closed 
    // captions.
    //
    // {{{enabled}}}: Whether the closed captions should be enabled or not.
    setClosedCaptionsEnabled: function(enabled)
    {
        if (!this._closedCaptionsAvailable) {
            return;
        }

        try {
            this.SetTrackEnabled(this._closedCaptionTrackIndex, enabled);

            // If the plugin call succeeds, go ahead and update class names
            // and the current state of the controller

            if (this.captioningToggle && enabled) {
                Element.addClassName(this.captioningToggle, 'ccEnabled');
            } else {
                Element.removeClassName(this.captioningToggle, 'ccEnabled');
            }

            this._closedCaptionsEnabled = enabled;

            if (this.delegate && typeof this.delegate.didSetClosedCaptions === "function") {
                this.delegate.didSetClosedCaptions(this, enabled);
            }

            this._eventSource.fire("QuickTime:didSetClosedCaptions", {controller: this, enabled: enabled});

        } catch (e) {}
    },

    // ** {{{ AC.QuickTime.Controller.closedCaptionsEnabled()) }}} **
    //
    // Returns whether or not closed captions are enabled for the 
    // receiver's attached media.
    closedCaptionsEnabled: function()
    {
        return this._closedCaptionsEnabled;
    },

    // ** {{{AC.QuickTime.Controller.suspend()}}} **
    //
    // Suspends monitoring the receiver's attached movie.
    // NOTE this does not suppress or remove event handlers at the moment
    suspend: function()
    {
        if (this.movieWatcher) {
            clearTimeout(this.movieWatcher);
            this.movieWatcher = null;
        }
    },

    // ** {{{AC.QuickTime.Controller.decelerate()}}} **
    //
    // Reduces the frequency of monitor updates, without stopping 
    // them entirely.
    decelerate: function()
    {
        this.setMonitorDelay(this.longMonitorDelay);
    },

    // ** {{{AC.QuickTime.Controller.resume()}}} **
    //
    // Resumes monitoring the receiver's attached movie at the normal
    // monitor rate.
    resume: function()
    {
        if (!this.movie) {
            return;
        }

        if (!this.movieWatcher) {
            this.monitorMovie();
        } else {
            this.setMonitorDelay(this.normalMonitorDelay);
        }
    },

    // ** {{{AC.QuickTime.Controller.setMonitorDelay(delay)}}} **
    // 
    // Sets the receiver's monitor delay duration and has it take 
    // effect immediately.
    // 
    // {{{delay}}}: the time, in milliseconds, to wait between monitor loops.
    // 
    setMonitorDelay: function(delay)
    {
        clearTimeout(this.movieWatcher);
        this._monitorDelay = delay;
        this.monitorMovie();
    },

    // ** {{{AC.QuickTime.Controller.monitorDelay()}}} **
    //
    // Returns the receiver's monitor delay duration.
    monitorDelay: function()
    {
        return this._monitorDelay;
    }

};

