// --------------------------------------------------------------------
// 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.display.Sprite;
import flash.display.Stage;
import flash.display.Stage3D;
import flash.display3D.Context3D;
import flash.display3D.Context3DBlendFactor;

import flash.system.Worker;
import flash.system.WorkerDomain;
import flash.system.MessageChannel;

import flash.geom.Matrix3D;

import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.TimerEvent;
import flash.events.IOErrorEvent;

import flash.errors.Error;

import flash.ui.Keyboard;

import flash.utils.ByteArray;

import flash.net.FileReference;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLVariables;

import flash.text.TextField;
import flash.text.TextFormat;

import flash.Vector;

import flash.xml.XML;
import flash.xml.XMLList;
import flash.xml.XMLParser;

import pdb.Pdb;
import wmutil.EmbedBitmap;
import wmutil.CubicCurve;

import tinylib.Point3D;
import tinylib.Matrix4;
import tinylib.Polygon;
import tinylib.Vertex;
import tinylib.Face;
import tinylib.TriFaceIDs;
import tinylib.UVCoord;
import tinylib.primitives.Cone3D;
import tinylib.primitives.Cube3D;
import tinylib.primitives.Cylinder;
import tinylib.primitives.RoundedCylinder;
import tinylib.primitives.Kishimen;
import tinylib.primitives.Ribbon3D;
import tinylib.primitives.Sphere3D;
import tinylib.primitives.Square3D;
import tinylib.primitives.Text3D;
import tinylib.primitives.Triangles;
import tinylib.primitives.Tube3D;

/**
  Watermelon: core class of wm3d
**/

class Watermelon {
  /**
    stage
  **/
  @:isVar public var stage( get, null ):Stage;
    /**
      getter of `stage`
    **/
    public function get_stage():Stage { return( stage ); }

  private var stage3d:Stage3D;
  private var c3d:Context3D;

  /**
    xml data in string
  **/
  @:isVar public var myString( get, set ):String;
    /**
      getter of `myString`
    **/
    public function get_myString():String { return( myString ); }
    /**
      setter of `myString`
    **/
    public function set_myString( s:String ):String {
      myString = s;
      return( myString );
    }

  /**
    pdb data if any
  **/
  @:isVar public var myPdb( get, null ):Pdb;
    /**
      getter of `myPdb`
    **/
    public function get_myPdb():Pdb { return( myPdb ); }

  // i do not know how to deal with Bytes
  private var myPicture:EmbedBitmap;

  private var myXml:Xml;
  private var __readingFrame:Int;

  /**
    SCENEs
  **/
  @:isVar public var systems( get, null ):Array< WMSystem >;
    /**
      getter of `systems`
    **/
    public function get_systems():Array< WMSystem > { return( systems ); }

  /**
    various parameters of wm3d
  **/
  public var params:WMParams;
  /**
    current status of wm3d
  **/
  public var states:WMStates;
  /**
    mouse events
  **/
  public var mevents:WMMouseEvents;

  // status text
  public var display_status:WMDisplayStatus;

  // remember initial width and height
  private var __keep_width:Int;
  private var __keep_height:Int;

  // temporary sprites
  private var __about:WMAbout;
  private var __edit:WMEdit;
  private var __pdb:WMPdb;

  // permanent sprites; controller
  private var __controller:WMController;
    /**
      get height of the controller/player
    **/
    public function getControllerHeight():Float { return( __controller.height ); }

  // misc.
  private var __fref_load:FileReference;

  private var __aliases:Array< WMAlias >;
  private var __bgWorker:Dynamic;
  private var __channel:Dynamic;
  private var __channel_worker:Dynamic;

  private var __amgr:WMActionMgr;
  private var __current_action:WMAction;
  private var __ap0:Float;
  private var __ap1:Float;
  private var __ap2:Float;
  private var __act_counter:Int;
  private var __current_renderer:Dynamic;

  /**
    Constructor
  **/
  public function new() {
    stage = flash.Lib.current.stage;
    stage.scaleMode = flash.display.StageScaleMode.NO_SCALE;
    __setStage3D();

    // register callback for webpage
    __setCallBacks();

    // create view
    systems = null;
    __current_renderer = null;
    visualize();
  }

  private function __setStage3D() {
    if ( Main.checkVersionWorker() ) {
      if ( !Worker.current.isPrimordial ) return;
    }
    stage3d = stage.stage3Ds[0];
  }

  private function __setCallBacks():Void {
    // only primordial worker register callbacks
    if ( Main.checkVersionWorker() ) {
      if ( !Worker.current.isPrimordial ) return;
      //// check
      //trace( "I am primordial worker." );
    }
    if ( flash.external.ExternalInterface.available ) {
      try {
        flash.external.ExternalInterface.addCallback( "Redraw", this.visualize );
        flash.external.ExternalInterface.addCallback( "GetXML", this.get_myString );
        flash.external.ExternalInterface.addCallback( "GetView", this.getCurrentViewInXml );
      } catch( e:flash.errors.SecurityError ) {
        // do nothing, just ignore
        // this is work around for PowerPoint, where ExternalInterface.available
        // is true but ExternalInterface.addCallback is not available
      }
    }
  }

  /**
    prepare polygons
  **/
  public function visualize( ?str:String = null ):Void {
    __removeAllStageEvents();
    __initializeVariables();
    if ( str != null ) myString = str;
    myXml = Xml.parse( myString );
    __generateController( true );
    flash.Boot.__clear_trace();
    haxe.Log.setColor( params.traceColor );
    if ( Main.checkVersionWorker() ) {
      __registerClassAliases();
      if ( !Worker.current.isPrimordial ) {
        __channel = Worker.current.getSharedProperty( "channel" );
        __channel_worker = Worker.current.getSharedProperty( "channel2" );
        // read all data
        __setXmlData( __readXmlData() );
        // context3d, stage3d are not sharable? ...why?
        WMSystem.readAtOnce = true;
        WMSystem.readChainAtOnce = true;
        for ( i in 0 ... systems.length ) {
          if ( i != 0 ) __setDefaultValues( systems[i] );
          systems[i].gen( null, params.dcActive, __aliases );
        }
        for ( i in 1 ... systems.length ) {
          var ba:ByteArray = new ByteArray();
          ba.writeObject( systems[i] );
          __channel.send( ba );
        }
        // my work finished.
        Worker.current.terminate();
      }
    }
    stage3d.addEventListener( Event.CONTEXT3D_CREATE, onReady );
    stage3d.requestContext3D();
  }

