// --------------------------------------------------------------------
// wm3d - A Flash Molecular Viewer
//
// Copyright (c) 2011-2013, tamanegi (tamanegi@users.sourceforge.jp)
// All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// --------------------------------------------------------------------

// Container for the status information used in wm3d.
// Almost all variables are referenced by parent Watermelon.
// So, most variables are defined as public.

import flash.geom.Vector3D;
import flash.geom.Matrix3D;

import flash.events.Event;
import flash.events.MouseEvent;
import flash.events.TimerEvent;

import flash.utils.Timer;

import tinylib.Camera;
import tinylib.Point3D;

/**
  Status variables in wm3d
**/

class WMStates {
  // parent Watermelon
  private var wm:Watermelon;

  /**
    Camera
  **/
  @:isVar public var camera( get, set ):Camera;
    /**
      getter of `camera`
    **/
    public function get_camera():Camera { return( camera ); }
    /**
      setter of `camera`
    **/
    public function set_camera( c:Camera ):Camera {
      camera = c.clone();
      return( camera );
    }

  /**
    rotation/translation matrix
  **/
  @:isVar public var mpos( get, set ):Matrix3D;
    public function get_mpos():Matrix3D { return( mpos ); } // not clone()
    public function set_mpos( m:Matrix3D ):Matrix3D {
      mpos = m.clone();
      updateMPos = true;
      return( mpos );
    }

  private var __mpos_init:Matrix3D;

  /**
    light direction; directional light source
  **/
  @:isVar public var light( get, set ):Vector3D;
    /**
      getter of `lught`
    **/
    public function get_light():Vector3D { return( light ); }
    /**
      setter of `lught`
    **/
    public function set_light( l:Vector3D ) {
      light = l.clone();
      return( light );
    }

  /**
    camera position
  **/
  @:isVar public var cpos( get, set ):Vector3D;
    /**
      getter of `cpos`
    **/
    public function get_cpos():Vector3D { return( cpos ); }
    /**
      setter of `cpos`
    **/
    public function set_cpos( p:Vector3D ):Vector3D {
      cpos = p.clone();
      return( cpos );
    }

  /**
    offset of view; not used now (maybe)
  **/
  @:isVar public var view_offset( get, set ):Vector3D;
    /**
      getter of `view_offset`
    **/
    public function get_view_offset():Vector3D { return( view_offset ); }
    /**
      setter of `view_offset`
    **/
    public function set_view_offset( o:Vector3D ):Vector3D {
      view_offset = o.clone();
      return( view_offset );
    }

  /**
    current auto-rotation degrees for x direction
  **/
  @:isVar public var arDegreeX( get, set ):Float;
    /**
      getter of `arDegreeX`
    **/
    public function get_arDegreeX():Float { return( arDegreeX ); }
    /**
      setter of `arDegreeX`
    **/
    public function set_arDegreeX( d:Float ):Float {
      arDegreeX = d;
      return( arDegreeX );
    }

  /**
    current auto-rotation degrees for y direction
  **/
  @:isVar public var arDegreeY( get, set ):Float;
    /**
      getter of `arDegreeY`
    **/
    public function get_arDegreeY():Float { return( arDegreeY ); }
    /**
      setter of `arDegreeY`
    **/
    public function set_arDegreeY( d:Float ):Float {
      arDegreeY = d;
      return( arDegreeY );
    }

  /**
    current auto-rotation degrees for z direction
  **/
  @:isVar public var arDegreeZ( get, set ):Float;
    /**
      getter of `arDegreeZ`
    **/
    public function get_arDegreeZ():Float { return( arDegreeZ ); }
    /**
      setter of `arDegreeZ`
    **/
    public function set_arDegreeZ( d:Float ):Float {
      arDegreeZ = d;
      return( arDegreeZ );
    }

  /**
    current mouse mode for left button.
    Valid values are listed in `WMMouseModeL` class.
  **/
  @:isVar public var mouseModeL( get, set ):Int;
    /**
      getter of `mouseModeL`
    **/
    public function get_mouseModeL():Int { return( mouseModeL ); }
    /**
      setter of `mouseModeL`
    **/
    public function set_mouseModeL( m:Int ) {
      mouseModeL = m;
      return( mouseModeL );
    }

