// --------------------------------------------------------------------
// wm3d - A Flash Molecular Viewer
//
// Copyright (c) 2011-2013, 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/.
// --------------------------------------------------------------------

package pdb;

import pdb.PdbAtom;
import pdb.PdbModel;
import pdb.PdbSecondary;

/**
  data container for pdb file

  - data hierarchy

  Pdb - PdbModel - PdbChain(use chain id) - PdbResidue - PdbAtom

  - PdbModel   : MODELs which are often used for NMR structure
  - PdbChain   : A chain
  - PdbResidue : A residue
  - PdbAtom    : An atom

  keywords to be interpretted are:
  TER, END, ENDMDL, HELIX, SHEET, ATOM, HETATM
**/

class Pdb {
  /**
    radius of coil used when generating xml
  **/
  static public var coil_radius:String = "3.5";
  /**
    radius of ribbon used when generating xml
  **/
  static public var ribbon_radius:String = "10.0";
  /**
    color of helix used when generating xml
  **/
  static public var helix_color:String = "red";
  /**
    thickness of helix used when generating xml
  **/
  static public var helix_thickness:String = "0.2";
  /**
    color of strand used in generating xml
  **/
  static public var strand_color:String = "blue";
  /**
    thickness of strand used in generating xml
  **/
  static public var strand_thickness:String = "0.2";

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

  /**
    Pdb MODEL container; which often appears in NMR structures.
  **/
  public var models( get, null ):Map< Int, PdbModel >;
    /**
      getter of `models`
    **/
    public function get_models():Map< Int, PdbModel > { return( models ); }

  // ######################################################################
  // local variables
  private var modelnum:Int;
  private var secs:Array< PdbSecondary >;
  private var prev:Dynamic;

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

  /**
    Constructor. If pdb text is given, input and secondary structure assign
    will be performed. See also `readAtOnce`.
  **/
  public function new( ?pdbText:String ) {
    initialize();
    if ( pdbText != null ) readAtOnce( pdbText );
  }

  /**
    initialize data
  **/
  public function initialize() {
    models = new Map< Int, PdbModel >();

    modelnum = 0;
    secs = new Array< PdbSecondary >();
    prev = null;
  }

  /**
    returns true if this instance is empty.
    Otherwise, returns false.
  **/
  public function empty():Bool {
    if ( !models.iterator().hasNext() ) return( true );
    return( false );
  }

  /**
    read a line of pdb file and parse it.
  **/
  public function readLine( ?line:String ):Void {
    if ( line.length >= 6 ) {
      var fieldname:String = StringTools.trim( line.substr( 0, 6 ) );
      switch ( fieldname ) {
        case "HELIX":
          var sec:PdbSecondary = PdbSecondary.readHelixFromText( line );
          secs.push( sec );
        case "SHEET":
          var sec:PdbSecondary = PdbSecondary.readStrandFromText( line );
          secs.push( sec );
        case "ATOM", "HETATM":
          var an = PdbAtom.readFromText( line );
          addAtom( modelnum, an.ci, an.rn, an.ri, an.at );
          prev = an;
        case "END", "ENDMDL":
          modelnum++;
      }
    } else if ( line.length >= 3 ) {
      // END statement is assumed to be equivalent to ENDMDL
      if ( line.substr( 0, 3 ) == "END" ) {
        modelnum++;
      }
    }
  }

  /**
    read whole pdb file and store it data. If `doPostProcess` is true,
    secondary structure will be assigned.
  **/
  public function readAtOnce( ?pdbText:String,
                              ?doPostProcess:Bool = true ):Void {
    if ( pdbText == null ) return;
    for ( line in pdbText.split( '\n' ) ) {
      readLine( line );
    }
    if ( doPostProcess ) postProcess();
  }

  /**
    assign secondary structure. Note that this function does not return
    the result of assignment. Use `genSecondaryString` function to get
    the result.
  **/
  public function postProcess():Void {
    for ( model in models ) {
      model.initializeSecondary();
    }
    // if no secondary structure information is described in the pdb file,
    // assign it.
    if ( secs.length == 0 ) {
      calcSecondaryStructures();
    }
    // assign secondary structure type
    for ( sec in secs ) {
      // secondary structures are assumed to be common for all the models
      for ( model in models ) {
        model.chains.get( sec.chainid ).assignSec( sec );
      }
    }
  }

  /**
    assign secondary structures
  **/
  public function calcSecondaryStructures() {
    // calculate secondary structure for each chain
    for ( model in models ) {
      for ( chain in model.chains ) {
        chain.calcSecondaryStructures();
      }
    }
  }

  /**
    add an atom to this instance.
    -mn: model number
    -cid: chain id
    -resname: residue name
    -atom: PdbAtom; contains atom name, atom index, positions
    see also `PdbAtom` class.
  **/
  public function addAtom( mn:Int,
                           cid:String,
                           resname:String,
                           resid:Int,
                           atom:PdbAtom ) {
    // add models if necessary
    if ( !models.exists( mn ) ) {
      models.set( mn, new PdbModel( mn ) );
    }
    var mymodel:PdbModel = models.get( mn );
    mymodel.addAtom( cid, resname, resid, atom );
  }

  /**
    generate XML string from PDB data and return it as a string.
  **/
  public function genXml():String {
    var ret:String = "<WMXML>\n";
    ret += "  <GLOBAL readatonce=\"1\" readchainatonce=\"1\">\n";
    ret += "    <RADIUS method=\"absolute\" />\n";
    ret += "    <AUTOSCALE manual=\"1\" />\n";
    ret += "    <RIBBON radius=\"" + Pdb.ribbon_radius + "\" />\n";
    ret += "    <COIL radius=\"" + Pdb.coil_radius + "\" />\n";
    ret += "  </GLOBAL>\n";
    ret += "  <ALIAS>\n";
    ret += "    <STRAND elem=\"RIBBON\" color=\"" + Pdb.strand_color +
                                "\" thickness=\"" + Pdb.strand_thickness +
                                "\" smoothing=\"1\" />\n";
    ret += "    <HELIX elem=\"RIBBON\" color=\"" + Pdb.helix_color +
                               "\" thickness=\"" + Pdb.helix_thickness +
                               "\" />\n";
    ret += "  </ALIAS>\n";
    for ( model in models ) {
      ret += model.genXml();
    }
    ret += "</WMXML>";
    return( ret );
  }

  /**
    returns secondary structure assignements as a string.
    Each secondary structure element is written in one-character code as

    - H: alpha helix
    - 3: 3-10 helix
    - P: pi-helix
    - E: beta-strand
    - C: coil
    - T: turn
    - B: bridge
  **/
  public function genSecondaryString():String {
    var ret:String = "";
    for ( model in models ) {
      ret += model.genSecondaryString();
      break; // secondary structures are common for all models
    }
    return( ret );
  }
}
