/*
    Some notes regarding cross browser compatibility
    
    The following is not supported FireFox and Safari (W3C DOM compliant browsers)
    1) document.all, use document.getElementById
    2) document.<element_id>, use document.getElementById
    3) node.all, there is not standard equitvalent but I have written a recursive getElementById(node, id) function
    
    XmlHttpRequest Object:
        if(window.XMLHttpRequest)               //For browser's other than IE
            req = new XMLHttpRequest(); 
        else if (window.ActiveXObject)          //For IE 6 and 7
            req  = new ActiveXObject("Microsoft.XMLHTTP"); 
            
    Note: IE 7 supports Microsoft object so one could use ActiveXObject(Microsoft.XMLHTTP); 
    but this will not work with IE 6 and Microsoft.XMLHTTP must be placed in paranthesis.
*/

/*
    Property Class
    Represents a name-value pair
*/
function Property()        
{
    this.Name = "";
    this.Value = "";
}

/*
    BibtexEntry Class
    A canonical representation of a single Bibtex entry such as:
    
    @inproceedings{Bar_Code_Recognition_in_Highly_Distorted_and_Low_Resolution_Images,
        author         = "R. Shams and P. Sadeghi",
        title          = "Bar Code Recognition in Highly Distorted and Low Resolution Images",
        booktitle      = "Proc. {IEEE} Int. Conf. on Acoustics, Speech, and Signal Processing ({ICASSP})",
        address        = "Honolulu, HI",
        month          = apr,
        volume         = "1",
        pages          = "I-737-I-740",
        year           = "2007",
        link          = "papers/2007/ICASSP_2007.pdf",
        linkname      = "PDF"
    };
           
*/
function BibtexEntry()
{
    this.Type = "";                     //bibtex type, e.g. inproceedings, book, article, etc.
    this.Name = "";                     //entry name which follows the type declaration
    this.bibtexHTML = "";               //The entry in bibtex format, prepared for HTML output
    this.Properties = new Array();      //An array of properties, i.e., Name-Value pairs.
}
       
BibtexEntry.prototype.getProperty = function(name)
{
    for (var i = 0; i < this.Properties.length; i++)
    {
        if (this.Properties[i].Name == name)
            return this.Properties[i].Value;
    }

    return null;
}

BibtexEntry.prototype.toString = function()
{
    var out = "";
    var authors = this.getProperty("author");
    if (authors != null) out += authors + ", ";
    var title = this.getProperty("title");
    if (title != null) out += "\"" + title + ",\" ";
    
    var publication;
    if (this.Type == "article")
        publication = this.getProperty("journal");
    else if (this.Type == "inproceedings")
        publication = this.getProperty("booktitle");
    else if (this.Type == "techreport")
        publication = this.getProperty("institution");
    if (publication != null)
    {
        this.Properties[this.Properties.length] = new Property();
        this.Properties[this.Properties.length - 1].Name = "publication";
        this.Properties[this.Properties.length - 1].Value = publication; 
        out += "<i>" + publication + "</i>, ";
    }
    
    if (this.Type == "techreport")
        out += "Tech. Rep., ";
    
    var address;
    address = this.getProperty("address");
    if (address != null) out += address + ", ";
        
    var volume = this.getProperty("volume");
    if (volume != null) volume = "vol. " + volume + ", ";             
    var no = this.getProperty("no");
    if (no != null) out += "no. " + no + ", ";
    
    var month = this.getProperty("month");
    var year = this.getProperty("year");
    
    if (this.Type == "article" )            
    {
        var pages = this.getProperty("pages");
        if (pages != null ) out += "pp. " + pages + ", ";

        if (month != null)
            month = this.getMonthName(month);
        if (month != null && year!= null) out += month + " " + year + ", ";
        else if (month == null) out += year + ", ";
        else if (year == null) out += month + ", ";
    }
    else if(this.Type == "inproceedings" || this.Type == "techreport")           //The order is swapped for proceedings
    {
        if (month != null)
            month = this.getMonthName(month);
        if (month != null && year!= null) out += month + " " + year + ", ";
        else if (month == null) out += year + ", ";
        else if (year == null) out += month + ", ";

        var pages = this.getProperty("pages");
        if (pages != null ) out += "pp. " + pages + ", ";
    }
                
    //Remove the last ,<space> 
    if (out.substr(out.length - 2, 2) == ", ")
        out = out.substr(0, out.length - 2);
    
    //add a '.' to the end.
    return out + ".";
}

BibtexEntry.prototype.getMonthName = function(month)
{
    var months = new Array("Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec.");
 
    return months[month - 1];
}

