// Quantum Virtual Timelines
// Copyright (c) 2003-2005 Move Networks
// Inserts stuff into the MN.QVT namespace

if (MN.QMP == null) MN.QMP = {};

MN.QMP.PlayState = {0:'Init', 1:'Opening', 2:'Buffering', 3:'Playing', 4:'Stopped', 5:'MediaEnded', 6:'Error',
                    7:'Stalled', 8:'Authorizing', 255:'Waiting'};
//MN.QMP.PlayState = {'INIT':0, 'OPENING':1, 'BUFFERING':2, 'PLAYING':3, 'STOPPED':4, 'MEDIAENDED':5, 'ERROR':6,
//                 'STALLED':7, 'AUTHORIZING':8, 'WAITING':255};
MN.QMP.PS = MN.QMP.PlayState; // shortcut

var _ps = MN.Update({}, MN.QMP.PS); // Also all in the other direction - make a copy first
for (var i in _ps)
{
    MN.QMP.PS[_ps[i]] = i; // pretty name
    MN.QMP.PS[_ps[i].toUpperCase()] = i; // all caps
}
MN.QMP.PS[2] = 'Loading';
MN.QMP.PS.Loading = MN.QMP.PS.LOADING = MN.QMP.PS.BUFFERING;
delete _ps;

MN.QVT =
{
    useServerClock : true, // try to correct local user's clock time based on server's time in HTTP Date header

    _helperPlayer : null, // player used for AsyncWork calls

    // internal utility func - if MN.QVT does not yet have a player instance, it saves a reference
    // to this one.
    _DeputizePlayer : function(playerID)
    {
        if (!MN.QVT._helperPlayer) // save a ref to a player so we can do AsyncWork calls
        {
            var player = $(playerID);
            MN.QVT._helperPlayer = player;
            MN.Event.Observe(player, 'AsyncTaskDone', MN.QVT._OnAsyncTaskDone);
        }
    },

    // Similar to MN.QMPInstall.CreatePlayer in that it creates a player object if possible, automatically
    // handling installation and upgrading as much as possible. When finished, DoneCB is called with
    // a single parameter which is either a new player wrapper object if successful, null on error.
    CreatePlayer : function(parentID, DoneCB, w, h, playerID)
    {
        if (playerID == null)
            playerID = MN.QMPInstall.MakePlayerID(parentID);
        var _MyDoneCB = function(doneOk)
        {
            if (doneOk)
            {
                MN.QVT._DeputizePlayer(playerID);
                return DoneCB(new MN.QVT.PlayerWrapper($(playerID)));
            }
            else
                return DoneCB(null);
        }

        MN.QMPInstall.CreatePlayer(parentID, _MyDoneCB, w, h);
    },

    // Writes out the appropriate object tag and returns a MN.QVT.PlayerWrapper instance
    EmitObj : function(parentID, w, h, playerID)
    {
        playerID = MN.QMPInstall.EmitObj(parentID, w, h, playerID);
        MN.QVT._DeputizePlayer(playerID);
        return new MN.QVT.PlayerWrapper($(playerID));
    },

    _taskID : 0, // global task ID; incremented each time a new task is started
    _tasks : {}, // taskID : {'cb':callback func, 'url':url passed in}

    _StartTask : function(taskName, url, doneCB) // cb is like doneCB(url, result)
    {
        // gen a unique work ID
        var taskID = MN.QVT._taskID + 1;
        MN.QVT._taskID = taskID;

        // save ID and cb, and then ask player to load it for us
        MN.QVT._tasks['w' + taskID] = {'cb':doneCB, 'url':url};

        var player = MN.QVT._helperPlayer;
        if (!player)
        {
            logError('No helper player exists!');
            MN.QVT._OnAsyncTaskDone(taskID, '', ''); // fake a done event
        }
        else
            player[taskName](taskID, url);
    },

    _qvtCache : {}, // mapping of MakeQVT raw --> MakeQVT result

    AcquireQVT : function(raw)
    {   // Returns the equivalent of 'new MN.QVT.QVT(raw)', except that the value may come from cache
        var ret = MN.QVT._qvtCache[raw];
        if (!ret)
        {
            ret = new MN.QVT.QVT(raw);
            MN.QVT._qvtCache[raw] = ret; // cache it for next time
            ret._qvtlibCacheKey = raw;
            ret._qvtlibCacheRefs = 0; // ref count;
        }

        ret.IncRef();
        return ret;
    },

    IncRefQVT : function(qvt)
    {   // call this before passing a QVT to somebody else. For convenience, returns obj passed in
        if (qvt && qvt._qvtlibCacheRefs != null)
            qvt._qvtlibCacheRefs += 1;
        return qvt;
    },

    ReleaseQVT : function(qvt)
    {   // called to parallel AcquireQVT to reduce ref count and possible release from cache. returns obj passed in
        if (qvt._qvtlibCacheRefs == null)
            logError('ReleaseQVT called with non-cached QVT');
        else
        {
            qvt._qvtlibCacheRefs -= 1;
            if (qvt._qvtlibCacheRefs <= 0)
            {
                qvt.allowReloads = false;
                if (MN.QVT._qvtCache[qvt._qvtlibCacheKey])
                    delete MN.QVT._qvtCache[qvt._qvtlibCacheKey];
            }
        }
        return qvt;
    },

    // Asynchronously loads a QVT or QVT metadata file if possible, and then calls the given callback with the result as a string
    LoadQVT : function(url, doneCB)
    {
        log('STARTING LOAD OF', url);
        MN.QVT._StartTask('AsyncLoadJSON', url, doneCB);
    },

    // Asynchronously loads a QMX if possible, and then calls the given callback with the result as a string
    LoadQMX : function(url, doneCB)
    {
        MN.QVT._StartTask('AsyncLoadQMX', url, doneCB);
    },

    // Receives OnAsyncTaskDone events from the player
    _OnAsyncTaskDone : function(taskID, result, respHeaders)
    {
        // lookup the callback func & remove it
        var taskID = 'w' + taskID;
        var task = MN.QVT._tasks[taskID];
        if (task)
        {
            delete MN.QVT._tasks[taskID];
            // call the cb asynchronously so as to not block the player
            setTimeout(function() { task.cb(task.url, result, respHeaders); }, 10);
        }
    },

    // Reads a property from QVT and returns a mapping object with properties type (utc or day),
    // offset (+/- number), anchor (bool), wrap (bool). Properly handles empty/null input
    ParseTimestamp : function(s)
    {
        var d = {};
        if (!s)
            return d;

        var parts = s.split(',')
        for (var i=0; i < parts.length; i++)
        {
            var part = parts[i].strip();
            if (part == 'gmt' || part == 'utc')
                d['type'] = 'utc';
            else if (part == 'day')
                d['type'] = 'day';
            else if (part == 'anchor')
                d['anchor'] = true;
            else if (part == 'wrap')
                d['wrap'] = true;
            else
            {   // assume it's an offset value
                var offset = parseFloat(part);
                if (!isNaN(offset))
                    d['offset'] = offset;
                else
                    log('ParseTimestamp failed to use', part);
            }
        }

        // it doesn't make sense to wrap but not anchor
        if (d.wrap && !d.anchor)
        {
            d.wrap = false;
            log('Ignoring timestamp wrap flag because not anchored');
        }

        return d;
    },

    _QMXCache : {}, // mapping of qmx URL --> objs with duration,live,startTimestamp properties

    Error : function(msg) { this.msg = this.message = msg; } // a custom "exception" to throw
}

