Source: src/handlers.js

import * as THREE from 'three';
import { Context } from '/js/src/context.js';
import TWEEN from '@tweenjs/tween.js';
import { GroundProjectedSkybox } from 'three/addons/objects/GroundProjectedSkybox.js';

/**
 * Handlers is the core event router for interactivity.
 * @module Handlers
 * @exports Handlers
 * 
 * @todo Confirm naming convention of object definitions and scopes.
 */
const Handlers = (() => {
  /**
   * The Context constant provides a neutral scope to use as part of the state handling tasks. see {@link module:Context}
   * @name module:Handlers.context
   * @private
   * 
   */
  const context = Context;

  /**
   * Animations Anim is the central action strip manager.<br>
   * It's behavior is polymorphic and dictated by the type of argument<br>
   * obj - string: obj represents a single animation contained in the current scene, it gets added to the tweenStack and play is invoked. <br>
   * obj - array: obj contains a list of animation names to play, it can also contain callback functions as array items. <br>
   * obj - object: obj contains configuration parameters as properties, eg 'concurrent':true
   * 
   * @class 
   * @name Handlers#animationAnim
   * @param {string|array|object} obj - A container of animation and callback instructions. 
   * @alias module:Handlers.animationAnim
   * 
   */
  const animationAnim = (obj = null) => {
    console.group("animationAnim");
    /**
     * Declaration of context.get.config
     * @name Handlers#animationAnim.context
     * @private
     */
    const config = context.get.config;

    /**
     * The current animation stack from config.animStack
     * @name Handlers#animationAnim.animStack
     * @private
     */
    const animStack = config.animStack;

    /**
     * add animation to animation stack.
     * @method
     * @name Handlers#animationAnim#add
     * @private
     * @param {string} animationName the name of an animation already existing within the scene.
     */
    const add = (animationName) => {
      animStack.push(animationName);
    };
    /**
     * play an animation from the animation stack
     * @method
     * @name Handlers#animationAnim#play
     * @private
     */
    const play = () => {
      if (context.get.config.animStack.length) {
        context.get.config.currentAnimation = context.get.config.animStack.shift();
        
        // triage current animation
        if (typeof context.get.config.currentAnimation == 'function') {
          // function request
          context.get.config.currentAnimation();
          context.get.config.currentAnimation = '';
          animationAnim();
        }
        else if (
          typeof context.get.config.currentAnimation === 'object' &&
          context.get.config.currentAnimation !== null &&
          context.get.config.currentAnimation.hasOwnProperty('concurrent')
        ) {
          console.log("CONCURRENT TWEEN", context.get.config.currentAnimation.concurrent);
          if (context.get.config.currentAnimation.concurrent === true) {
            while(context.get.config.animStack.length){
              const animStack_item = context.get.config.animStack.shift();
              typeof  animStack_item === 'string' ?
              simpleAnim(animStack_item) :
              animStack_item();
            }
          }else {
            animationAnim();
          }
        }
        else {
          console.log("GOING SIMPLE", context.get.config.currentAnimation);
          simpleAnim(context.get.config.currentAnimation);
        }
      }
    };

    // runtime logic
    if (Array.isArray(obj)) {
      // An array of animation names
      obj.map((animItem) => {
        add(animItem);
      });
      play();
    } else if (obj !== null && typeof obj === 'string') {
      // A single animation
      add(obj);
      play()
    } else {
      play();
    }
    console.groupEnd();
  };

  /**
   * Tweening Tween is the central Tween manager.<br>
   * It has a polymorphic behaviour that is trigerred by the type of the param content:<br>
   * obj - array: obj represents a sequence of tweens, each an array in itself.<br>
   * obj - object: obj is an configuration object whose attributes are parsed for values, eg.- concurrent.<br>
   * 
   * @class
   * @name Handlers#tweeningTween
   * @param {array|object|null} obj - A container of Tween and callback instructions.
   * @alias module:Handlers.tweeningTween
   */
  const tweeningTween = (obj = null) => {
    console.group("tweeningTween");
    const tweenStack = context.get.config.tweenStack;

    /**
     * The add method takes a tween animation array or a configuration object and adds it to the context tweenStack. see {@link module:Context}
     * @method
     * @name Handlers#tweeningTween#add
     * @param {array} obj An array that contains instructions for a TWEEN animation.
     * @private
     */
    const add = (obj) => {

      tweenStack.push(obj);
    };

    /**
     * The play method relies on the Context instance, gathering information from the tweenStack. See {@link module:Context#}
     * 
     * @method
     * @name Handlers#tweeningTween#play
     * @private
     */
    const play = () => {

      if (context.get.config.tweenStack.length) {
        if (typeof context.get.config.tweenStack[0] == 'object' && context.get.config.tweenStack[0].concurrent) {
          // purge config
          const purge = context.get.config.tweenStack.shift();
          console.log("CONCURRENT PLAY", purge);
          playConcurrent();
        }
        else {
          context.get.config.tweenCurrentAnimation = context.get.config.tweenStack.shift();
          simpleTween(
            context.get.config.tweenCurrentAnimation[0],
            context.get.config.tweenCurrentAnimation[1],
            context.get.config.tweenCurrentAnimation[2] ? context.get.config.tweenCurrentAnimation[2] : 700,
            context.get.config.tweenCurrentAnimation[3] ? context.get.config.tweenCurrentAnimation[3] : null
          );
        }
      }
    };

    /**
     * The play concurrent method consumes the tweenStack in a single iteration and doesnt wait for event loops to complete
     * 
     * @method
     * @name Handlers#tweeningTween#playConcurrent
     * @private
     */
    const playConcurrent = () => {
      for (const anim of tweenStack) {
        simpleTween(
          anim[0],
          anim[1],
          anim[2] ? anim[2] : 700,
          anim[3] ? anim[3] : null
        );
        context.get.config.tweenStack = [];
      }
    }

    // runtime logic
    if (Array.isArray(obj)) {
      // An array of animation names
      obj.map((tweenItem) => {
        add(tweenItem);
      });
      play();
    } else {
      // obj is null or malformed
      play();
    }
    console.groupEnd();
  };

  /**
   * Change Sky manipulates the texture of the skybox mesh.
   * @alias module:Handlers.changeSky
   * @param {string} tex_ - A texture file name available in the default textures folder.
   */
  const changeSky = (tex_ = "blender_1.jpg") => {
    console.group("changeSky")
    try {

      const tex = `/hdri/${tex_}`;

      // Import from context: scene
      const scene = context.get.scene;

      // Import from context: gltf
      const gltf = context.get.gltf;
      const obj = gltf.scene.getObjectByName('Sky');

      // Import from context: renderer
      const renderer = context.get.renderer;

      // Import writeable from context: skybox
      let skybox = context.get.skybox;

      // Set emissive intensity.
      obj.material.emissiveIntensity = 1;

      // Texture loader for the Sky Dome and Ground Projected Sky Box
      new THREE.TextureLoader().load(
        tex,
        texture => {
          try {
            //Update Texture for the SkyDome.            
            texture.flipY = false;
            texture.wrapS = THREE.RepeatWrapping;
            texture.repeat.x = -1;
            texture.mapping = THREE.EquirectangularReflectionMapping;
            texture.colorSpace = THREE.SRGBColorSpace;

            // Apply texture to SkyDome
            obj.material.map = texture;
            obj.material.emissiveMap = texture;
            obj.material.needsUpdate = true;




            // // Reflog skybox
            const bgtex = texture.clone();
            bgtex.flipY = true;
            // const rebox = new GroundProjectedSkybox(bgtex);
            // rebox.name = `Rebox for ${tex_}`;
            // rebox.scale.setScalar(100);
            // rebox.height = 20;
            // rebox.radius = 200;
            // console.log(`Rebox tex ${tex_}`);

            // Swap Skybox
            // scene.add(rebox);
            // scene.remove(skybox);

            // Update texture for the background
            // Background and environmnet
            scene.background = bgtex;
            scene.environment = bgtex;

            // general pipeline intensity
            context.get.renderer.toneMappingExposure = .5;
          } catch (e) {
            console.error(e.message)
          }
        },
        // called while loading is progressing
        (xhr) => {

          console.log((xhr.loaded / xhr.total * 100) + '% loaded');

        },
        // called when loading has errors
        (error) => {
          console.log(`There was an Error Loading the GLTF: ${error.message}`, "error");
        }
      )
    }
    catch (e) {
      console.log(`changeSky ${e.message}`);
    }
    console.groupEnd();
  };

  /**
   * Simple Anim executes parametrized animation strips or the content of a callback.
   * @alias module:Handlers.simpleAnim
   * @param {string|function} anim - An animation clip string to play, or a callback function.
   */
  const simpleAnim = (anim = false) => {
    console.group("simpleAnim");
    console.log("anim:", anim)
    try {
      !anim ?
        () => { throw new Error('simpleAnim error: anim param not provided') } :
        '';

      // Import from context: mixer
      const mixer = context.get.mixer;

      // Import from context: gltf
      const gltf = context.get.gltf;

      // Get clip
      const clips = gltf.animations;
      const clip = THREE.AnimationClip.findByName(clips, anim);

      // Build animation action
      const action = mixer.clipAction(clip);
      action.repetitions = 1;
      action.clampWhenFinished = true;

      // Play reversed if extended
      if (action.paused) {
        action.reset();
        action.timeScale *= -1;
      }
      action.play();
      
    } catch (e) {
      console.log(`e.message: ${e.message}`);
    }
    console.groupEnd();
  };

  /**
   * Simple Tween takes the movement, timing, and objective parameters to create TWEEN slerped animations, it can implement a callback as part of the tween on complete event.
   * @alias module:Handlers.simpleTween
   * @param {object} target - Reference to a Scene instantiated 3D object tweenable property
   * @param {THREE.Vector3 | THREE.color} vector - The amount to tween, expressed in either color or Vector3
   * @param {number} speed - Time in milliseconds to spend animating
   * @param {function} callback - Callback code to be executed on completion.
   */
  const simpleTween = (target, vector, speed = 700, callback = null) => {
    console.group("simpleTween");
    console.log(target);
    try {
      new TWEEN.Tween(target)
        .to(vector, speed)
        // .onUpdate() lerp maybe
        .easing(TWEEN.Easing.Cubic.InOut)
        .start()
        .onStart((e) => { context.get.config.tweenCurrentAnimation = 'SimpleTween'; })
        .onComplete((e) => {
          context.get.config.tweenCurrentAnimation = '';
          if (callback) {
            callback();
          }
          tweeningTween();
        });
    } catch (e) {
      console.log(e);
    }
    console.groupEnd();
  };

  /**
   * Feature Anim received a desired animation set to follow which is defined in its switch.
   * This feature is then built up of tweens and animations.
   * @alias module:Handlers.featureAnim
   * @param {string} feature - A convention of a given feature.
   * @returns void
   */
  const featureAnim = (feature) => {
    console.group("featureAnim");
    // prevent stacking animations
    if (context.get.config.currentAnimation !== '') {
      return;
    }

    // Place the camera
    simpleTween(
      // Target
      context.get.camera.position,
      // Vector
      new THREE.Vector3().addVectors(
        new THREE.Vector3(-0.0712, .112, .1010),
        context.get.stageProps.bootProps[context.get.config.sceneCurrentStage].target.positionTo
      ),
      // Speed
      700,
      // Callback
      () => console.log(`${feature} Camera Tween`)
    );

    // feature animation sequences
    switch (feature) {
      case "activeCooler":
        console.log("ACTIVE COOLER TRIGGER",context.get.config.sceneCurrentCase);
        context.get.config.sceneCurrentCase !== '' ? // Terniary is the question asked
          animationAnim(
            [
              context.get.config.sceneCurrentCase,
              'Active-CoolerAction',
              context.get.config.sceneCurrentCase
            ]) :// Terniary replaces if else
          animationAnim([
            'Active-CoolerAction',
          ]); // Terniary is the statement
        break;

      case "sdCard":
        animationAnim('SD_CARD_Action');
        break;
      default:
      // Wonder about the new normal.
    };
    iddleAnim();
    console.groupEnd();
  };

  /**
   * The Section Tween generates camera movement that does not interfere with the scene.<br>
   * Section tweens 
   * @alias module:Handlers.SectionTween
   * @param {string} target_id - A convention of a given coordinates entry, see {@link module:Context}.
   * @returns void
   */
  const SectionTween = (target_id) => {

    console.group("SectionTween");
    const tweenStack = [
      [context.get.controls.object.position, new THREE.Vector3().addVectors(context.coords.target, context.coords[target_id])],
      [context.get.controls.target, context.coords.target]
    ];

    tweeningTween(tweenStack);
    iddleAnim();
    console.groupEnd();
  };

  /**
   * Response to the color-picker event. The function destructs the color and transfors it from 0-255 to 0-1 range per channel.
   * @alias module:Handlers.onColorTweenColor
   * @param {object} color - A convention of a given color coordinate.
   * @param {number} color.r - Red Channel
   * @param {number} color.g - Green Channel
   * @param {number} color.b - Blue Channel
   * @returns void
   */
  const onColorTweenColor = (color) => {
    console.group("onColorTweenColor");
    const { r, g, b } = color;
    simpleTween(context.get.scene.getObjectByName('Cases_char').children[0].material.color, { r: (r / 255), g: (g / 255), b: (b / 255) });
    console.groupEnd();
  };

  /**
   * The case Char(acter) Anim(ation) handles an individual object state within context parameters, 
   * using them to process logic for the animation requirement.
   * @alias module:Handlers.caseCharAnim
   * @param {string} anim - A pre-established case animation.
   * @returns void
   */
  const caseCharAnim = (anim) => {
    console.group("char")
    console.log("char!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");

    // import from context, current animation
    const anim_check = context.get.config.currentAnimation;
    const case_check = context.get.config.sceneCurrentCase;
    let caseTweenStack = [];
    context.get.config.currentAnimation = anim;

    console.log(anim_check, anim, case_check)
    // Case animations (from Blender Action to GLTF Animation, converted)
    // Bottom Animation
    const anim_bottom = 'CaseBottomAction';

    // Top Animations
    const anims_top = [
      'CaseTopAction',
      'CaseTopGpioAccessAction',
      'CaseTopHexagonsAction',
      'CaseTopPortAccessAction'
    ];

    // prevent overlapping animations.
    if ([...[anim_bottom], ...anims_top].includes(anim_check)) return;

    const bottomAction = context.get.mixer.clipAction(
      THREE.AnimationClip.findByName(context.get.gltf.animations, anim_bottom)
    );

    let anims_Stack = [];
    // Treat overlapping Animations to the active menu pattern
    // get all animations of top
    for (const anim_item of anims_top) {
      console.log("FOR: ", anim_item, anim, anim_check,);
      let anim_runner = THREE.AnimationClip.findByName(context.get.gltf.animations, anim_item);
      let topAction = context.get.mixer.clipAction(anim_runner);
      let caseIsActive = bottomAction.time > 0 && topAction.time > 0 ? true : false;
      let caseIsStoped = bottomAction.time === 0 && topAction.time === 0 ? true : false;

      if (anim_item != anim) {
        console.log("anim_item != anim");
        if (caseIsActive) {
          console.log("anim_item != anim::caseIsActive", caseIsActive);
          anims_Stack.unshift(anim_item);
        } else {
          console.log("anim_item != anim::caseIsStoped", caseIsStoped)
        }
      } else {
        // Toggle current Bottom
        if (caseIsActive || caseIsStoped) {
          // Play or reverse Bottom into full loop.
          // caseTween does a little tweem-animation-tween sequence to try to prevent box from clipping with environment.
          caseTweenStack = [
            [
              context.get.scene.getObjectByName(context.get.config.subject).position,
              new THREE.Vector3().addVectors(
                context.get.stageProps.bootProps[context.get.config.sceneCurrentStage].target.positionTo,
                context.coords.sbc_offset_jump
              ),
              caseIsActive ? 1400 : 300,
              simpleAnim(anim_bottom)
            ],
            [
              context.get.scene.getObjectByName(context.get.config.subject).position,
              context.get.stageProps.bootProps[context.get.config.sceneCurrentStage].target.positionTo,
              caseIsStoped ? 1400 : 300,
              () => {
                // Set context: sceneCurrentCase, currentAnimation
                context.get.config.sceneCurrentCase = caseIsActive ? '' : anim;
                context.get.config.currentAnimation = '';
              }
            ]
          ];

          if (caseIsActive) {
            anims_Stack.push(anim_item);
          }
        }
        context.get.config.sceneCurrentCase = caseIsActive ? '' : anim;
      }
    };

    // Toggle top
    anims_Stack.push(anim);

    // run animation and tween stacks
    animationAnim(anims_Stack);
    tweeningTween(caseTweenStack);
    iddleAnim();

    console.groupEnd();
  };

  /**
   * The Scene Stage Anim(ation) function takes care of orchestrating the transition between scenes.
   * Scenes are described as Context entries that are scanned for "props".
   * @alias module:Handlers.sceneStageAnim
   * @param {string} stage - The name of a scene already existing as a record in the context environment.
   * @returns void
   */
  const sceneStageAnim = (stage) => {
    console.group("sceneStageAnim");
    console.log(stage, context.get.config.sceneCurrentStage, context.get.config.sceneCurrentAnimation);
    // prevent running stage animation if stage is self
    if (context.get.config.sceneCurrentStage == stage) return;

    // prevent stacking stage transitions out of sync.
    if (context.get.config.sceneCurrentAnimation !== '') return;
    context.get.config.sceneCurrentAnimation = stage;
    // Object Target
    // in this project; the raspberry, mostly
    // The target exists because we want to have an objective for the purpose of the  Orbit Controls
    // Otherwise the Object Target represents the initial point of reference for other controls.

    // All the meshes that comprise a stage to put on scene
    // A Stage can be defined as all props that are active and visible within the skydome at any one given point.
    const stageProps = {
      // lights{}
      'lights': {
        // sea[]
        'sea': {
          // proplist
        },
        // grassland[]
        'grassland': {
          // proplist
        },
        // snow[]
        'snow': {
          // proplist
        },
      },
      // anim into place
      'bootProps': {
        'sea': {
          'canoe': {
            'spawnAnim': 'KenuAction'
          },
          'paddle': {
            'spawnAnim': 'woodenRowAction'
          },
          'fishingRod': {},
          'target': {
            'positionTo': new THREE.Vector3(0, 0.47375, -2.652),
          }
        },
        'grassland': {
          'bench': {
            'spawnAnim': 'BenchAction'
          },
          'beer': {
            'spawnAnim': 'BeerAction'
          },
          'target': {
            'positionTo': new THREE.Vector3(0, 0.71684, 0),
          }
        },
        'snow': {
          'sled': {
            'spawnAnim': 'DogSledAction'
          },
          'goggles': {
            'spawnAnim': 'gogglesAction'
          },
          'target': {
            'positionTo': new THREE.Vector3(0.057598, 0.855723, -1.05209),
          }
        },
        'icosphere': {
          'icosphere': {
            'spawnAnim': 'IcosphereAction'
          },
          'target': {
            'positionTo': new THREE.Vector3(0, 1.55, 0),
          }

        }
      },
      // external
      'external': {
        'sea': {},
        'grassland': {},
        'snow': {}
      },
      // scene, controls, renderer settings.
      'settings': {
        'sea': {},
        'grassland': {},
        'snow': {}
      },
      'scenesAllowed': ['sea', 'grassland', 'snow', 'icosphere'],
      'stageCurrentName': false,
    };

    // flog context for stageProps
    if (undefined === context.get.stageProps) {
      context.add({ "name": "stageProps", "obj": stageProps });
    }

    // Start Scene animation
    // Lock controls
    context.get.controls.enabled = false;
    iddleAnim();
    let animQueue = [];

    try {
      animQueue.push({'concurrent':context.get.config.sceneStageAnim.concurrent});
      animQueue.push(() => {
        // animate target to safe height
        sceneStageAnim_setTarget(context.get.config.transitionTarget);
      });
      // get current stage, clear its props, if no stage set, clear all props
      const current_stage_spawn_anim = sceneStageAnim_getCurrentStage(
        context.get.stageProps.stageCurrentName
      ).filter(checkAnimationIsExtended);

      for (const stage_prop_anim_current of current_stage_spawn_anim) {
        console.log("WTF CURRENTS", stage_prop_anim_current);
        animQueue.push(stage_prop_anim_current);
      }

      // get listing of new props and iterate spawn animation for new stage
      const { Iteratable, target } = sceneStageAnim_getNextStage(stage);
      for (const stage_prop_anim_next of Iteratable) {
        console.log("WTF NEXT", stage_prop_anim_next);
        animQueue.push(stage_prop_anim_next);
      }


      animQueue.push(() => {
        if (undefined !== context.get.config.skydome.textures[context.get.config.sceneCurrentStage]) {
          changeSky(context.get.config.skydome.textures[stage])
        } else {
          changeSky(context.get.config.skydome.textures[context.get.config.sceneDefaultStage])
        }

        context.get.config.sceneCurrentStage = stage;
        sceneStageAnim_setTarget(context.get.stageProps.bootProps[context.get.config.sceneCurrentStage].target.positionTo);
        context.get.controls.enabled = true;
        context.get.controls.MaxDistance = context.get.config.controlsMaxDistance;
      });
      // run the animation set
      animationAnim(animQueue);
    } catch (e) {
      // unLock controls
      context.get.controls.enabled = true;
    }
    console.groupEnd();
  };

  /**
   * Set Target is a private sub routine meant to place the camera rig and controls center of action along with the subject to a new position.
   * @alias module:Handlers.sceneStageAnim_setTarget
   * @private
   * @param {THREE.Vector3} coords A final destination for the target  and subject.
   */
  const sceneStageAnim_setTarget = (coords) => {
    console.group("sceneStageAnim_setTarget");
    const tweenstack = [
      // Make animation concurrent
      { 'concurrent': context.get.config.setTarget.concurrent },

      // Orbit Controls Target
      [context.get.controls.target, coords],

      // Camera
      [context.get.camera.position, new THREE.Vector3().addVectors(coords, context.coords.mainmenuitem_choose_a_setting)],

      // Subject
      [context.get.gltf.scene.getObjectByName(context.get.config.subject).position, coords, 700, () => {
        context.get.config.sceneCurrentAnimation = '';

      }],

    ];
    tweeningTween(tweenstack);
    console.groupEnd();
  };

  /**
   * Fetches all props from all stages if no stage is defined
   * @alias module:Handlers.sceneStageAnim_getCurrentStage
   * @private
   * @param {string} stage The currently enabled stage. 
   * @returns {array} an array of prop pointers.
   */
  const sceneStageAnim_getCurrentStage = (stage) => {
    console.group("sceneStageAnim_getCurrentStage");
    const sp = context.get.stageProps;
    let Iterated = [];
    if (!stage) {
      for (const allStages of sp.scenesAllowed) {
        const { Iteratable, target } = sceneStageAnim_getNextStage(allStages);
        if (Iteratable) {
          Iterated = [...Iterated, ...Iteratable];
        }
      }
    }
    else {
      const { Iteratable, target } = sceneStageAnim_getNextStage(stage);
      Iterated = Iteratable;
    }
    console.groupEnd();
    return Iterated;
  };

  /**
   * Looks up the required props to instantiate the scene by the provided stage string.              
   * @alias module:Handlers.sceneStageAnim_getNextStage
   * @private
   * @param {string} stage The name of a pre existing stage from the context config object.
   * @returns {object} An object containing an array of prop names and a vector of the subject target for this scene.
   */
  const sceneStageAnim_getNextStage = (stage) => {
    console.group("sceneStageAnim_getNextStage");
    const sp = context.get.stageProps;
    const Iteratable = [];
    let target;

    for (const propType in sp) {
      if (
        sp[propType].hasOwnProperty(stage)
        &&
        Object.keys(sp[propType][stage]).length > 0
      ) {
        for (const prop in sp[propType][stage]) {
          console.log(prop);
          if (
            sp[propType][stage][prop].hasOwnProperty('spawnAnim')
          ) {
            Iteratable.push(sp[propType][stage][prop]['spawnAnim']);
            target = sp[propType][stage]['target'];
          }
        }
      }
    }
    console.log("sceneStageAnim_getNextStage", stage, Iteratable, target);
    console.groupEnd();
    return { Iteratable, target };
  };

  /**
   * Simple timeout manager, articulates the camera and starts auto rotate mode after a set period of time.
   *
   * @alias module:Handlers.iddleAnim
   */
  const iddleAnim = () => {
    console.group("iddleAnim");

    const timeToIddle = context.get.config.secToIddle;

    if (context.get.config.timeoutToIddle) {
      context.get.controls.autoRotate = false;
      window.clearTimeout(context.get.config.timeoutToIddle);
    }

    context.get.config.timeoutToIddle = window.setTimeout(
      (e) => {
        simpleTween(context.get.camera.position, 
          new THREE.Vector3().addVectors(context.get.controls.target, context.coords.cam_iddle)
        );
        context.get.controls.autoRotate = true;
        console.log("ahem");
      },
      timeToIddle * 1000
    );
    console.groupEnd();
  };

  /**
   * Check if an animation has been rolled out to its extended position for a given prop.
   * @alias module:Handlers.checkAnimationIsExtended
   * @private
   * @param {string} spawnAnim String that contains the name of a prop animation.
   * @returns void
   */
  const checkAnimationIsExtended = (spawnAnim) => {
    console.group("checkAnimationIsExtended");
    const saAction = context.get.mixer.clipAction(
      THREE.AnimationClip.findByName(
        context.get.gltf.animations,
        spawnAnim
      )
    );
    console.groupEnd();
    return saAction.time > 0 && saAction.paused === true ?
      true : false;

  };

  /**
   * Console based tool that gives out re usable local coordinates for posing the camera.
   * 
   * @alias module:Handlers.getStarterView
   */
  const getStarterView = () => {
    console.group("getStarterView");
    let anim_start = {};
    // Import from context
    const camera = context.get.camera;

    // Import from context
    const controls = context.get.controls;

    // Define anim points used
    anim_start.camera_position = camera.position;

    // calculate relative camera position
    console.log(camera.position, controls.target)
    anim_start.relative_camera_position = camera.position.addScaledVector(controls.target, -1);
    anim_start.target = controls.target;
    anim_start.camera_rotation = camera.rotation;
    anim_start.object_rotation = controls.object.rotation;
    anim_start.azimuthal = controls.getAzimuthalAngle();
    anim_start.q = controls.object.quaternion;


    console.log(anim_start);
    console.groupEnd();
  };

  // Event Listensers

  /**
   * Handler renderer reflog on window resize.
   * @listens resize
   * @alias module:Handlers.onWindowResize
   */
  const on_window_resize = () => {
    try {
      // Import from context: camera
      const camera = context.get.camera;
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();

      // Import from context: renderer
      const renderer = context.get.renderer;
      renderer.setSize(window.innerWidth, window.innerHeight);

      //animate();
    } catch (e) {
      console.log(`onWindowResize: ${e.message}`);
    }

  };

  /**
   * Handler Animation clip loop event
   * @listens loop
   * @alias module:Handlers.onMixerLoop
   * @param {object} e Event object.
   */
  const on_AnimationClip_loop = (e) => {
    console.log("LOOP!", e);
  };

  /**
   * Handler Animation clip finished event
   * @alias module:Handlers.onMixerFinished
   * @param {object} e Event object 
   */
  const on_AnimationClip_finished = (e) => {
    console.log("Animation clip finished: ", e.action._clip.name, e);
    context.get.config.currentAnimation = '';
    // Attempt next animation in queue if any.
    animationAnim();
  };

  /**
   * Receive the event from Orbit Controls activated by the user.
   * @alias module.Handlers.onControlsChange
   * @listens change
   * @param {object} e Event Object.
   */
  const on_controls_change = (e) => {
  };

  /**
   * Receive the event from Orbit Controls when user starts interaction.
   * @alias module:Handlers.onControlsStart
   * @param {object} e 
   * @listens start
   * 
   */
  const on_controls_start = (e) => {
    context.get.controls.autoRotate = false;
  };

  /**
   * Receive the event from Orbit Controls when user starts interaction.
   * 
   * @alias module:Handlers.onControlsEnd
   * @param {object} e Event object.
   * @listens end
   */
  const on_controls_end = (e) => {
    iddleAnim();
  };


  // Handlers return
  return {
    'animationAnim': (anim) => animationAnim(anim),
    'changeSky': (tex_) => changeSky(tex_),
    'simpleAnim': (anim) => simpleAnim(anim),
    'getStarterView': () => getStarterView(),
    'SectionTween': (tid) => SectionTween(tid),
    'onMixerFinished': (e) => on_AnimationClip_finished(e),
    'onMixerLoop': (e) => on_AnimationClip_loop(e),
    'onWindowResize': () => on_window_resize(),
    'caseCharAnim': (anim) => caseCharAnim(anim),
    'sceneStageAnim': (anim) => sceneStageAnim(anim),
    'featureAnim': (feature) => featureAnim(feature),
    'onColorTweenColor': (color) => onColorTweenColor(color),
    'onControlsChange': (e) => on_controls_change(e),
    'onControlsStart': (e) => on_controls_start(e),
    'onControlsEnd': (e) => on_controls_end(e),
  }
})();

// Handler exports
window.Handlers = Handlers;

export { Handlers };