class PdbAtom {
  static public function readFromText( text:String ):Dynamic {
    var fieldname:String = text.substr( 0, 6 );
    var ishetero:Bool = ( fieldname == "HETATM" );
    var atomindex:Int = Std.parseInt( text.substr( 6, 5 ) );
    var atomname:String = text.substr( 12, 4 );
    var resname:String = text.substr( 17, 3 );
    var resid:Int = Std.parseInt( text.substr( 22, 5 ) );
    var chainid:String = text.substr( 21, 1 );
    var x:Float = Std.parseFloat( text.substr( 30, 8 ) );
    var y:Float = Std.parseFloat( text.substr( 38, 8 ) );
    var z:Float = Std.parseFloat( text.substr( 46, 8 ) );
    var atom:PdbAtom = new PdbAtom( atomname, atomindex, new Point3D( x, y, z ) );
    if ( text.length >= 66 ) {
      var oc:Float = Std.parseFloat( text.substr( 54, 6 ) );
      var bf:Float = Std.parseFloat( text.substr( 60, 6 ) );
      atom.occupancy = oc;
      atom.bfactor = bf;
    }
    // replace element name
    if ( text.length >= 78 ) {
      atom.element = StringTools.trim( text.substr( 76, 2 ) );
    }
    var anon = { ci:chainid, het:ishetero, rn:resname, ri:resid, at:atom.clone() };
    return( anon );
  }

  static public function guessElementName( n:String ):String {
    var len:Int = n.length;
    var str:String = "";
    for ( i in 0 ... len ) {
      var s:String = n.charAt(i);
      // extract only alphabets
      if ( s.toLowerCase() != s.toUpperCase() ) str += s;
    }
    return( PdbAtom.elementName( str ) );
  }

  static public function getDistance( a0:PdbAtom,
                                      a1:PdbAtom ):Float {
    return( Point3D.getSub( a0.pos, a1.pos ).norm() );
  }

  // tentatively defined values; there is no physical/chemical meaning
  static public function getRadii( e:String ):Float {
    switch ( e.toUpperCase() ) {
      case "H":
        return( 1.5 );
      case "C", "O", "N", "F":
        return( 2.0 );
      case "NA", "MG":
        return( 2.5 );
      case "P", "S", "CL":
        return( 2.8 );
      case "K", "CAL", "FE", "ZN":
        return( 3.0 );
    }
    return( 2.0 );
  }

  static public function genBondXml( a0:PdbAtom,
                                     a1:PdbAtom ):String {
    return( "    <BOND pos0=\"" + a0.pos.x + " " + a0.pos.y + " " + a0.pos.z +
                   "\" pos1=\"" + a1.pos.x + " " + a1.pos.y + " " + a1.pos.z +
                   "\" col0=\"" + PdbAtom.getColor( a0.element ) +
                   "\" col1=\"" + PdbAtom.getColor( a1.element ) +
                   "\" />\n" );
  }

  static public function getColor( e:String ):String {
    switch ( e.toUpperCase() ) {
      case "H":
        return( "white" );
      case "C":
        return( "lime" );
      case "O":
        return( "red" );
      case "N":
        return( "blue" );
      case "F":
        return( "pink" );
      case "NA", "MG":
        return( "magenta" );
      case "P":
        return( "pink" );
      case "S":
        return( "yellow" );
      case "CL":
        return( "green" );
      case "K", "CAL", "FE", "ZN":
        return( "silver" );
    }
    return( "lime" );
  }

  static public function elementName( n:String ):String {
    while( n.length >= 1 ) {
      switch ( n.toUpperCase() ) {
        case "H": // hydrogen
          return( "H" );
        case "C", "CA": // carbon; CA is assumed to be alpha carbon
          return( "C" );
        case "O": // oxygen
          return( "O" );
        case "N": // nitrogen
          return( "N" );
        case "F": // fluroine
          return( "F" );
        case "NA", "SOD": // sodium (cation)
          return( "NA" );
        case "MG", "MAG": // magnesium (cation)
          return( "MG" );
        case "P": // phosphorus
          return( "P" );
        case "S": // sulfur
          return( "S" );
        case "CL", "CLA": // chloride (anion)
          return( "CL" );
        case "K": // potassium
          return( "K" );
        case "CAL": // calcium
          return( "CA" );
        case "FE": // iron
          return( "FE" );
        case "ZN": // zinc
          return( "ZN" );
        default:
          n = n.substr( 0, n.length - 1 );
      }
    }
    return( "" );
  }

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

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

  public var name( __getName, __setName ):String;
    public function __getName():String { return( name ); }
    public function __setName( n:String ):String { name = n; return( name ); }

  public var pos( __getPos, __setPos ):Point3D;
    public function __getPos():Point3D { return( pos ); }
    public function __setPos( p:Point3D ):Point3D { pos = p.clone(); return( pos ); }
  // alternate positions
  public var altpos( __getAltPos, null ):Array< Point3D >;
    public function __getAltPos():Array< Point3D > { return( altpos ); }

  public var occupancy( __getOcc, __setOcc ):Float;
    public function __getOcc():Float { return( occupancy ); }
    public function __setOcc( o:Float ):Float { occupancy = o; return( occupancy ); }

  public var bfactor( __getBFactor, __setBFactor ):Float;
    public function __getBFactor():Float { return( bfactor ); }
    public function __setBFactor( f:Float ):Float { return( bfactor ); }

  public var element( __getElement, __setElement ):String;
    public function __getElement():String { return( element ); }
    public function __setElement( e:String ):String { element = e; return( element ); }

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

  public function new( ?n:String = "",
                       ?i:Int = 1,
                       ?p:Point3D,
                       ?oc:Float = 1.0,
                       ?bf:Float = 0.0,
                       ?e:String = "" ) {
    name = n;
    index = i;
    if ( p != null ) pos = p;
    occupancy = oc;
    bfactor = bf;
    element = e;
    if ( name != "" && element == "" ) {
      element = PdbAtom.guessElementName( name );
    }
  }

  public function clone():PdbAtom {
    var ret:PdbAtom = new PdbAtom();
    ret.name = name;
    ret.index = index;
    ret.pos = pos;
    ret.occupancy = occupancy;
    ret.bfactor = bfactor;
    ret.element = element;
    return( ret );
  }

  public function addAlternatePos( p:Point3D ):Void {
    altpos.push( p.clone() );
  }

  public function genAtomXml():String {
    return( "    <ATOM pos=\"" + pos.x + " " + pos.y + " " + pos.z +
                   "\" color=\"" + PdbAtom.getColor( element ) +
                   "\" />\n" );
  }
}
