import xpath.XPath;
import xpath.xml.XPathXml;
import xpath.xml.XPathHxXml;

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 {
  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 objects( null, null ):Array< Dynamic >;
  private var objnum( null, null ):Int;

  public var autoScale( __getAutoScale, __setAutoScale ):Bool;
  public var scaleFactor( __getScaleFactor, __setScaleFactor ):Float;
  public var scaleFactorManual( __getScaleFactorManual, __setScaleFactorManual ):Float;

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

  public var bgcolor( __getBackgroundColor, __setBackgroundColor ):Int;
  private var xmldata( null, null ):Xml;

  public function new() {
    __clearValues();
  }

  public function clear():Void {
    atoms = [];
    bonds = [];
    chains = [];
    shapes = [];
    labels = [];
    obj3ds = [];
    prepareObjects();
    __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;
  }

  // something experimental
  private function prepareObjects():Void {
    objects = [];
    objects[0] = atoms;
    objects[1] = bonds;
    objects[2] = chains;
    objects[3] = shapes;
    objects[4] = labels;
    objects[5] = obj3ds;
    objnum = objects.length;
  }

  public function draw( c:Context3D,
                        mpos:Matrix3D,
                        proj:Matrix3D,
                        voffset:Vector3D,
                        light:Vector3D,
                        cpos:Vector3D ):Bool {
    for ( i in 0 ... objnum ) {
      var num:Int = objects[i].length;
      for ( j in 0 ... num ) {
        if ( !objects[i][j].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 gen( ?c:Context3D = null,
                       ?a:Array< WMAlias > = null,
                       ?myxml:Xml = null ):Void {
    if ( c == null ) return;
    if ( myxml == null ) {
      loadXML( xmldata, a );
    } else {
      loadXML( myxml, a );
    }
    initializeCrd();
    for ( i in 0 ... objnum ) {
      var num:Int = objects[i].length;
      for ( j in 0 ... num ) {
        objects[i][j].gen( c );
      }
    }
  }

  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;
    for ( i in 0 ... objnum ) {
      var num:Int = objects[i].length;
      for ( j in 0 ... num ) {
        totnum += objects[i][j].num();
        ret.add( objects[i][j].sumPos() );
      }
    }
    ret.multiply( 1.0 / totnum );
    return( ret );
  }

  public function translate( p:Point3D ):Void {
    for ( i in 0 ... objnum ) {
      var num:Int = objects[i].length;
      for ( j in 0 ... num ) {
        objects[i][j].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;
    for ( i in 0 ... objnum ) {
      var num:Int = objects[i].length;
      for ( j in 0 ... num ) {
        var pmax:Point3D = objects[i][j].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 {
    for ( i in 0 ... objnum ) {
      var num:Int = objects[i].length;
      for ( j in 0 ... num ) {
        objects[i][j].scaleCoord( scale );
      }
    }
  }

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

    // read local settings
    var xml_local:XPath = new XPath( "LOCAL" );
    __loadLocalSettings( xml_local.selectNodes( XPathHxXml.wrapNode( myxml ) ) );

    // read chains and their elements
    var xml_chain:XPath = new XPath( "CHAIN" );
    __loadXMLChains( xml_chain.selectNodes( XPathHxXml.wrapNode( myxml ) ), aliases );

    // read labels and their elements
    var xml_label:XPath = new XPath( "LABEL" );
    __loadXMLLabels( xml_label.selectNodes( XPathHxXml.wrapNode( myxml ) ) );

    // read shapes and their elements
    var xml_shape:XPath = new XPath( "SHAPE" );
    __loadXMLShapes( xml_shape.selectNodes( XPathHxXml.wrapNode( myxml ) ) );

    // read 3d-objects
    var xml_obj3d:XPath = new XPath( "OBJ3D" );
    __loadXMLObj3Ds( xml_obj3d.selectNodes( XPathHxXml.wrapNode( myxml ) ) );

    // read ATOMs
    var xml_atom:XPath = new XPath( "ATOM" );
    __loadXMLAtoms( xml_atom.selectNodes( XPathHxXml.wrapNode( myxml ) ) );

    // read BONDs
    var xml_bond:XPath = new XPath( "BOND" );
    __loadXMLBonds( xml_bond.selectNodes( XPathHxXml.wrapNode( myxml ) ) );

    // aliases
    for ( alias in aliases ) {
      var xml_al:XPath = new XPath( alias.name );
      var nodes:Iterable< XPathXml > = xml_al.selectNodes( XPathHxXml.wrapNode( myxml ) );
      for ( nd in nodes ) {
        var ndi:Xml = cast( nd, XPathHxXml ).getWrappedXml();
        switch( alias.elem ) {
          case "ATOM":
            var at:WMAtom = alias.mybase.clone();
            at.loadFromXmlWOClear( ndi );
            atoms.push( at );
          case "BOND":
            var bd:WMBond = alias.mybase.clone();
            bd.loadFromXmlWOClear( ndi );
            bonds.push( bd );
          case "LABEL":
            var lb:WMLabel = alias.mybase.clone();
            lb.loadFromXmlWOClear( ndi );
            labels.push( lb );
          case "SHAPE":
            var sp:WMShape = alias.mybase.clone();
            sp.loadFromXmlWOClear( ndi );
            shapes.push( sp );
          case "OBJ3D":
            var ob:WMObject3D = alias.mybase.clone();
            ob.loadFromXmlWOClear( ndi );
            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( ?nodes:Iterable< XPathXml > = null ):Void {
    for ( nd in nodes ) {
      __parseSettingAttributes( cast( nd, XPathHxXml ).getWrappedXml() );
      var ndio:Xml = cast( nd, XPathHxXml ).getWrappedXml();
      for ( ndc in ndio.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( ?nodes:Iterable< XPathXml > = null,
                                    ?aliases:Array< WMAlias > = null ):Void {
    for ( nd in nodes ) {
      var ni:Int = 1;
      var ndi:Xml = cast( nd, XPathHxXml ).getWrappedXml();
      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" );
        continue;
      }
      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( ?nodes:Iterable< XPathXml > = null ):Void {
    for ( nd in nodes ) {
      var ndi:Xml = cast( nd, XPathHxXml ).getWrappedXml();
      var lb:WMLabel = new WMLabel();
      lb.loadFromXml( ndi, Defaults );
      labels.push( lb );
    }
  }

  private function __loadXMLShapes( ?nodes:Iterable< XPathXml > = null ):Void {
    for ( nd in nodes ) {
      var ndi:Xml = cast( nd, XPathHxXml ).getWrappedXml();
      var shape:WMShape = new WMShape();
      shape.loadFromXml( ndi, Defaults );
      shapes.push( shape );
    }
  }

  private function __loadXMLObj3Ds( ?nodes:Iterable< XPathXml > = null ):Void {
    for ( nd in nodes ) {
      var ndi:Xml = cast( nd, XPathHxXml ).getWrappedXml();
      var ob:WMObject3D = new WMObject3D();
      ob.loadFromXml( ndi, Defaults );
      obj3ds.push( ob );
    }
  }

  private function __loadXMLAtoms( ?nodes:Iterable< XPathXml > = null ):Void {
    for ( nd in nodes ) {
      var ndi:Xml = cast( nd, XPathHxXml ).getWrappedXml();
      var at:WMAtom = new WMAtom();
      at.loadFromXml( ndi, Defaults );
      atoms.push( at );
    }
  }

  private function __loadXMLBonds( ?nodes:Iterable< XPathXml > = null ):Void {
    for ( nd in nodes ) {
      var ndi:Xml = cast( nd, XPathHxXml ).getWrappedXml();
      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 ); }

  // getter
  public function __getBackgroundColor():Int { return( bgcolor ); }
  public function __getAutoScale():Bool { return( autoScale ); }
  public function __getScaleFactor():Float { return( scaleFactor ); }
  public function __getScaleFactorManual():Float { return( scaleFactorManual ); }
  // setter
  public function __setBackgroundColor( c:Int ):Int {
    bgcolor = c;
    return( bgcolor );
  }
  public function __setAutoScale( a:Bool ):Bool {
    autoScale = a;
    return( autoScale );
  }
  public function __setScaleFactor( f:Float ):Float {
    scaleFactor = f;
    return( scaleFactor );
  }
  public function __setScaleFactorManual( f:Float ):Float {
    scaleFactorManual = f;
    return( scaleFactorManual );
  }
}