  // set default values
  private function __initializeVariables():Void {
    myPicture = null;
    myString = "";
    myPdb = null;

    var ls:Array< String > = haxe.Resource.listNames();
    for ( name in ls ) {
      switch( name ) {
        case "structure":
          // read XML data from resource file attached in compilation phase.
          // -resource (filename):structure
          myString = haxe.Resource.getString( "structure" );
          // load picture if exists
        case "image":
          myPicture = new EmbedBitmap( "image", stage );
          myPicture.addEventListener( EmbedBitmap.COMPLETE, __addNowLoading );
        case "pdb":
          if ( myPdb == null ) {
            myPdb = new Pdb( haxe.Resource.getString( "pdb" ) );
          }
      }
    }

    // if pdb is available, generate xml string from its data
    if ( myPdb != null ) {
      myString = myPdb.genXml();
    }

    __discardShaderCache(); // remove cache if any
    systems = new Array< WMSystem >();
    __readingFrame = 0;
    __initializeStageSettings();
    __initializeDisplayStatus();
    WMBase.setScaleBase();
    params = new WMParams( this );
    states = new WMStates( this );
    mevents = new WMMouseEvents( this );
    __aliases = new Array< WMAlias >();
    __amgr = new WMActionMgr();
  }

  // initialize stage
  private function __initializeStageSettings() {
    __keep_width = stage.stageWidth;
    __keep_height = stage.stageHeight;
    if ( Main.checkVersionWorker() ) {
      if ( !Worker.current.isPrimordial ) {
        __keep_width = Worker.current.getSharedProperty( "stageWidth" );
        __keep_height = Worker.current.getSharedProperty( "stageHeight" );
      }
    }
    WMBase.stageWidth = __keep_width;
    WMBase.stageHeight = __keep_height;
  }

  // initialize display status text
  private function __initializeDisplayStatus() {
    if ( Main.checkVersionWorker() ) {
      if ( !Worker.current.isPrimordial ) return;
    }
    if ( display_status != null ) display_status.removeAllMessages();
    display_status = new WMDisplayStatus( this );
    display_status.setTextFormat( new TextFormat( "Arial", 14, 0xaaaaaa, true, null, null, null ) );
  }

  /**
    initialization of Stage3D and data
  **/
  public function onReady( _ ) {
    c3d = stage3d.context3D;
    //c3d.enableErrorChecking = true;
    c3d.enableErrorChecking = false;
    // antialiasing does not work? I cannot recognize any changes.
    c3d.configureBackBuffer( stage.stageWidth, stage.stageHeight, 0, true );
    // parse xml
    __setXmlData( __readXmlData() );
    // show now loading text
    if ( myPicture == null ) __addNowLoading();
    states.beginLoadSysTimer();
  }

  private function __readXmlData():Array< Xml > {
    var ret:Array< Xml > = new Array< Xml >();
    var __runworker:Bool = false;
    for ( wmxml in myXml.elements() ) {
      // top element which shall be "WMXML"
      if ( wmxml.nodeName.toUpperCase() == "WMXML" ) {
        if ( Main.checkVersionWorker() ) {
          // old players never touch this block
          // common swfs can be used both for old(<11.4) and new(>=11.4) players
          if ( wmxml.exists( "worker" ) ) {
            if ( ( Std.parseInt( wmxml.get( "worker" ) ) > 0 && Worker.current.isPrimordial ) ) {
              __runworker = true;
              // launch Worker here; do not write directly here.
              // we have to use function to cheat player
              __launchWorker();
            }
          }
        }
        // check culling
        if ( wmxml.exists( "culling" ) ) {
          params.doCulling = Std.parseInt( wmxml.get( "culling" ) ) > 0;
        }
        var load_scene = false;
        // 2nd level elements; GLOBAL, ALIAS, and SCENE
        for ( node in wmxml.elements() ) {
          switch( node.nodeName.toUpperCase() ) {
            case "GLOBAL":
              // parse global settings
              __loadGlobalSettings( node );
            case "ALIAS":
              // aliases
              __loadAliases( node );
            case "SCENE":
              // if another Worker exists, master process reads only one SCENE
              if ( __runworker && load_scene ) continue;
              // store scenes, but not read, since scene depends on aliases
              ret.push( node );
              load_scene = true;
            case "AMGR": // ActionManager
              __amgr.loadXml( node );
          }
        }
      }
    }
    return( ret );
  }

  private function __launchWorker():Void {
    __bgWorker = WorkerDomain.current.createWorker( flash.Lib.current.loaderInfo.bytes );
    __channel = __bgWorker.createMessageChannel( Worker.current );
    __channel_worker = __bgWorker.createMessageChannel( Worker.current );
    __bgWorker.setSharedProperty( "channel", __channel );
    __bgWorker.setSharedProperty( "channel2", __channel_worker );
    __bgWorker.setSharedProperty( "stageWidth", __keep_width );
    __bgWorker.setSharedProperty( "stageHeight", __keep_height );
    __channel.addEventListener( Event.CHANNEL_MESSAGE, __workerIncomingMessageHandler );
    __channel_worker.addEventListener( Event.CHANNEL_MESSAGE, __workerTraceMessageHandler );
    __bgWorker.start();
  }

  private function __workerIncomingMessageHandler( e:Event ):Void {
    var data:ByteArray = __channel.receive();
    var s:WMSystem = data.readObject();
    s.gen2( c3d, params.dcActive );
    systems.push( s );
    // forward scene if not background read
    if ( !params.readBackground ) {
      states.frameIndex = systems.length - 1;
      states.updateScene = true;
    }
  }

  private function __workerTrace( msg:String ):Void {
    if ( !Main.checkVersionWorker() ) return;
    if ( Worker.current.isPrimordial ) return;
    var ba:ByteArray = new ByteArray();
    ba.writeObject( msg );
    __channel_worker.send( ba );
  }

