#! /usr/bin/python

# COPYRIGHT 2001 KEN SUGINO
# NEEDS
# 1) Python (www.python.org) (tested version 1.5.2)
# 2) wxPython (www.wxpython.org) (tested version:2.2.5)
# USAGE
# 0) change first line of this file appropriately, i.e. /usr/bin/python->actual path to python
# 1) give executable property to this file: chmod +x pmBib
# 2) ./pmBIb &
# * optionally set BROWSER_CMD below to appropriate one url will be inserted to %s

#-------- PREFERENCES ----------------
MAXRETRIEVE = 200
BROWSER_CMD = "netscape -remote 'openURL(%s)'"

WIN_WIDTH = 850
WIN_HEIGHT = 750

COL_NUM_W = 35
COL_YEAR_W = 45
COL_TITLE_W = 370
COL_AUTHOR_W = 200
COL_JOURNAL_W = 200

#-------- HELP TEXT ------------------

pmHelpText ="""
<H2>Pub Med Search Help</H2>
<HR>
See <a href="http://www.ncbi.nlm.nih.gov:80/entrez/query/static/help/pmhelp.html">
NCBI PubMed help page</a> for the details.

<H3>Field Specification</H3>
example: Einstein A[au] : specifies author fields [au]

<H3>Boolean Operator</H3>
AND, OR : use parenthesis to create logical block

<H3>Date Range</H3>
example: 1999:2001[edat] : entrez date from 1999 to 2001

"""

#-------- PROGRAM CODE ---------------
from wxPython.wx import *
from wxPython.html import *
from wxPython.lib.anchors import LayoutAnchors
import urllib,string,os,re


TEMPLATE_NEW = """% bibtex file made by pmBib\n"""

app = None

class pmApp(wxApp):
    def OnInit(self):
        global app
        app = self
        self.frame = pmFrame(NULL,-1,"PubMedQueryToBibTex",app=self)
        self.frame.Show(true)
        self.SetTopWindow(self.frame)
        return true

            
ID_ABOUT = 101
ID_EXIT = 102
ID_OPEN = 103
ID_CLOSEFILE = 104
ID_NEW = 105

ID_TERM = 501
ID_FIND = 502

ID_SPLITTER = 1000
ID_LIST = 1001

ID_NOTE = 200
ID_SEARCH_PANE = 201
ID_HELP_PANE = 202

ID_OFFSET = 10000