// QVT class
MN.QVT.QVT = MN.Class(MN.EventSource);
var _qp = MN.QVT.QVT.prototype;
_qp.initialize = function(raw)
{
    MN.EventSource.prototype.initialize.apply(this)
    this.playState = MN.QMP.PS.INIT;
    this.meta = {}; // the eval'd form of the QVT JSON
    this.initRaw = raw; // the raw data originally passed to our init (so we can reload as needed)
    this.loadedRaw = ''; // the result of loading - raw QVT
    this.metadataLoaded = false; // external metadata has finished loading (if no ext metadata, immediate gets set to true)
    this.initialLoadDone = false; // true once the first load has completed, even if unsuccessfully
    this.haveTimestamp = false; // true once we have a timestamp of some sort (or enough info to compute one)
    this.timestampObj = {}; // the result of ParseTimestamp(this.meta.timestamp)
    this.willLoadQMX = false; // true if load causes async load of any QMX's
    this.loading = false;
    this.valid = false;
    this.duration = 0;
    this.anchored = false;
    this.startTimestamp = 0;
    this.httpRespHeaders = {}; // HTTP response headers from the QVT request
    this.clockAdjustSec = 0; // how many seconds to add to this machine's UTC time to bring it into line with server's notion of UTC time
    this.useServerClock = MN.QVT.useServerClock; // if true, we use the server's Date HTTP response header to deduce how incorrect the local clock is
    this.shows = []; // list of show objects with .tlStartTime, .tlStopTime, .title, and custom metadata
    this.haveAnyLive = false; // true if any clips are marked as live
    this.allowReloads = true; // if false, QVT will not reload
    this.ForceReload(); // uses initRaw
}

_qp.GetSource = function() { return this.initRaw; }
_qp.IncRef = function() { return MN.QVT.IncRefQVT(this); }
_qp.DecRef = function() { return MN.QVT.ReleaseQVT(this); }
_qp.IsValid = function() { return this.valid; }
_qp.StartTimestamp = function() { return this.startTimestamp; }

// Loading the given source requires several steps. First, the caller's data is inspected to
// see if it is a QMX URL, a QVT URL, or an actual QVT object string. If it's a QVT URL, it is
// asynchronously retrieved. Once that step is complete, the QVT string is evaluated to become
// an actual object. Then each clip is inspected to see if a range is given. If not, the QMX for
// each is loaded to determine the content length. At that point, the QVT clips are considered
// to be loaded sufficiently for playback to begin. Meanwhile, external metadata references are
// loaded, evaluated, and applied, which results in one or more OnMetadataLoaded events being
// fired. At this point the QVT is considered fully loaded. However, if the QVT is marked as
// still being open, it is reloaded occasionally from scratch to see if anything has changed.
// Additionally, metadata is loaded periodically on live and new content to see if it has
// been updated, at which point any updates are also applied.
_qp.ForceReload = function()
{   // initiates the load/reload of whatever was originally passed in to the constructor
    log('ForceReload started');
    if (this.loading) // already loading it seems
        return;

    this.loading = true;
    this.baseURL = ''; // Base URL used to load this QVT file
    var raw = this.initRaw;
    if (raw instanceof Object)
    {   // we've been handed a Javascript object and it needs to be converted into a real QVT instance
        // mostly just pass this on to the reader
        raw.reload = 0;
        this._ReadQVT(raw);
    }
    else
    {   // it better be a string of some sort
        // raw can be (1) a string representing a complete QVT object, (2) a QVT URL, (3) a QMX URL,
        // or (4) a Javascript object that looks like a QVT object
        // Convert cases #2 and #3 to #1
        var raw = raw.strip();
        var inspect = raw.toLowerCase();
        if (inspect.startswith('http://') || inspect.startswith('qsp://'))
        {   // It's a URL of some sort
            this.baseURL = MN.URL.Base(inspect);
            if (inspect.endswith('.qmx'))
            {   // it's a QMX file, so wrap it into a QVT raw string
                raw = "{'clips':[{'url':'" + raw + "'}]}";
                this._ReadQVT(raw);
            }
            else if (inspect.endswith('.qvt'))
            {   // It's a QVT URL, so ask a player instance to go fetch it
                var _ReadQVTCB = this._ReadQVT;
                MN.QVT.LoadQVT(raw, function(url, result, respHeaders) { _ReadQVTCB(result, respHeaders); });
            }
            else
                throw new MN.QVT.Error('Unrecognized URL format - expected .qvt or .qmx');
        }
        else if (!inspect.startswith('{') || !inspect.endswith('}'))
            throw new MN.QVT.Error('Unrecognized QVT format');
        else
            this._ReadQVT(raw);
    }
}

_qp._ReadQVT = function(raw, respHeaders)
{   // called from ForceReload (or an async loader callback) to evaluate the raw JSON QVT data
    // and do the initial pass on inspecting the individual clips. For any that are missing explicit
    // ranges and/or durations, we have to fire off requests for the QMX files to determine them.
    // note: we save meta and clips to temp variables so that they stomp earlier copies in this.meta/.clips
    // in one step
    var utcNow = (new Date()).getTime();

    log('READ QVT')
    this.loadedRaw = raw;
    if (!raw)
    {
        this.valid = false;
        this.loading = false;
        log('_ReadQVT failed to load');
        this.FireEvent('TimelineLoaded', this);
        return;
    }

    var obj = null;
    // if it's not an object yet, make it one
    if (raw instanceof Object)
        obj = raw;
    else
        obj = MN.EvalJSON(raw);

    var meta = MN.Update({}, obj);
    var clips = [];

    if (respHeaders)
    {
        log("RESP HEADERS", respHeaders)
        respHeaders = respHeaders.split('\n');
        var headers = {};
        for (var i=0; i < respHeaders.length; i++)
        {
            var h = respHeaders[i];
            var x = h.indexOf(':');
            if (x != -1)
                headers[h.substr(0, x).toLowerCase()] = h.substr(x+1).strip();
        }
        this.httpRespHeaders = headers;

        // If the server gave us a Date header, we can use it for a better estimate of the actual current time
        var serverDate = headers.date;
        if (serverDate && this.useServerClock)
        {
            serverDate = (new Date(serverDate)).getTime();
            if (isFinite(serverDate))
                this.clockAdjustSec = (serverDate - utcNow) / 1000;
            log('clock adjust', this.clockAdjustSec);
        }
    }

    // setup each clip
    for (var i in meta.clips)
    {
        var clip = meta.clips[i];
        clip.isGap = (clip.url == null);
        clip.duration = null;
        clip.durationGrowing = false; // true if content is live and duration is growing over time
        clip.rangeStart = 0; // start always defaults to 0 if not present
        clip.rangeEnd = null;
        clip.tlStartTime = null; // overall start and stop times in the timeline
        clip.tlStopTime = null;

        clip.timesReady = false; // true when start time, stop time, and duration is known

        if (clip.range)
        {
            var rangeParts = clip.range.split(',');
            if (rangeParts.length == 2)
            {
                clip.rangeStart = parseFloat(rangeParts[0]) || 0;
                clip.rangeEnd = parseFloat(rangeParts[1]) || null;
            }
        }

        // if it's a gap clip, a range is required
        if (!clip.url && clip.rangeEnd == null)
        {
            log('skipping bad gap clip - no range end');
            continue;
        }

        //log('ReadQVT clip', i, clip.rangeStart, clip.rangeEnd, clip.duration);
        clips.push(clip);
    }

    meta.clips = clips; // stomp any invalid entries
    this.meta = meta;

    this.metadataLoaded = false;
    var metaURL = meta.metadata;
    if (metaURL)
    {
        metaURL = this._ExpandURL(metaURL);

        // Fire off an async load of the external metadata file
        var _ApplyExtCB = this._ApplyExternalMetadata;
        log('starting external metadata load of', metaURL);
        MN.QVT.LoadQVT(metaURL, function(url, result, respHeaders)
                                {
                                    var obj = MN.EvalJSON(result);
                                    _ApplyExtCB(obj);
                                });
    }
    else
        this.metadataLoaded = true; // no external metadata, so no need to delay

    this.haveTimestamp = false;
    this.timestampObj = {'empty':true};

    this._RecalcClipTimes();

    // Now go load QMX's for any clips that we lack info on
    this.willLoadQMX = false;
    for (var i=0; i < meta.clips.length; i++)
    {
        var clip = meta.clips[i];
        var inCache = (MN.QVT._QMXCache[clip.url] != null);
        if (!clip.timesReady && !inCache)
        {
            this.willLoadQMX = true;
            //log('kicking off retrieval for', i);
            var _RecalcCB = this._RecalcClipTimes;
            log('starting check for', clip.url);
            MN.QVT.LoadQMX(clip.url, function(url, result, respHeaders)
                                     {
                                         var obj = MN.EvalJSON(result);
                                         MN.QVT._QMXCache[url] = obj;
                                         //log(' check for', url, 'done; obj.dur=', obj.duration);
                                         _RecalcCB();
                                     });
        }
    }
}