/*
    Bibtex Class
    Used to parse bibtex files and represents a collection of a bibtex entries.
*/
function Bibtex(str)
{
    this.Entries = new Array();         //An array of bibtex entries
    
    this.Parse(str);
}

/*
    propName: "date", "publication", "title"
    asc: true for ascending and descending otherwise 
    uses two global varialbes to communicate with the sort function.
*/        
var g_propName, g_asc;
Bibtex.prototype.Sort = function(propName, asc)
{
    g_propName = propName;
    g_asc = asc;
    this.Entries.sort(sortFunction);
}

function sortFunction(a,b)
{
    var propName = g_propName;
    if (g_propName == "publication")
    {
        if(a.Type == "article")
            propName = "journal";
        else if(a.Type == "inproceedings")                
            propName = "booktitle";
        value_a = a.getProperty(propName);

        if(b.Type == "article")
            propName = "journal";
        else if(b.Type == "inproceedings")                
            propName = "booktitle";
        value_b = b.getProperty(propName);                                
    }
    else if (g_propName == "date")
    {
        year = a.getProperty("year");
        if (year==null) year = "0000";                    
        month = a.getProperty("month");
        if (month==null) month = "00"; 
        value_a = year + month;

        year = b.getProperty("year");
        if (year==null) year = "0000";                    
        month = b.getProperty("month");
        if (month==null) month = "00"; 
        value_b = year + month;
    }
    else
    {
        value_a = a.getProperty(g_propName);
        value_b = b.getProperty(g_propName);
    }

    if (value_a == null && value_b == null)
        return 0;
    else if (value_a == null)
        return g_asc ? 1 : -1;
    else if (value_b == null)
        return g_asc ? -1 : 1;    
    
    if (g_asc)
        return ((value_a < value_b) ? -1 : (value_a > value_b) ? 1 : 0)
    else
        return ((value_a < value_b) ? 1 : (value_a > value_b) ? -1 : 0)
}