class pmFrame(wxFrame):
    def __init__(self,parent,ID,title,app):
        wxFrame.__init__(self,parent,ID,title,
                         wxDefaultPosition,wxSize(WIN_WIDTH,WIN_HEIGHT))
        self.CreateStatusBar()
        self.dirname = os.path.abspath('.')
        self.filename = ''
        self.files = []
        self.panes = []
        self.app = app
        
        self.SetAutoLayout(true)

        # query term input text and button
        self.term  = wxTextCtrl(self, ID_TERM, "", wxPoint(0,0), wxSize(WIN_WIDTH-60, 20))
        self.term.SetConstraints(LayoutAnchors(self.term,true,true,true,false))
        EVT_CHAR(self.term, self.OnEvtChar)
        self.findbtn = wxButton(self, ID_FIND,"find",wxPoint(WIN_WIDTH-55,0),wxSize(50,20))
        self.findbtn.SetConstraints(LayoutAnchors(self.findbtn,false,true,true,false))
        EVT_BUTTON(self, ID_FIND, self.OnFind)
        
        # tabs
        self.nb = wxNotebook(self, ID_NOTE,pos=wxPoint(0,22),size=wxSize(WIN_WIDTH,WIN_HEIGHT-50))
        self.nb.SetConstraints(LayoutAnchors(self.nb,true,true,true,true))
        
        # panes
        # HELP
        self.helppage = pmHtmlWindow(self.nb,wxNewId())
        self.helppage.SetConstraints(LayoutAnchors(self.helppage,true,true,true,true))

        helptext = """<HTML><BODY TEXT="#000000" BGCOLOR="#FFFFFF">\n""" + pmHelpText
        helptext = helptext + '<H3>Fields</H3><TABLE border=1 cellpadding=2 cellspacing=0>\n'
        keys = med2bib.keys()
        keys.sort()
        for i in range(len(keys)):
            key = keys[i]
            if i % 2 == 0:
                rows = "<TR>"
                rowe = "</TR>"
            else:
                rows = ""
                rowe = ""
            helptext = helptext+"""%s<TD width=70><b>%s</b></TD><TD>%s</TD>%s\n
            """ % (rows,key,med2bib[key],rowe)
        helptext = helptext + '</TABLE></BODY></HTML>'
        self.helppage.SetPage(helptext)
        self.nb.AddPage(self.helppage,"Help")
        # SEARCH RESULT
        self.searchresult = pmPanel(self.nb,ID_SEARCH_PANE,"SearchResult",
                                    pos= wxPoint(0,0),size=self.nb.GetSize())
        self.searchresult.SetConstraints(LayoutAnchors(self.searchresult,true,true,true,true))
        self.nb.AddPage(self.searchresult,"SearchResult")
        
        # menu
        menu = wxMenu()
        menu.Append(ID_ABOUT,"&About","More info about this program")
        menu.AppendSeparator()
        menu.Append(ID_NEW, "&New\tAlt-N","Make New .bib file")
        menu.Append(ID_OPEN,"&Open\tAlt-O","Open .bib file")
        menu.Append(ID_CLOSEFILE, "&Close\tAlt-W", "Close file")
        menu.Append(ID_EXIT,"E&xit\tAlt-X","Terminate this program")
        menubar = wxMenuBar()
        menubar.Append(menu,"&File")
        self.SetMenuBar(menubar)

        EVT_MENU(self, ID_ABOUT, self.OnAbout)
        EVT_MENU(self, ID_EXIT, self.OnExit)
        EVT_MENU(self, ID_NEW, self.OnNew)
        EVT_MENU(self, ID_OPEN, self.OnOpen)
        EVT_MENU(self, ID_CLOSEFILE, self.OnCloseFile)

    def OnEvtChar(self, event):
        if event.GetKeyCode() == WXK_RETURN:
            self.OnFind(event)
        else:
            event.Skip()
        
    def OnAbout(self, event):
        msg = """This program retrieves reference records
        from PubMed and export them as bibtex records"""
        dlg = wxMessageDialog(self, msg, "About this program",
                              wxOK|wxICON_INFORMATION)

        dlg.ShowModal()
        dlg.Destroy()

    def OnExit(self, event):
        self.Close(true)

    def OnNew(self, event):
        dlg = wxFileDialog(self, "Save as", ".", "", "*.bib", wxSAVE)
        if dlg.ShowModal() == wxID_OK:
            fname = dlg.GetFilename()
            fpath = os.path.join(dlg.GetDirectory(),fname)
            open(fpath,'w').write(TEMPLATE_NEW)
            self.open_bibtex(fpath,fname)
        dlg.Destroy()

        
    def OnOpen(self, event):
        dlg = wxFileDialog(self,"Choose a file", self.dirname,"","*.bib",wxOPEN)
        if dlg.ShowModal() == wxID_OK:
            fname = dlg.GetFilename()
            fpath = os.path.join(dlg.GetDirectory(),fname)
            self.open_bibtex(fpath,fname)
        dlg.Destroy()

    def open_bibtex(self, fpath, fname):
        bibs = parse_bibtex(open(fpath,'r').readlines())
        try:
            idx = self.files.index(fpath)
            self.panes[idx].set_bibrec(bibs)
            self.nb.SetSelection(idx+2)
        except:
            pane = pmPanel(self.nb,wxNewId(),fname,wxPoint(0,0),self.nb.GetSize())
            pane.SetConstraints(LayoutAnchors(pane,true,true,true,true))
            pane.set_bibrec(bibs)
            self.files.append(fpath)
            self.panes.append(pane)
            self.nb.AddPage(pane,fname)
            self.nb.SetSelection(self.nb.GetPageCount()-1)
        
    def OnCloseFile(self, event):
        idx = self.nb.GetSelection()
        if idx < 2:
            return 0
        pane = self.nb.GetPage(idx)
        self.nb.RemovePage(idx)
        pane.Destroy()
        del self.files[idx-2]
        del self.panes[idx-2]

    
    def OnFind(self, event):
        term = self.term.GetValue()
        if term == '':
            return 0
        bibs = get_bibrec(term,MAXRETRIEVE)
        self.searchresult.set_bibrec(bibs)
        self.nb.SetSelection(1)
        self.SetStatusText("%d hit(s)" % (len(bibs),))
        
