// --------------------------------------------------------------------
// wm3d - A Flash Molecular Viewer
//
// Copyright (c) 2011-2014, 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/.
// --------------------------------------------------------------------

import flash.display3D.Context3D;

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

import flash.events.TimerEvent;

import flash.utils.Timer;

import tinylib.Point3D;

/**
  A Chain; a chain consists of control points and 3D objects such as RIBBONs
  and COILs

  A chain is a smooth line generated by Catmull-Rom method.
**/

class WMChain {
  /**
    container of the smooth line
  **/
  @:isVar public var chain( get, null ):WMSmoothChain;
    /**
      getter of `chain`
    **/
    public function get_chain():WMSmoothChain { return( chain ); }
  /**
    array of RIBBONs belonging to this instance
  **/
  @:isVar public var ribbons( get, null ):Array< WMRibbon >;
    /**
      getter of `ribbons`
    **/
    public function get_ribbons():Array< WMRibbon > { return( ribbons ); }
  /**
    arrays of COILs belonging to this instance
  **/
  @:isVar public var coils( get, null ):Array< WMRibbon >;
    /**
      getter of `coils`
    **/
    public function get_coils():Array< WMRibbon > { return( coils ); }
  private var __indexes:Array< Int >;
  /**
    status variable used when reading xml background
  **/
  @:isVar public var genCompleted( get, null ):Bool;
    /**
      getter for `genCompleted`
    **/
    public function get_genCompleted():Bool { return( genCompleted ); }
  private var __keepDCActive:Bool;
  private var __chainTimer:Timer;
  private var __currentIndex:Int;
  private var __objlist:Array< Dynamic >;
  private var __c3d:Dynamic;

  // ######################################################################

  /**
    Constructor. Do nothing.
  **/
  public function new() {
    ribbons = new Array< WMRibbon >();
    coils = new Array< WMRibbon >();
    genCompleted = false;
  }

  /**
    Read a set of control point by array `pos`.
    If `ni` is given, number of points automatiaclly inserted between given
    points can be assigned. Default value of `ni` is 2.
  **/
  public function setPositions( pos:Array< Dynamic >,
                                ?ni:Int = 2 ):Void {
    var pa:Array< Dynamic > = new Array< Dynamic >();
    __indexes = new Array< Int >();
    var my_ni:Int = ni + 1;
    for ( p in pos ) {
      my_ni = ni + 1;
      if ( p.n != null && p.n >= 0 ) {
        my_ni = p.n + 1;
        pa.push( { p:Point3D.fromStringInverted( p.pos ), n:my_ni, dir:p.dir } );
      } else {
        pa.push( { p:Point3D.fromStringInverted( p.pos ), n:-1, dir:p.dir } );
      }
      for ( i in 0 ... my_ni ) __indexes.push( p.index );
    }
    // cut last my_ni - 1 elements ... in ad hoc way ... orz
    for ( i in 0 ... my_ni - 1 ) __indexes.pop();
    chain = new WMSmoothChain( pa, ni + 1 );
  }

  /**
    register a RIBBON or COIL given by `r`. The type of object can be
    determined by isRibbon variable of `r`.
  **/
  public function register( r:WMRibbon ):Void {
    var rc:WMRibbon = r.clone();
    var a:Array< Int > = __getIndex( rc.initXML, rc.lastXML );
    if ( a == null ) return; // invalid ribbon/coil. do nothing.
    rc.init = a[0];
    rc.last = a[1];
    // if init == last, that object should be ignored
    if ( rc.last > rc.init ) {
      if ( rc.isRibbon ) {
        ribbons.push( rc );
      } else {
        coils.push( rc );
      }
    }
  }

  /**
    generate polygon and set corresponding shaders;
    this function calls `pregen` and `gen2`.
  **/
  public function gen( c:Context3D,
                       ?is_dc_active:Bool = false ):Void {
    if ( WMSystem.readChainAtOnce ) {
      for ( rb in ribbons ) rb.gen( c, chain, is_dc_active );
      for ( cl in coils ) cl.gen( c, chain, is_dc_active );
      genCompleted = true;
    } else {
      if ( __chainTimer == null ) {
        __keepDCActive = is_dc_active;
        __chainTimer = new Timer( 0.2 );
        __chainTimer.addEventListener( TimerEvent.TIMER, __genObjs );
        __chainTimer.start();
        __objlist = new Array< Dynamic >();
        for ( rb in ribbons ) __objlist.push( rb );
        for ( cl in coils ) __objlist.push( cl );
        __currentIndex = 0;
        if ( Main.checkVersionWorker() ) {
          if ( flash.system.Worker.current.isPrimordial ) __c3d = c;
        } else {
          __c3d = c;
        }
      }
    }
  }

  /**
    preparation for generating polygon. This function is explicitly used in
    non-primordial Worker, where Context3D of the primordial Worker is not
    available. If Worker is not used, this function should not be called
    explicitly.
  **/
  public function pregen() {
    for ( rb in ribbons ) rb.pregen( chain );
    for ( cl in coils ) cl.pregen( chain );
    genCompleted = true;
  }