Bibtex.prototype.Parse = function(str)
{
    //Remove all comments
    str = str.replace(/%.*\s*/g, "");
                                                 
    //find all entries
    var re = /@([^{]*)\{([^;]*)\}\s*;/gm;
    var arr = str.match(re);
    
    for (var i = 0; i < arr.length; i++)
    {
        var s = new String(arr[i]);
        this.Entries[i] = new BibtexEntry();
        this.Entries[i].Type = s.replace(re, "$1").trim();
        var body = s.replace(re, "$2");                                              
        this.Entries[i].Properties = this.NormalizeProperties(this.ParseProperties(this.Entries[i], body), this.Entries[i].Type);
    }
}

/*
    This method normalizes the property name and values by converting the name to lower-case
    and converting the value to lower case and removing {} which are meant to preserve the case.
*/
Bibtex.prototype.NormalizeProperties = function(props, type)
{
    var publication;
    for (var i = 0; i < props.length; i++)
    {
        props[i].Name = props[i].Name.toLowerCase();                
        var str = new String(props[i].Value);                
        
        if (props[i].Name == "title")                
            props[i].Value = this.ToLowerCaseWithBraces(str);
        else if (props[i].Name == "author")
            props[i].Value = this.NormalizeAuthors(str);
        else if (
            (props[i].Name == "journal" && type == "article") ||
            (props[i].Name == "booktitle" && type == "inproceedings") ||
            (props[i].Name == "institution" && type == "techreport"))
        {
            props[i].Value = this.RemoveBraces(str);
            publication = props[i].Value;
        }
        else
            props[i].Value = this.RemoveBraces(str);            
    }                       
    
    if (publication != null)
    {
        props[props.length] = new Property();
        props[props.length - 1].Name = "publication";
        props[props.length - 1].Value = publication;         
    }
    
    return props;
}

/*
    Authors are expected to be seperated by 'and',
    There two variations in specifying authors, 'Surname, Firstname' or
    'Firstname Surname'. This method converts everything to the latter format.
    At the end all 'and's are converted to ',' except for the last one.
    
    '~' character in author name is typcially used as a short space, we simply
    replace it with space here.   
*/
Bibtex.prototype.NormalizeAuthors = function(str)
{
    var re = /and/g;
    var arr = str.split(re);
    
    for (var i = 0; i < arr.length; i++)
    {
        var author = arr[i].toString().trim();
        var names = author.split(/,/);          //Split over the first ',' if one exists
        if (names.length > 1)
            arr[i] = names[1].toString().trim() + " " + names[0].toString().trim();
    }

    out = "";
    for (var i = 0; i < arr.length - 2; i++)
        out += arr[i].toString().trim() + ", " ;
    
    if (arr.length > 1)
        out += arr[arr.length - 2].toString().trim() + " and ";
    out += arr[arr.length - 1].toString().trim();
    
    out = out.replace(/~/g, " ");   
    
    return out;
}

Bibtex.prototype.RemoveBraces = function(str)
{
    var re = /[\{\}]/g;
    
    return str.replace(re, "");
}
/*
    Converts the string to lower case except for characters between curly braces, 
    capitalize the frist character and remove curly braces.
*/
Bibtex.prototype.ToLowerCaseWithBraces = function(str)
{
    var out = "" ;
    var start = 0;
    var idx = str.indexOf("{");
    while(idx != -1)
    {
        out += str.substr(start, idx - start).toLowerCase();
        start = idx + 1;
        idx = str.indexOf("}", start);
        if (idx == -1)
            break;          //Unbalanced {} pair, do not continue
            
        out += str.substr(start, idx - start);            //Keep the charcter case
            
        start = idx + 1;
        idx = str.indexOf("{", start);
    }                    
    
    out = out + str.substr(start, str.length - start).toLowerCase();
    out = out.charAt(0).toUpperCase() + out.substr(1, out.length);
    return out;        
}
       
Bibtex.prototype.ParseProperties = function(entry, str)
{
    //get the entry name
    var re=/,/;
    var m = str.match(re);
    var lastIndex = m[0].length + m.index;
    entry.Name = str.substr(0,lastIndex - 1).trim();
    str = str.substr(lastIndex, str.length - lastIndex + 1);
    
    //Split the string on ',' characters but the problem is ',' may appear inside qoutation marks or {}, in
    // which case it needs to skipped.
    var arr = str.split(/,/g);
    var out = new Array();
    
    var qoutes = 0;
    var out_idx = 0;
    var join = false;
    for (var i = 0; i < arr.length; i++)
    {
        // This is a hack because I am counting either { or } as a quotation mark instead of properly checking if , is enclosed between {}.
        arr_qoutes = arr[i].match(/\"|{|}/g);
        if (arr_qoutes != null) qoutes += arr_qoutes.length;
        
        if (qoutes % 2 == 0 && join == false)
            out[out_idx++] = arr[i];
        else if (join == false)
        {
            out[out_idx] = arr[i] + "," ;        //prepare to join with the next one and put "," back
            join = true;
        }
        else 
        {
            if (qoutes % 2 == 0)                //Last join
            {
                out[out_idx] += arr[i];
                out_idx++;           
                join = false;
            }
            else
                out[out_idx] += arr[i] + ",";
        }
    }
    
    arr = out;
       
    entry.bibtexHTML = "@" + entry.Type + "{" + entry.Name + ",<br/>";
    var props = new Array();

    for (i = 0; i < arr.length; i++)
    {    
        str = new String(arr[i]);
        if (str.trim() == "")
            continue;
        m = str.split(/=/);
        
        props[i] = new Property();
        
        props[i].Name = m[0].trim();
        props[i].Value = m[1].trim();
        if (props[i].Value.charAt(0) == "\"" || props[i].Value.charAt(0) == "{")            //Remove qoutation marks, or {} pair and trim
        {
            props[i].Value = props[i].Value.substring(1,props[i].Value.length - 1);
            props[i].Value = props[i].Value.trim();
        }
        else                                            //Try to resolve the variable
        {
            var value = this.ResolveVariable(props[i].Value);
            if (value != null) props[i].Value = value;
        }
        
        if (props[i].Name != "link" && props[i].Name != "linkname" && props[i].Name != "icon" && props[i].Name != "selected")
            entry.bibtexHTML += "&nbsp;&nbsp;&nbsp;&nbsp;" + str + ",<br/>";
    }
    
    entry.bibtexHTML += "};";
               
    return props;
}

Bibtex.prototype.ResolveVariable = function(str)
{
    if (str == "jan")
        return "01";
    else if (str == "feb")
        return "02";
    else if (str == "mar")
        return "03";
    else if (str == "apr")
        return "04";
    else if (str == "may")
        return "05";
    else if (str == "jun")
        return "06";
    else if (str == "jul")
        return "07";
    else if (str == "aug")
        return "08";
    else if (str == "sep")
        return "09";
    else if (str == "oct")
        return "10";
    else if (str == "nov")
        return "11";
    else if (str == "dec")
        return "12";
    else
        return null;
}

/*
    A useful addition to the built-in String class.
*/
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }

