// BEHAVIOUR CLASS: PROJECTOR SLIDESHOW (background)

// (c) 2006 Toowoomba Motor Village - all rights reserved

// DESCRIPTION: carousel-projector slide-show - projection-screen is any element's background.
// FEATURES:    provides parameterised auto-advance carousel slideshow.
//              - to aid page readability, carousel pauses when mouse hovers over either of two nominated elements.
//              - carousel can be advanced to next slide by mouse-click on 1 nominated element (once 2 slides downloaded)
//              - silent abort & shutdown when fatal-errors. Note: at least 2 slides is mandatory.
// STATUS:      formal production operation.     v0.9 (rc1)     
//              audit: revise/review all comments to ensure current & accurate. caveat lector  - medium priority.
//              upgrade: consider comprehensive checking of ProjectorBg class arguments        - low    priority.
// TECH-NOTES:  EcmaScript3 encapsulated non-prototype classes (multi-instantiation).  [see notes at end-of-script]
//              Class 'Projector', a related class, in a better pattern for reuse thsn yhis class. 



function ProjectorBg (                                                              // CLASS CONSTRUCTOR //
                        screenElementID,     // target element for slideshow
                        slide1Time,          // first-slide  duration in secs
                        slideTime,           // other-slides duration in secs
                        resumeDelay,         // resume-after-pause delay in secs
                        loadDelay,           // start delay within instance, in secs (breathing-space for browser)
                        nextclickElmtID,     // [bg-only] target element  for next-click  - for optional, use ''
                        pauseElmt1ID,        // [bg-only] target element1 for hover-pause - for optional, use ''
                        pauseElmt2ID,        // [bg-only] target element2 for hover-pause - for optional, use ''
                        slidesFolder,        // relative to page (else '')
                        slideName1,          // mandatory (usually same as bkgd of elementID if it has one)
                        slideName2           // mandatory - at least 2 image URLs are mandatory, more are allowed
                     )
{

  try
  {
  
    var ME = this;                                                   // persistent instance-reference

    var ARGUMENTS_MANDATORY = 11;                                    // fixed args (including mandatory first 2 image urls)
    
    if (!(this instanceof ProjectorBg))                    { throw new Error('ProjectorBg: constructor CALLed as function!'); }
    if (!(arguments.length >= ARGUMENTS_MANDATORY))        { throw new Error('ProjectorBg: mandatory arguments missing'); }
    if (!window.document.getElementById(screenElementID))  { throw new Error('ProjectorBg: screen-element invalid'); }

    if (!ProjectorBg.enabled)  { return null; }


    this.showNextSlide = function ()
                         {
                             try
                             {
                                 if (!ProjectorBg.enabled)  { return; }
                                 ME.changer.scheduleNext();
                                 if (ME.paused)             { return; }
                                 ME.screen.show(ME.carousel.nextSlideURL());
                             }
                             catch(e){ME.changer.cancelNext();}
                         };

    this.pause         = function ()
                         {
                             try
                             {
                                 ME.paused = true;
                                 if (!ME.loaded)  { return; }
                                 ME.changer.cancelNext();
                             }
                             catch(e){ME.changer.cancelNext();}
                         };
    this.resume        = function ()
                         {
                             try
                             {
                                 ME.paused = false;
                                 if (!(ProjectorBg.enabled && ME.loaded))  { return; }
                                 ME.changer.resume();
                             }
                             catch(e){ME.changer.cancelNext();}
                         };

    this.onloadSlides  = function (slideCount)  { };                                     // unused
    this.onload2Slides = function ()  { ME.user.enableNextSlide(); };

    this.unload        = function ()
                         {
                             try
                             {
                                 ME.changer.cancelNext();
                                 ME.carousel.unload();
                             }
                             catch(e){}
                         };
    this.load          = function ()
                         {
                             try
                             {
                                 ME.changer.start();
                                 ME.carousel.load();
                                 ME.loaded = true;
                             }
                             catch(e){ME.changer.cancelNext();}
                         };
     
    var slidesURLs  = ProjectorBg.encaseArgs(arguments, (ARGUMENTS_MANDATORY - 2), slidesFolder);  // get array of all image-URLs  (class-static)
    var loadWait_ms = (loadDelay > 0.1) ? Math.round(loadDelay * 1000) : 100;                      // activation delay (browser breathing-space)

    this.loaded     = false;                                                             // true == all initialisation initiated
    this.paused     = false;
    this.carousel   = new Carousel(this.onloadSlides, this.onload2Slides, slidesURLs);
    this.screen     = new Screen(screenElementID);
    this.changer    = new Timer(this.showNextSlide, slideTime, slide1Time, resumeDelay, loadDelay);
    this.user       = new UserInterface(this.showNextSlide, this.pause, this.resume,
                                        nextclickElmtID, pauseElmt1ID, pauseElmt2ID);
    ProjectorBg.bindEvent(window, this.unload, 'unload', 'onunload');
                                        
    var timerLoad   = window.setTimeout(this.load, loadWait_ms);												 // note: creates a closure
  }
  catch(e)
  {
    if (timerLoad)  { window.clearTimeout(timerLoad); }
  }



    // class ProjectorBg - private classes (components)

    function Screen (screenElementID)                                               // CLASS CONSTRUCTOR //
    {
        var Me = this;                                                         // persistent instance-reference

        this.screenElmt  = window.document.getElementById(screenElementID);
        this.elmtStyle   = this.screenElmt.style;      
        
        this.show        = function (url)                                      
                           {
                               if (!url)  { return; }
                               Me.elmtStyle.backgroundImage = 'url(' + url + ')';
                           };
    }                                                                               // END: Screen CLASS & CONSTRUCTOR //



    function Timer (showNextSlideCallback, slideDuration, slide1Duration,
                    resumeDelay, loadDelay)                                         // CLASS CONSTRUCTOR //
    {
        var Me = this;                                                         // persistent instance-reference

        this.showSlide      = showNextSlideCallback;
        this.slideTime      = slideDuration;
        this.slide1Time     = slide1Duration;
        this.resumeDelay    = resumeDelay;
        this.loadDelay      = loadDelay;

        this.start          = function ()  { Me.schedule(Me.slide1Time - Me.loadDelay); };
        this.resume         = function ()  { Me.schedule(Me.resumeDelay); };
        this.scheduleNext   = function ()  { Me.schedule(Me.slideTime);   };
        this.cancelNext     = function ()  { if (Me.timerRef) {window.clearTimeout(Me.timerRef);} };
        this.schedule       = function (wait_secs)
                              {
                                  Me.cancelNext();
                                  var wait_ms = Math.round(wait_secs * 1000);
                                  Me.timerRef = window.setTimeout(Me.showSlide, wait_ms);
                              };
    }                                                                               // END: Timer CLASS & CONSTRUCTOR //



    function UserInterface (nextclickCallback, pauseCallback, resumeCallback,       // CLASS CONSTRUCTOR //
                            nextElmtID, pauseElmt1ID, pauseElmt2ID)                 // user-interface module
    {
        var Me = this;                                                         // persistent instance-reference
        
        this.nextclick  = nextclickCallback;
        this.nextElmt   = window.document.getElementById(nextElmtID);
        this.pauseElmt1 = window.document.getElementById(pauseElmt1ID);
        this.pauseElmt2 = window.document.getElementById(pauseElmt2ID);
        this.nextTitle  = ' \n*** click here to change the \'slide\' ***'; 
        var bind = ProjectorBg.bindEvent;                                      // static-method of ProjectorBg-class
        if (this.pauseElmt1)
        {
            bind(this.pauseElmt1, pauseCallback,  'mouseover', 'onmouseover');
            bind(this.pauseElmt1, resumeCallback, 'mouseout',  'onmouseout');
        }
        if (this.pauseElmt2)
        {
            bind(this.pauseElmt2, pauseCallback,  'mouseover', 'onmouseover');
            bind(this.pauseElmt2, resumeCallback, 'mouseout',  'onmouseout');
        }

        this.enableNextSlide  = function ()                                    // 'nextslide' click-element enable, etc.
                                {
                                    var el = Me.nextElmt;
                                    if (!el)  { return; }                      // non existent element - abandon
                                    var tl = el.title;
                                    ProjectorBg.bindEvent(el, Me.nextclick, 'click', 'onclick');
                                    el.style.cursor = 'pointer';
                                    if (!tl)  { tl = 'advance to next-slide'; }
                                    el.title = tl + Me.nextTitle;
                                };

    }                                                                               // END: UserInterface CLASS & CONSTRUCTOR //


    // class Carousel - common projector-lib - (used in Projector, ProjectorBg)

    function Carousel (onloadAllSlidesCallback, onload2SlidesCallback, urls)        // CLASS CONSTRUCTOR // 
    {
        var Me = this;                                                         // persistent instance-reference

        this.onloadAllNotify   = onloadAllSlidesCallback;
        this.onload2SldsNotify = onload2SlidesCallback;
        this.size              = urls.length;             // carousel capacity in slides
        this.filled            = false;                   // carousel filled? (all slides fully downloaded?)
        this.received          = 0;                       // count of slides fully downloaded
        this.slideNum          = 1;                       // current slide (at start, assumes 1st slide shown as static)
        this.slides            = new Array();             // INDEXED AS base-1 !!!

        this.nextSlideURL = function ()
                            {
                                if (Me.received < 2)  { return false; }                  // 'false' is type-safe as url can't be only a 0
                                Me.slideNum += 1;
                                if (Me.slideNum > Me.received)  { Me.slideNum = 1; }
                                return (Me.slides[Me.slideNum].url);
                            };

        this.rewind       = function ()
                            {
                                Me.slideNum = 1;                               // slide #2 is to be next-slide
                            };
        this.unload       = function ()
                            {
                                if (Me.filled)  { return; }                    // if no image is being downloaded, leave
                                Me.slides[Me.received+1].unload();
                            };
        this.unloadAll    = function ()                                        // not used
                            {
                                Me.received = 0;
                                var s = Me.size;
                                for (var i=1; i<=s; i++)  { Me.slides[i].unload(); }
                            };

        this.load         = function ()                                        // retrieve serially (see also 'onloadSlide')
                            {
                                if (Me.filled)  { return; }                    // prevent multiple loads
                                window.status = Me.size;
                                Me.slides[2].load();                           // 2nd slide
                            };
        this.onloadSlide  = function (slideNum)
                            {
                                Me.received += 1;
                                if (Me.received == 2)  { Me.onload2SldsNotify(); }
                                if (Me.received < Me.size)
                                {
                                    window.status = Me.size - (slideNum - 1);
                                    var nextSld = slideNum + 1;
                                    if (nextSld > Me.size)  
                                    { 
                                       nextSld = 1;
                                       if (window.opera)  { Me.received = Me.size; }        // opera remedial (see tech-notes)
                                    }
                                    Me.slides[nextSld].load();           
                                }
                                else
                                {
                                    Me.filled = true;
                                    window.status = '';
                                    Me.onloadAllNotify(Me.received);
                                }
                            };

        var s = this.size;
        for (var i=1; i<=s; i++)
        {
           this.slides[i] = new Slide(this.onloadSlide, i, urls[i-1]);
        }


        // class Carousel - private classes (components)

        function Slide (onloadCallback, slideNum, url)                              // CLASS CONSTRUCTOR //
        {
            var me = this;                                                     // persistent instance-reference

            this.onloadNotify = onloadCallback;
            this.slideNum     = slideNum;
            this.loaded       = false;
            this.url          = url;
            this.img          = new Image();
            this.img.onload   = null;
            this.img.setAttribute('src', null);

            this.load   = function ()
                          {
                              me.img.onload = function ()                      // image-loaded event-handler
                                              {
                                                  try
                                                  {
                                                       me.loaded = true;
                                                       me.onloadNotify(me.slideNum);
                                                  }
                                                  catch(e){}
                                              };
                              me.img.setAttribute('src', me.url);              // initiates image retrieval
                          };
            this.unload = function ()
                          {
                              me.loaded = false;
                              me.img.onload = null;
                              me.img.setAttribute('src', null);
                              me.img = null;
                          };
        }                                                                           // END: Slide     CLASS & CONSTRUCTOR //

    }                                                                               // END: Carousel  CLASS & CONSTRUCTOR //

}                                                                                   // END: ProjectorBg CLASS-CONSTRUCTOR //



// class ProjectorBg - class features (statics), public (can't be private) - generic methods are non class-specific

ProjectorBg.enabled    = true;                                                   // property: master-state for class
ProjectorBg.encaseArgs = function (argsObject, iFirstArg, optionalFolder)        // generic: parse function-arguements & return array
                         {
                             var folder = (optionalFolder) ? (optionalFolder + '/') : '' ;
                             var nURLs  = argsObject.length - iFirstArg;                            // iFirstArg is idx base-0
                             var args   = new Array();                                              // idx base-0
                             for (var i=0; i<nURLs; i++)  
                             {
                                args[i] = folder + argsObject[i + iFirstArg];
                            }
                            return args;                      
                         };
ProjectorBg.bindEvent  = function (obj, evtHandler, onEvt_W3C, onEvt_MSIE)        // generic: DOM-event bindery
                         {
                             var CAPTURE  = false;
                             var W3C      = !!(window.addEventListener);
                             var MSIE     = !!(window.attachEvent);
                             if (W3C)  { obj.addEventListener(onEvt_W3C, evtHandler, CAPTURE); }
                             else 
                             if (MSIE) { obj.attachEvent(onEvt_MSIE, evtHandler); }
                         };

//                                                                                  // END: ProjectorBg CLASS //





// TECH-NOTES - PROJECTORBG CLASS & RELATED CLASSES                    
//
// NOTES: [0]   Note: 'Projector' class is an evolution of this 'ProjectorBg' class; 'Projector' class
//              is a better pattern with more generic internal classes (eg. Timer -> Animator).
//        [1]   The ProjectorBg class enhances a complete web-page by adding non-essential functionality.
//              The page needs to be complete and not dependant on this class as the script-engine might be
//              disabled in some browsers. For this reason, the image that is the first slide should
//              be used in the web-page markup/styling as normal (as would be done without the class).
//              Hence, the ProjectorBg class assumes that first slide is visible to the user at pageload,
//              and displays the second slide at the appropriate time; the initial slide displayed by the
//              class is actually the second slide in the carousel.
//        [2]   When the ProjectorBg class is instantiated, the instance-reference is typically not stored
//              explicitly. Such a reference is unnecessary as [a] the class does not need
//              public instance 'features', and, [b] instance remains in existence due to various
//              external references - those assigned to the DOM (unload, etc) & global-object (timer-callback).
//        [3]   These classes are time-based: practically all methods are invoked either via
//              a system-timer callback, ondownload callback, or occasionally, a inter-class callback].
//        [4]   Opera 9 fails to trigger the 'onload' event for an image that has been loaded by the host-page 
//              (typically the case with slide1). Opera-specific remedial-code in Carousel.onloadSlide method  
//              partially compensates (so all slides are displayed) - this remedial-code shall be removed 
//              once defective Operas are no longer in the public domain of the market segment 
//              (possibly as late as 2008 or 2010? given the bug exists in dec2006).
//              Note: this bug manifests with most methods of loading a page into Opera, but not all -
//                    in one case, it works correctly & has identical performance to IE & FireFox!
//
// CONVENTIONS: Instance references are formalised as an custom object-reference (me) to attain persistency:
//              - 'me' is an persistent reference to the instance (used when 'this' not applicable).
//              - 'me' (not 'this') MUST be used within methods invoked directly of indirectly via call-backs.
//              - 'me' is expressed in differing case in nested classes purely for reader clarity.
//              - In these classes (and similar classes based on this MeClosures pattern)
//                both 'this' & 'me' instance object-references are used in the following manner:
//                  - 'this' refers to instance AT 'constructor-time' and is exclusively used for all such references.
//                     Thus, 'this' tags class-constructor stage, a side-effect of which is improved readability.
//                  - 'me' refers to the instance AFTER 'constructor-time' and MUST used for such references.
//                     note: 'this' must NOT be used, as it refers elsewhere (to the calling-object,
//                     which is often the global-object given use of system-methods in classes).
//              - To 'tag' closures, 'me' is used explicitly, even if other identifiers are in lexical scope (parameters, etc);
//                this convention may result in additional class properties than otherwise (trivial given clarity).
//                The single exception (timerLoad) of the ProjectorBg constructor is noted inline.
//              - Public static class-members require a suitable host-object - the
//                class constructor-function itself is ideal.
//              - For frequent reference to public static class-members (not the case in these classes),
//                an additional reference 'we' could be used, to access & tag such public static class-members
//                for convenience (and reference speed).
//
// PATTERN:     MeClosures - full-callback to instance - multi-instantiation.
//              - encapsulated classes using constructor-persistence via closures.
//              - the class is encapsulated in external terms, but is within scope of internal nested-classes.
//              - encapsulation/privacy is intended to avoid typical unintentional misuse (non maleficent).
//              - stronger external instance-privacy, if warranted, requires addition of getters/setters, etc.
//              - class reserves only *one* global-identifier (ProjectorBg) - no global-variables used.
//
//              Callbacks into instances are attained by use of higher-order functions with their closures (as in LISP)
//              and do have a cost, using more memory per instance than ecmaScript prototype-classes.
//              In classes such as these, the memory overhead is trivial & resulting encapsulation worth the cost.
//              Browser memory-leaks with patterns using closures are known bugs (esp older browsers)
//              and need to be at least considered. Memory-leaks, if any, are not apparent with these classes,
//              would probably be miniscule, and, probably unimportant given the transient page-life of a browser.


// (c) 2006 Ian Brown

// end script