  private function __workerTraceMessageHandler( e:Event ):Void {
    // special color for worker trace
    haxe.Log.setColor( 0x00FF00 );
    var data:ByteArray = __channel_worker.receive();
    var msg:String = data.readObject();
    trace( msg );
  }

  private function __setXmlData( scenes:Array< Xml > ):Void {
    systems = [];
    for ( sc in scenes ) {
      var sys:WMSystem = new WMSystem();
      __setDefaultValues( sys );
      // only register xml data here
      sys.registerXml( sc );
      systems.push( sys );
    }
  }

  private function __removePicture():Void {
    myPicture.removeFromStage();
    myPicture = null;
  }

  private function __addNowLoading( ?e:Event = null ):Void {
    display_status.showMessageCenter( WMDisplayStatusMessages.MSG_LOADING );
  }

  private function __removeNowLoading():Void {
    display_status.removeMessageCenter();
  }

  /**
    timer event; load a SCENE
  **/
  public function loadSystems( ?e:TimerEvent = null ):Void {
    if ( __readingFrame == systems.length ) {
      states.stopLoadSysTimer();
      __removeNowLoading();
      // if no SCENE found, add an empty frame
      if ( systems.length == 0 ) {
        systems.push( new WMSystem() );
        __beginStandardHandlers();
      }
      // finish reading
      if ( systems.length > 1 && states.playingNow ) {
        if ( states.playReverse ) {
          states.playBackward();
        } else {
          states.playForward();
        }
      } else {
        states.pausePlay();
        states.playReverse = false;
      }
      return;
    } else {
      if ( __readingFrame != 0 && !systems[__readingFrame].reading ) {
        __setDefaultValues( systems[__readingFrame] );
      }
      systems[__readingFrame].gen( c3d, params.dcActive, __aliases );
      if ( e != null ) e.updateAfterEvent();
      if ( !params.readBackground ) states.frameIndex = __readingFrame;
      if ( systems[__readingFrame].completed ) ++__readingFrame;
      states.updateScene = true;
    }
    // activate standard events when first frame is prepared
    if ( !stage.hasEventListener( Event.ENTER_FRAME ) ) {
      __set_renderer( render );
      stage.addEventListener( Event.RESIZE, __resize );
      __beginStandardHandlers();
    }
    // if actions exist, stop auto-rotation and invoke first action
    if ( !__amgr.isEmpty() && __amgr.cur == -1 ) {
      states.arNow = false;
      __current_action = __amgr.getNextAction();
      if ( __current_action == null ) {
        states.actionNow = false;
      } else {
        states.actionNow = true;
        while( __invokeAction( __current_action ) ) {
          __current_action = __amgr.getNextAction();
        }
        if ( __current_action == null ) states.actionNow = false;
      }
    }
  }

  // set default variables to new WMSystem
  private function __setDefaultValues( sys:WMSystem ):Void {
    sys.scaleFactor = params.scaleFactorAuto;
    if ( params.inheritScale && systems.length != 0 ) {
      sys.autoScale = false;
      sys.scaleFactorManual = systems[0].scaleFactorManual;
      sys.origin = systems[0].origin;
    } else {
      sys.autoScale = params.doAutoScale;
      sys.scaleFactorManual = params.scaleFactorManual;
    }
  }

  private function __removeAllStageEvents() {
    if ( Main.checkVersionWorker() ) {
      if ( !Worker.current.isPrimordial ) return;
    }
    if ( stage.hasEventListener( Event.ENTER_FRAME ) ) __remove_renderer();
    if ( stage.hasEventListener( Event.RESIZE ) ) {
      stage.removeEventListener( Event.RESIZE, __resize );
    }
    if ( stage.hasEventListener( KeyboardEvent.KEY_DOWN ) ) {
      stage.removeEventListener( KeyboardEvent.KEY_DOWN, __pressKey );
    }
    if ( mevents != null ) mevents.removeStandardEvents();
  }

  // in HxSL2, we need to clear shader cache
  private function __discardShaderCache() {
    if ( Main.checkVersionWorker() ) {
      if ( !Worker.current.isPrimordial ) return;
    }
    if ( systems != null ) {
      for ( sys in systems ) {
        sys.dispose();
      }
    }
  }

  /**
    draw polygons; called every frames
  **/
  public function render( _ ) {
    states.processCounter();
    if ( c3d == null || !states.needToUpdate() ) return;
    if ( myPicture != null ) __removePicture();
    if ( states.arNow ) states.applyAutoRotation();
    __render_wrapper();
  }

  private function __set_renderer( f:Dynamic ) {
    stage.addEventListener( Event.ENTER_FRAME, f );
    __current_renderer = f;
  }

  private function __remove_renderer() {
    if ( __current_renderer != null ) {
      stage.removeEventListener( Event.ENTER_FRAME, __current_renderer );
    }
    __current_renderer = null;
  }

  private function __render_wrapper() {
    // display settings; background color etc.
    __render_setContext3D();
    // update camera
    __render_updateCamera();
    // render on the display
    __render();
  }

  private function __render_setContext3D() {
    var bgcolor:Int = systems[states.frameIndex].bgcolor;
    c3d.clear( cast( ( bgcolor & 0xFF0000 ) >> 16, Float ) / 255.0,
               cast( ( bgcolor & 0x00FF00 ) >> 8, Float ) / 255.0,
               cast( ( bgcolor & 0x0000FF ), Float ) / 255.0, 1.0 );
    c3d.setDepthTest( true, flash.display3D.Context3DCompareMode.LESS_EQUAL );
    c3d.setBlendFactors( Context3DBlendFactor.SOURCE_ALPHA, Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA );
    if ( params.doCulling ) {
      c3d.setCulling( flash.display3D.Context3DTriangleFace.BACK );
    } else {
      c3d.setCulling( flash.display3D.Context3DTriangleFace.NONE );
    }
  }

  private function __render_updateCamera() {
    states.camera.update();
    if ( states.updateCameraPos ) states.updateCPos();
  }

  private function __render() {
    var proj:Matrix3D = states.camera.m.toMatrix3D();
    // draw returns false if error, such as context3d disposed, occurs.
    if ( systems[states.frameIndex].draw( c3d, states.mpos, proj, states.view_offset, states.light, states.cpos, params.dcActive, params.dcCoeff, params.dcLength ) ) {
      // standard situation
      try {
        c3d.present();
      } catch( e:Error ) {
        // c3d is disposed in too very good timing?
        //trace( "failed to present" );
        states.updateScene = true;
      }
      states.resetFlags();
    } else {
      // if fails to draw triangles, do nothing
      // new CONTEXT3D_CREATE event would be invoked automatically
      //trace( "failed to draw" );
      states.resetFlags();
      states.updateScene = true;
    }
  }

  // XML
  private function __loadGlobalSettings( ?node:Xml = null ):Void {
    __parseGlobalSettingAttributes( node );
    for ( nd in node.elements() ) {
      var nn:String = nd.nodeName.toUpperCase();
      switch( nn ) {
        case "ATOM":
          WMDefaults.gl_Atom.loadFromXml( nd );
        case "BOND":
          WMDefaults.gl_Bond.loadFromXml( nd );
          var strs = [ "rounded", "round" ];
          for ( s in strs ) {
            if ( nd.exists( s ) ) {
              WMDefaults.gl_BondRound = ( Std.parseInt( nd.get( s ) ) > 0 );
            }
          }
          strs = [ "exc", "exclude" ];
          for ( s in strs ) {
            if ( nd.exists( s ) ) {
              WMDefaults.gl_BondExclude = ( Std.parseInt( nd.get( s ) ) > 0 );
            }
          }
          strs = [ "dash", "dashed" ];
          for ( s in strs ) {
            if ( nd.exists( s ) ) {
              WMDefaults.gl_BondDashed = Std.parseInt( nd.get( s ) );
            }
          }
        case "RIBBON":
          WMDefaults.gl_Ribbon.loadFromXml( nd );
          var strs = [ "thick", "thickness" ];
          for ( s in strs ) {
            if ( nd.exists( s ) ) {
              WMDefaults.gl_RibbonThickness = Std.parseFloat( nd.get( s ) );
            }
          }
        case "COIL":
          WMDefaults.gl_Coil.loadFromXml( nd );
        case "SHAPE":
          WMDefaults.gl_Shape.loadFromXml( nd );
        case "OBJ3D":
          WMDefaults.gl_Object3D.loadFromXml( nd );
          if ( nd.exists( "type" ) ) WMDefaults.gl_Object3DType = nd.get( "type" );
        case "LABEL":
          WMDefaults.gl_Label.loadFromXml( nd );
          if ( nd.exists( "font" ) ) WMDefaults.gl_LabelFont = nd.get( "font" );
          if ( nd.exists( "fontsize" ) ) WMDefaults.gl_LabelSize = Std.parseFloat( nd.get( "fontsize" ) );
        case "CAMERA":
          if ( nd.exists( "z" ) ) {
            states.setCameraPosZ( Std.parseFloat( nd.get( "z" ) ) );
          }
        case "PROTECT":
          params.protectData = true;
        case "WHEEL":
          if ( nd.exists( "scale" ) ) {
            params.scaleWheel = Std.parseFloat( nd.get( "scale" ) );
          }
          if ( nd.exists( "depth" ) ) {
            params.depthWheel = Std.parseFloat( nd.get( "depth" ) );
          }
        case "MATRIX":
          if ( nd.exists( "data" ) ) {
            states.mpos = Matrix4.fromString( nd.get( "data" ) ).toMatrix3D();
            states.setMposInit();
          }
        case "RADIUS":
          if ( nd.exists( "method" ) ) {
            var met:String = nd.get( "method" );
            if ( met.toLowerCase() == "absolute" ) {
              WMBase.useRelative = false;
            } else if ( met.toLowerCase() == "relative" ) {
              WMBase.useRelative = true;
            }
          }
          if ( nd.exists( "scale" ) ) {
            WMBase.characteristicSize = Std.parseFloat( nd.get( "scale" ) );
          }
        case "AUTOSCALE":
          if ( nd.exists( "manual" ) ) {
            var n:Int = Std.parseInt( nd.get( "manual" ) );
            if ( n > 0 ) params.doAutoScale = false;
          }
          if ( nd.exists( "autoscale" ) ) {
            params.scaleFactorAuto = Std.parseFloat( nd.get( "autoscale" ) );
          }
          if ( nd.exists( "manualscale" ) ) {
            params.scaleFactorManual = Std.parseFloat( nd.get( "manualscale" ) );
          }
          if ( nd.exists( "inherit" ) ) {
            params.inheritScale = ( Std.parseInt( nd.get( "inherit" ) ) > 0 );
          }
        case "DC":
          if ( nd.exists( "active" ) ) {
            params.dcActive = ( Std.parseInt( nd.get( "active" ) ) > 0 );
          }
          if ( nd.exists( "coeff" ) ) {
            params.dcCoeff = Std.parseFloat( nd.get( "coeff" ) );
            params.dcCoeff /= __getWindowMinSize();
          }
          if ( nd.exists( "length" ) ) {
            params.dcLength = Std.parseFloat( nd.get( "length" ) );
            params.dcLength *= __getWindowMinSize();
          }
      }
    }
  }

  private function __getWindowMinSize():Float {
    return( Math.min( stage.stageWidth, stage.stageHeight ) );
  }

  private function __parseGlobalSettingAttributes( nd:Xml ):Void {
    if ( nd.exists( "light" ) ) {
      states.setLightDirection( Point3D.fromStringInverted( nd.get( "light" ) ) );
    }
    if ( nd.exists( "arrate" ) ) {
      params.arDegree = Std.parseFloat( nd.get( "arrate" ) );
    }
    if ( nd.exists( "framerate" ) ) {
      stage.frameRate = Std.parseFloat( nd.get( "framerate" ) );
    }
    if ( nd.exists( "play" ) ) {
      var v:Int = Std.parseInt( nd.get( "play" ) );
      if ( v > 0 ) {
        states.playingNow = true;
        states.playReverse = false;
      } else if ( v < 0 ) {
        states.playingNow = true;
        states.playReverse = true;
      }
    }
    if ( nd.exists( "readatonce" ) ) {
      WMSystem.readAtOnce = ( Std.parseInt( nd.get( "readatonce" ) ) > 0 );
    }
    if ( nd.exists( "readchainatonce" ) ) {
      WMSystem.readChainAtOnce = ( Std.parseInt( nd.get( "readchainatonce" ) ) > 0 );
    }
    if ( nd.exists( "playrate" ) ) {
      params.playFrameRate = Std.parseFloat( nd.get( "playrate" ) );
    }
    var strs = [ "readbackground", "bgread", "readbg" ];
    for ( s in strs ) {
      // never mind the value
      if ( nd.exists( s ) ) params.readBackground = true;
    }
  }

