/*  Copyright (C) 2003-2011 Raik Nagel and JabRef contributors
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
//
// function : Handling of bibtex fields.
//            All bibtex-field related stuff should be placed here!
//            Because we can export these informations into additional
//            config files -> simple extension and definition of new fields....
//
// todo     : - handling of identically fields with different names
//              e.g. LCCN = lib-congress
//            - group id for each fields, e.g. standard, jurabib, bio....
//            - add a additional properties functionality into the
//              BibtexSingleField class
//
// modified : r.nagel 25.04.2006
//            export/import of some definition from/to a xml file

package net.sf.jabref ;

import java.util.HashMap;
import java.util.Vector;
import java.util.HashSet;

import net.sf.jabref.specialfields.SpecialFieldsUtils;
import net.sf.jabref.util.TXMLReader;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class BibtexFields
{
  public static final String KEY_FIELD = "bibtexkey" ;

  // some internal fields
  public static final String
      SEARCH = "__search",
      GROUPSEARCH = "__groupsearch",
      MARKED = "__markedentry",
      OWNER = "owner",
      TIMESTAMP = "timestamp", // it's also definied at the JabRefPreferences class
      ENTRYTYPE = "entrytype",

      // Using this when I have no database open or when I read
      // non bibtex file formats (used by the ImportFormatReader.java)
      DEFAULT_BIBTEXENTRY_ID = "__ID" ;

  public static final String[] DEFAULT_INSPECTION_FIELDS = new String[]
          {"author", "title", "year", KEY_FIELD};


  // singleton instance
  private static final BibtexFields runtime = new BibtexFields() ;

  // contains all bibtex-field objects (BibtexSingleField)
  private HashMap<String, BibtexSingleField> fieldSet;

  // contains all known (and public) bibtex fieldnames
  private String[] PUBLIC_FIELDS = null ;

  private BibtexFields()
  {
    fieldSet = new HashMap<String, BibtexSingleField>();
    BibtexSingleField dummy = null ;

    // FIRST: all standard fields
    // These are the fields that BibTex might want to treat, so these
    // must conform to BibTex rules.
    add( new BibtexSingleField( "address", true, GUIGlobals.SMALL_W  ) ) ;
    // An annotation. It is not used by the standard bibliography styles,
    // but may be used by others that produce an annotated bibliography.
    // http://www.ecst.csuchico.edu/~jacobsd/bib/formats/bibtex.html
    add( new BibtexSingleField( "annote", true, GUIGlobals.LARGE_W  ) ) ;
    add( new BibtexSingleField( "author", true, GUIGlobals.MEDIUM_W, 280 ) ) ;
    add( new BibtexSingleField( "booktitle", true, 175 ) ) ;
    add( new BibtexSingleField( "chapter", true, GUIGlobals.SMALL_W  ) ) ;
    add( new BibtexSingleField( "crossref", true, GUIGlobals.SMALL_W ) ) ;
    add( new BibtexSingleField( "edition", true, GUIGlobals.SMALL_W  ) ) ;
    add( new BibtexSingleField( "editor", true, GUIGlobals.MEDIUM_W, 280  ) ) ;
    add( new BibtexSingleField( "howpublished", true, GUIGlobals.MEDIUM_W  ) ) ;
    add( new BibtexSingleField( "institution", true, GUIGlobals.MEDIUM_W  ) ) ;

    dummy = new BibtexSingleField( "journal", true, GUIGlobals.SMALL_W ) ;
    dummy.setExtras("journalNames");
    add(dummy) ;
    add( new BibtexSingleField( "key", true ) ) ;
    add( new BibtexSingleField( "month", true, GUIGlobals.SMALL_W ) ) ;
    add( new BibtexSingleField( "note", true, GUIGlobals.MEDIUM_W  ) ) ;
    add( new BibtexSingleField( "number", true, GUIGlobals.SMALL_W, 60  ).setNumeric(true) ) ;
    add( new BibtexSingleField( "organization", true, GUIGlobals.MEDIUM_W  ) ) ;
    add( new BibtexSingleField( "pages", true, GUIGlobals.SMALL_W ) ) ;
    add( new BibtexSingleField( "publisher", true, GUIGlobals.MEDIUM_W  ) ) ;
    add( new BibtexSingleField( "school", true, GUIGlobals.MEDIUM_W  ) ) ;
    add( new BibtexSingleField( "series", true, GUIGlobals.SMALL_W  ) ) ;
    add( new BibtexSingleField( "title", true, 400 ) ) ;
    add( new BibtexSingleField( "type", true, GUIGlobals.SMALL_W  ) ) ;
    add( new BibtexSingleField( "language", true, GUIGlobals.SMALL_W  ) ) ;
    add( new BibtexSingleField( "volume", true, GUIGlobals.SMALL_W, 60  ).setNumeric(true) ) ;
    add( new BibtexSingleField( "year", true, GUIGlobals.SMALL_W, 60 ).setNumeric(true) ) ;

    // custom fields not displayed at editor, but as columns in the UI
    dummy =  new BibtexSingleField( SpecialFieldsUtils.FIELDNAME_RANKING , false) ;
    if (!Globals.prefs.getBoolean(SpecialFieldsUtils.PREF_SERIALIZESPECIALFIELDS)) {
    	dummy.setPrivate();
    	dummy.setWriteable(false);
    	dummy.setDisplayable(false);
    }
    add(dummy) ;
    dummy =  new BibtexSingleField( SpecialFieldsUtils.FIELDNAME_PRIORITY , false) ;
    if (!Globals.prefs.getBoolean(SpecialFieldsUtils.PREF_SERIALIZESPECIALFIELDS)) {
    	dummy.setPrivate();
    	dummy.setWriteable(false);
    	dummy.setDisplayable(false);
    }
    add(dummy) ;
    dummy =  new BibtexSingleField( SpecialFieldsUtils.FIELDNAME_RELEVANCE , false) ;
    if (!Globals.prefs.getBoolean(SpecialFieldsUtils.PREF_SERIALIZESPECIALFIELDS)) {
    	dummy.setPrivate();
    	dummy.setWriteable(false);
    	dummy.setDisplayable(false);
    }
    add(dummy) ;
    dummy =  new BibtexSingleField( SpecialFieldsUtils.FIELDNAME_QUALITY , false) ;
    if (!Globals.prefs.getBoolean(SpecialFieldsUtils.PREF_SERIALIZESPECIALFIELDS)) {
    	dummy.setPrivate();
    	dummy.setWriteable(false);
    	dummy.setDisplayable(false);
    }
    add(dummy) ;
    
    // some semi-standard fields
    dummy = new BibtexSingleField( KEY_FIELD, true ) ;
    dummy.setPrivate();
    add( dummy ) ;

    dummy = new BibtexSingleField( "doi", true, GUIGlobals.SMALL_W ) ;
    dummy.setExtras("external");
    add(dummy) ;
    add( new BibtexSingleField( "eid", true, GUIGlobals.SMALL_W  ) ) ;

    dummy = new BibtexSingleField( "date", true ) ;
    dummy.setPrivate();
    add( dummy ) ;

    add(new BibtexSingleField("pmid", false, GUIGlobals.SMALL_W, 60).setNumeric(true));

    // additional fields ------------------------------------------------------ 
    add( new BibtexSingleField( "location", false ) ) ;
    add( new BibtexSingleField( "abstract", false, GUIGlobals.LARGE_W, 400  ) ) ;

    dummy =  new BibtexSingleField( "url", false, GUIGlobals.SMALL_W) ;
    dummy.setExtras("external");
    add(dummy) ;

    dummy = new BibtexSingleField( "pdf", false, GUIGlobals.SMALL_W ) ;
    dummy.setExtras("browseDoc");
    add(dummy) ;

    dummy = new BibtexSingleField( "ps", false, GUIGlobals.SMALL_W ) ;
    dummy.setExtras("browseDocZip");
    add(dummy) ;
    add( new BibtexSingleField( "comment", false, GUIGlobals.MEDIUM_W  ) ) ;
    add( new BibtexSingleField( "keywords", false, GUIGlobals.SMALL_W  ) ) ;
    //FIELD_EXTRAS.put("keywords", "selector");


    dummy = new BibtexSingleField(GUIGlobals.FILE_FIELD, false);
    dummy.setEditorType(GUIGlobals.FILE_LIST_EDITOR);
    add(dummy);


    add( new BibtexSingleField( "search", false, 75 ) ) ;


    // some internal fields ----------------------------------------------
    dummy = new BibtexSingleField( GUIGlobals.NUMBER_COL, false, 32  ) ;
    dummy.setPrivate() ;
    dummy.setWriteable(false);
    dummy.setDisplayable(false);
    add( dummy ) ;

    dummy = new BibtexSingleField( OWNER, false, GUIGlobals.SMALL_W ) ;
    dummy.setExtras("setOwner");
    dummy.setPrivate();
    add(dummy) ;

    dummy = new BibtexSingleField( TIMESTAMP, false, GUIGlobals.SMALL_W ) ;
    dummy.setExtras("datepicker");
    dummy.setPrivate();
    add(dummy) ;

    dummy =  new BibtexSingleField( ENTRYTYPE, false, 75 ) ;
    dummy.setPrivate();
    add(dummy) ;

    dummy =  new BibtexSingleField( SEARCH, false) ;
    dummy.setPrivate();
    dummy.setWriteable(false);
    dummy.setDisplayable(false);
    add(dummy) ;

    dummy =  new BibtexSingleField( GROUPSEARCH, false) ;
    dummy.setPrivate();
    dummy.setWriteable(false);
    dummy.setDisplayable(false);
    add(dummy) ;

    dummy =  new BibtexSingleField( MARKED, false) ;
    dummy.setPrivate();
    dummy.setWriteable(true); // This field must be written to file!
    dummy.setDisplayable(false);
    add(dummy) ;

     // read external field definitions
    readXML( Globals.additionalFields ) ;

    // collect all public fields for the PUBLIC_FIELDS array
    Vector<String> pFields = new Vector<String>( fieldSet.size()) ;
    for (BibtexSingleField sField : fieldSet.values()){
      if (sField.isPublic() )
      {
        pFields.add( sField.getFieldName() );
        // or export the complet BibtexSingleField ?
        // BibtexSingleField.toString() { return fieldname ; }
      }
    }

    PUBLIC_FIELDS = pFields.toArray(new String[pFields.size()]);
    // sort the entries
    java.util.Arrays.sort( PUBLIC_FIELDS );

  }

    /**
     * Read the "numericFields" string array from preferences, and activate numeric
     * sorting for all fields listed in the array. If an unknown field name is included,
     * add a field descriptor for the new field.
     */
    public static void setNumericFieldsFromPrefs() {
        String[] numFields = Globals.prefs.getStringArray("numericFields");
        if (numFields == null)
            return;
        // Build a Set of field names for the fields that should be sorted numerically:
        HashSet<String> nF = new HashSet<String>();
        for (int i = 0; i < numFields.length; i++) {
            nF.add(numFields[i]);
        }
        // Look through all registered fields, and activate numeric sorting if necessary:
        for (String fieldName : runtime.fieldSet.keySet()) {
            BibtexSingleField field = runtime.fieldSet.get(fieldName);
            if (!field.isNumeric() && nF.contains(fieldName)) {
                field.setNumeric(nF.contains(fieldName));
            }
            nF.remove(fieldName); // remove, so we clear the set of all standard fields.
        }
        // If there are fields left in nF, these must be non-standard fields. Add descriptors for them:
        for (String fieldName : nF) {
            BibtexSingleField field = new BibtexSingleField(fieldName, false);
            field.setNumeric(true);
            runtime.fieldSet.put(fieldName, field);
        }

    }


  /** insert a field into the internal list */
  private void add( BibtexSingleField field )
  {
    // field == null check
    String key = field.name ;
    fieldSet.put( key, field ) ;
  }

  /** read a xml definiton file and put only NEW fields into the field list */
  private void readXML( String resName )
  {
    TXMLReader reader = new TXMLReader(resName) ;
    if (reader.isReady() )
    {
      // get a list of all fields
      NodeList fieldNodes = reader.getNodes("field") ;

      int tagsCount = fieldNodes.getLength() ;
      for (int t = 0 ; t < tagsCount ; t++)
      {
        Element entry = (Element) fieldNodes.item(t) ;
        String fName = reader.readStringAttribute(entry, "name", null) ;
        if (fName != null)  // something found ?
        {
          fName = fName.toLowerCase() ;
          BibtexSingleField dummy = fieldSet.get( fName ) ;
          if (dummy == null)  // unknown field
          {
            dummy = new BibtexSingleField(reader, entry) ;
            fieldSet.put(fName, dummy) ;
          }
        }
      }
    }
  }

  // --------------------------------------------------------------------------
  //  the "static area"
  // --------------------------------------------------------------------------
  private static final BibtexSingleField getField( String name )
  {
    if (name != null)
    {
      return runtime.fieldSet.get(name.toLowerCase()) ;
    }

    return null ;
  }

  public static String getFieldExtras( String name )
  {
    BibtexSingleField sField = getField( name ) ;
    if (sField != null)
    {
      return sField.getExtras() ;
    }
    return null ;
  }


  public static int getEditorType(String name) {
    BibtexSingleField sField = getField( name ) ;
    if (sField != null)
    {
      return sField.getEditorType();
    }
    return GUIGlobals.STANDARD_EDITOR;      
  }

  public static double getFieldWeight( String name )
  {
    BibtexSingleField sField = getField( name ) ;
    if (sField != null)
    {
      return sField.getWeight() ;
    }
    return GUIGlobals.DEFAULT_FIELD_WEIGHT ;
  }

  public static void setFieldWeight( String fieldName, double weight )
  {
    BibtexSingleField sField = getField( fieldName ) ;
    if (sField != null)
    {
      sField.setWeight( weight ) ;
    }
  }

  public static int getFieldLength( String name )
  {
    BibtexSingleField sField = getField( name ) ;
    if (sField != null)
    {
      return sField.getLength() ;
    }
    return GUIGlobals.DEFAULT_FIELD_LENGTH ;
  }

  // returns an alternative name for the given fieldname
  public static String getFieldDisplayName( String fieldName )
  {
    BibtexSingleField sField = getField( fieldName ) ;
    if (sField != null)
    {
      return sField.getAlternativeDisplayName() ;
    }
    return null ;
  }

  public static boolean isWriteableField( String field )
  {
    BibtexSingleField sField = getField( field ) ;
    if (sField != null)
    {
      return sField.isWriteable() ;
    }
    return true ;
  }

  public static boolean isDisplayableField( String field )
  {
    BibtexSingleField sField = getField( field ) ;
    if (sField != null)
    {
      return sField.isDisplayable() ;
    }
    return true ;
  }

  /**
   * Returns true if the given field is a standard Bibtex field.
   *
   * @param field a <code>String</code> value
   * @return a <code>boolean</code> value
   */
  public static boolean isStandardField( String field )
  {
    BibtexSingleField sField = getField( field ) ;
    if (sField != null)
    {
      return sField.isStandard() ;
    }
    return false ;
  }

    public static boolean isNumeric( String field ) {
        BibtexSingleField sField = getField( field ) ;
        if (sField != null)
        {
            return sField.isNumeric() ;
        }
        return false ;
    }

  /** returns an string-array with all fieldnames */
  public static String[] getAllFieldNames()
  {
    return runtime.PUBLIC_FIELDS ;
  }
  
  /** returns an string-array with only private fieldnames */
  public static String[] getAllPrivateFieldNames(){
	  Vector<String> pFields = new Vector<String>() ;
	  for (BibtexSingleField sField : runtime.fieldSet.values()){
		  if (sField.isPrivate()) {
			  pFields.add( sField.getFieldName() );
		  }
	  }
	  return pFields.toArray(new String[pFields.size()]);

 }


  /** returns the fieldname of the entry at index t */
  public static String getFieldName( int t )
  {
    return  runtime.PUBLIC_FIELDS[t] ;
  }

  /** returns the number of available fields */
  public static int numberOfPublicFields()
  {
    return runtime.PUBLIC_FIELDS.length ;
  }

  /*
     public static int getPreferredFieldLength(String name) {
     int l = DEFAULT_FIELD_LENGTH;
     Object o = fieldLength.get(name.toLowerCase());
     if (o != null)
     l = ((Integer)o).intValue();
     return l;
     }*/


  // --------------------------------------------------------------------------
  // a container class for all properties of a bibtex-field
  // --------------------------------------------------------------------------
  private static class BibtexSingleField
  {
    private static final int
        STANDARD       = 0x01,  // it is a standard bibtex-field
        PRIVATE        = 0x02,  // internal use, e.g. owner, timestamp
        DISPLAYABLE    = 0x04,  // These fields cannot be shown inside the source editor panel
        WRITEABLE      = 0x08 ; // These fields will not be saved to the .bib file.

    // the fieldname
    private String name ;

    // contains the standard, privat, displayable, writable infos
    // default is: not standard, public, displayable and writable
    private int flag = DISPLAYABLE | WRITEABLE ;

    private int length = GUIGlobals.DEFAULT_FIELD_LENGTH ;
    private double weight = GUIGlobals.DEFAULT_FIELD_WEIGHT ;

    private int editorType = GUIGlobals.STANDARD_EDITOR;

    // a alternative displayname, e.g. used for
    // "citeseercitationcount"="Popularity"
    private String alternativeDisplayName = null ;

    // the extras data
    // fieldExtras contains mappings to tell the EntryEditor to add a specific
    // function to this field, for instance a "browse" button for the "pdf" field.
    private String extras = null ;

    // This value defines whether contents of this field are expected to be
    // numeric values. This can be used to sort e.g. volume numbers correctly:
    private boolean numeric = false;

      // a comma separated list of alternative bibtex-fieldnames, e.g.
    // "LCCN" is the same like "lib-congress"
    // private String otherNames = null ;

    // a Hashmap for a lot of additional "not standard" properties
    // todo: add the handling in a key=value manner
    // private HashMap props = new HashMap() ;

    // some constructors ;-)
    public BibtexSingleField( String fieldName )
    {
      name = fieldName ;
    }

    public BibtexSingleField( String fieldName, boolean pStandard )
    {
      name = fieldName ;
      setFlag( pStandard, STANDARD) ;
    }

    public BibtexSingleField( String fieldName, boolean pStandard, double pWeight)
    {
      name = fieldName ;
      setFlag( pStandard, STANDARD) ;
      weight = pWeight ;
    }

    public BibtexSingleField( String fieldName, boolean pStandard, int pLength)
    {
      name = fieldName ;
      setFlag( pStandard, STANDARD) ;
      length = pLength ;
    }

    public BibtexSingleField( String fieldName, boolean pStandard,
                              double pWeight, int pLength)
    {
      name = fieldName ;
      setFlag( pStandard, STANDARD) ;
      weight = pWeight ;
      length = pLength ;
    }

    /** the constructor reads all neccessary data from the xml file */
    public BibtexSingleField( TXMLReader reader, Element node)
    {
      // default is: not standard, public, displayable and writable
      flag = DISPLAYABLE | WRITEABLE ;

      name = reader.readStringAttribute(node, "name", "field") ;
      name = name.toLowerCase() ;

      // read the weight
      String wStr = reader.readStringAttribute(node, "weight", null) ;
      if (wStr != null)
      {
        int hCode = wStr.toLowerCase().hashCode() ;
        if (hCode == "small".hashCode())
        {
          weight = GUIGlobals.SMALL_W ;
        }
        else if (hCode == "medium".hashCode())
        {
          weight = GUIGlobals.MEDIUM_W ;
        }
        else if (hCode == "large".hashCode())
        {
          weight = GUIGlobals.LARGE_W ;
        }
        else // try to convert to a double value
        {
          try
          {
            weight = Double.parseDouble(wStr) ;
            if ((weight < 0.0) || (weight > GUIGlobals.MAX_FIELD_WEIGHT))
            {
              weight = GUIGlobals.DEFAULT_FIELD_WEIGHT ;
            }
          }
          catch (Exception e)
          {
            weight = GUIGlobals.DEFAULT_FIELD_WEIGHT ;
          }
        }
      }
      length = reader.readIntegerAttribute( node, "length", GUIGlobals.DEFAULT_FIELD_LENGTH ) ;

      extras = reader.readStringAttribute(node, "extras", null) ;
    }

    // -----------------------------------------------------------------------
    // -----------------------------------------------------------------------



    private void setFlag( boolean onOff, int flagID)
    {
      if (onOff)  // set the flag
      {
        flag = flag | flagID ;
      }
      else // unset the flag,
      {
        flag = flag & ( 0xff ^ flagID ) ;
      }
    }

    private boolean isSet( int flagID )
    {
      if ( (flag & flagID) == flagID)
        return true ;

      return false ;
    }

    // -----------------------------------------------------------------------
    public boolean isStandard()
    {
      return isSet( STANDARD ) ;
    }

    public void setPrivate()
    {
      flag = flag | PRIVATE ;
    }

    public boolean isPrivate()
    {
      return isSet( PRIVATE ) ;
    }

    public void setPublic()
    {
      setFlag( false, PRIVATE ) ;
    }

    public boolean isPublic()
    {
      return !isSet( PRIVATE ) ;
    }

    public void setDisplayable(boolean value)
    {
      setFlag( value, DISPLAYABLE ) ;
    }

    public boolean isDisplayable()
    {
      return isSet(DISPLAYABLE) ;
    }


    public void setWriteable(boolean value)
    {
      setFlag( value, WRITEABLE ) ;
    }

    public boolean isWriteable()
    {
      return isSet( WRITEABLE ) ;
    }

    // -----------------------------------------------------------------------
    public void setAlternativeDisplayName( String aName)
    {
      alternativeDisplayName = aName ;
    }

    public String getAlternativeDisplayName()
    {
      return alternativeDisplayName ;
    }
    // -----------------------------------------------------------------------

    public void setExtras( String pExtras)
    {
      extras = pExtras ;
    }

    // fieldExtras contains mappings to tell the EntryEditor to add a specific
    // function to this field, for instance a "browse" button for the "pdf" field.
    public String getExtras()
    {
      return extras ;
    }

    public void setEditorType(int type) {
        editorType = type;
    }

    public int getEditorType() {
        return editorType;
    }
    // -----------------------------------------------------------------------

    public void setWeight( double value )
    {
      this.weight = value ;
    }

    public double getWeight()
    {
      return this.weight ;
    }

    // -----------------------------------------------------------------------
    public int getLength()
    {
      return this.length ;
    }

    // -----------------------------------------------------------------------

    public String getFieldName()
    {
      return name ;
    }


      /**
       * Set this field's numeric propery
       * @param numeric true to indicate that this is a numeric field.
       * @return this BibtexSingleField instance. Makes it easier to call this
       *   method on the fly while initializing without using a local variable.
       */
      public BibtexSingleField setNumeric(boolean numeric) {
          this.numeric = numeric;
          return this;
      }

      public boolean isNumeric() {
          return numeric;
      }

  }
}