_qp._ApplyExternalMetadata = function(meta)
{
    if (!meta)
        log('Failed to load external metadata')
    else
    {
        log('Applying external metadata');
        // we have to do a "filtered" copy of everything in the external metadata, just in
        // case the format is invalid
        for (var prop in meta)
        {
            if (prop == 'clips')
            {
                for (var i in meta.clips)
                {
                    var metaClip = meta.clips[i];
                    var thisClip = this.meta.clips[i];
                    for (var clipProp in metaClip)
                        if (clipProp != 'url' && clipProp != 'range')
                            thisClip[clipProp] = metaClip[clipProp];
                }
            }
            else
                this.meta[prop] = meta[prop];
        }
        //Update(this.meta, meta)
        // read and apply, set metadataLoaded, call RecalcClipTimes
    }
    this.metadataLoaded = true;
    this._RecalcClipTimes();
}

_qp._RecalcClipTimes = function()
{   // inspects each clip to see if any of its time-related properties (rangeStart, duration, etc)
    // need to be updated. Attempts to pull values from the QMX cache if possible. If all values
    // are ready, sets the clip's timesReady prop to true. Also updates this.duration member
    // to reflect how much of the timeline has been loaded (enables play start before fully loaded).

    if (this.metadataLoaded && this.timestampObj.empty)
    {   // now that metadata has loaded, we can check to see if we have enough info for
        // a timestamp or if we'll need to force load a QMX to get a timestamp
        this.timestampObj = MN.QVT.ParseTimestamp(this.meta.timestamp);
        var ts = this.timestampObj;

        // if a QVT-wide timestamp property fully specifies a timestamp, then we don't need
        // to look further
        if (ts.type == 'day' || (ts.type == 'utc' && ts.offset != null))
            this.haveTimestamp = true;
    }

    var clips = this.meta.clips;

    var badClips = []; // list of indices of clips that are just plain bad and must be removed
    var allPresent = true;
    var knownDur = 0; // how much of the timeline has loaded so far
    for (var i=0; i < clips.length; i++)
    {
        var clip = clips[i];
        if (clip.timestamp)
            this.haveTimestamp = true;

        var cached = MN.QVT._QMXCache[clip.url];

        if (cached)
        {   // we now have enough to compute an overall timestamp if necessary
            clip._qmxTimestamp = (cached.startTimestamp || 0) + clip.rangeStart;
            this.haveTimestamp = true;
        }

        // if the end was not specified, we'll have to look up the duration in the QMX
        if (clip.rangeEnd == null)
        {
            if (cached)
            {
                if (cached.duration != null)
                    clip.rangeEnd = cached.duration;
                else
                {
                    badClips.push(i);
                    continue;
                }

                if (cached.live) // don't cache live QMX
                {
                    clip.duration = clip.rangeEnd - clip.rangeStart;
                    clip.rangeEnd = null; // now that we know the dur, set the range end back to null since it's live
                    clip.live = true;
                    this.haveAnyLive = true;
                    clip.durationGrowing = true;
                    delete MN.QVT._QMXCache[clip.url];

                    // current behavior: the starttimestamp we report is zero OR
                    // the timestamp of the LAST QMX in the clips list that is marked as live
                    if (!clip.timestamp)
                        clip.timestamp = clip._qmxTimestamp;
                }
            }
        }

        // If all the time pieces are here, then the clip is ready
        if (clip.rangeStart != null && (clip.live || clip.rangeEnd != null))
        {
            if (clip.rangeEnd)
                clip.duration = clip.rangeEnd - clip.rangeStart;
            clip.timesReady = true;

            if (allPresent && this.metadataLoaded)
            {   // so far, everything encountered in the list is present, so we can also
                // calculate some values like tlStart
                clip.tlStartTime = knownDur;
                clip.tlStopTime = knownDur + clip.duration;
                knownDur = clip.tlStopTime;

                // now fixup show info
                if (!clip.shows) // make up a single show entry if none are present
                    clip.shows = [MN.Update({}, clip)];
                else
                {
                    // go through and set timeline times
                    var curPos = clip.tlStartTime;
                    var shows = [];
                    for (var j=0; j < clip.shows.length; j++)
                    {
                        var show = clip.shows[j];
                        if (j == 0)
                            show.tlStartTime = clip.tlStartTime;
                        else
                            show.tlStartTime = clip.tlStartTime + (show.start || 0);
                        if (show.tlStartTime >= clip.tlStopTime)
                            show.tlStartTime = clip.tlStopTime - 1;

                        show.tlStopTime = clip.tlStopTime;
                        show.duration = show.tlStopTime - show.tlStartTime;
                        if (j > 0)
                        {
                            var prevShow = shows[j-1];
                            prevShow.tlStopTime = show.tlStartTime;
                            prevShow.duration = prevShow.tlStopTime - prevShow.tlStartTime;
                        }

                        shows.push(show);
                    }
                    clip.shows = shows; // this eliminates any bad ones
                }
            }
        }
        else
            allPresent = false;

        //log('clip %s: st=%s, end=%s, dur=%s'.format(i, clip.rangeStart, clip.rangeEnd, clip.duration));
    }

    if (badClips.length > 0)
    {   // some clips were just plain bad and must be chucked (maybe someday we'll do something more
        // intelligent with them - generate a gap in the timeline at least). Remove them from the
        // list of clips
        badClips.reverse();
        for (var i=0; i < badClips.length; i++)
        {
            clips.splice(badClips[i], 1);
            log(clips);
        }
    }

    //log('metaloaded=%s, haveTS=%s, willLoadQMX=%s'.format(this.metadataLoaded, this.haveTimestamp, this.willLoadQMX));
    if (allPresent && this.metadataLoaded && !this.haveTimestamp && !this.willLoadQMX)
    {   // we *have* to come up with a timestamp, and all indications are that we won't get
        // one any other way, so it looks like we'll need to force the load of a QMX from our
        // clip list in order to get *something* as a frame of reference. Search backwards for one to use
        var meta = this.meta;
        for (var i=meta.clips.length-1; i >= 0; i--)
        {
            var clip = meta.clips[i];
            if (!clip.url)
                continue;

            var inCache = (MN.QVT._QMXCache[clip.url] != null);
            if (!inCache)
            {
                this.willLoadQMX = true; // so we don't load it over and over
                var _RecalcCB = this._RecalcClipTimes;
                log('starting timestamp check for', clip.url);
                MN.QVT.LoadQMX(clip.url, function(url, result, respHeaders)
                                         {
                                             var obj = MN.EvalJSON(result);
                                             MN.QVT._QMXCache[url] = obj;
                                             //log(' check for', url, 'done; obj.dur=', obj.duration);
                                             _RecalcCB();
                                         });
                break; // we need only one!
            }
        }

        // if we didn't do anything, this QVT is probably somewhat bogus, so do *something*
        // just so we can move forward
        if (!this.willLoadQMX)
            this.haveTimestamp = true; // timestamp will probably end up being 0
    }

    this.duration = knownDur;
    if (allPresent && this.metadataLoaded && this.haveTimestamp)
    {
        // figure out QVT-wide timestamp, if any. Current behavior: if there is a QVT-wide timestamp
        // specified, it is used. Otherwise, we look at the clips and calc one from there. If multiple clips
        // have a timestamp specified, the last one is used. Note that any live clip that doesn't have a
        // timestamp specified is automatically given one based on its QMX timestamp (adjusting for selected
        // range, of course). If multiple are listed as live, we also clear the live flag on all but the
        // last one, assuming the incorrectness is due to a crashed Atomizer or something.
        var lastWithTimestamp = -1; // index of last clip with a timestamp
        var lastWithQMXTimestamp = -1; // index of last clip with a QMX timestamp (i.e. we loaded it and looked)
        for (var i=0; i < this.meta.clips.length; i++)
        {
            var clip = this.meta.clips[i];

            if (clip.timestamp != null)
                lastWithTimestamp = i;
            if (clip._qmxTimestamp != null)
                lastWithQMXTimestamp = i;
        }

        // if no timestamp was found, try to use the timestamps we got from inspecting QMX files directly
        var useQMXTimestamp = false;
        if (lastWithTimestamp == -1)
        {
            lastWithTimestamp = lastWithQMXTimestamp;
            useQMXTimestamp = true;
        }

        this.startTimestamp = 0;
        if (lastWithTimestamp != -1)
        {   // calculate an overall timeline timestamp based on the clips. We have to do this now, even
            // if it later gets overwritten by a true timeline timestamp because the format allows
            // explicit timeline timestamps to be partially specified (e.g. it could specify 'anchor'
            // but rely on the timestamp from an individual clip).
            //  -
            // Compute the timeline timestamp by taking the timestamp of the reference clip and subtracting
            // the durations of all earlier clips
            var clip = this.meta.clips[lastWithTimestamp];
            this.startTimestamp = useQMXTimestamp ? clip._qmxTimestamp : clip.timestamp;
            for (var i=0; i < lastWithTimestamp; i++)
            {
                var clip = this.meta.clips[i];
                this.startTimestamp -= clip.duration;
                clip.live = false;
            }
        }

        // a QVT-wide timestamp always takes precedence
        if (this.meta.timestamp)
        {
            var ts = this.timestampObj;
            // {'type':'utc', 'offset':0, 'anchor':false, 'wrap':false};

            // we inherit some values from clips if not specified in the QVT-wide timestamp property
            // (hmm... I don't think we really want to support that, do we?)
            if (ts.type == 'utc' && ts.offset != null)
                this.startTimestamp = ts.offset;

            if (ts.type == 'day')
            {   // convert a day-relative timestamp to a utc (absolute) timestamp
                var today = new Date();
                today.setHours(0,0,0,0); // set to 12:00:00 AM
                this.startTimestamp = today.getTime()/1000;

                if (ts.offset != null)
                    this.startTimestamp += ts.offset;
            }

            this.anchored = ts.anchor;
            if (this.anchored && ts.wrap)
            {   // if the timeline is set to wrap, we adjust the anchored timestamp so that the current
                // timeline time falls within the timeline. This also works if the current timeline time
                // is *before* the anchored timestamp.
                var tlNow = this.GetTimelineNow();
                this.startTimestamp += (this.duration * Math.floor(tlNow / this.duration));
            }
        }
        log('TL TIMESTAMP', this.startTimestamp);

        // build master list of all shows
        var shows = [];
        for (var i=0; i < this.meta.clips.length; i++)
            shows = shows.concat(this.meta.clips[i].shows);

        if (this.meta.shows)
        {   // if a global show list was provided, use it instead - but we need to calc the timeline times
            shows = [];
            for (var i=0; i < this.meta.shows.length; i++)
            {
                var show = this.meta.shows[i];
                show.tlStartTime = parseFloat(show.start) || 0;
                show.tlStopTime = this.duration; // this gets overwritten on all but the last
                if (i > 0)
                    shows[i-1].tlStopTime = show.tlStartTime;
                shows.push(show);
            }

            // calc show durations too
            for (var i=0; i < shows.length; i++)
            {
                var show = shows[i];
                show.duration = show.tlStopTime - show.tlStartTime;
            }
        }

        // If the show list has a timestamp, we need to move the show times around to reflect what
        // portion of the show list overlaps the clip list (by original definition, the show list
        // is merely a representation of the timeline, but in practice it's sometimes handy to be
        // able have shows specified relative to some point in time rather than the start of the timeline
        if (shows.length > 0 && this.meta.showsTimestamp)
        {
            var tsObj = MN.QVT.ParseTimestamp(this.meta.showsTimestamp);
            if (tsObj.type != 'utc')
            {   // only utc is supported right now
                log('showsTimestamp has an invalid type, ignoring', tsObj.type);
                tsObj.offset = 0;
            }

            var adjust = tsObj.offset - this.startTimestamp;
            log('Will adjust timeline by', adjust);
            shows[0].start = shows[0].start || 0; // make sure it has one explicitly

            // shift all show start times by the needed amount
            for (var i=0; i < shows.length; i++)
            {
                var show = shows[i];
                show.start += adjust;
                show.tlStartTime += adjust;
                show.tlStopTime += adjust;
            }

            // get rid of any shows that are now done before the timeline even starts
            while (shows.length > 0 && shows[0].tlStopTime <= 0)
                shows.shift();

            // fixup what is now the first show in the timeline
            if (shows.length > 0)
            {
                var show = shows[0];

                if (show.start <= 0)
                {   // clip this first show that it starts at 0
                    show.start = 0;
                    show.tlStartTime = 0;
                    show.duration = show.tlStopTime;
                }
                else
                {
                    // add a new show to act as a gap between the start of the timeline and the first real show
                    var newShow = {};
                    newShow.start = 0;
                    newShow.tlStartTime = 0;
                    newShow.title = '';
                    newShow.duration = show.tlStartTime;
                    newShow.tlStopTime = show.tlStartTime
                    newShow._filler = true;
                    shows.unshift(newShow);
                }
            }
        }

        // By definition, the show list's duration cannot exceed the timeline duration, so
        // clip it here if needed (if the content is still growing, mark the QVT as reloadable)
        for (var i=0; i < shows.length; i++)
        {
            var s = shows[i];
            if (s.tlStartTime <= this.duration && s.tlStopTime > this.duration)
            {
                s.tlStopTime = this.duration;
                s.duration = s.tlStopTime - s.tlStartTime;
                shows = shows.slice(0, i+1);
                break;
            }
        }

        //for (var i=0; i < shows.length; i++)
        //{
        //    var s = shows[i];
        //    log('SHOW', i, s.tlStartTime, s.tlStopTime, s.duration, this.duration);
        //}

        this.shows = shows;

        this.initialLoadDone = true; // may get set multiple times, but that's ok - it's just a flag
        this.loading = false;
        this.valid = this.meta.clips.length > 0;

        this.FireEvent('TimelineLoaded', this);
        if (this.IsOpenEnded())
            setTimeout(this.ForceReload, this.meta.reload * 1000);

        log('RELOADED', 'showcount=', this.shows.length);
    }
}