  private function __loadAliases( ?nd:Xml = null ):Void {
    // register an alias
    // this will still violate standards of XML, though
    for ( ndc in nd.elements() ) {
      if ( ndc.exists( "elem" ) ) {
        var wmal:WMAlias = new WMAlias();
        wmal.register( ndc.nodeName.toUpperCase(), ndc );
        __aliases.push( wmal );
        //trace( "wm3d: register alias " + wmal.name );
      } else {
        // alias without "elem" attribute is ignored
        trace( "wm3d::alias - elem attribute is required for an alias" );
      }
    }
  }

  private function __hasController():Bool {
    if ( __controller == null ) return( false );
    return( __controller.enabled() );
  }

  private function __generateController( ?rebuild = false ):Void {
    if ( __hasController() && !rebuild ) return;
    if ( Main.checkVersionWorker() ) {
      if ( !Worker.current.isPrimordial ) return;
    }
    __controller = new WMController( this );
    stage.addChild( __controller.draw() );
  }

  //// Event-related functions
  private function __beginStandardHandlers():Void {
    mevents.beginStandardEvents();
    stage.addEventListener( KeyboardEvent.KEY_DOWN, __pressKey );
  }

  /**
    is mouse cursor on the Controller (Player)?
  **/
  public function onController( mx:Float,
                                my:Float ):Bool {
    if ( my >= ( stage.stageHeight - __controller.height ) ) return( true );
    return( false );
  }

  /**
    zoom scene by `d`
  **/
  public function handleZoom( d:Int ):Void {
    if ( states.mouseModeW == WMMouseModeW.MOUSE_W_SCALE_MODE ) {
      states.changeScale( d );
    } else if ( states.mouseModeW == WMMouseModeW.MOUSE_W_DEPTH_MODE ) {
      states.changeDepth( d );
    }
  }

  private function __pressKey( event:KeyboardEvent ):Void {
    if ( states.busyNow ) return;
    switch( event.keyCode ) {
      //case Keyboard.LEFT: // left arrow
      case 37: // left arrow
        if ( states.mouseModeL == WMMouseModeL.MOUSE_L_TRANS_MODE ) {
          states.mpos.appendTranslation( 1, 0, 0 );
          states.updateMPos = true;
        }
      //case Keyboard.UP, 187: // up arrow, +
      case 38, 186, 187: // up arrow, +
        if ( states.mouseModeL == WMMouseModeL.MOUSE_L_TRANS_MODE ) {
          states.mpos.appendTranslation( 0, -1, 0 );
          states.updateMPos = true;
        } else {
          handleZoom( 1 );
        }
      //case Keyboard.RIGHT: // right arrow
      case 39: // right arrow
        if ( states.mouseModeL == WMMouseModeL.MOUSE_L_TRANS_MODE ) {
          states.mpos.appendTranslation( -1, 0, 0 );
          states.updateMPos = true;
        }
      //case Keyboard.DOWN, 189: // down arrow, -
      case 40, 189: // down arrow, -
        if ( states.mouseModeL == WMMouseModeL.MOUSE_L_TRANS_MODE ) {
          states.mpos.appendTranslation( 0, 1, 0 );
          states.updateMPos = true;
        } else {
          handleZoom( -1 );
        }
      case 13: // "return"
        states.forwardScene();
      case 65: // "a" - about
        __showAbout();
      case 66: // "b" - back
        if ( event.shiftKey ) { // "B" - back to 1st frame
          states.gotoInitScene();
        } else {
          states.backScene();
        }
      case 67: // "c" - clear, reset view, "C" - clear trace message
        if ( event.shiftKey ) {
          flash.Boot.__clear_trace();
        } else {
          states.resetView();
          visualize();
        }
      case 68: // "d" - depth mode
        if ( event.shiftKey ) {
          var xmldata:String = "<WMXML>\n";
          xmldata += "  <GLOBAL>\n";
          xmldata += getCurrentViewInXml();
          xmldata += "  </GLOBAL>\n";
          for ( sys in systems ) {
            xmldata += "  <SCENE>\n";
            xmldata += sys.dump();
            xmldata += "  </SCENE>\n";
          }
          xmldata += "</WMXML>";
          if ( !params.protectData ) {
            var fref:FileReference = new FileReference();
            try {
              fref.save( xmldata, "wmdump.xml" );
            } catch( e:Error ) {
              trace( "Error: failed to save XML data into local file" );
              trace( "ErrorID: " + e.errorID + " Message: " + e.message );
            }
          }
        } else {
          states.invokeDepthMW();
        }
      case 69: // "e" - edit mode
        if ( !params.protectData ) __editXML();
      case 70: // "f" - forward
        if ( event.shiftKey ) { // "F" - move to the last frame
          states.gotoLastScene();
        } else {
          states.forwardScene();
        }
      case 73: // "i" - interrupt action execution
        if ( states.actionNow ) {
          __removeAction( __current_action );
          __current_action = null;
          states.actionNow = false;
        }
      case 76: // "l" - load xml from file
        __fref_load = new FileReference();
        __fref_load.addEventListener( Event.SELECT, __loadFile );
        try {
          __fref_load.browse();
        } catch( e:Error ) {
          trace( "Error: failed to load local file" );
          trace( "ErrorID: " + e.errorID + " Message: " + e.message );
        }
      case 77: // "m" - toggle status message state
        states.toggleStatusMessageOnScreen();
      case 80: // "p" - play, "P" - load pdb
        if ( event.shiftKey ) {
          states.busyNow = true;
          if ( __pdb == null ) {
            __pdb = new WMPdb( this );
          }
          stage.addChild( __pdb );
        } else {
          if ( states.playingNow ) {
            states.pausePlay();
          } else {
            if ( event.shiftKey ) {
              states.playBackward();
            } else {
              states.playForward();
            }
          }
        }
      case 82: // "r" - rotation mode
        states.invokeRotationML();
      case 83: // "s" - scale mode
        if ( event.shiftKey ) {
          if ( !params.protectData ) {
            var fref:FileReference = new FileReference();
            try {
              fref.save( __addCurrentSetting( myString ), "watermelon.xml" );
            } catch( e:Error ) {
              trace( "Error: failed to save XML data into local file" );
              trace( "ErrorID: " + e.errorID + " Message: " + e.message );
            }
          }
        } else {
          states.invokeScalingMW();
        }
      case 84: // "t" - translation mode
        states.invokeTranslationML();
      case 86: // "v" - reset view
        states.resetView();
        states.updateMPos = true;
      case 88: // "x" - generate swf
        __generateSwf( event.shiftKey );
    }
  }

