Source: src/core/nav.js

/**
 * Handles scene transitions and modal display.
 * <br>- A modally presented scene can be replaced with a non-modal scene then dismissed to return to original scene.
 * <br>- A modal can be presented over an existing modal.
 * @module nav
 */

import { utils,scaler } from './../storymode.js';

//import AlphaFilter from './../filters/alphafilter.js';

let locked = false; // Nav is locked
let transStack = [];
let pendingModalTrans = null;

let sceneHolder;
let inputScreen;
let bg; // Tinted screen for background color
let scenes;

// Load all transitions
let trans = {};
const _trans = utils.requireAll(require.context('./../trans', false, /.js$/));
for (let transMod of _trans) {
  registerTrans(transMod);
}

/**
 * Register a custom transition.
 * @param {Module} transModule
 * @example
// In app startup:
import * as CustomTrans from './trans/mytrans.js';
nav.registerTrans(CustomTrans);
 */
function registerTrans(transMod){
  if (transMod.id){
    let ids = Array.isArray(transMod.id) ? transMod.id : [transMod.id];
    for (var id of ids) {
      trans[id] = transMod.default;
    }
  }
}


/**
 * Registers all scenes to be loaded.
 * @param {Object} scenes - Scene configuration object
 * @example
// app.js
const scenes = {
  home: {class: Home, sceneData: {}, default:true, defaultTransID:'pan:down', defaultBgCol:0xff3300},
  play: {class: Play, sceneData: {}}
}
nav.setScenes(scenes);
 */
function setScenes(_scenes){
  scenes = _scenes;
}

/**
 * Called by `storymode` during setup.
 * @param {Pixi.Stage} stage
 * @param {number} bgAlpha
 * @private
 */
function setupStage(stage, bgAlpha){

  //ticker.add(function(time) {
  //  gsap.set(pixiApp.view, {opacity:1.0});
  //});

  let defaultBgCol = 0x000000;
  for (const sceneID in scenes){
    if (scenes[sceneID].default && scenes[sceneID].defaultBgCol){
      defaultBgCol = scenes[sceneID].defaultBgCol;
    }
  }

  let defaultBgFadeInDur = 0.6;
  for (const sceneID in scenes){
    if (scenes[sceneID].default && scenes[sceneID].defaultBgFadeInDur){
      defaultBgFadeInDur = scenes[sceneID].defaultBgFadeInDur;
    }
  }
  
  // Add background
  bg = new PIXI.Sprite(PIXI.Texture.WHITE);
  bg.width = scaler.stageW;
  bg.height = scaler.stageH;
  bg.tint = defaultBgCol; // Set in the index css
  stage.addChild(bg);
  gsap.fromTo(bg, defaultBgFadeInDur, {pixi:{alpha:0.0}},{pixi:{alpha:bgAlpha}, ease:Linear.easeNone})

  // Create a container for scenes
  sceneHolder = new Container();

  // stage.filters = [new PIXI.filters.CrossHatchFilter()]; // new PIXI.filters.TiltShiftFilter(27, 1000)]; //[new PIXI.filters.CRTFilter()]
  stage.addChild(sceneHolder);

  inputScreen = new PIXI.Sprite(PIXI.Texture.EMPTY);
  inputScreen.interactive = true;
  inputScreen.width = scaler.stageW;
  inputScreen.height = scaler.stageH;
  //inputScreen.cursor = 'auto' 'not-allowed';

  stage.addChild(inputScreen);
  inputScreen.visible = false;

  scaler.on('resize_immediate', onResizeImmediate);
  scaler.on('resize', onResize);

}

/**
 * Listener for stage resize, called continously.
 * @param {number} stageW - Stage width in points.
 * @param {number} stageH - Stage height in points.
 * @private
 */
function onResizeImmediate(_stageW,_stageH){

  bg.width = _stageW;
  bg.height = _stageH;

  inputScreen.width = scaler.stageW;
  inputScreen.height = scaler.stageH;

}

/**
 * Listener for stage resize, debounced.
 * @param {number} stageW - Stage width in points.
 * @param {number} stageH - Stage height in points.
 * @private
 */
function onResize(stageW,stageH){
  reloadSceneStack();
}

/**
 * Toggle input for entire stage.
 * @param {boolean} enable
 */
function enableInput(enable){
  inputScreen.visible = !enable;
}

/**
 * Will return if current scene has a transparent background.
 * @returns {boolean} sceneIsTransparent
 */
function isScenePresentedWithTransparentBg(){

  if (pendingModalTrans && pendingModalTrans.isTransparent){
    return true;
  }
  for (let i = transStack.length - 1; i >= 0; i--){
    if (transStack[transStack.length-1].isTransparent){
      return true;
    }
  }
  return false;

}

/**
 * Opens default scene. Called internally by `storymode` on startup.
 * @returns {boolean} success
 * @private
 */
