#!/usr/bin/env python

import xml.parsers.expat
import sys
import string
import mx.DateTime
import zlib

try:
	import psyco
	psyco.full()
except:
	print "No psyco acceleration."

"""
BIND Parser version 3. Uses just expat, very fast and parse a lot of stuff. The best implementation. It also contains the necessary classes and codes for insertion into mysql.
"""	

tables = {}

tables["bind_interactions"] = [
	["iid", "int(10) unsigned", "not null primary key", ""],
	["bindid", "int(10) unsigned", "not null", "yes"],
	["updated", "date", "not null", "yes"], # the most recent updated date
	["source", "longblob", "not null", ""],
	["description", "longtext", "", ""],
	]

tables["bind_molecules"] = [
	["iid", "int(10) unsigned", "not null", "yes"],
	["mid", "int (10) unsigned", "not null primary key", ""],
	["type", "varchar(255)", "not null", "yes"],
	["name", "varchar(255)", "not null", "yes"],
	["taxid", "int(10) unsigned", "", "yes"],
	["description", "longtext", "", ""],
	["which", "char(1)", "not null", "yes"], # either a or b
	]

tables["bind_molecule_ids"] = [
	["mid", "int(10) unsigned", "not null", "yes"],
	["db", "varchar(255)", "not null", "yes"],
	["accession", "varchar(255)", "not null", "yes"],
	]

tables["bind_molecule_links"] = [
	["mid", "int(10) unsigned", "not null", "yes"],
	["db", "varchar(255)", "not null", "yes"],
	["strid", "varchar(255)", "", "yes"],
	["intid", "varchar(255)", "", "yes"],
	]
	
tables["bind_molecule_aliases"] = [
	["mid", "int(10) unsigned", "not null", "yes"],
	["alias", "varchar(255)", "not null", "yes"],
	]

tables["bind_experiments"] = [
	["iid", "int(10) unsigned", "not null", "yes"],
	["eid", "int(10) unsigned", "not null primary key", ""],
	["id", "int(10) unsigned", "not null", "yes"],
	["general", "varchar(255)", "not null", "yes"],
	["system", "varchar(255)", "not null", "yes"],
	["description", "longtext", "", ""],
	]

tables["bind_experiment_sites"] = [
	["eid", "int(10) unsigned", "not null", "yes"],
	["sid", "int(10) unsigned", "not null", "yes"],
	["which", "char(1)", "not null", "yes"],
	]

tables["bind_experiment_pmids"] = [
	["eid", "int(10) unsigned", "not null", "yes"],
	["pmid", "int(10) unsigned", "not null", "yes"],
	["type", "varchar(255)", "not null", "yes"],
	]

tables["bind_sites"] = [
	["iid", "int(10) unsigned", "not null", "yes"],
	["sid", "int(10) unsigned", "not null primary key", ""],
	["id", "int(10) unsigned", "not null", "yes"],
	["which", "char(1)", "not null", "yes"],
	["description", "longtext", "", ""],
	]

tables["bind_site_positions"] = [
	["sid", "int(10) unsigned", "not null", "yes"],
	["start", "int(10) unsigned", "not null", "yes"],
	["end", "int(10) unsigned", "not null", "yes"],
	]

tables["bind_site_points"] = [
	["sid", "int(10) unsigned", "not null", "yes"],
	["point", "int(10) unsigned", "not null", "yes"],
	]