_qp.IsAnchored = function() { return this.anchored; }
_qp.IsOpenEnded = function() { return this.allowReloads && this.meta.reload != null && this.meta.reload > 0; }
_qp.IsLoading = function() { return this.loading; }
_qp.HaveAnyLive = function() { return this.haveAnyLive; }
_qp.InitialLoadDone = function() { return this.initialLoadDone; }

// Returns the current position in the timeline that corresponds to right now according to the anchor.
// If IsAnchored is false, returns 0.
_qp.GetTimelineNow = function()
{
    if (!this.anchored)
        return 0;

    return this.clockAdjustSec + (new Date()).getTime()/1000 - this.startTimestamp;
}

_qp._ExpandURL = function(url)
{   // Expands the given URL to a full URL as needed, using this.baseURL
    if (!url) return url;
    return MN.URL.Join(this.baseURL, url);
}

// Info APIs generally take an index parameter to refer to a specific show, or nothing to
// refer to the entire timeline
_qp.Metadata = function(name, i)
{
    var ret = '';
    if (typeof(i) == 'number')
    {
        if (i >= 0 && i < this.shows.length)
            ret = this.shows[i][name];
    }
    else
        ret = this.meta[name];

    return ret;
}

_qp.Title = function()
{   // util func to retrieve title metadata from QVT or a show
    var params = MN.ToArray(arguments);
    params.unshift('title');
    return this.Metadata.apply(this, params);
}