#         hit = get_num(term)
#         msg = "%d hit(s): Retrieve or Cancel?" % hit
#         dlg = wxMessageDialog(self, msg, "hits", wxOK|wxCANCEL|wxICON_QUESTION)
#         if dlg.ShowModal() == wxID_OK:# retrieve
#             bibs = get_bibrec(term, hit)
#             self.searchresult.set_bibrec(bibs)
#             self.nb.SetSelection(1)
#         dlg.Destroy()
            
class pmHtmlWindow(wxHtmlWindow):
    def __init__(self, parent, id):
        wxHtmlWindow.__init__(self, parent, id)

    def OnLinkClicked(self, linkinfo):
        link = string.lower(linkinfo.GetHref())
        #print linkinfo.GetHref(),link
        cmd = BROWSER_CMD % link
        os.system(cmd)
        
    
class pmPanel(wxSplitterWindow):
    def __init__(self,parent,ID,title,pos,size):
        wxSplitterWindow.__init__(self,parent,ID,pos,size,wxSP_3D,title)

        self.bibs = None
        #self.text = wxTextCtrl(self,1,style=wxTE_MULTILINE)
        self.text = pmHtmlWindow(self,wxNewId())

        listid = wxNewId()
        self.list = wxListCtrl(self,listid,style=wxLC_REPORT|wxSUNKEN_BORDER)
        self.list.InsertColumn(0,"#")
        self.list.InsertColumn(1,"Year")
        self.list.InsertColumn(2,"Title")
        self.list.InsertColumn(3,"Author")
        self.list.InsertColumn(4,"Journal")

        self.list.SetColumnWidth(0,COL_NUM_W)
        self.list.SetColumnWidth(1,COL_YEAR_W)
        self.list.SetColumnWidth(2,COL_TITLE_W)
        self.list.SetColumnWidth(3,COL_AUTHOR_W)
        self.list.SetColumnWidth(4,COL_JOURNAL_W)

        self.list.SetToolTip(wxToolTip(
            "Shift/Ctrl+Click:multiple selection Right+Click:append to bib file"))
        
        self.SetMinimumPaneSize(100)
        self.SplitHorizontally(self.list,self.text)
        self.SetSashPosition(200)

        EVT_LIST_COL_CLICK(self,listid, self.OnColClick)
        EVT_LIST_ITEM_SELECTED(self, listid, self.OnItemSelected)
        EVT_RIGHT_DOWN(self.list, self.OnRightDown)

    def OnColClick(self, event):
        self.col = event.m_col
        self.list.SortItems(self.ColumnSorter)
 
    def ColumnSorter(self, i1, i2):
        if self.col == 0:
            return i1 - i2
        key = ['year','title','author','journal'][self.col-1]
        item1 = self.bibs[i1][key]
        item2 = self.bibs[i2][key]
        if item1 == item2:  return 0
        elif item1 < item2: return -1
        else:               return 1

    def set_bibrec(self, bibs):
        self.bibs = bibs
        #print "# hits = ", len(bibs)
        self.list.DeleteAllItems()
        for i in range(len(bibs)):
            self.list.InsertStringItem(i,"%d" % i)
            if bibs[i].has_key('year'):
                self.list.SetStringItem(i,1,bibs[i]['year'])
            if bibs[i].has_key('title'):
                self.list.SetStringItem(i,2,bibs[i]['title'])
            if bibs[i].has_key('author'):
                self.list.SetStringItem(i,3,bibs[i]['author'])
            if bibs[i].has_key('journal'):
                self.list.SetStringItem(i,4,bibs[i]['journal'])
            self.list.SetItemData(i,i)
            #print '#',i, bibs[i]['year'], bibs[i]['title'],bibs[i]['journal']

    def append_bibrec(self,bibs):
        num = self.list.GetItemCount()
        self.bibs.extend(bibs)
        for i in range(len(bibs)):
            self.list.InsertStringItem(i+num,"%d" % (i+num))
            if bibs[i].has_key('year'):
                self.list.SetStringItem(i+num,1,bibs[i]['year'])
            if bibs[i].has_key('title'):
                self.list.SetStringItem(i+num,2,bibs[i]['title'])
            if bibs[i].has_key('author'):
                self.list.SetStringItem(i+num,3,bibs[i]['author'])
            if bibs[i].has_key('journal'):
                self.list.SetStringItem(i+num,4,bibs[i]['journal'])
            self.list.SetItemData(i,i)
            self.list.SetItemState(i+num, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
        self.list.EnsureVisible(num+len(bibs)-1)
        
    def OnItemSelected(self, event):
        self.currentItem = event.m_itemIndex
        #print "selected:",self.currentItem
        if self.bibs != None:
            #bibrec = bibtex_record(self.bibs[self.currentItem])
            bibrec = self.bibs[self.currentItem]
            html = bibrec_to_html(bibrec)
            #self.text.SetValue(bibrec)
            self.text.SetPage(html)

    def OnRightDown(self, event):
        sel = self.get_selection()
        if len(sel)==0:
            return 0
        menu = wxMenu()
        global app
        frame = app.frame
        nb = frame.nb
        me =  nb.GetSelection() -2
        if nb.GetPageCount() == 2: return 0
        if (me > -1) and (nb.GetPageCount()==3): return 0
        for i in range(nb.GetPageCount() - 2):
            if me == i: continue
            menu.Append(ID_OFFSET+i,"append to:"+frame.files[i])
            EVT_MENU(self,ID_OFFSET+i,self.OnAppendTo)
        self.PopupMenu(menu, wxPoint(event.GetX(),event.GetY()))
        menu.Destroy()
        #event.Skip()

    def get_bibrec(self, i):
        return self.bibs[i]
    
    def OnAppendTo(self, event):
        idx = event.GetId()-ID_OFFSET
        global app
        frame = app.frame
        nb = frame.nb
        target = frame.files[idx]
        bibs = self.bibs
        sel = map(self.get_bibrec, self.get_selection())
        if len(sel)==0:return 0
        # append to file
        bibtex = string.join(map(bibrec_to_bibtex,sel),'\n')
        open(target,'a').write(bibtex)
        # append to display
        pane = frame.panes[idx]
        pane.append_bibrec(sel)
        nb.SetSelection(idx+2)


    def get_selection(self):
        sel = []
        item = -1
        while 1:
            item = self.list.GetNextItem(item,
                                         wxLIST_NEXT_ALL,
                                         wxLIST_STATE_SELECTED);
            if ( item == -1 ):
                break
            sel.append(item)
        return sel

def parse_bibtex(lines,head=re.compile(r'^@([A-Za-z ]+){(.+),(.*)$'),
                 fieldhead=re.compile(r'^([^=]+)=\s*(["{]*)(.*)$'),
                 endbrace=re.compile(r'^\s*}\s*$')):
    bibs = []
    curbib = None
    curfld = None
    curdata = ''
    for line in lines:
        # ignore empty line, brace, comment
        if line == '':
            continue
        if endbrace.match(line):
            continue
        if line[0] in ['#', '%']:
            continue
        # new record?
        m = head.match(line)
        if m:
            # save previous
            if curbib != None:
                if curfld != None:
                    curbib[curfld] = balance(curdata)
                bibs.append(curbib)
            # setup new
            curbib={}
            curbib['type'] = string.strip(m.group(1))
            curbib['label'] = string.strip(m.group(2))
            curdata = ''
            curfld = None
            continue
        # new field?
        m = fieldhead.match(line)
        if m and curbib:
            if (m.group(2) != None) or (len(string.split(m.group(3),' ')) ==1):
                # save previous field
                if curfld != None:
                    curbib[curfld] = balance(curdata)
                # setup new
                curfld = string.strip(m.group(1))
                curdata = m.group(3)
                continue
                    
        # body of field
        if curfld != None:
            curdata = curdata + ' '+ string.strip(line)
    # save last entry
    if curbib != None:
        if curfld != None:
            curbib[curfld] = balance(curdata)
        bibs.append(curbib)
    return bibs


def balance(text):
    text = trim_trailing_camma(text)
    text = balance_braces(text)
    text = balance_quotes(text)
    return text

def balance_braces(text):
    if len(text)==0:
        return text
    left = string.count(text,"{")
    right = string.count(text,"}")
    str = text
    if left > right:
        str = text + "}"*(left-right)
    if right > left:
        str = "{"*(right-left)+text
    if str[0] == '{' and str[-1]=='}':
        str =string.strip(str[1:-1])
    return str

def balance_quotes(text):
    if text=='':
        return ''
    if text[0] == '"' and text[-1] != '"':
        return text+'"'
    if text[-1] == '"' and text[0] != '"':
        return '"'+text
    return text

def trim_trailing_camma(text):
    text = string.strip(text)
    if text == '':
        return ''
    if text[-1] == ',':
        return text[:-1]
    return text

#dbs = ['PubMed','Genome','Nucleotide','OMIM','PopSet','Protein','Structure']
#formats = ['Summary','Brief','Abstract','Citation','MEDLINE','ASN.1','XML','Text']
#cmds = ['Search','Retrieve','Link','Text']

med2bib = {
    'AB' : 'abstract',
    'AD' : 'address',
    'AID': 'ariticle_id',
    'AU' : 'author',
    'CI' : 'copyright',
    'CIN': 'comment_in',
    'CN' : 'collective_name',
    'CON': 'comment_on',
    'CY' : 'country',
    'DA' : 'date_created',
    'DCOM': 'date_completed',
    'DEP' : 'date_of_electronic_publication',
    'DP' : 'date',
    'EDAT': 'entrez_date',
    'EIN': 'erratum_in',
    'GS' : 'gene_symbol',
    'ID' : 'id',
    'IP' : 'number',
    'IS' : 'ISSN',
    'JC' : 'journal_code',
    'JID': 'journal_id',
    'LA' : 'language',
    'LR' : 'last_revision_date',
    'MH' : 'keywords',
    'MHDA': 'MeSH_date',
    'PG' : 'pages',
    'PHST': 'publication_history',
    'PMID': 'PMID',
    'PS' : 'personal_name_as_subject',
    'PST': 'publication_status',
    'PT' : 'publication_type',
    'RF' : 'number_of_ref',
    'RIN': 'retraction_in',
    'RN' : 'EC_RN_number',
    'ROF': 'retraction_of',
    'RPF': 'republished_from',
    'RPI': 'republished_in',
    'SB' : 'journal_subset',
    'SI' : 'secondary_source_id',
    'SO' : 'source',
    'TA' : 'journal',
    'TI' : 'title',
    'TT' : 'transliterated_title',
    'UI' : 'medlineref',
    'UIN': 'updated_in',
    'UOF': 'update_of',
    'URL': 'url',
    'URLF': 'url_fulltext',
    'URLS': 'url_summary',
    'VI' : 'volume',    
}


def get_url(term,base = "http://www.ncbi.nlm.nih.gov/entrez/query.fcgi",
            cmd='Search',db='PubMed',format='MEDLINE',maxnum=1):
    term = urllib.quote_plus(string.upper(term))
    url = """%s?cmd=%s&db=%s&term=%s&doptcmdl=%s&dispmax=%d
    """ % (base,cmd,db,term,format,maxnum)
    return url
        
def get_num(term):
    numSt = '<td align="center" width="50%">Items 1-1 of'
    numEd = '</td>'
    result = urllib.urlretrieve(get_url(term,format='Summary'))
    try:
        html = open(result[0]).read()
        st = string.index(html,numSt)
        st = st + len(numSt)
        ed = string.index(html,numEd,st+1)
    except:
        return 0
    urllib.urlcleanup()
    return string.atoi(html[st:ed])

def get_medline(term,maxnum=100):
    """ returns list of MEDLINE records"""
    result = urllib.urlretrieve(get_url(term,maxnum=maxnum))
    html = open(result[0]).read()
    tmplist = string.split(html,"<pre>")[1:]
    records  = map(lambda x:string.split(x,"</pre>")[0], tmplist)
    return  map(parse_medline,records)

def get_bibtex(term,maxnum=100):
    bibtex = map(bibrec_to_bibtex, get_bibrec(term,maxnum))
    return string.join(bibtex,'\n')

def get_bibrec(term, maxnum=100):
    return  map(medline_to_bibrec,get_medline(term,maxnum))
    
def parse_medline(text,header = re.compile ('^(\w\w[\w ][\w ])- (.*)$'),
                  body   = re.compile ('^      (.*)$')):
    lines = string.split(text,'\n')
    rec = {} # keeps paresed
    key = None
    data = ''
    for line in lines:
        m = header.match(line)
        if m:
            if key: # save previous 
                if rec.has_key(key):
                    rec[key].append(data)
                else:
                    rec[key] = [data]
            key = string.strip(m.group(1))
            data = string.rstrip(m.group(2))
        else:
            m = body.match(line)
            if m:
                data = data + ' '+string.rstrip(m.group(1))
    if key: # save last
        if rec.has_key(key):
            rec[key].append(data)
        else:
            rec[key] = [data]
    return rec

def reverse_author(author):
    tmp = string.split(author,' ')
    tmp.reverse()
    return string.join(tmp,' ')

def medline_to_bibrec(rec):
    keys = rec.keys()
    bib = {}
    # type
    bib['type'] = 'article'
    # label
    year = 'unknown'
    if rec.has_key('DP'):
        date = rec['DP'][0]
        tmp = string.split(date,' ')
        if len(tmp)==2:
            year,month = tmp
        else:
            year = tmp[0]
            month = ''
    elif rec.has_key('EDAT'):
        year = rec['EDAT'][0][:4]
    bib['year'] = year
    if rec.has_key('PMID'):
        id = rec['PMID'][0]
    else:
        id = rec['UI'][0]
    label = string.join(string.split(rec['AU'][0],' '),'')+year[-2:]+':'+id
    bib['label'] = label
    # author
    au = rec['AU']
    au = map(reverse_author,au)
    bib['author'] = '{'+string.join(au,'} and {')+'}'
    keys.remove('AU')
    # title
    bib['title'] = rec['TI'][0]
    keys.remove('TI')
    # journal
    bib['journal'] = rec['TA'][0]
    keys.remove('TA')
    # year
    bib['year'] = year
    # the rest
    for key in keys:
        if med2bib.has_key(key):
            bib[med2bib[key]] = rec[key][0]
        else:
            bib['medline_'+key] = rec[key][0]
    return bib

def bibrec_to_html(bibrec):
    html = "<HTML><BODY>\n<TABLE border=1 cellpadding=2 cellspacing=0>"
    keys = bibrec.keys()
    ths = "<TD width=150 valign=TOP align=LEFT><b>"
    the = "</b></TD>"
    first = ['label','year','author','title','abstract']
    urls = ['url','url_fulltext','url_summary']
    for key in first:
        try:
            html = html+"<TR>%s%s%s<TD>%s</TD></TR>\n" % (ths,key,the,bibrec[key])
            keys.remove(key)
        except:
            pass

    for key in urls:
        try:
            html=html+"""<TR>%s%s%s<TD><a href=%s>%s</a></TD></TR>\n
            """ % (ths,key,the,bibrec[key],bibrec[key])
            keys.remove(key)
        except:
            pass
    # the rest
    for key in keys:
        html = html + """<TR>%s%s%s<TD>%s</TD></TR>\n""" % (ths,key,the, bibrec[key])

    html = html + "</TABLE></BODY></HTML>"
    return html

def bibrec_to_bibtex(bib):
    keys = bib.keys()
    str = '@'+bib['type']+'{'+bib['label']+',\n'
    keys.remove('type')
    keys.remove('label')
    for key in ['author','title','journal','year']:
        str = str+key+' = {'+bib[key]+'},\n'
        keys.remove(key)
    for key in keys:
        str = str+key+' = {'+bib[key]+'},\n'
    str = str + '}\n\n'
    return str


if __name__ == '__main__':
    app = pmApp(0)
    app.MainLoop()

#     import sys
#     if len(sys.argv) == 3:
#         switch,term = sys.argv[1:3]
#         num = 100
#     elif len(sys.argv) == 4:
#         switch,term,num = sys.argv[1:4]
#     else:
#         sys.stderr.write( "usage: pubmed.py -n|q term [maxnum]\n")
#         sys.exit(0)
#     if switch == '-n':
#         num = get_num(term)
#         sys.stdout.write("%d\n" % num)
#         sys.exit(0)
#     elif switch == '-q':
#         bib = get_bibtex(term,num)
#         sys.stdout.write(bib)
#         sys.exit(0)
#     else:
#         sys.stderr.write("usage: pubmed.py -n|q term [maxnum]\n")
#         sys.exit(0)