  // 2012/7/19: should be fixed
  private function __resize( event:Event ):Void {
    WMBase.setScaleBase();
    // this is not completely correct.
    // i do not know why i am using scale factor of 3.
    states.view_offset.x = 3 * ( stage.stageWidth - __keep_width );
    states.view_offset.y = - 3 * ( stage.stageHeight - __keep_height );
    states.updateViewOffset = true;
    states.camera.determineFov( stage.stageHeight, Math.abs( states.camera.pos.z ) );
  }

  //// ACTIONs
  private function __removeAction( act:WMAction ) {
    switch( act.type ) {
      case WMActionType.AC_AUTOROT, WMActionType.AC_AUTOTRANS,
           WMActionType.AC_AUTOSCALE, WMActionType.AC_AUTODEPTH,
           WMActionType.AC_WAIT:
        __remove_renderer();
      case WMActionType.AC_PLAYFOR:
        // reserved
      case WMActionType.AC_PLAYBACK:
        // reserved
    }
    if ( !stage.hasEventListener( Event.ENTER_FRAME ) ) __set_renderer( render );
  }

  private function __replaceRender( f:Dynamic ) {
    if ( stage.hasEventListener( Event.ENTER_FRAME ) ) __remove_renderer();
    __set_renderer( f );
  }

  private function __getActionParam( act:WMAction,
                                     key:String ):String {
    var ret:String = act.getParam( key );
    if ( ret == null ) return( "0" );
    return( ret );
  }

  // return true if the action is on the fly one
  private function __invokeAction( act:WMAction ):Bool {
    if ( act == null ) return( false );
    __act_counter = 0;
    var ret:Bool = false;
    switch( act.type ) {
      case WMActionType.AC_AUTOROT:
        states.arDegreeX = Std.parseFloat( __getActionParam( act, "x" ) ) / act.nframes;
        states.arDegreeY = Std.parseFloat( __getActionParam( act, "y" ) ) / act.nframes;
        states.arDegreeZ = Std.parseFloat( __getActionParam( act, "z" ) ) / act.nframes;
        __replaceRender( __render_autoRot );
      case WMActionType.AC_AUTOTRANS:
        __ap0 = Std.parseFloat( __getActionParam( act, "x" ) ) / act.nframes;
        __ap1 = Std.parseFloat( __getActionParam( act, "y" ) ) / act.nframes;
        __ap2 = Std.parseFloat( __getActionParam( act, "z" ) ) / act.nframes;
        __replaceRender( __render_autoTrans );
      case WMActionType.AC_AUTOSCALE:
        __ap0 = Std.parseFloat( __getActionParam( act, "v" ) ) / act.nframes;
        __replaceRender( __render_autoScale );
      case WMActionType.AC_AUTODEPTH:
        __ap0 = Std.parseFloat( __getActionParam( act, "v" ) ) / act.nframes;
        __replaceRender( __render_autoDepth );
      case WMActionType.AC_PLAYFOR:
        // reserved
      case WMActionType.AC_PLAYBACK:
        // reserved
      case WMActionType.AC_WAIT:
        __replaceRender( __render_wait );
      //// on the fly actions
      case WMActionType.AC_ROT:
        __ap0 = Std.parseFloat( __getActionParam( act, "x" ) );
        __ap1 = Std.parseFloat( __getActionParam( act, "y" ) );
        __ap2 = Std.parseFloat( __getActionParam( act, "z" ) );
        states.mpos.appendRotation( __ap0, flash.geom.Vector3D.X_AXIS );
        states.mpos.appendRotation( __ap1, flash.geom.Vector3D.Y_AXIS );
        states.mpos.appendRotation( __ap2, flash.geom.Vector3D.Z_AXIS );
        states.updateMPos = true;
        ret = true;
      case WMActionType.AC_TRANS:
        __ap0 = Std.parseFloat( __getActionParam( act, "x" ) );
        __ap1 = Std.parseFloat( __getActionParam( act, "y" ) );
        __ap2 = Std.parseFloat( __getActionParam( act, "z" ) );
        states.applyAutoTranslation( __ap0, __ap1, __ap2 );
        ret = true;
      case WMActionType.AC_SCALE:
        states.applyAutoScaling( Std.parseFloat( __getActionParam( act, "v" ) ) );
        ret = true;
      case WMActionType.AC_DEPTH:
        states.applyAutoDepth( Std.parseFloat( __getActionParam( act, "v" ) ) );
        ret = true;
      case WMActionType.AC_MOVESCENE:
        var sc = Std.parseInt( __getActionParam( act, "n" ) );
        if ( sc < 0 || sc >= systems.length ) {
          trace("Action: non-exist scene");
        } else {
          states.frameIndex = sc;
          states.updateScene = true;
        }
        ret = true;
      case WMActionType.AC_FORWARDSCENE:
        var sc = states.frameIndex + 1;
        if ( sc >= systems.length ) {
          trace("Action: non-exist scene");
        } else {
          states.frameIndex = sc;
          states.updateScene = true;
        }
        ret = true;
      case WMActionType.AC_BACKSCENE:
        var sc = states.frameIndex - 1;
        if ( sc < 0 ) {
          trace("Action: non-exist scene");
        } else {
          states.frameIndex = sc;
          states.updateScene = true;
        }
        ret = true;
      case WMActionType.AC_RESETVIEW:
        states.resetView();
        ret = true;
      case WMActionType.AC_RESET:
        states.mpos.identity();
        visualize();
    }
    return( ret );
  }