class Molecule:
	def __init__(self):
		self.type = None
		self.ids = []
		self.name = None
		self.aliases = []
		self.taxid = None
		self.links = []
		self.description = None
	def __str__(self, prefix=""):
		text = []
		text.append("%sName        : %s" % (prefix, self.name))
		text.append("%sType        : %s" % (prefix, self.type))
		text.append("%sIDs         : %s" % (prefix, self.ids))
		text.append("%sTax         : %s" % (prefix, self.taxid))
		text.append("%sLinks       : %s" % (prefix, self.links))
		text.append("%sDescription : %s" % (prefix, self.description))
		return string.join(text, "\n")
	def loadIntoDatabase(self, ms, iid, which):
		# get the latest mid
		mid = ms.getNextIdField("bind_molecules", "mid")
		# insert into molecules
		ms.execute("insert into bind_molecules (iid, mid, type, name, taxid, which, description) values(%s, %s, %s, %s, %s, %s, %s)", (iid, mid, self.type, self.name, self.taxid, which, self.description))
		# insert into ids
		for id in self.ids:
			ms.execute("insert into bind_molecule_ids (mid, db, accession) values(%s, %s, %s)", (mid, id[0], id[1]))
		# insert into links
		for link in self.links:
			ms.execute("insert into bind_molecule_links (mid, db, intid, strid) values(%s, %s, %s, %s)", (mid, link[0], link[1], link[2]))
		# insert into aliases
		for alias in self.aliases:
			ms.execute("insert into bind_molecule_aliases (mid, alias) values(%s, %s)", (mid, alias))

class Experiment:
	def __init__(self):
		self.id = None
		self.general = None
		self.system = None
		self.description = None
		self.pmids = []
		self.asites = [] # list of mol a site ids
		self.bsites = [] # list of mol b site ids
	def __str__(self, prefix=""):
		text = []
		text.append("%sID          : %s" % (prefix, self.id))
		text.append("%sGeneral     : %s" % (prefix, self.general))
		text.append("%sSystem      : %s" % (prefix, self.system))
		text.append("%sDescription : %s" % (prefix, self.description))
		text.append("%sPMIDs       : %s" % (prefix, self.pmids))
		text.append("%sSite A      : %s" % (prefix, string.join(self.asites, ",")))
		text.append("%sSite B      : %s" % (prefix, string.join(self.bsites, ",")))
		return string.join(text, "\n")
	def getPmids(self):
		results = []
		for pmid in self.pmids:
			if pmid[0] == "support":
				results.append(pmid[1])
		return results
	def loadIntoDatabase(self, ms, iid):
		# get the latest eid
		eid = ms.getNextIdField("bind_experiments", "eid")
		# insert into experiments
		ms.execute("insert into bind_experiments (iid, eid, id, general, system, description) values(%s, %s, %s, %s, %s, %s)", (iid, eid, self.id, self.general, self.system, self.description))
		# insert into sites
		for site in self.asites:
			ms.execute("insert into bind_experiment_sites (eid, sid, which) values(%s, %s, %s)", (eid, site, "a"))
		for site in self.bsites:
			ms.execute("insert into bind_experiment_sites (eid, sid, which) values(%s, %s, %s)", (eid, site, "b"))
		# insert pmids
		for pmid in self.pmids:
			if pmid[1] == None:
				continue
			ms.execute("insert into bind_experiment_pmids (eid, pmid, type) values(%s, %s, %s)", (eid, pmid[1], pmid[0]))

class Site:
	def __init__(self):
		self.id = None
		self.starts = []
		self.ends = []
		self.points = []
		self.description = None
	def __str__(self, prefix=""):
		text = []
		text.append("%sID          : %s" % (prefix, self.id))
		text.append("%sStarts      : %s" % (prefix, self.starts))
		text.append("%sEnds        : %s" % (prefix, self.ends))
		text.append("%sDescription : %s" % (prefix, self.description))
		return string.join(text, "\n")
	def loadIntoDatabase(self, ms, iid, which):
		# get latest sid
		sid = ms.getNextIdField("bind_sites", "sid")
		# insert into sites
		ms.execute("insert into bind_sites (iid, sid, id, which, description) values(%s, %s, %s, %s, %s)", (iid, sid, self.id, which, self.description))
		# insert into positions
		if len(self.starts) != len(self.ends):
			print "Number of starts and ends do not tally."
			sys.exit()
		for i in range(len(self.starts)):
			ms.execute("insert into bind_site_positions (sid, start, end) values(%s, %s, %s)", (sid, self.starts[i], self.ends[i]))
		# into into points
		for point in self.points:
			ms.execute("insert into bind_site_points (sid, point) values(%s, %s)", (sid, point))

