import flash.geom.Matrix3D;
import flash.geom.Vector3D;
import flash.display3D.Context3D;

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

class WMSystem {
  // a flag to define the behaviour of reading.
  // If this value is true, all the polygons are created by calling gen().
  // If false, gen() must be called several times.
  static public var readAtOnce:Bool = true;
  static public var readChainAtOnce:Bool = true;
  // approximate data size which is read by single call of gen()
  // when readAtOnce is false
  static public var readSize:Int = 100;

  private var atoms( null, null ):Array< WMAtom >;
  private var bonds( null, null ):Array< WMBond >;
  private var chains( null, null ):Array< WMChain >;
  private var shapes( null, null ):Array< WMShape >;
  private var labels( null, null ):Array< WMLabel >;
  private var obj3ds( null, null ):Array< WMObject3D >;
  private var prims( null, null ):Array< WMPolygon >;

  private var objects( null, null ):Array< Dynamic >;
  // current index
  // note: this variable name is confusing; data of numFinishedIndex is
  //       not yet read
  private var numFinishedIndex( null, null ):Int;
  public var reading( __getReading, null ):Bool;
    public function __getReading():Bool { return( reading ); }
  public var completed( __getCompleted, null ):Bool;
    public function __getCompleted():Bool { return( completed ); }

  public var autoScale( __getAutoScale, __setAutoScale ):Bool;
    public function __getAutoScale():Bool { return( autoScale ); }
    public function __setAutoScale( a:Bool ):Bool {
      autoScale = a;
      return( autoScale );
    }
  public var scaleFactor( __getScaleFactor, __setScaleFactor ):Float;
    public function __getScaleFactor():Float { return( scaleFactor ); }
    public function __setScaleFactor( f:Float ):Float {
      scaleFactor = f;
      return( scaleFactor );
    }
  public var scaleFactorManual( __getScaleFactorManual, __setScaleFactorManual ):Float;
    public function __getScaleFactorManual():Float { return( scaleFactorManual ); }
    public function __setScaleFactorManual( f:Float ):Float {
      scaleFactorManual = f;
      return( scaleFactorManual );
    }

  private var Defaults( null, null ):WMDefaults;
  private var origin( null, null ):Point3D;

  public var bgcolor( __getBackgroundColor, __setBackgroundColor ):Int;
    public function __getBackgroundColor():Int { return( bgcolor ); }
    public function __setBackgroundColor( c:Int ):Int {
      bgcolor = c;
      return( bgcolor );
    }
  private var xmldata( null, null ):Xml;

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

  public function new() {
    __clearValues();
  }

  public function clear():Void {
    atoms = [];
    bonds = [];
    chains = [];
    shapes = [];
    labels = [];
    obj3ds = [];
    prims = [];
    __clearValues();
  }

  private function __clearValues():Void {
    //// initial values of following three variables are given by parent
    //autoScale = true;
    //scaleFactor = 0.3;
    //scaleFactorManual = 10.0;
    Defaults = new WMDefaults();
    bgcolor = 0x000000; // black
    origin = null;
    numFinishedIndex = 0;
    completed = false;
    reading = false;
  }

  // something experimental
  private function reinitObjects():Void {
    objects = [];
    for ( atom in atoms ) objects.push( atom );
    for ( bond in bonds ) objects.push( bond );
    for ( chain in chains ) objects.push( chain );
    for ( shape in shapes ) objects.push( shape );
    for ( label in labels ) objects.push( label );
    for ( obj3d in obj3ds ) objects.push( obj3d );
    for ( prim in prims ) objects.push( prim );
  }

  public function draw( c:Context3D,
                        mpos:Matrix3D,
                        proj:Matrix3D,
                        voffset:Vector3D,
                        light:Vector3D,
                        cpos:Vector3D ):Bool {
    for ( i in 0 ... numFinishedIndex ) {
      if ( !objects[i].draw( c, mpos, proj, voffset, light, cpos ) ) {
        return( false );
      }
    }
    return( true );
  }

  public function registerXml( myxml:Xml ):Void {
    // this works? Why Xml does not have new()/clone()/copy()?
    xmldata = myxml;
  }

  public function dump():String {
    var ret:String = "";
    var objnum:Int = objects.length;
    for ( i in 0 ... objnum ) {
      ret += objects[i].dump();
    }
    return( ret );
  }