  private function __checkAction() {
    if ( __act_counter >= __current_action.nframes ) {
      __removeAction( __current_action );
      __current_action = __amgr.getNextAction();
      if ( __current_action == null ) {
        states.actionNow = false;
      } else {
        states.actionNow = true;
        while(__invokeAction( __current_action ) ) {
          __current_action = __amgr.getNextAction();
        }
        if ( __current_action == null ) states.actionNow = false;
      }
      // force update scene
      states.updateScene = true;
    }
  }

  // almost identical to the original function render.
  // therefore, this function will be removed in the future version
  private function __render_autoRot( _ ) {
    __act_counter++;
    states.processCounter();
    // c3d check skipped
    if ( myPicture != null ) __removePicture();
    states.applyAutoRotation();
    __render_wrapper();
    __checkAction();
  }

  private function __render_autoTrans( _ ) {
    __act_counter++;
    states.processCounter();
    if ( myPicture != null ) __removePicture();
    states.applyAutoTranslation( __ap0, __ap1, __ap2 );
    __render_wrapper();
    __checkAction();
  }

  private function __render_autoScale( _ ) {
    __act_counter++;
    states.processCounter();
    if ( myPicture != null ) __removePicture();
    states.applyAutoScaling( __ap0 );
    __render_wrapper();
    __checkAction();
  }

  private function __render_autoDepth( _ ) {
    __act_counter++;
    states.processCounter();
    if ( myPicture != null ) __removePicture();
    states.applyAutoDepth( __ap0 );
    __render_wrapper();
    __checkAction();
  }

  // do nothing, just render current scene if necessary
  private function __render_wait( _ ) {
    __act_counter++;
    states.processCounter();
    if ( myPicture != null ) __removePicture();
    if ( states.needToUpdate() ) __render_wrapper();
    __checkAction();
  }

  //// misc functions
  ////// ABOUT (see WMAbout.hx)
  private function __showAbout():Void {
    if ( states.busyNow ) return;
    if ( __about == null ) {
      // need to create
      __about = new WMAbout();
      // modify color/font/font size here if neccessary
    }
    if ( stage.contains( __about ) ) {
      // need to remove
      stage.removeChild( __about );
    } else {
      stage.addChild( __about.drawAbout() );
    }
  }

  ////// EDIT
  private function __editXML():Void {
    if ( states.busyNow ) return;
    states.busyNow = true; // this flag is resetted when removign window
    __edit = new WMEdit( this );
    stage.addChild( __edit.draw() );
  }

  //// file load
  private function __loadFile( e:Event ):Void {
    __fref_load.removeEventListener( Event.SELECT, __loadFile );
    __fref_load.addEventListener( Event.COMPLETE, __loadFileComplete );
    __fref_load.load();
  }

  private function __loadFileComplete( e:Event ):Void {
    __fref_load.removeEventListener( Event.COMPLETE, __loadFileComplete );
    if ( __fref_load.type != ".xml" && __fref_load.type != ".pdb" ) {
      trace( "Watermelon::__loadFileComplete: file must be .xml or .pdb. Stop loading file." );
      return;
    }
    states.mpos.identity();
    if ( __fref_load.type == ".xml" ) {
      visualize( __fref_load.data.toString() );
    } else if ( __fref_load.type == ".pdb" ) {
      myPdb = new Pdb( __fref_load.data.toString() );
      visualize( myPdb.genXml() );
    }
  }

  /**
    return current camera state as string; this function can be called by
    web pages
  **/
  public function getCurrentViewInXml():String {
    var camera_xml:String = "    <CAMERA z=\"" + states.camera.pos.z + "\" />\n";
    var rd:flash.Vector< Float > = states.mpos.rawData;
    var rmat:String = "    <MATRIX data=\"" + rd[0] + " " + rd[1] + " " +
                                rd[2] + " " + rd[3] + " " + rd[4] + " " +
                                rd[5] + " " + rd[6] + " " + rd[7] + " " +
                                rd[8] + " " + rd[9] + " " + rd[10] + " " +
                                rd[11] + " " + rd[12] + " " + rd[13] + " " +
                                rd[14] + " " + rd[15] + "\" />\n";
    return( camera_xml + rmat );
  }