  /**
    curret mode of mouse wheel
    Valid values are listed in `WMMouseModeW` class.
  **/
  @:isVar public var mouseModeW( get, set ):Int;
    /**
      getter of `mouseModeW`
    **/
    public function get_mouseModeW():Int { return( mouseModeW ); }
    /**
      setter of `mouseModeW`
    **/
    public function set_mouseModeW( m:Int ) {
      mouseModeW = m;
      return( mouseModeW );
    }

  /**
    is auto-rotating now?
  **/
  @:isVar public var arNow( get, set ):Bool;
    /**
      getter of `arNow`
    **/
    public function get_arNow():Bool { return( arNow ); }
    /**
      setter of `arNow`
    **/
    public function set_arNow( f:Bool ):Bool {
      arNow = f;
      return( arNow );
    }

  /**
    is processing action now?
  **/
  @:isVar public var actionNow( get, set ):Bool;
    /**
      getter of `actionNow`
    **/
    public function get_actionNow():Bool { return( actionNow ); }
    /**
      setter of `actionNow`
    **/
    public function set_actionNow( f:Bool ):Bool {
      actionNow = f;
      return( actionNow );
    }

  /**
    whether updating camera position is requested
  **/
  @:isVar public var updateCameraPos( get, set ):Bool;
    /**
      getter of `updateCameraPos`
    **/
    public function get_updateCameraPos():Bool { return( updateCameraPos ); }
    /**
      setter of `updateCameraPos`
    **/
    public function set_updateCameraPos( f:Bool ) {
      updateCameraPos = f;
      return( updateCameraPos );
    }

  /**
    whether updating translation/rotation matrix is requested
  **/
  @:isVar public var updateMPos( get, set ):Bool;
    /**
      getter of `updateMPos`
    **/
    public function get_updateMPos():Bool { return( updateMPos ); }
    /**
      setter of `updateMPos`
    **/
    public function set_updateMPos( f:Bool ):Bool {
      updateMPos = f;
      return( updateMPos );
    }

  /**
    whether updating offset of view is requested
  **/
  @:isVar public var updateViewOffset( get, set ):Bool;
    /**
      getter of `updateViewOffset`
    **/
    public function get_updateViewOffset():Bool { return( updateViewOffset ); }
    /**
      setter of `updateViewOffset`
    **/
    public function set_updateViewOffset( f:Bool ) {
      updateViewOffset = f;
      return( updateViewOffset );
    }

  /**
    whether display mouse mode
  **/
  @:isVar public var showMouseMode( get, set ):Bool;
    /**
      getter for `showMouseMode`
    **/
    public function get_showMouseMode():Bool { return( showMouseMode ); }
    /**
      setter for `showMouseMode`
    **/
    public function set_showMouseMode( f:Bool ):Bool {
      showMouseMode = f;
      return( showMouseMode );
    }

  /**
    whether updating scene is required
  **/
  @:isVar public var updateScene( get, set ):Bool;
    /**
      getter of `updateScene`
    **/
    public function get_updateScene():Bool { return( updateScene ); }
    /**
      setter of `updateScene`
    **/
    public function set_updateScene( f:Bool ):Bool {
      updateScene = f;
      return( updateScene );
    }

  /**
    general flag; whether i am busy on doing something now?
  **/
  @:isVar public var busyNow( get, set ):Bool;
    /**
      getter of `busyNow`
    **/
    public function get_busyNow():Bool { return( busyNow ); }
    /**
      setter of `busyNow`
    **/
    public function set_busyNow( f:Bool ):Bool {
      busyNow = f;
      return( busyNow );
    }

  /**
    is playing scenes now?
  **/
  @:isVar public var playingNow( get, set ):Bool;
    /**
      getter of `playingNow`
    **/
    public function get_playingNow():Bool { return( playingNow ); }
    /**
      setter of `playingNow`
    **/
    public function set_playingNow( f:Bool ):Bool {
      playingNow = f;
      return( playingNow );
    }