function openDefaultScene(){

  if (!scenes){
    throw new Error('Scenes never set. Call `nav.setScenes(..)` before initiating app.')
  }

  for (const sceneID in scenes){
    if (scenes[sceneID].default){
      openScene(sceneID, false, scenes[sceneID].defaultTransID ? scenes[sceneID].defaultTransID : 'fade');
      return true;
    }
  }
  return false;
}

/**
 * @typedef {'fade'|'over'|'pan:%direction%'|'parallax:%direction%'|'mario:%bgColor%'|'pixelate'} TransitionID
 */

/**
 * Transition to a scene.
 * @param {string} sceneID - Scene identifier.
 * @param {boolean} [isModal=false] - Whether scene should be loaded modally (over the top).
 * @param {TransitionID} [transID='fade'] - Transition identifier.
 * @param {Object} [sceneData=null] - Optional data to be passed to scene.
 * @returns {boolean} success.
 */
function openScene(sceneID, isModal = false, transID = 'fade', sceneData = null){

  if (locked){
    return false;
  }

  locked = true;
  enableInput(false);

  if (!scenes[sceneID]){
    throw new Error('Scene not found `'+sceneID+'`');
  }

  // Param can be supplied in format `transID:transConfigStr`
  const transIDParts = transID.split(':');
  const transConfigStr = transIDParts.length > 1 ? transIDParts[1] : null;
  transID = transIDParts[0];

  if (!trans[transID]){
    throw new Error('Transition not found `'+transID+'`');
  }

  const scene = new scenes[sceneID].class(Object.assign({sceneID:sceneID, instanceID:createInstanceID()}, scenes[sceneID].sceneData, sceneData)); // Merge sceneDatas (set in config.js and sent to this method)
  scene.visible = false; // Hide for now.
  scene.on('ready', onSceneReady); // Listen for custom scene `ready` event

  const scenePrev = transStack.length > 0 ? transStack[transStack.length-1].scene : null;

  const transInstance = new trans[transID](scene, scenePrev, isModal, transConfigStr, transID);

  if (!transInstance.isModal && transStack.length > 1){
    // Remove previous
    let prevModalTrans = transStack.splice(transStack.length-1,1)[0];
    if (prevModalTrans.isModal) {
      // When this transition arrives it will be replaced with this modal.
      pendingModalTrans = prevModalTrans;
    } else {
      prevModalTrans.scene = null;
    }
  }

  transStack.push(transInstance);

  sceneHolder.addChild(scene); // Wait for on ready

  return true;

}

/**
 * Creates a 7 character integer.
 * @returns {integer} id
 * @private
 */
function createInstanceID(){
  return 1000000 + Math.round(Math.random()*8999999); // 7 char integer
}

/**
 * Called when scene being loaded is ready to be presented.
 * <br>Triggers entry transition to begin.
 * @param {Scene} scene
 * @private
 */
function onSceneReady(scene){

  scene.off('ready', onSceneReady);

  if (transStack[transStack.length-1].scenePrev){
    transStack[transStack.length-1].scenePrev.onWillExit(transStack[transStack.length-1].isModal); // Exit to modal
  }
  transStack[transStack.length-1].scene.onWillArrive(false); // First scene arrival will never be modal
  transStack[transStack.length-1].performIn(onSceneIn);

}

/**
 * Will return if current scene is presented modally.
 * @returns {boolean} sceneIsModal
 */
function isScenePresentedModally(){
  return transStack[transStack.length-1].isModal || pendingModalTrans; // If `pendingModalTrans` is set scene is not modal yet though will be
}

/**
 * Will return if current scene is presented modally.
 * @returns {boolean} sceneIsModal
 * @private
 */
function isPresentingModal(){
  return !(transStack.length < 2 || !transStack[transStack.length-1].isModal || !transStack[transStack.length-1].scenePrev);
}

/**
 * Dismiss currently presented modal scene.
 * @param {Object} [dismissData=null] - Optionally supply an object to be passed to parent scene's `onWillArrive()` method
 */
function dismissScene(dismissData = null){

  if (locked){
    return;
  }
  locked = true;
  enableInput(false);

  if (!this.isPresentingModal()){
    throw new Error('Cannot dismiss scene');
  }

  transStack[transStack.length-1].scene.onWillExit(false); // Exit to be destroyed
  if (transStack[transStack.length-1].scenePrev){
    transStack[transStack.length-1].scenePrev.onWillArrive(true, dismissData); // Re-arrive modally
  }

  transStack[transStack.length-1].performOut(onSceneOut);

}

/**
 * Called when arriving scene transition is complete.
 * @private
 */