_qp.Timezone = function()
{   // retrieves the broadcast timezone (if known) or null. Returned value is
    // in hours to add to UTC to get broadcast time (e.g. MST is returned as -7.0).
    return this.Metadata('timezone');
}

_qp.PosToDatetime = function(pos)
{   // converts the given timeline position to an object with year, month, day, hour, minute, second
    // and dow (day of week) members in broadcast-local time. If the QVT does not have a timezone set,
    // the returned date will be in UTC time. For day of week, 0=Sunday.
    var tzOffsetSec = (this.Timezone() || 0) * 3600;
    var d = new Date(1000 * (pos + this.StartTimestamp() + tzOffsetSec));
    return {year:d.getUTCFullYear(), month:d.getUTCMonth(), day:d.getUTCDate(),
            hour:d.getUTCHours(), minute:d.getUTCMinutes(), second:d.getUTCSeconds(),
            dow:d.getUTCDay()};
}

_qp.StartDatetime = function()
{   // returns the timeline start time/date in broadcast-local time as an object with year, month, day
    // hour, minute, second and dow (day of week) members. If the QVT does not have a timezone set, the
    // date will be returned in UTC time.
    return this.PosToDatetime(0);
}

_qp.UpdateDuration = function(clipNum, curClipDur)
{
    var oldDur = this.meta.clips[clipNum].duration;
    var curClipDur = curClipDur - this.meta.clips[clipNum].rangeStart;
    this.meta.clips[clipNum].duration = curClipDur;
    this.duration = this.duration - oldDur + curClipDur;
}

_qp.IsGap = function(i)
{
    if (i == null)
        return false; // err... this doesn't make sense
    return this.Metadata('isGap', i) ? true : false;
}

_qp.Duration = function(i)
{
    if (i == null)
    {
        if (this.anchored)
        {   // if the content is anchored, the duration "grows" over time to take into
            // account the current timeline "now"
            var tlNow = this.GetTimelineNow();
            return Math.min(tlNow, this.duration);
        }
        else
            return this.duration;
    }
    return this.Metadata('duration', i);
}

_qp.StartTime = function(i)
{   // returns the timeline start time of the given show
    return this.Metadata('tlStartTime', i);
}

_qp.StopTime = function(i)
{   // returns the timeline stop time of the given show
    return this.Metadata('tlStopTime', i);
}

_qp.NextURL = function()
{   // Returns the URL for the next QVT after this one or ''
    return this._ExpandURL(this.Metadata('next'));
}

_qp.PrevURL = function()
{   // Returns the URL for the prev QVT before this one or ''
    return this._ExpandURL(this.Metadata('prev'));
}

_qp.GetSummary = function()
{   // returns an object summarizing the timeline: .duration is the total duration of the
    // timeline, .shows is an array of objects with .startTime, .stopTime, .duration, .title,
    // .showNum properties, and .live is true if there are any live clips
    var o = {'duration':this.Duration(), 'shows':[], 'live':this.HaveAnyLive() };
    if (!this.IsValid())
        return o;

    for (var i=0; i < this.shows.length; i++)
    {
        var show = this.shows[i];
        var c = {'startTime':this.StartTime(i), 'stopTime':this.StopTime(i), 'duration':this.Duration(i),
                 'title':this.Title(i), 'showNum':i, 'isGap':this.IsGap(i)};
        o.shows.push(c);
    }

    return o;
}

_qp.ShowCount = function()
{
    if (!this.IsValid())
        return 0;
    return this.shows.length;
}

_qp.ClipCount = function()
{
    if (!this.IsValid())
        return 0;

    return this.meta.clips.length;
}

_qp.ClipToTimeline = function(clipNum, clipPos)
{   // given a clip number and position, return an overall timeline time
    if (!this.IsValid())
        return 0;

    if (clipNum >= this.meta.clips.length)
        return this.duration; // beyond our number of clips
    if (clipNum < 0)
        return 0;

    var clip = this.meta.clips[clipNum];
    var relPos = clipPos - clip.rangeStart;
    if (relPos < 0)
        relPos = 0; // on initial playback, pos is often reported as 0
    return clip.tlStartTime + relPos;
}

_qp._MakePosObj = function(url, clipNum, startPos, endPos)
{
    return {'url':url, 'clipNum':clipNum, 'startPos':startPos, 'stopPos':endPos || 0};
}

_qp.GetClipAsPosObj = function(clipNum)
{
    var c = this.meta.clips[clipNum];
    return this._MakePosObj(c.url, clipNum, c.rangeStart, c.rangeEnd);
}

_qp.GetClip = function(clipNum)
{
    if (clipNum < 0 || !this.meta.clips || clipNum >= this.meta.clips.length)
        return null;
    return this.meta.clips[clipNum];
}

_qp.TimelineToShow = function(pos)
{   // given a time in the timeline, returns a show number
    if (!this.IsValid())
        return -1;

    if (pos >= this.duration)
        return this.shows.length;

    for (var i=0; i < this.shows.length; i++)
    {
        var show = this.shows[i];
        if (show.tlStartTime <= pos && pos < show.tlStopTime)
            return i;
    }

    return -1;
}