  /**
    is current playing mode is remove mode?
  **/
  @:isVar public var playReverse( get, set ):Bool;
    /**
      getter of `playReverse`
    **/
    public function get_playReverse():Bool { return( playReverse ); }
    /**
      setter of `playReverse`
    **/
    public function set_playReverse( f:Bool ):Bool {
      playReverse = f;
      return( playReverse );
    }

  /**
    timer for loading systems
  **/
  @:isVar public var myLoadSysTimer( get, null ):Timer;
    /**
      getter of `myLoadSysTimer`
    **/
    public function get_myLoadSysTimer():Timer { return( myLoadSysTimer ); }

  /**
    frame counter for play mode
  **/
  @:isVar public var frameCounter( get, set ):Int;
    /**
      getter of `frameCounter`
    **/
    public function get_frameCounter():Int { return( frameCounter ); }
    /**
      setter of `frameCounter`
    **/
    public function set_frameCounter( c:Int ):Int {
      frameCounter = c;
      return( frameCounter );
    }

  /**
    current frame
  **/
  @:isVar public var frameIndex( get, set ):Int;
    /**
      getter of `frameIndex`
    **/
    public function get_frameIndex():Int { return( frameIndex ); }
    /**
      setter of `frameIndex`
    **/
    public function set_frameIndex( i:Int ):Int {
      frameIndex = i;
      return( frameIndex );
    }

  // #######################################################################
  /**
    Constructor.

    Default values:

    - arDegreeX: 0.0
    - arDegreeY: params.arDegree (see WMParams)
    - arDegreeZ: 0.0
    - mouseModeL: MOUSE_L_ROTAT_MODE (rotation)
    - mousemodeW: MOUSE_W_SCALE_MODE (scale)
    - camera position: -max( stageWidth, stageHeight ) * 2;
    - camera aspect: stageWidth / stageHeight
    - camera fov angle: determineFov( stageHeight, abs( camera.pos.z ) )
    - lght source: ( 1, -1 -1 )
  **/
  public function new( w:Watermelon ) {
    wm = w;
    showMouseMode = false;

    // default values
    arDegreeX = 0.0;
    arDegreeY = wm.params.arDegree; // params must be initialized first
    arDegreeZ = 0.0;
    invokeRotationML(); // rotation mode
    //invokeTranslationML(); // translation mode
    invokeScalingMW(); // scale mode
    //invokeDepthMW(); // depth mode

    camera = new Camera();
    camera.pos.z = -Math.max( wm.stage.stageWidth, wm.stage.stageHeight ) * 2;
    camera.ratio = wm.stage.stageWidth / wm.stage.stageHeight;
    camera.determineFov( wm.stage.stageHeight, Math.abs( camera.pos.z ) );

    light = new Vector3D( 1, -1, -1 );
    light.normalize();

    cpos = new Vector3D();
    mpos = new Matrix3D();
    mpos.identity();
    __mpos_init = null;

    view_offset = new Vector3D();

    arNow = true;
    actionNow = false;
    busyNow = false;
    updateCameraPos = true;
    updateMPos = true;
    updateViewOffset = false;
    updateScene = true;

    playingNow = false;
    playReverse = false;

    frameCounter = 0;
    frameIndex = 0;
  }

  /**
    toggle message status
  **/
  public function toggleStatusMessageOnScreen() {
    showMouseMode = !showMouseMode;
    if ( showMouseMode ) {
      // off -> on; show states
      showStatusMessageOnScreen();
    } else {
      // on -> off; remove states messages
      removeStatusMessageFromScreen();
    }
  }

  /**
    show current mouse mode and status lines
  **/
  public function showStatusMessageOnScreen() {
    if ( mouseModeL == WMMouseModeL.MOUSE_L_ROTAT_MODE ) invokeRotationML();
    if ( mouseModeL == WMMouseModeL.MOUSE_L_TRANS_MODE ) invokeTranslationML();
    if ( mouseModeW == WMMouseModeW.MOUSE_W_SCALE_MODE ) invokeScalingMW();
    if ( mouseModeW == WMMouseModeW.MOUSE_W_DEPTH_MODE ) invokeDepthMW();
  }