  public function gen( ?c:Context3D = null,
                       ?a:Array< WMAlias > = null,
                       ?myxml:Xml = null ):Void {
    if ( c == null ) return;
    if ( !reading ) {
      if ( myxml == null ) {
        loadXML( xmldata, a );
      } else {
        loadXML( myxml, a );
      }
      reinitObjects();
      initializeCrd();
      reading = true;
      completed = false;
    }
    if ( numFinishedIndex == objects.length ) {
      if ( __checkFinishReadingChain() ) {
        completed = true;
        reading = false;
      }
    } else {
      var tryread:Int = objects.length;
      // variable readAtOnce is static member; global setting
      if ( !WMSystem.readAtOnce ) tryread = getReadLimit();
      for ( i in numFinishedIndex ... tryread ) {
        objects[i].gen( c );
      }
      numFinishedIndex = tryread;
      // check whether finish reading
      if ( numFinishedIndex == objects.length ) {
        if ( __checkFinishReadingChain() ) {
          completed = true;
          reading = false;
        }
      }
    }
  }

  private function __checkFinishReadingChain():Bool {
    for ( chain in chains ) {
      if ( !chain.genCompleted ) return( false );
    }
    return( true );
  }

  private function getReadLimit():Int {
    var objnum:Int = objects.length;
    // read at least one object
    var numFinishedData:Int = objects[numFinishedIndex].getDataSize();
    var ret:Int = numFinishedIndex + 1;
    while( numFinishedData < WMSystem.readSize && ret < objnum ) {
      numFinishedData += objects[ret].getDataSize();
      ret++;
    }
    return( ret );
  }

  public function initializeCrd():Void {
    if ( empty() ) return;
    if ( origin == null ) origin = geometricCenter();
    translate( Point3D.getMultiply( origin, -1.0 ) );
    rescaleCoord();
  }

  public function geometricCenter():Point3D {
    var ret:Point3D = new Point3D( 0.0, 0.0, 0.0 );
    var totnum = 0;
    var objnum:Int = objects.length;
    for ( i in 0 ... objnum ) {
      totnum += objects[i].num();
      ret.add( objects[i].sumPos() );
    }
    ret.multiply( 1.0 / Math.max( 1, totnum ) );
    return( ret );
  }

  public function translate( p:Point3D ):Void {
    var objnum:Int = objects.length;
    for ( i in 0 ... objnum ) {
      objects[i].translate( p );
    }
  }

  public function rescaleCoord():Void {
    var swidth = flash.Lib.current.stage.stageWidth * scaleFactor;
    var sheight = flash.Lib.current.stage.stageHeight * scaleFactor;
    if ( !autoScale ) {
      scaleCoord( scaleFactorManual );
      return;
    }
    // origin is assumed to be processed
    var xmax:Float = 0.0;
    var ymax:Float = 0.0;
    var objnum:Int = objects.length;
    for ( i in 0 ... objnum ) {
      var pmax:Point3D = objects[i].absmax();
      xmax = Math.max( xmax, Math.max( pmax.x, pmax.z ) );
      ymax = Math.max( xmax, pmax.y );
    }
    xmax = Math.max( 1.0, xmax );
    ymax = Math.max( 1.0, ymax );
    // store scale size to scaleFactorManual
    scaleFactorManual = Math.min( swidth / xmax, sheight / ymax );
    // scale coordinate
    scaleCoord( scaleFactorManual );
  }

  public function scaleCoord( scale:Float ):Void {
    var objnum:Int = objects.length;
    for ( i in 0 ... objnum ) {
      objects[i].scaleCoord( scale );
    }
  }