_qp.TimelineToClip = function(pos)
{   // given a time in the timeline, returns a position object
    // if pos is -1: if any live clips are present, play -1 in them, else pos=0
    // NOTE: this function violates the separation of concerns that should exist between the player
    // wrapper and a QVT object - this function originally just mapped a timline time to a clip,
    // but now also enforces the notion of "timeline now". At the very least, we should have 2 functions.
    if (!this.IsValid())
        return 0;

    var clipStopPos = -1;
    var clipStartPos = 0;
    var clipNum = 0;
    var tlNow = this.GetTimelineNow();

    // for anchored content, Play(-1) *always* means 'play at "live"' on the timeline
    if (pos == -1 && this.anchored)
        pos = tlNow;

    if (pos == -1)
    {
        if (this.haveAnyLive)
        {
            // look for a live clip
            for (var i=0; i < this.meta.clips.length; i++)
            {
                var clip = this.meta.clips[i];
                if (clip.live)
                {
                    // in theory, playing -1 in the clip could put us outside the requested
                    // clip boundaries. Hmm....sorta: we currently "say" that if a clip
                    // has an endPos specified in the .qvt file, then the timeline must
                    // be anchored. :)
                    return this._MakePosObj(clip.url, i, -1, clip.rangeEnd);
                }
            }
        }

        // no live - playback starts at beginning of content
        pos = 0;
    }

    // prevent seek-beyond-now on anchored content
    if (this.anchored)
        pos = Math.min(pos, tlNow);

    if (pos >= this.duration)
    {
        clipNum = this.meta.clips.length - 1;
        clipStopPos = this.meta.clips[clipNum].rangeEnd; // start right at end to trigger a MEDIAENDED event
        if (clipStopPos == null)
        {
            clipStartPos = -1;
            clipStopPos = 0;
        }
        else
        {
            log('Requested pos %s beyond timeline (%s), seeking to end'.format(pos, this.duration));
            clipStartPos = clipStopPos - 3.5; // todo: either change this to 0.5 (a small value) or (better yet) do this without actually playing - fire the event ourselves maybe
        }
    }
    else
    {
        // find the right clip
        for (var i in this.meta.clips)
        {
            var clip = this.meta.clips[i];
            if (clip.tlStartTime <= pos && pos < clip.tlStartTime + clip.duration)
            {
                clipNum = i;
                clipStartPos = pos - clip.tlStartTime + clip.rangeStart;
                clipStopPos = clip.rangeEnd;
                break;
            }
        }
    }

    url = this.meta.clips[clipNum].url;
    var ret = this._MakePosObj(url, clipNum, clipStartPos, clipStopPos);
    //log('TimelineToClip returning', ret.clipNum, ret.startPos, ret.stopPos);
    return ret;
}

delete _qp; // clean up the namespace

// The PlayerWrapper is a Javscript class that wraps an actual player instance
// (OCX or Moz plugin) and exposes player APIs plus QVT-specific functionality
MN.QVT.PlayerWrapper = MN.Class(MN.EventSource);
_pwp = MN.QVT.PlayerWrapper.prototype;
_pwp.initialize = function(playerObj)
{
    MN.EventSource.prototype.initialize.apply(this)
    this.playbackStarted = false; // true once playback has actually started
    this.qvt = null;
    this.playState = MN.QMP.PS.INIT;
    this.currentClipNum = 0;
    this.currentURL = '';
    this.currentStopPos = -1;
    this.player = playerObj;
    this.playerVersion = playerObj.Version;
    this.autoPlayNext = true; // automatically move to next QVT if there's a next link
    this.waitingForMoreTimeline = false; // when true, playback is done but QVT is open-ended & we're waiting for more
    this.waitForInitialLoadID = -1;
    this.reportedSeekPos = 0;
    this.inGap = false; // true when current clip is a gap clip
    this.gapIntervalID = -1;
    this.gapCurrentPos = 0; // during gap playback, where we currently are
    this.gapPaused = false; // we have to manually track paused state when playing a gap
    this.scrubbing = false;
    this.preScrubPaused = false; // were we paused before scrubbing
    MN.Event.Observe(playerObj, 'PlayStateChanged', this._PlayerPlayStateChanged);
    MN.Event.Observe(playerObj, 'BitRateChanged', this._PlayerBitRateChanged);
    MN.Event.Observe(playerObj, 'Error', this._PlayerError);
    MN.Event.Observe(playerObj, 'Script', this._PlayerScript);
    MN.Event.Observe(playerObj, 'AudioControl', this._PlayerAudioControl);
    MN.Event.Observe(playerObj, 'ScrubBumper', this._PlayerScrubBumper);

    this.lastCurrentShow = -1; // last value retrieved in _CheckCurrentShow
    setInterval(this._CheckCurrentShow, 1000);
}

_pwp.Scrub = function(rate)
{   // Begins or adjusts scrubbing rate. Returns true if a scrub command was actually issued
    //if (!this.player['Scrub'])
    //    return false;

    if (!this.scrubbing)
    {
        this.scrubbing = true;
        var ps = this.playState;
        var PS = MN.QMP.PS;
        if (0) //ps != PS.PLAYING)
        {
            log('Ignoring scrub command - current play state is', PS[ps]);
            return false;
        }
        log('Entering scrub mode');
        this.preScrubPaused = this.player.Paused;
    }

    if (this.player.Scrub(rate >= 0, Math.abs(rate)) != 0)
    {
        log('no scrub!')
        this.scrubbing = false;
        this.player.ScrubStop(this.preScrubPaused ? 1 : 0);
        return false;
    }

    return true;
}

_pwp.StopScrubbing = function()
{
    if (!this.scrubbing)
    {
        log('Ignoring scrubstop ocmmand - not scrubbing');
        return;
    }

    // to use stob scrubbing, we have to translate the play state into a custom enum type
    var mode = 0;
    if (this.preScrubPaused)
        mode = 1;
    this.scrubbing = false;
    this.player.ScrubStop(mode);
    log('Scrubbing stopped');
}

_pwp.SetAutoPlayNext = function(b) { this.autoPlayNext = b; }
_pwp.GetCurrentURL = function() { return this.currentURL; }

_pwp._CheckCurrentShow = function()
{   // inspects current show number and fires and event if the current show changes
    var showNum = this.CurrentShow();
    if (showNum >= 0 && showNum != this.lastCurrentShow)
    {
        this.lastCurrentShow = showNum;
        this.FireEvent('ShowChanged', showNum, this.qvt.Title(showNum) || '');
    }
}