class Interaction:
	def __init__(self):
		self.bid = None
		self.description = ""
		self.date = None
		self.updates = [] # last updated date as a string in YYYYMMDD in a list
		self.a = Molecule()
		self.b = Molecule()
		self.experiments = []
		self.asites = []
		self.bsites = []
		self.xml = ""
	def __str__(self):
		text = []
		text.append("BIND ID     : %s" % self.bid)
		text.append("Description : %s" % self.description)
		text.append("Date        : %s" % (self.date))
		text.append("Updates     : %s" % (self.updates))
		text.append("Latest      : %s" % (self.getLatestDate()))
		text.append("Mol A:\n%s" % self.a.__str__(prefix="\t"))
		text.append("Mol B:\n%s" % self.b.__str__(prefix="\t"))
		for experiment in self.experiments:
			text.append("Experiment:\n%s" % experiment.__str__(prefix="\t"))
		for site in self.asites:
			text.append("Site A:\n%s" % site.__str__(prefix="\t"))
		for site in self.bsites:
			text.append("Site B:\n%s" % site.__str__(prefix="\t"))
		return string.join(text, "\n")
	def getLatestDate(self):
		"""
		Method to return a mx.DateTime instance of the latest date.
		"""
		mdate = max(map(int, ([self.date] + self.updates)))
		return mx.DateTime.strptime(str(mdate), "%Y%m%d")
	def laterThanDatabase(self, ms):
		"""
		Method to check whether this record is more updated than the one in the database.
		"""
		if len(ms.get("select * from bind_interactions where bindid = %s and updated < %s", (self.bid, self.getLatestDate()))) > 0:
			return 1
		else:
			return 0
	def loadIntoDatabase(self, ms):
		# get the latest iid
		iid = ms.getNextIdField("bind_interactions", "iid")
		# insert into interactions
		ms.execute("insert into bind_interactions (iid, bindid, updated, source, description) values(%s, %s, %s, %s, %s)", (iid, self.bid, self.getLatestDate(), zlib.compress(self.xml.encode("ascii", "ignore")), self.description))
		# insert into molecules
		self.a.loadIntoDatabase(ms, iid, "a")
		self.b.loadIntoDatabase(ms, iid, "b")
		# insert into experiments
		for experiment in self.experiments:
			experiment.loadIntoDatabase(ms, iid)
		# insert into sites
		for site in self.asites:
			site.loadIntoDatabase(ms, iid, "a")
		for site in self.bsites:
			site.loadIntoDatabase(ms, iid, "b")
	def inDatabase(self, ms):
		"""
		Method to check whether the record exist in the database.
		"""
		if len(ms.getField("select iid from bind_interactions where bindid = %s", [self.bid])) == 1:
			return 1
		else:
			return 0
	def deleteFromDatabase(self, ms):
		"""
		Delete the record from database.
		"""
		# get the current iid
		iid = ms.getSingleField("select iid from bind_interactions where bindid = %s", [self.bid])
		if iid == None:
			print "BIND ID %s does not exist, cannot delete." % self.bid
			sys.exit()
		# get mid
		mids = ms.getField("select mid from bind_molecules where iid = %s", [iid])
		for mid in mids:
			ms.execute("delete from bind_molecules where mid = %s", [mid])
			ms.execute("delete from bind_molecule_links where mid = %s", [mid])
			ms.execute("delete from bind_molecule_ids where mid = %s", [mid])
			ms.execute("delete from bind_molecule_aliases where mid = %s", [mid])
		# get eid
		eids = ms.getField("select eid from bind_experiments where iid = %s", [iid])
		for eid in eids:
			ms.execute("delete from bind_experiments where eid = %s", [eid])
			ms.execute("delete from bind_experiment_sites where eid = %s", [eid])
			ms.execute("delete from bind_experiment_pmids where eid = %s", [eid])
		# get sid
		sids = ms.getField("select sid from bind_sites where iid = %s", [iid])
		for sid in sids:
			ms.execute("delete from bind_sites where sid = %s", [sid])
			ms.execute("delete from bind_site_positions where sid = %s", [sid])
			ms.execute("delete from bind_site_points where sid = %s", [sid])