  /**
    generate polygon and shader. Call `pregen` function before.
    This function should not be called explicitly unless you are using
    multiple Workers.
  **/
  public function gen2( c:Context3D,
                        ?is_dc_active:Bool = false ) {
    for ( rb in ribbons ) rb.gen2( c, is_dc_active );
    for ( cl in coils ) cl.gen2( c, is_dc_active );
    genCompleted = true;
  }

  /**
    read objects in background
  **/
  private function __genObjs( ?e:TimerEvent ):Void {
    if ( __currentIndex < __objlist.length ) {
      __objlist[__currentIndex++].gen( __c3d, chain, __keepDCActive );
    }
    if ( __currentIndex == __objlist.length ) {
      genCompleted = true;
      __chainTimer.stop();
      __chainTimer.removeEventListener( TimerEvent.TIMER, __genObjs );
      __chainTimer = null;
    }
  }

  /**
    dump xml string of RIBBONs and COILs of this instance
  **/
  public function dump():String {
    var ret:String = "";
    for ( rb in ribbons ) ret += rb.dump();
    for ( cl in coils ) ret += cl.dump();
    return( ret );
  }

  /**
    draw `_polygon` on Stage3D

    - light: direction of light
    - cpos: position of camera
    - dcActive: is depth cueing is active (false)
    - dcCoeff: depth cueing coefficient (0.0)
    - dcLength: depth cueing characteristic length (0.0)
  **/
  public function draw( c:Context3D,
                        mpos:Matrix3D,
                        proj:Matrix3D,
                        voffset:Vector3D,
                        ?light:Vector3D = null,
                        ?cpos:Vector3D = null,
                        ?dcActive:Bool = false,
                        ?dcCoeff:Float = 0.0,
                        ?dcLength:Float = 0.0 ):Bool {
    for ( rb in ribbons ) {
      if ( !rb.draw( c, mpos, proj, voffset, light, cpos,
                     dcActive, dcCoeff, dcLength ) ) return( false );
    }
    for ( cl in coils ) {
      if ( !cl.draw( c, mpos, proj, voffset, light, cpos,
                     dcActive, dcCoeff, dcLength ) ) return( false );
    }
    return( true );
  }

  private function __getIndex( init:Int,
                               last:Int ):Array< Int > {
    var ret:Array< Int > = new Array< Int >();
    var keep:Int = -1;
    // dirty code ... orz
    var flag:Bool = false;
    for ( i in 0 ... __indexes.length ) {
      if ( init == __indexes[i] && !flag ) {
        ret.push( i );
        flag = true;
      }
      if ( last < __indexes[i] && flag ) {
        ret.push( i );
        break;
      }
      if ( last == __indexes[i] && flag ) keep = i + 1;
    }
    if ( ret.length == 1 && keep > 0 ) ret.push( keep );
    if ( ret.length != 2 ) {
      trace( "WMChain::getIndex: unexpected error. Invalid \"init\" and \"last\" are given?" );
      return( null );
    }
    return( ret );
  }

  /**
    dispose shader instance
  **/
  public function dispose():Void {
    for ( ribbon in ribbons ) {
      ribbon.dispose();
    }
    for ( coil in coils ) {
      coil.dispose();
    }
  }

  /**
    get array of control points of this chain
  **/
  public function getPositions():Array< Point3D > {
    return( chain.getPositions() );
  }

  /**
    number of elements inside; return the number of control points.
    Both of given and automatically inserted points are considered.
  **/
  public function num():Int {
    return( chain.controls.length );
  }
  /**
    sum of positions
  **/
  public function sumPos():Point3D {
    var ret:Point3D = new Point3D( 0, 0, 0 );
    for ( cont in chain.controls ) ret.add( cont.pos );
    return( ret );
  }
  /**
    translate coordinate by `p`
  **/
  public function translate( p:Point3D ):Void {
    for ( cont in chain.controls ) cont.pos.add( p );
  }
  /**
    absolute max of position: max(abs(x),abs(y),abs(z))
  **/
  public function absmax():Point3D {
    var ret:Point3D = new Point3D( 0, 0, 0 );
    for ( cont in chain.controls ) {
      ret.x = Math.max( Math.abs( cont.pos.x ), ret.x );
      ret.y = Math.max( Math.abs( cont.pos.y ), ret.y );
      ret.z = Math.max( Math.abs( cont.pos.z ), ret.z );
    }
    return( ret );
  }
  /**
    scale position by `scale`
  **/
  public function scaleCoord( scale:Float ):Void {
    for ( cont in chain.controls ) {
      cont.pos.multiply( scale );
    }
  }
  /**
    predefined cost for drawing; return number of control points
  **/
  public function getDataSize():Int { return( chain.controls.length ); }
}