  /**
    remove mouse and status messages
  **/
  public function removeStatusMessageFromScreen() {
    wm.display_status.removeAllMessages();
  }

  /**
    change mouse left button mode to rotation
  **/
  public function invokeRotationML() {
    if ( showMouseMode ) {
      wm.display_status.showMessageTopLeft( WMDisplayStatusMessages.MSG_ROTATION );
    }
    mouseModeL = WMMouseModeL.MOUSE_L_ROTAT_MODE;
  }

  /**
    change mouse left button mode to translation
  **/
  public function invokeTranslationML() {
    if ( showMouseMode ) {
      wm.display_status.showMessageTopLeft( WMDisplayStatusMessages.MSG_TRANSLATION );
    }
    mouseModeL = WMMouseModeL.MOUSE_L_TRANS_MODE;
  }

  /**
    change mouse wheel mode to scaling
  **/
  public function invokeScalingMW() {
    if ( showMouseMode ) {
      wm.display_status.showMessageTopRight( WMDisplayStatusMessages.MSG_SCALING );
    }
    mouseModeW = WMMouseModeW.MOUSE_W_SCALE_MODE;
  }

  /**
    change mouse wheel mode to camera translation
  **/
  public function invokeDepthMW() {
    if ( showMouseMode ) {
      wm.display_status.showMessageTopRight( WMDisplayStatusMessages.MSG_VIEWPOINT );
    }
    mouseModeW = WMMouseModeW.MOUSE_W_DEPTH_MODE;
  }

  /**
    return whether is update of the scene is needed
  **/
  public function needToUpdate():Bool {
    return( updateCameraPos || updateMPos || arNow || updateViewOffset ||
            updateScene );
  }

  /**
    update camera position; copy `camera.pos` to `cpos`
  **/
  public function updateCPos():Void {
    cpos.x = camera.pos.x;
    cpos.y = camera.pos.y;
    cpos.z = camera.pos.z;
    updateCameraPos = false;
  }

  /**
    reset instant flags
  **/
  public function resetFlags():Void {
    updateCameraPos = false;
    updateMPos = false;
    updateViewOffset = false;
    updateScene = false;
  }

  /**
    set camera z position (camera.pos.z) to given `z`
  **/
  public function setCameraPosZ( z:Float ):Void {
    camera.pos.z = z;
    updateCameraPos = true;
  }

  /**
    setter of light
  **/
  public function setLightDirection( p:Point3D ):Void {
    light.x = p.x;
    light.y = p.y;
    light.z = p.z;
    light.normalize();
  }

  /**
    mouse action: scale coordinate by d
  **/
  public function changeScale( d:Int ):Void {
    var sc:Float = 1.0 + wm.params.scaleWheel * d;
    mpos.appendScale( sc, sc, sc );
    updateMPos = true;
  }

  /**
    mouse action: change camera z position
  **/
  public function changeDepth( d:Int ):Void {
    camera.pos.z += wm.params.depthWheel * d;
    camera.pos.z = Math.min( 0, camera.pos.z );
    camera.determineFov( wm.stage.stageHeight, Math.abs( camera.pos.z ) );
    updateCameraPos = true;
  }

  /**
    apply auto-rotation to the rotation matrix
  **/
  public function applyAutoRotation() {
    mpos.appendRotation( arDegreeZ, flash.geom.Vector3D.Z_AXIS );
    mpos.appendRotation( arDegreeY, flash.geom.Vector3D.Y_AXIS );
    mpos.appendRotation( arDegreeX, flash.geom.Vector3D.X_AXIS );
    updateMPos = true;
  }

  /**
    begin auto-rotation
  **/
  public function beginAutoRotation():Void { arNow = true; }
  /**
    stop auto-rotation
  **/
  public function stopAutoRotation():Void { arNow = false; }

  /**
    auto-translation
  **/
  public function applyAutoTranslation( px:Float,
                                        py:Float,
                                        pz:Float ) {
    mpos.appendTranslation( px, py, pz );
    updateMPos = true;
  }