  private function __addCurrentSetting( s:String ):String {
    var ret:String = s;
    var xml_begin:String = "  <GLOBAL>\n";
    var xml_end:String = "  </GLOBAL>\n";
    var base_xml:String = getCurrentViewInXml() + xml_end;
    // regular expression to find GLOBAL element
    var r0:EReg = ~/[ \t]*<GLOBAL[^>]*>.*<\/GLOBAL[\t ]*>/s;
    var r0p:EReg = ~/[ \t]*<GLOBAL[^>]*>/;
    if ( r0p.match( s ) ) {
      if ( r0p.matched(0).length > 8 ) {
        var rxp:EReg = ~/\/[ \t]*>/;
        xml_begin = rxp.replace( r0p.matched(0), ">" ) + "\n";
      }
    }
    // extract setting related lines
    ret = r0.replace( ret, "" );  // REMOVE all settings
    ret = r0p.replace( ret, "" ); // REMOVE <GLOBAL*/> if any
    var myxml:String = "";
    if ( r0.match( s ) ) myxml = r0.matched(0);
    if ( myxml.length == 0 ) { // no settings in original XML
      myxml = xml_begin + base_xml;
    } else {
      var r:EReg = ~/[ \t]*<\/*(GLOBAL|CAMERA|MATRIX)[^>]*>[ \t]*$/mg;
      myxml = r.replace( myxml, "" );
      myxml = xml_begin + myxml + base_xml;
    }
    var r1:EReg = ~/<WMXML[ \t]*>/;
    ret = r1.replace( ret, "<WMXML>\n" + myxml );
    return( ret );
  }

  //// Load Pdb
  ////// This function NEVER works due to the crossdomain problem
  ////// of flash player (at least now).
  ////// RCSB and PDBj does not have crossdomain.xml,
  ////// PDBe has crossdomain.xml but access from outside is forbidden.
  /**
    does not work now
  **/
  public function retrievePdbFromRCSB( id:String ) {
    // URL: http://www.rcsb.org/pdb/files/(pdbid).pdb.gz
    // may not neccessary to convert
    var pdbid:String = StringTools.trim( id.toUpperCase() );
    // verify id
    if ( pdbid.length != 4 ) {
      trace( "retrievePdb - PDB id must be 4-character identifier." );
      return;
    }
    var url:String = "http://www.rcsb.org/pdb/download/downloadFile.do";
    var urlLoader:URLLoader = new URLLoader();
    urlLoader.addEventListener( Event.COMPLETE, __loadPdbComplete );
    var urlReq:URLRequest = new URLRequest( url );
    // setup cgi params
    var urlVars:URLVariables = new URLVariables();
    urlVars.structureId = pdbid;
    urlVars.compression = "NO";
    urlVars.fileFormat = "text";
    urlReq.data = urlVars;
    try {
      urlLoader.load( urlReq );
    } catch ( e:flash.errors.ArgumentError ) {
      trace( "retrievePdb - met ArgumentError");
    } catch ( e:flash.errors.SecurityError ) {
      trace( "retrievePdb - met SecurityError");
    }
  }

  private function __loadPdbComplete( e:Event ) {
    myPdb = new Pdb( e.target.data.toString() );
    visualize( myPdb.genXml() );
  }

  /**
    generate SWF; if the argument b is true, current view (matrix) also saved
  **/
  private function __generateSwf( ?b:Bool = false ):Void {
    var info = stage.loaderInfo;
    var myself = info.bytes;
    var swfinp = new haxe.io.BytesInput( haxe.io.Bytes.ofData( myself ) );
    var reader = new format.swf.Reader( swfinp );
    var swf = reader.read();
    swfinp.close();

    var str = myString;
    if ( b ) {
      str = __addCurrentSetting( myString );
    }
    var mydata = haxe.io.Bytes.ofString( str );

    for ( i in 0 ... swf.tags.length ) {
      var tag = swf.tags[i];
      switch( tag ) {
        // assume TBinaryData is unique and XML data is stored here
        case TBinaryData( id, data ):
          swf.tags[i] = TBinaryData( id, mydata );
        case _:
      }
    }

    swf.header.compressed = true;
    var swfout = new haxe.io.BytesOutput();
    var writer = new format.swf.Writer( swfout );
    writer.write( swf );
    var data = swfout.getBytes();
    var fref = new FileReference();
    try {
      fref.save( data.getData(), "wm3d.swf" );
    } catch( e:Error ) {
      trace( "ErrorID: " + e.errorID + " Message: " + e.message );
    }
  }

  /**
    required for Flash Worker
  **/
  private function __registerClassAliases() {
    haxe.remoting.AMFConnection.registerClassAlias( "WMSystem", WMSystem );
    haxe.remoting.AMFConnection.registerClassAlias( "WMDefaults", WMDefaults );
    haxe.remoting.AMFConnection.registerClassAlias( "WMBase", WMBase );
    haxe.remoting.AMFConnection.registerClassAlias( "WMObjBase", WMObjBase );
    haxe.remoting.AMFConnection.registerClassAlias( "WMAlias", WMAlias );
    haxe.remoting.AMFConnection.registerClassAlias( "WMAtom", WMAtom );
    haxe.remoting.AMFConnection.registerClassAlias( "WMBond", WMBond );
    haxe.remoting.AMFConnection.registerClassAlias( "WMChain", WMChain );
    haxe.remoting.AMFConnection.registerClassAlias( "WMRibbon", WMRibbon );
    haxe.remoting.AMFConnection.registerClassAlias( "WMSmoothChain", WMSmoothChain );
    haxe.remoting.AMFConnection.registerClassAlias( "CubicCurve", CubicCurve );
    haxe.remoting.AMFConnection.registerClassAlias( "WMCPoint", WMCPoint );
    haxe.remoting.AMFConnection.registerClassAlias( "WMShape", WMShape );
    haxe.remoting.AMFConnection.registerClassAlias( "WMLabel", WMLabel );
    haxe.remoting.AMFConnection.registerClassAlias( "WMObject3D", WMObject3D );
    haxe.remoting.AMFConnection.registerClassAlias( "WMPolygon", WMPolygon );
    // base classes
    haxe.remoting.AMFConnection.registerClassAlias( "Point3D", Point3D );
    haxe.remoting.AMFConnection.registerClassAlias( "Polygon", Polygon );
    haxe.remoting.AMFConnection.registerClassAlias( "Vertex", Vertex );
    haxe.remoting.AMFConnection.registerClassAlias( "Face", Face );
    haxe.remoting.AMFConnection.registerClassAlias( "TriFaceIDs", TriFaceIDs );
    haxe.remoting.AMFConnection.registerClassAlias( "UVCoord", UVCoord );
    // primitives
    haxe.remoting.AMFConnection.registerClassAlias( "Cone3D", Cone3D );
    haxe.remoting.AMFConnection.registerClassAlias( "Cube3D", Cube3D );
    haxe.remoting.AMFConnection.registerClassAlias( "Cylinder", Cylinder );
    haxe.remoting.AMFConnection.registerClassAlias( "Kishimen", Kishimen );
    haxe.remoting.AMFConnection.registerClassAlias( "Ribbon3D", Ribbon3D );
    haxe.remoting.AMFConnection.registerClassAlias( "RoundedCylinder", RoundedCylinder );
    haxe.remoting.AMFConnection.registerClassAlias( "Sphere3D", Sphere3D );
    haxe.remoting.AMFConnection.registerClassAlias( "Square3D", Square3D );
    haxe.remoting.AMFConnection.registerClassAlias( "Text3D", Text3D );
    haxe.remoting.AMFConnection.registerClassAlias( "Triangles", Triangles );
    haxe.remoting.AMFConnection.registerClassAlias( "Tube3D", Tube3D );
  }
}