_pwp._Play = function(posObj) // posObj is returned from TimelineToClip
{ // called in response to Play or setCurrentPosition
    // stop any previous gap interval
    if (this.gapIntervalID != -1)
    {
        clearInterval(this.gapIntervalID);
        this.gapIntervalID = -1;
    }

    log('now playing', posObj.url, posObj.startPos, posObj.stopPos);
    this.currentClipNum = posObj.clipNum;
    var prevURL = this.currentURL;
    var prevStopPos = this.currentStopPos;
    this.currentURL = posObj.url;
    this.currentStopPos = posObj.stopPos;
    this.inGap = (this.currentURL == null);
    var clip = this.qvt.GetClip(posObj.clipNum);
    if (!this.inGap)
    {
        // if the user requested a stop position and it falls within this clip, we shouldn't play
        // past that position
        if (this.requestedStopSec != -1 && this.requestedStopSec >= clip.tlStartTime && this.requestedStopSec < clip.tlStopTime)
        {
            this.currentStopPos = clip.rangeStart + (this.requestedStopSec - clip.tlStartTime);
            if (posObj.stopPos)
                this.currentStopPos = Math.min(this.currentStopPos, posObj.stopPos);
        }
        log('current stop', this.currentStopPos);

        // if we're in the same QMX file as before, we can be more efficient and just seek
        // instead of doing a full player.Play call
        if (this.currentURL == prevURL && this.playerVersion >= '06020906')
        {
            this.player.CurrentPosition = posObj.startPos;
            this.player.SetStopPosition(this.currentStopPos);
        }
        else
            this.player.Play(posObj.url, posObj.startPos, this.currentStopPos);
    }
    else
    {   // it's a gap!
        this.player.Stop();
        this.gapPaused = false;
        this.gapLastUpdate = (new Date()).getTime();
        log('stuff', clip.tlStartTime, posObj.startPos, clip.rangeStart, '-->', clip.tlStartTime + (posObj.startPos - clip.rangeStart));
        this.gapCurrentPos = clip.tlStartTime + (posObj.startPos - clip.rangeStart);
        this.gapEndPos = clip.tlStopTime;
        if (this.requestedStopSec != -1 && this.requestedStopSec < this.gapEndPos)
            this.gapEndPos = this.requestedStopSec;
        this._SetPlayState(MN.QMP.PS.PLAYING);
        this.gapIntervalID = setInterval(this._GapUpdateCB, 100);
    }
}

_pwp._GapUpdateCB = function()
{   // repeatedly called while playing a gap to update our fake current position and to
    // trigger the move to the next clip
    var now = (new Date()).getTime();
    if (!this.gapPaused)
    {
        this.gapCurrentPos += (now - this.gapLastUpdate)/1000.0;
        if (this.gapCurrentPos >= this.gapEndPos)
            this._MoveToNextClip();
    }
    this.gapLastUpdate = now;
}

_pwp._MoveToNextClip = function()
{   // attempts to move to the next clip. If none are present but there is a next timeline and moving
    // to the next timeline automatically is enabled, moves to the next timeline. Either way, returs true
    // if it moved to new content, false otherwise
    if (this.currentClipNum < this.qvt.ClipCount()-1)
    {   // More clips left, so just move to the next one
        log('Moving to next clip')
        this.currentClipNum++;

        // if the user has requested a stop position, don't move to the next clip if we're already past that stop pos
        if (this.requestedStopSec == -1 || this.requestedStopSec > this.qvt.StartTime(this.currentClipNum))
        {
            this.FireEvent('NextClip', this.currentClipNum);
            this._Play(this.qvt.GetClipAsPosObj(this.currentClipNum));
            return true;
        }
    }

    // we've played all the clips we have, but if the QVT is open-ended, then there may be
    // more coming later, so we have to shift into a waiting state of some sort. Otherwise,
    // if there is a 'next' timeline link, we can start playing it, or fire the MEDIAENDED state change.
    if ((!this.currentStopPos || (this.currentStopPos > 0 && this.CurrentPosition() >= this.currentStopPos)) && this.qvt.IsOpenEnded())
    {
        log('Playback reached end of open-ended QVT. Will wait for more');
        this.waitingForMoreTimeline = true;
        this._SetPlayState(MN.QMP.PS.WAITING);
        return true;
    }

    // See if there's a next URL we can move to
    var next = this.qvt.NextURL();
    if (next && this.autoPlayNext)
    {
        this.FireEvent('NextTimeline', next);
        this.Play(next, 0); // start at the beginning of the next timeline to make playback more continuous
        return true;
    }

    return false;
}

_pwp._PlayerPlayStateChanged = function(oldS, newS)
{
    var PS = MN.QMP.PS;
    if (newS == PS.PLAYING)
        this.playbackStarted = true;

    if (newS == PS.MEDIAENDED)
    {
        if (this._MoveToNextClip())
            return; // otherwise, fall through
    }
    else if (newS == PS.OPENING || newS == PS.STOPPED)
    {   // we swallow these to make clip transitions more seamless
        return;
    }

    // Nothing else to do - just fire the media ended event
    this._SetPlayState(newS);
}

_pwp._SetPlayState = function(newState)
{
    if (newState == this.playState)
        return; // fire an event only if we actually change states
    var oldState = this.playState;
    this.playState = newState;
    var Fire = this.FireEvent;
    setTimeout(function() { Fire('PlayStateChanged', oldState, newState); }, 1); // some callers need this to happen slightly asynchronously
}

_pwp._PlayerBitRateChanged = function(newRateKBps)
{
    this.FireEvent('BitRateChanged', newRateKBps);
}

_pwp._PlayerError = function(msg)
{
    logError('PlayerError', msg);

    // on error, try to move to the next bit of content, otherwise fire the error event to the app
    // (not sure if this is really the right thing to do but...)
    if (!this._MoveToNextClip())
        this.FireEvent('Error', msg);
}

_pwp._PlayerScript = function(key, value)
{
    this.FireEvent('Script', key, value);
}

_pwp._PlayerAudioControl = function(muted, volume)
{
    this.FireEvent('AudioControl', muted, volume);
}

_pwp._PlayerScrubBumper = function(atBeginning)
{
    log('bump!', atBeginning);
    this.FireEvent('ScrubBumper', atBeginning);
}

_pwp.Load = function(qvt)
{   // method to load a QVT without playing it
    this.playbackStarted = false;
    this.player.Stop();
    this._SetPlayState(MN.QMP.PS.OPENING);

    if (qvt != null)
    {
        // clear out any previous qvt listener
        if (this.qvt)
        {
            MN.Event.StopObserving(this.qvt, 'TimelineLoaded', this._OnTimelineLoaded);
            this.qvt.DecRef();
        }

        if (qvt instanceof MN.QVT.QVT)
            this.qvt = qvt;
        else
            this.qvt = MN.QVT.AcquireQVT(qvt);
        MN.Event.Observe(this.qvt, 'TimelineLoaded', this._OnTimelineLoaded);

        // If the QVT is not loading, then it probably came from cache, but we still want to
        // fire a loaded event so that the app can do some initial work with the QVT.
        if (!this.qvt.IsLoading())
            this._OnTimelineLoaded(this.qvt);
    }

    if (this.waitForInitialLoadID != -1)
        clearInterval(this.waitForInitialLoadID);
    this.lastCurrentShow = -1; // so event will fire event if previously already on first clip
}

_pwp.Play = function(qvt, startSec, stopSec)
{   // start playing a new QVT. If qvt is null, the current QVT isn't tossed out but is used instead
    this.currentURL = '';
    this.Load(qvt);

    if (startSec == null) startSec = -1;
    if (stopSec == null) stopSec = -1; // play all
    log('play', startSec, stopSec);

    this.requestedStartSec = startSec;
    this.requestedStopSec = stopSec;
    this.waitForInitialLoadID = setInterval(this.WaitForInitialLoad, 100);
}

_pwp.PlayClip = function(qmxURL, title, startSec, stopSec, playStartPos)
{   // helper function to take a QMX URL and wrap it up into a QVT on the fly to
    // play just a given range (i.e. the QVT ends up with the given title and a
    // duration of stopSec-startSec). Both start and stop positions are optional
    // but recommended.
    var clip = {};
    clip.url = qmxURL;
    clip.title = title || '';
    if (startSec == null)
        startSec = -1;
    if (stopSec == null)
        stopSec = '';
    clip.range = startSec + ',' + stopSec;

    var qvt = { "clips" : [clip] };
    this.Play(qvt, playStartPos);
}