  /**
    auto-scaling
  **/
  public function applyAutoScaling( p:Float ) {
    var sc:Float = 1.0 + p;
    mpos.appendScale( sc, sc, sc );
    updateMPos = true;
  }

  /**
    auto-depth
  **/
  public function applyAutoDepth( p:Float ) {
    camera.pos.z += p;
    camera.pos.z = Math.min( 0, camera.pos.z );
    camera.determineFov( wm.stage.stageHeight, Math.abs( camera.pos.z ) );
    updateCameraPos = true;
  }

  /**
    pause playing
  **/
  public function pausePlay( ?e:MouseEvent = null ):Void { playingNow = false; }

  /**
    begin playing scenes (if there is only one scene, do nothing.).
    Usually this function is called by `playForward` or `playReverse`
  **/
  public function initPlay():Void {
    if ( wm.systems.length <= 1 ) return;
    wm.params.assignNumFramesPerScene();
    playReverse = false;
    playingNow = true;
    frameCounter = 0;
  }

  /**
    begin playing: forward mode
  **/
  public function playForward( ?e:MouseEvent = null ):Void {
    if ( wm.systems.length <= 1 || myLoadSysTimer.running ) return;
    initPlay();
  }

  /**
    begin playing: backward mode
  **/
  public function playBackward( ?e:MouseEvent = null ):Void {
    if ( wm.systems.length <= 1 || myLoadSysTimer.running ) return;
    initPlay();
    playReverse = true;
  }

  /**
    go to next scene
  **/
  public function forwardScene( ?e:MouseEvent = null ):Void {
    if ( myLoadSysTimer.running ) return;
    frameIndex = ( frameIndex + 1 ) % wm.systems.length;
    updateScene = true;
  }

  /**
    go to last scene
  **/
  public function gotoLastScene( ?e:MouseEvent ):Void {
    if ( myLoadSysTimer.running ) return;
    frameIndex = wm.systems.length - 1;
    updateScene = true;
  }

  /**
    go to previous scene
  **/
  public function backScene( ?e:MouseEvent = null ):Void {
    if ( myLoadSysTimer.running ) return;
    if ( --frameIndex < 0 ) frameIndex = wm.systems.length + frameIndex;
    updateScene = true;
  }

  /**
    go to first scene
  **/
  public function gotoInitScene( ?e:MouseEvent = null ):Void {
    if ( myLoadSysTimer.running ) return;
    frameIndex = 0;
    updateScene = true;
  }

  /**
    begin load system timer
  **/
  public function beginLoadSysTimer( ?delay:Float = 2.0 ):Void {
    myLoadSysTimer = new Timer( delay );
    myLoadSysTimer.addEventListener( TimerEvent.TIMER, wm.loadSystems );
    myLoadSysTimer.start();
  }

  /**
    stop load system timer
  **/
  public function stopLoadSysTimer():Void { myLoadSysTimer.stop(); }

  /**
    return if load system timer is running
  **/
  public function isLoadSysTimerRunning():Bool {
    if ( myLoadSysTimer != null ) {
      if ( myLoadSysTimer.running ) return( true );
    }
    return( false );
  }

  /**
    count up frames for playing
  **/
  public function processCounter():Void {
    // this function is meaningful only when in play mode
    if ( isLoadSysTimerRunning() || wm.params.numFramesPerScene <= 0 ) return;
    if ( playingNow ) {
      if ( ++frameCounter >= wm.params.numFramesPerScene ) {
        frameCounter = 0;
        if ( playReverse ) {
          backScene();
        } else {
          forwardScene();
        }
      }
    }
  }

  /**
    set default camera status; current camera state (mpos) is used.
  **/
  public function setMposInit():Void {
    __mpos_init = mpos.clone();
  }

  /**
    reset view:
    If __mpos_init exists, the value is used.
    Otherwise, mpos is set to identity matrix.
  **/
  public function resetView():Void {
    if ( __mpos_init == null ) {
      mpos.identity();
    } else {
      mpos = __mpos_init.clone();
    }
  }
}