  public function loadXML( ?myxml:Xml = null,
                           ?aliases:Array< WMAlias > = null ):Void {
    clear();
    if ( myxml == null ) return;

    for ( node in myxml.elements() ) {
      switch( node.nodeName.toUpperCase() ) {
        case "LOCAL":
          // read local settings
          __loadLocalSettings( node );
        case "CHAIN":
          // read chains and their elements
          __loadXMLChains( node, aliases );
        case "LABEL":
          // read labels and their elements
          __loadXMLLabels( node );
        case "SHAPE":
          // read shapes and their elements
          __loadXMLShapes( node );
        case "OBJ3D":
          // read 3d-objects
          __loadXMLObj3Ds( node );
        case "ATOM":
          // read ATOMs
          __loadXMLAtoms( node );
        case "BOND":
          // read BONDs
          __loadXMLBonds( node );
        case "POLYGON":
          // read precreated POLYGONs
          __loadXMLPolygons( node );
        default:
          // aliases
          for ( alias in aliases ) {
            if ( node.nodeName.toUpperCase() == alias.name ) {
              switch( alias.elem ) {
                case "ATOM":
                  var at:WMAtom = alias.mybase.clone();
                  at.loadFromXmlWOClear( node );
                  atoms.push( at );
                case "BOND":
                  var bd:WMBond = alias.mybase.clone();
                  bd.loadFromXmlWOClear( node );
                  bonds.push( bd );
                case "LABEL":
                  var lb:WMLabel = alias.mybase.clone();
                  lb.loadFromXmlWOClear( node );
                  labels.push( lb );
                case "SHAPE":
                  var sp:WMShape = alias.mybase.clone();
                  sp.loadFromXmlWOClear( node );
                  shapes.push( sp );
                case "OBJ3D":
                  var ob:WMObject3D = alias.mybase.clone();
                  ob.loadFromXmlWOClear( node );
                  obj3ds.push( ob );
              }
            }
          }
      }
    }
  }

  private function __parseSettingAttributes( nd:Xml ):Void {
    if ( nd.exists( "origin" ) ) {
      origin = Point3D.fromString( nd.get( "origin" ) );
    }
    var strs = [ "bg", "bgcolor", "background", "backgroundcolor" ];
    for ( str in strs ) {
      if ( nd.exists( str ) ) {
        bgcolor = WMBase.parseColor( nd.get( str ) );
      }
    }
  }

  private function __loadLocalSettings( ?nd:Xml = null ):Void {
    __parseSettingAttributes( nd );
    for ( ndc in nd.elements() ) {
      var nn:String = ndc.nodeName.toUpperCase();
      switch( nn ) {
        case "ATOM":
          Defaults.Atom.loadFromXml( ndc );
        case "BOND":
          Defaults.Bond.loadFromXml( ndc );
          var strs = [ "rounded", "round" ];
          for ( s in strs ) {
            if ( ndc.exists( s ) ) {
              Defaults.BondRound = ( Std.parseInt( ndc.get( s ) ) > 0 );
            }
          }
          strs = [ "exc", "exclude" ];
          for ( s in strs ) {
            if ( ndc.exists( s ) ) {
              Defaults.BondExclude = ( Std.parseInt( ndc.get( s ) ) > 0 );
            }
          }
          strs = [ "dash", "dashed" ];
          for ( s in strs ) {
            if ( ndc.exists( s ) ) {
              Defaults.BondDashed = Std.parseInt( ndc.get( s ) );
            }
          }
        case "RIBBON":
          Defaults.Ribbon.loadFromXml( ndc );
          var strs = [ "thick", "thickness" ];
          for ( s in strs ) {
            if ( ndc.exists( s ) ) {
              Defaults.RibbonThickness = Std.parseFloat( ndc.get( s ) );
            }
          }
        case "COIL":
          Defaults.Coil.loadFromXml( ndc );
        case "SHAPE":
          Defaults.Shape.loadFromXml( ndc );
        case "OBJ3D":
          Defaults.Object3D.loadFromXml( ndc );
          if ( ndc.exists( "type" ) ) Defaults.Object3DType = ndc.get( "type" );
        case "LABEL":
          Defaults.Label.loadFromXml( ndc );
          if ( ndc.exists( "font" ) ) Defaults.LabelFont = ndc.get( "font" );
          if ( ndc.exists( "fontsize" ) ) Defaults.LabelSize = Std.parseFloat( ndc.get( "fontsize" ) );
        case "AUTOSCALE":
          if ( ndc.exists( "manual" ) ) {
            var n:Int = Std.parseInt( ndc.get( "manual" ) );
            if ( n > 0 ) autoScale = false;
          }
          if ( ndc.exists( "autoscale" ) ) {
            scaleFactor = Std.parseFloat( ndc.get( "autoscale" ) );
          }
          if ( ndc.exists( "manualscale" ) ) {
            scaleFactorManual = Std.parseFloat( ndc.get( "manualscale" ) );
          }
      }
    }
  }