_pwp._OnTimelineLoaded = function(qvt)
{
    if (qvt == this.qvt) // sometimes we still get events from old ones??
    {
        if (this.qvt.IsValid())
            this.FireEvent('TimelineLoaded', qvt);
        else
        {
            this._PlayerPlayStateChanged(this.playState, MN.QMP.PS.ERROR);
            this.FireEvent('Error', 'Failed to load requested QVT');
            return;
        }

        // by reloading the timeline, the stop position for our current clip may have
        // have changed (i.e. the publisher may have edited it). So we need to "reapply"
        // the stop position just in case. The logic below closely mirrows that of _Play.
        var clip = this.qvt.GetClip(this.currentClipNum);
        if (this.inGap)
        {
            this.gapEndPos = clip.tlStopTime;
            if (this.requestedStopSec != -1 && this.requestedStopSec < this.gapEndPos)
                this.gapEndPos = this.requestedStopSec;
        }
        else
        {
            // if the user requested a stop position and it falls within this clip, we shouldn't play
            // past that position
            if (this.requestedStopSec != -1 && this.requestedStopSec >= clip.tlStartTime && this.requestedStopSec < clip.tlStopTime)
                this.currentStopPos = clip.rangeStart + (this.requestedStopSec - clip.tlStartTime);
            this.player.SetStopPosition(this.currentStopPos);
        }

        // If we were previously waiting for the timeline to grow, we can now check it
        if (this.waitingForMoreTimeline)
        {
            log('Timeline has been updated, will try to resume playback');
            this.waitingForMoreTimeline = false;
            this._MoveToNextClip();
        }
    }
}

_pwp.WaitForInitialLoad = function()
{   // called until this.qvt.initialLoadDone is true, at which playback starts
    // (playback start is deferred until the timeline has loaded)
    if (this.qvt.InitialLoadDone())
    {
        clearInterval(this.waitForInitialLoadID);
        this.waitForInitialLoadID = -1;
        if (this.qvt.IsValid())
        {
            log('WaitForInitialLoad - done, starting playback');
            this.reportedSeekPos = this.requestedStartSec;
            this._Play(this.qvt.TimelineToClip(this.requestedStartSec));
        }
    }
}

_pwp.Stop = function() { this.player.Stop(); this._SetPlayState(MN.QMP.PS.STOPPED); }

_pwp.CurrentPosition = function()
{
    if (arguments.length == 0)
    {
        if (this.InGap())
            this.reportedSeekPos = this.gapCurrentPos;
        else if (this.player.CurrentPlayState == MN.QMP.PS.PLAYING)
            this.reportedSeekPos = this.qvt.ClipToTimeline(this.currentClipNum, this.player.CurrentPosition);
        return this.reportedSeekPos;
    }
    else
    {
        this.reportedSeekPos = parseFloat(arguments[0]);
        if (this.reportedSeekPos != 0 && !this.reportedSeekPos)
            this.reportedSeekPos = -1;
        this._Play(this.qvt.TimelineToClip(this.reportedSeekPos));
    }
}

_pwp.CurrentClip = function() { return this.currentClipNum; }

_pwp.CurrentShow = function(showNum)
{   // returns the current show number or -1 if not playing or moves playback to that show
    if (!this.qvt)
        return -1;
    if (showNum == null) // requesting current show num
        return this.qvt.TimelineToShow(this.CurrentPosition());
    var showNum = parseInt(showNum);
    if (showNum >= 0 && showNum < this.qvt.ShowCount())
    {
        var pos = this.qvt.StartTime(parseInt(showNum));
        if (pos != null)
            this.CurrentPosition(pos);
    }
}

_pwp.InGap = function() { return this.inGap; }

_pwp.Muted = function()
{
    if (arguments.length == 0)
        return this.player.Muted;
    else
    {
        var muted = !!arguments[0];
        this.player.Muted = muted;
        this.FireEvent('MutedChanged', muted);
    }
}

_pwp.Paused = function()
{
    if (arguments.length == 0)
    {
        if (this.InGap())
            return this.gapPaused;
        return this.player.Paused;
    }
    else
    {
        var paused = !!(arguments[0] || 0);
        if (this.InGap())
            this.gapPaused = paused;
        else
            this.player.Paused = paused;
        this.FireEvent('PausedChanged', paused);
    }
}

_pwp.Volume = function()
{
    if (arguments.length == 0)
        return this.player.Volume;
    else
    {
        this.player.Volume = arguments[0] || 0;
        this.FireEvent('VolumeChanged', this.player.Volume);
    }
}

_pwp.CurrentQVT = function() { return this.qvt; } // caller "promises" not to fiddle with this
_pwp.CurrentPlayState = function() { return this.playState; }
_pwp.CurrentBitRate = function() { return this.player.CurrentBitRate; }
_pwp.Live = function() { return this.qvt.HaveAnyLive(); }
_pwp.SingleStep = function() { this.player.SingleStep(); }
_pwp.Duration = function()
{
    var q = this.qvt;
    if (!q || !q.IsValid())
        return 0;
    var clip = q.GetClip(this.currentClipNum);
    if (q.HaveAnyLive() && clip && clip.durationGrowing && this.playbackStarted)
    {   // this clip is live, so we need to actively update the duration
        q.UpdateDuration(this.currentClipNum, this.player.Duration);
    }
    return q.Duration();
}

_pwp.Width = function(w)
{
    if (arguments.length == 0)
        return parseInt(this.player.style.width);
    else
        this.player.style.width = w + 'px';
}

_pwp.Height = function(h)
{
    if (arguments.length == 0)
        return parseInt(this.player.style.height);
    else
        this.player.style.height = h + 'px';
}

_pwp.GetSetting = function(name) { return this.player.GetSetting(name); }
_pwp.PutSetting = function(name, value) { this.player.PutSetting(name, value); }
_pwp.MetadataByName = function(name) { return this.player.MetadataByName(name); }
_pwp.MetadataName = function(index) { return this.player.MetadataName(index); }
_pwp.MetadataByIndex = function(index) { return this.player.MetadataByIndex(index); }

_pwp.RegistryVersion = function() { return this.player.RegistryVersion; }
_pwp.Version = function() { return this.player.Version; }
_pwp.FillColor = function(color) { this.player.FillColor = color;}
_pwp.VideoWidth = function() { return this.player.VideoWidth; }
_pwp.VideoHeight = function() { return this.player.VideoHeight; }
_pwp.HasAudio = function() { return this.player.HasAudio; }
_pwp.HasVideo = function() { return this.player.HasVideo; }
_pwp.MetadataCount = function() { return this.player.MetadataCount; }
_pwp.StartTimestamp = function() { return this.qvt.StartTimestamp(); }
_pwp.Encrypted = function() { return this.player.Encrypted; }
_pwp.RealAspectRatio = function() { return this.player.RealAspectRatio; }

_pwp.UserAspectRatio = function(ar)
{
    if (arguments.length == 0)
        return this.player.UserAspectRatio;
    else
        this.player.UserAspectRatio = ar || 0;
}

delete _pwp; // clean up the namespace

MN.LoadLibrary('qmpinstall');