def createTables(ms):
	"""
	Function to create the tables required.
	"""
	for table in tables.keys():
		if ms.checkTable(table):
			if not ms.checkTableStructure(table, tables[table]):
				ms.setupTable(table, tables[table])
		else:
			ms.setupTable(table, tables[table])

def dropTables(ms):
	"""
	Function to drop all the tables.
	"""
	for table in tables.keys():
		ms.dropTable(table)

class BindParser:
	def __init__(self):
		self.p = xml.parsers.expat.ParserCreate()
		self.p.StartElementHandler = self.start_element
		self.p.EndElementHandler = self.end_element
		self.p.CharacterDataHandler = self.char_data
		self.i = None
		self.data = ""
		self.nodes = []
		self.state = ""
		self.xml = []
		self.siteref = None
	def parseFile(self, infile):
		self.p.ParseFile(infile)
	def start_element(self, name, attrs):
		# add node to node list
		self.nodes.append(name)
		# actual parsing
		if self.searchNodes("BIND-Interaction"):
			self.i = Interaction()	
			self.xml = []
		elif self.searchNodes("BIND-Interaction_a"):
			self.state = "a"
		elif self.searchNodes("BIND-Interaction_b"):
			self.state = "b"
		elif self.searchNodes("BIND-loc-gen_a-sites", "BIND-loc-gen", "BIND-loc_general", "BIND-loc", "BIND-descr_binding-sites"):
			self.state = "asite"
		elif self.searchNodes("BIND-loc-gen_b-sites", "BIND-loc-gen", "BIND-loc_general", "BIND-loc", "BIND-descr_binding-sites"):
			self.state = "bsite"
		# condition general
		elif self.searchNodes("BIND-condition_general", "BIND-condition"):
			self.i.experiments[-1].general = attrs["value"]
		# condition system
		elif self.searchNodes("BIND-experimental-system", "BIND-condition_system", "BIND-condition"):
			self.i.experiments[-1].system = attrs["value"]
		# condition pmid state
		elif self.searchNodes("BIND-pub-object_opinion", "BIND-pub-object", "BIND-pub-set_pubs", "BIND-pub-set", "BIND-condition_source"):
			self.i.experiments[-1].pmids.append([attrs["value"], None])
		# condition site ref
		elif self.searchNodes("BIND-loc-site-ref_molecule", "BIND-loc-site-ref", "BIND-condition_site"):
			if attrs["value"] == "a":
				self.siteref = self.i.experiments[-1].asites
			elif attrs["value"] == "b":
				self.siteref = self.i.experiments[-1].bsites
			else:
				print "Error with site value %s" % attrs["value"]
				sys.exit()
		# dump xml
		tag = "<%s" % name
		for key in attrs.keys():
			tag += "%s=\"%s\"" % (key, attrs[key])
		tag += ">"
		self.xml.append(tag)
	def extractMolecule(self, mol, name):
		# short label
		if self.searchNodes("BIND-object_short-label", "BIND-object"):
			mol.name = self.data
		# protein
		elif self.searchNodes("BIND-object-type-id_protein"):
			mol.type = "protein"
		# dna
		elif self.searchNodes("BIND-object-type-id_dna"):
			mol.type = "dna"
		# rna
		elif self.searchNodes("BIND-object-type-id_rna"):
			mol.type = "rna"
		# gene
		elif self.searchNodes("BIND-object-type-id_gene"):
			mol.type = "gene"
		# small molecule
		elif self.searchNodes("BIND-object-type-id_small-molecule"):
			mol.type = "small-molecule"
		# ns
		elif self.searchNodes("BIND-object-type-id_not-specified"):
			mol.type = "non-specified"
		# photon
		elif self.searchNodes("BIND-object-type-id_photon"):
			mol.type = "photon"
		# complex
		elif self.searchNodes("BIND-object-type-id_complex"):
			mol.type = "complex"
		# gi
		elif self.searchNodes("Geninfo-id", "BIND-id_gi", "BIND-id"):
			if self.data != "0":
				mol.ids.append(["gi", self.data])
		# gene
		elif self.searchNodes("Domain-id", "BIND-id_di", "BIND-id"):
			if self.data != "0":
				mol.ids.append(["gene", self.data])
		# complex id
		elif self.searchNodes("Molecular-Complex-id", "BIND-object-type-id_complex"):
			mol.ids.append(["bind_complex", self.data])
		# small molecule
		elif self.searchNodes("BIND-other-db_dbname", "BIND-other-db", "BIND-small-molecule-id_other-db", "BIND-small-molecule-id", "BIND-object-type-id_small-molecule"):
			mol.ids.append([self.data, None])
		elif self.searchNodes("BIND-other-db_intp", "BIND-other-db", "BIND-small-molecule-id_other-db", "BIND-small-molecule-id", "BIND-object-type-id_small-molecule"):
			if mol.ids[-1][1] == None:
				mol.ids[-1][1] = self.data
		elif self.searchNodes("BIND-other-db_strp", "BIND-other-db", "BIND-small-molecule-id_other-db", "BIND-small-molecule-id", "BIND-object-type-id_small-molecule"):
			if mol.ids[-1][1] == None:
				mol.ids[-1][1] = self.data
			else:
				mol.ids.append([mol.ids[-1][0], self.data])
		# links
		elif self.searchNodes("BIND-other-db_dbname", "BIND-other-db", "BIND-object_extref"):
			mol.links.append([self.data, None, None])
		elif self.searchNodes("BIND-other-db_intp", "BIND-other-db", "BIND-object_extref"):
			if self.data != "" and self.data != "0":
				mol.links[-1][1] = self.data
		elif self.searchNodes("BIND-other-db_strp", "BIND-other-db", "BIND-object_extref"):
			if self.data != "" and self.data != "0":
				mol.links[-1][2] = self.data
		# description
		elif self.searchNodes("BIND-object_descr"):
			mol.description = self.data
		# taxid
		elif self.searchNodes("Object-id_id", "Object-id", "Dbtag_tag", "Dbtag", "Org-ref_db", "Org-ref", "BioSource_org"):
			mol.taxid = self.data
		# remove states
		elif self.searchNodes("BIND-Interaction_a"):
			self.state = ""
		elif self.searchNodes("BIND-Interaction_b"):
			self.state = ""
	def extractSite(self, site, name):
		if self.searchNodes("BIND-Seq-loc-id", "BIND-loc-site_slid", "BIND-loc-site"):
			site.append(Site())
			site[-1].id = self.data
		elif self.searchNodes("Seq-interval_from", "Seq-interval", "Seq-loc_int", "Seq-loc"):
			site[-1].starts.append(self.data)
		elif self.searchNodes("Seq-interval_to", "Seq-interval", "Seq-loc_int", "Seq-loc"):
			site[-1].ends.append(self.data)
		elif self.searchNodes("Seq-point_point", "Seq-point", "Seq-loc_pnt", "Seq-loc"):
			site[-1].points.append(self.data)
		elif self.searchNodes("Object-id_str", "Object-id", "Feat-id_local", "Feat-id", "Seq-loc_feat", "Seq-loc"):
			site[-1].description = self.data
		# remove states
		elif self.searchNodes("BIND-loc-gen_a-sites"):
			self.state = ""
		elif self.searchNodes("BIND-loc-gen_b-sites"):
			self.state = ""
	def end_element(self, name):
		# dump xml
		self.xml.append(self.data)
		self.xml.append("</%s>" % name)
		# molecule a
		if self.state == "a":
			self.extractMolecule(self.i.a, name)
		# molecule b
		elif self.state == "b":
			self.extractMolecule(self.i.b, name)
		# site a
		elif self.state == "asite":
			self.extractSite(self.i.asites, name)
		# site b
		elif self.state == "bsite":
			self.extractSite(self.i.bsites, name)
		# finished with record
		elif self.searchNodes("BIND-Interaction"):
			self.i.xml = string.join(self.xml, "")
			self.newRecord()
		# bind id
		elif self.searchNodes("Interaction-id", "BIND-Interaction_iid"):
			self.i.bid = self.data
		# date
		elif self.searchNodes("Date-std_year", "Date-std", "Date_std", "Date", "BIND-Interaction_date"):
			self.i.date = "%04d" % int(self.data)
		elif self.searchNodes("Date-std_month", "Date-std", "Date_std", "Date", "BIND-Interaction_date"):
			self.i.date += "%02d" % int(self.data)
		elif self.searchNodes("Date-std_day", "Date-std", "Date_std", "Date", "BIND-Interaction_date"):
			self.i.date += "%02d" % int(self.data)
		# updates
		elif self.searchNodes("Date-std_year", "Date-std", "Date_std", "Date", "BIND-update-object_date", "BIND-update-object"):
			self.i.updates.append("%04d" % int(self.data))
		elif self.searchNodes("Date-std_month", "Date-std", "Date_std", "Date", "BIND-update-object_date", "BIND-update-object"):
			self.i.updates[-1] += "%02d" % int(self.data)
		elif self.searchNodes("Date-std_day", "Date-std", "Date_std", "Date", "BIND-update-object_date", "BIND-update-object"):
			self.i.updates[-1] += "%02d" % int(self.data)
		# interaction description
		elif self.searchNodes("BIND-descr_simple-descr", "BIND-descr", "BIND-Interaction_descr"):
			self.i.description = self.data
		# condition id
		elif self.searchNodes("Internal-conditions-id", "BIND-condition_icid", "BIND-condition"):
			self.i.experiments.append(Experiment())
			self.i.experiments[-1].id = self.data
		# condition description
		elif self.searchNodes("BIND-condition_descr", "BIND-condition"):
			self.i.experiments[-1].description = self.data
		# condition pmid
		elif self.searchNodes("Pub_muid", "Pub", "BIND-pub-object_pub", "BIND-pub-object", "BIND-pub-set_pubs", "BIND-pub-set", "BIND-condition_source"):
			self.i.experiments[-1].pmids[-1][1] = self.data
		elif self.searchNodes("PubMedId", "Pub_pmid", "Pub", "BIND-pub-object_pub", "BIND-pub-object", "BIND-pub-set_pubs", "BIND-pub-set", "BIND-condition_source"):
			self.i.experiments[-1].pmids[-1][1] = self.data
		# site references
		elif self.searchNodes("BIND-Seq-loc-id", "BIND-loc-site-ref_site", "BIND-loc-site-ref", "BIND-condition_site"):
			self.siteref.append(self.data)
		# remove the last node
		del self.nodes[-1]
		# clear the data
		self.data = ""
	def char_data(self, data):
		self.data += data.strip().encode("ascii", "ignore")
	def searchNodes(self, *path):
		for i in range(len(path)):
			if path[i] != self.nodes[-(i+1)]:
				return 0
		return 1
	def newRecord(self):
		print self.i
		pass

if __name__ == "__main__":
	bp = BindParser()
	bp.parseFile(open(sys.argv[1]))