  private function __loadXMLChains( ?ndi:Xml = null,
                                    ?aliases:Array< WMAlias > = null ):Void {
    var ni:Int = 1;
    if ( ndi.exists( "N" ) ) ni = Std.parseInt( ndi.get( "N" ) );
    // get child elements describing control points
    var children:Array< Dynamic > = new Array< Dynamic >();
    for ( child in ndi.elementsNamed( "POINT" ) ) {
      var anon = { pos:child.get( "pos" ), index:Std.parseInt( child.get( "index" ) ), dir:null, n:-1 };
      if ( child.exists( "N" ) ) anon.n = Std.parseInt( child.get( "N" ) );
      if ( child.exists( "dir" ) ) anon.dir = child.get( "dir" );
      children.push( anon );
    }
    // ignore too short chains, since catmull-rom interpolation is not
    // available for such short chains
    if ( children.length < 4 ) {
      trace( "__loadXMLChains: ignoring very short chain... " );
      trace( "__loadXMLChains: A <CHAIN> must contain at least 4 <POINT>s" );
      return;
    }
    children.sort( __hasSmallIndex );
    var chain:WMChain = new WMChain();
    chain.setPositions( children, ni );
    // read ribbons and coils
    for ( child in ndi.elementsNamed( "RIBBON" ) ) {
      var rib:WMRibbon = new WMRibbon( true );
      rib.loadFromXml( child, Defaults );
      chain.register( rib );
    }
    for ( child in ndi.elementsNamed( "COIL" ) ) {
      var coi:WMRibbon = new WMRibbon( false );
      coi.loadFromXml( child, Defaults );
      chain.register( coi );
    }
    for ( alias in aliases ) {
      if ( alias.elem == "RIBBON" ) {
        for ( child in ndi.elementsNamed( alias.name ) ) {
          var rib:WMRibbon = alias.mybase.clone();
          rib.loadFromXmlWOClear( child );
          chain.register( rib );
        }
      } else if ( alias.elem == "COIL" ) {
        for ( child in ndi.elementsNamed( alias.name ) ) {
          var coi:WMRibbon = alias.mybase.clone();
          coi.loadFromXmlWOClear( child );
          chain.register( coi );
        }
      }
    }
    chains.push( chain );
  }

  private function __hasSmallIndex( o0:Dynamic,
                                    o1:Dynamic ):Int {
    if ( o0.index == o1.index ) return(0);
    if ( o0.index < o1.index ) return(-1);
    return(1);
  }

  private function __loadXMLLabels( ?ndi:Xml = null ):Void {
    var lb:WMLabel = new WMLabel();
    lb.loadFromXml( ndi, Defaults );
    labels.push( lb );
  }

  private function __loadXMLShapes( ?ndi:Xml = null ):Void {
    var shape:WMShape = new WMShape();
    shape.loadFromXml( ndi, Defaults );
    shapes.push( shape );
  }

  private function __loadXMLObj3Ds( ?ndi:Xml = null ):Void {
    var ob:WMObject3D = new WMObject3D();
    ob.loadFromXml( ndi, Defaults );
    obj3ds.push( ob );
  }

  private function __loadXMLAtoms( ?ndi:Xml = null ):Void {
    var at:WMAtom = new WMAtom();
    at.loadFromXml( ndi, Defaults );
    atoms.push( at );
  }

  private function __loadXMLPolygons( ?ndi:Xml = null ):Void {
    var pl:WMPolygon = new WMPolygon();
    pl.loadFromXml( ndi );
    prims.push( pl );
  }

  private function __loadXMLBonds( ?ndi:Xml = null ):Void {
    var bd:WMBond = new WMBond();
    bd.loadFromXml( ndi, Defaults );
    bonds.push( bd );
  }

  public function empty():Bool {
    if ( numAtoms() + numBonds() + numChains() + numShapes() == 0 ) return( true );
    return( false );
  }
  public function numAtoms():Int { return( atoms.length ); }
  public function numBonds():Int { return( bonds.length ); }
  public function numChains():Int { return( chains.length ); }
  public function numShapes():Int { return( shapes.length ); }
}
