class PdbModel {
  public var chains( __getChains, null ):Hash< PdbChain >;
    public function __getChains():Hash< PdbChain > { return( chains ); }

  public var index( __getIndex, __setIndex ):Int;
    public function __getIndex():Int { return( index ); }
    public function __setIndex( i:Int ):Int { return( index ); }

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

  public function new( ?i:Int = 1 ) {
    chains = new Hash< PdbChain >();
    index = i;
  }

  public function addAtom( cid:String,
                           resname:String,
                           resid:Int,
                           ishet:Bool,
                           atom:PdbAtom ) {
    
    if ( !chains.exists( cid ) ) {
      chains.set( cid, new PdbChain( cid ) );
    }
    var mychain:PdbChain = chains.get( cid );
    mychain.addAtom( resname, resid, ishet, atom );
  }

  public function addTer( cid:String,
                          resname:String,
                          resid:Int ) {
    if ( !chains.exists( cid ) ) {
      trace( "PdbModel: internal error; unexpected inconsistency found." );
      return;
    }
    var mychain:PdbChain = chains.get( cid );
    mychain.addTer( resname, resid );
  }

  public function genXml():String {
    var ret:String = "  <SCENE>\n";
    for ( chain in chains ) {
      ret += chain.genXml();
    }
    ret += genXmlHeteroes();
    ret += "  </SCENE>\n";
    return( ret );
  }

  public function genXmlHeteroes():String {
    return( genXmlNotChains( gatherHeteroes() ) );
  }

  // generate non-CHAIN xml data; ATOMs and BONDs
  public function genXmlNotChains( atoms:Array< PdbAtom > ):String {
    // bond counter for each atom
    // atoms without bonds are shown in sphere, others are shown in sticks
    var offset:Int = 100; // ad hoc offset of the array index
    var indexes = new Array();
    var grid:Float = 3.0; // grid size for cell division
    // elements of a position must be larger than - (offset*grid)
    var minx:Int = 99999999; // ad hoc
    var maxx:Int = 0;
    var miny:Int = 99999999;
    var maxy:Int = 0;
    var minz:Int = 99999999;
    var maxz:Int = 0;
    // cell division
    for ( i in 0 ... atoms.length ) {
      var indx:Int = Std.int( atoms[i].pos.x / grid ) + offset;
      var indy:Int = Std.int( atoms[i].pos.y / grid ) + offset;
      var indz:Int = Std.int( atoms[i].pos.z / grid ) + offset;
      // terrible
      if ( indexes[indx] == null ) {
        indexes[indx] = new Array< Array< Array< Int > > >();
      }
      if ( indexes[indx][indy] == null ) {
        indexes[indx][indy] = new Array< Array< Int > >();
      }
      if ( indexes[indx][indy][indz] == null ) {
        indexes[indx][indy][indz] = new Array< Int >();
      }
      indexes[indx][indy][indz].push( i );
      // nothing to say
      minx = Std.int( Math.min( minx, indx ) );
      miny = Std.int( Math.min( miny, indy ) );
      minz = Std.int( Math.min( minz, indz ) );
      maxx = Std.int( Math.max( maxx, indx ) );
      maxy = Std.int( Math.max( maxy, indy ) );
      maxz = Std.int( Math.max( maxz, indz ) );
    }
    return( generateXmlAtomsBonds( minx, miny, minz, maxx, maxy, maxz,
                                   atoms, indexes ) );
  }

  public function generateXmlAtomsBonds( minx:Int,
                                         miny:Int,
                                         minz:Int,
                                         maxx:Int,
                                         maxy:Int,
                                         maxz:Int,
                                         atoms:Array< PdbAtom >,
                                         indexes:Array< Array< Array< Array< Int > > > > ):String {
    var ret:String = "";
    // number of bonds for each atom
    var nbonds:Array< Int > = new Array< Int >();
    for ( i in 0 ... atoms.length ) nbonds.push(0);
    // four-dimensional array is so terrible
    var cells:Array< CellIndex > = CellIndex.adjCells();
    for ( i in minx ... maxx + 1 ) {
      if ( indexes[i] == null ) continue;
      for ( j in miny ... maxy + 1 ) {
        if ( indexes[i][j] == null ) continue;
        for ( k in minz ... maxz + 1 ) {
          if ( indexes[i][j][k] == null ) continue;
          var num:Int = indexes[i][j][k].length;
          // intra-cell
          for ( i0 in 0 ... num ) {
            var myid0 = indexes[i][j][k][i0];
            var myatom0 = atoms[myid0];
            var myradii0:Float = PdbAtom.getRadii( myatom0.element );
            for ( i1 in i0 + 1 ... num ) {
              var myid1 = indexes[i][j][k][i1];
              var myatom1 = atoms[myid1];
              var dist = PdbAtom.getDistance( myatom0, myatom1 );
              if ( dist < ( myradii0 + PdbAtom.getRadii( myatom1.element ) ) / 2 ) {
                // there is a bond between these two atoms
                ret += PdbAtom.genBondXml( myatom0, myatom1 );
                nbonds[myid0] += 1;
                nbonds[myid1] += 1;
              }
            }
          }
          // inter-cell
          for ( cell in cells ) {
            var t = new CellIndex( i + cell.i, j + cell.j, k + cell.k );
            if ( indexes[t.i] == null ) continue;
            if ( indexes[t.i][t.j] == null ) continue;
            if ( indexes[t.i][t.j][t.k] == null ) continue;
            var num1:Int = indexes[t.i][t.j][t.k].length;
            // this loop will be merged into the for loop above?
            for ( i0 in 0 ... num ) {
              var myid0 = indexes[i][j][k][i0];
              var myatom0 = atoms[myid0];
              var myradii0:Float = PdbAtom.getRadii( myatom0.element );
              for ( i1 in 0 ... num1 ) {
                var myid1 = indexes[t.i][t.j][t.k][i1];
                var myatom1 = atoms[myid1];
                var dist = PdbAtom.getDistance( myatom0, myatom1 );
                if ( dist < ( myradii0 + PdbAtom.getRadii( myatom1.element ) ) / 2 ) {
                  ret += PdbAtom.genBondXml( myatom0, myatom1 );
                  nbonds[myid0] += 1;
                  nbonds[myid1] += 1;
                }
              }
            }
          }
        }
      }
    }
    for ( i in 0 ... atoms.length ) {
      if ( nbonds[i] == 0 ||
           atoms[i].element == "NA" ||
           atoms[i].element == "MG" ||
           atoms[i].element == "ZN" ||
           atoms[i].element == "FE" ) {
        ret += atoms[i].genAtomXml();
      }
    }
    return( ret );
  }

  // gather hetero residues from all chains
  public function gatherHeteroes():Array< PdbAtom > {
    var atoms:Array< PdbAtom > = new Array< PdbAtom >();
    for ( chain in chains ) {
      for ( residue in chain.residues ) {
        if ( residue.hetero ) {
          for ( atom in residue.atoms ) {
            atoms.push( atom );
          }
        }
      }
    }
    return( atoms );
  }
}