function onSceneIn(){

  if (transStack[transStack.length-1].scenePrev){

    transStack[transStack.length-1].scenePrev.onDidExit(transStack[transStack.length-1].isModal);

    // Remove old scene if no longer required
    if (!transStack[transStack.length-1].isModal){
      let scenePrev = transStack[transStack.length-1].scenePrev
      sceneHolder.removeChild(transStack[transStack.length-1].scenePrev); // Destroy will be called by scene class
      transStack[transStack.length-1].scenePrev = null; // Don't retain reference to scene
      if (transStack[transStack.length-2].scene == scenePrev){
        transStack.splice(transStack.length-2,1);
      }
    }

  }

  if (pendingModalTrans){

    // Scene replaces previous modal scene. Swap them in trans stack.

    pendingModalTrans.scene = transStack[transStack.length-1].scene;
    transStack[transStack.length-1].scene = null;
    transStack.splice(transStack.length-1,1);
    transStack.push(pendingModalTrans);
    pendingModalTrans = null;

  }

  locked = false;
  enableInput(true);

  transStack[transStack.length-1].scene.onDidArrive(transStack[transStack.length-1].isModal);

  checkForNextArriveEvents();

}

/**
 * Called when exiting scene transition is complete.
 * @private
 */
function onSceneOut(){

  transStack[transStack.length-1].scene.onDidExit(false); // Not modal, about to be destroyed

  // Scene has dismissed
  sceneHolder.removeChild(transStack[transStack.length-1].scene); // Destroy will be called by scene class
  transStack[transStack.length-1].scene = null; // Don't retain reference to scene
  if (transStack[transStack.length-1].scenePrev) {
    transStack[transStack.length-1].scenePrev = null; // Don't retain reference to scene
  }

  transStack.splice(transStack.length-1,1);

  locked = false;
  enableInput(true);

  transStack[transStack.length-1].scene.onDidArrive(true); // Return from modal dismissal

  checkForNextArriveEvents();

}

/**
 * Reload scene stack if `nav` was waiting to scene to arrive first.
 * @private
 */
function checkForNextArriveEvents(){

  if (transStack[transStack.length-1].destroyOnNextArrive){
    let _callback = transStack[transStack.length-1].destroyOnNextArrive;
    transStack[transStack.length-1].destroyOnNextArrive = null;
    delete transStack[transStack.length-1].destroyOnNextArrive
    destroy(_callback[0],_callback[1]);
    return;
  }

  if (transStack[transStack.length-1].reloadOnNextArrive){
    delete transStack[transStack.length-1].reloadOnNextArrive;
    reloadSceneStack();
  }
}

/**
 * Reload all scene instances with new instances.
 * @param {boolean} [force=false] - To ignore the scene's `shouldReloadOnStageResize()`.
 */
function reloadSceneStack(force = false){

  // Called during a scene transition, wait for arrival.
  if (locked){
    transStack[transStack.length-1].reloadOnNextArrive = true;
    return;
  }

  // If scene is presented modally over other scenes
  // Hide them until they can reload themselved when next presented.
  for (let i = 0; i < transStack.length -1; i++){
    if (transStack[i].scene._shouldReloadOnStageResize(scaler.stageW, scaler.stageH)){
      transStack[i].scene.visible = false; // Optional
      transStack[i].reloadOnNextArrive = true;
    }
  }

  let _scene = transStack[transStack.length-1].scene;
  if (force || _scene._shouldReloadOnStageResize(scaler.stageW, scaler.stageH)){
    let sceneID = _scene.sceneData.sceneID;
    let sceneData = utils.cloneObj(_scene.sceneData);
    delete sceneData.sceneID;
    delete sceneData.instanceID;
    openScene(sceneID, false, 'fade', sceneData);
  }

}

/**
 * Write to the console a basic outline of child display objects.
 */
function debugTransStack(){
  for (let i = transStack.length - 1; i >= 0; i--){
    console.log(String(i) + ') `'+transStack[i].scene.name+'` ' + (i == transStack.length - 1 ? '*top*' : ''));
  }
}

/**
 * Called during `storymode.unmount`.
 * @private
 */
function destroy(reset = false, callback = null){

  if (!callback){
    throw new Error('Nav destroy requires a callback argument.')
  }

  if (locked){
    transStack[transStack.length-1].destroyOnNextArrive = [reset,callback]; // Wait for trans to finish.
    return;
  }

  // Clear stack
  for (let i = 0; i < transStack.length; i++){

    transStack[i].scene.onWillExit(false);
    transStack[i].scene.onDidExit(false);

    sceneHolder.removeChild(transStack[i].scene)
    transStack[i].scenePrev = null;
    transStack[i] = null;

    transStack.splice(i, 1)
    i--;
  }

  if (!reset){
    transStack = null;
    inputScreen.parent.removeChild(inputScreen)
    inputScreen = null;
    bg.parent.removeChild(bg)
    bg = null;
    transStack = null;
    scenes = null;
    sceneHolder = null;
    trans = null;
  }

  callback();

}

/**
 * Will return the current sceneID.
 * @returns {string} sceneID
 * @private
 */
function getCurrentSceneID(){
  return transStack[transStack.length-1].scene.sceneData.sceneID
}



export { scenes, getCurrentSceneID, transStack }
export { isPresentingModal, openDefaultScene,setupStage,isScenePresentedModally,isScenePresentedWithTransparentBg,openScene,dismissScene,bg,inputScreen,sceneHolder,setScenes,reloadSceneStack,registerTrans }
export { destroy }