"""IMS.py
Inventory Management System
"""

__author__ = "Josh English (Joshua.R.English@gmail.com)"
__version__ = "$Revision: 1.03 $"
__date__ = "$Date: 2006/07/14 15:55:42 $"
__copyright__ = "Copyright (c) 2006 Josh English"
__license__ = "Python"


__history__ = """History:
	1.01 (Jan 9,  2006) Adding a submission to a story also changes the story's status to OnMarket
	1.02 (March,  2006) Adding a submission to a story only changes status to OnMarket if previous status was 'draft'
	1.03 (Jul 14, 2006) Adding a submission to a story that is sold changes the status to Reprint
"""

from xml.dom import minidom
from datetime import date
import time
import os


class MethodException(Exception): pass
class DateException(Exception): pass
class StoryError(Exception): pass
class SubmissionError(Exception): pass

###Constants
subfile = "Marvin:Documents:Fiction:subs.xml"
storyfile = "Marvin:Documents:Fiction:stories.xml"
marketfile = "Marvin:Documents:Ficiton:markets.xml"

submissionStatusCodes =['InMail','Reject','Sale']
storyStatusCodes = ['Treatment','Draft','OnMarket','Sold','Reprint','Retired']

marketdict={
		'WotF':'WotF',
		'FSF':'FSF',
		'Analog':'Anlg',
		'Asimovs':'Asmv',
		'Andromeda':'ASIM',
		'Gateway':'Gway',
		'StrangeHorizons':'SHrz',
		'Lenox':'LxAv',
		'Talebones':'TlBn',
		'Ideomancer':'Ideo',
		'AlienSkin':'Skin',
		'SonAndFoe':'SFoe',
		'LadyChurchills':'LCRW',
		'Apex':'Apex',
		'Polyphony':'Poly',
		'Byzarium':'Bzrm',
		'WierdTales':'WdTl',
		"LeadingEdge":'Edge',
		"LoneStar":'Lone',
		"Neo-opsis":'NeoO',
		"Flytrap":'FlyT',
		"FicticiousForce":'FiFo',
		"Shimmer":'Shmr',
		'QuantumMuse':'Muse',
		"RealmsOfFantasy":'Rof_',
		"OnSpec":'OnSp',
		"FUHU":'FUHU',
		"Abyss":'Abys',
		"Baen":'Baen',
		"StrangerBox":'SBox',
		'EscapePod':'EPod',
		'OSCIM':'Card',
		"Fantasy":'Fant',
		"DarkEnergy":'Dark',
		"ChaosTheory":'Chos',
		"Atomjack":'Atom',
		"Grendelsong":'Gsng',
		"Noneuclidean Cafe":'Cafe',
		"Cafe Irreal": 'CfIr',
		"Flash Me": 'FlsM',
		"DarkRecesses":'DkRc',
		"SandAndSea":'SSea',
		"Flashquake":'FlQk',
		"Gryphonwood":'Gryf',
		}
		
marketcodes = marketdict.keys()

submissionMethods=['mail','email']

### Date procedures

dateformats = ['%b %d, %Y','%b %d, %y','%m %d %Y','%Y %m %d','%m/%d/%y','%m-%d-%y']
preffereddateformat = dateformats[0]


def StringToDate(ts):
	"""StringToDate(string)
	Takes a string and returns a Date object
	"""
	for df in dateformats:
		try:
			t = time.strptime(ts,df)
			return date(t[0],t[1],t[2])
		except:
			pass
	raise "Cannot parse date string"

WORDOS = 'wordos'
NINA = 'nina'
LEGUIN = 'leguin'
CRITTERS = 'critters'
WELORIANS = 'welorians'

workshops = [WORDOS,NINA,LEGUIN,CRITTERS,WELORIANS]

###  ANYTHING YOU WOULD WANT TO CUSTOMIZE SHOULD BE ABOVE THIS LINE

### Customized Filers
class Filer:
	def __init__(self,path,tag="story",key="code"):
		self.SourcePath = path	### source of the original data
		self.TagName = tag		### tag to search for 'story','submission', etc.
		self.Keyword = key		### unique keyword to control data
		
		docElement = minidom.parse(path)
		
		self.Main = docElement.firstChild
		self.SourceName = self.Main.tagName  ### saved for the Save() method
		
		self._dict={}
		
		self.DataDirectory = os.path.join(os.getcwd(),"Data")
		if not os.path.exists(self.DataDirectory):
			os.makedirs(self.DataDirectory)
		
		for item in self.Main.getElementsByTagName(tag):
			### Search for an attribute key
			codekey = str(item.getAttribute(key))
			### Otherwise look for text inside a subelement with name key
			if not codekey:
				codekey = str(item.getElementsByTagName(key)[0].firstChild.data.strip())
			filename = "%s--%s" % (self.TagName,codekey)
			filename = os.path.join(self.DataDirectory,filename)
			out = open(filename,'w')
			out.write(item.toxml())
			out.close()
			self._dict[codekey]=filename
			#self._dict[codekey].write(item.toxml())
	
#	def __del__(self):
#		for k in self._dict.keys():
#			self._dict[k].close()
			
	def keys(self):
		return self._dict.keys()
		
	def getData(self,codekey):
		"""returns the text of the file"""
		out = file(self._dict[codekey],'r')
		return out.read()
	
	def getXML(self,codekey):
		"""returns the minidom element represented by the file."""
		return minidom.parseString(self.getData(codekey)).firstChild
		
	def __getitem__(self,key):
		"""Returns a link to the open file"""
		return self._dict[key]
	
	def getFile(self,key):
		return self._dict[key]
	
	def has_key(self,item):
		return self._dict.has_key(item)
	
	def replace(self,codekey,data):
		"""Replace the old codekey with a new minidom element"""
		if data.tagName != self.TagName:
			raise "Cannot replace data"
		
		#self._dict[codekey].close()  ### gets rid of old file (loses data) !!!!!!
		#self._dict[codekey]=NamedTemporaryFile()  ### make a new file
		out = file(self._dict[codekey],'w')
		out.write(data.toxml())
	
	def Save(self):
		"""Saves the data to the original path"""
		from time import asctime
		e = minidom.Element(self.SourceName)
		e.setAttribute('mod_date',asctime())
		e.setAttribute('path',self.SourcePath)
		
		NL = minidom.Text()
		NL.data =u"\n"
		e.appendChild(NL)
		for key in self.keys():
			x = self.getXML(key)
			e.appendChild(x)
			NL = minidom.Text()
			NL.data =u"\n"
			e.appendChild(NL)
		dump = open(self.SourcePath,'w')
		dump.write(e.toxml())
		dump.close()
		#print e.toxml()
	
	def New(self,codekey,text):
		"""New(codekey,text)
		Writes the text into a new file
		"""
		if self._dict.has_key(codekey): raise "Cannot create new file, duplicate code %s" % codekey
		filename = "%s--%s" % (self.TagName,codekey)
		filename = os.path.join(self.DataDirectory,filename)
		out = open(filename,'w')
		out.write(text)
		out.close()
		self._dict[codekey]=filename
		#self._dict[codekey]=NamedTemporaryFile()
		#self._dict[codekey].write(text)
		
class StoryFiler(Filer):
	def __init__(self):
		Filer.__init__(self,storyfile,"story","code")
		import os.path
		nm,ext = os.path.splitext(storyfile)
		bu = nm+"BACKUP"+ext
		b = open(bu,"w")
		o = open(storyfile,"r")
		b.write(o.read())
		o.close()
		b.close()
	
#	def __del__(self):
#		for key in self.keys(): self[key].close()
		
	def getStory(self,key):
		return NodeToStory(self.getXML(key))
	
	def Save(self):
		from time import asctime
		e = minidom.Element(self.SourceName)
		e.setAttribute('mod_date',asctime())
		e.setAttribute('path',self.SourcePath)
		
		for key in self.keys():
			x = self.getXML(key)
			e.appendChild(x)
		
		dump = open(self.SourcePath,'w')
		p = Printer(e)
		dump.write(p.write())
		dump.close()

class SubFiler(Filer):
	def __init__(self):
		Filer.__init__(self,subfile,"submission","code")
	
	def getSubmission(self,key):
		return NodeToSubmission(self.getXML(key))
	
	def Save(self):
		from time import asctime
		e = minidom.Element(self.SourceName)
		e.setAttribute('mod_date',asctime())
		e.setAttribute('path',self.SourcePath)
		
		for key in self.keys():
			x = self.getXML(key)
			e.appendChild(x)
		
		dump = open(self.SourcePath,'w')
		p = Printer(e)
		dump.write(p.write())
		dump.close()

### Tools for dealing with XML
def toNode(nodeName,data):
	"""returns a minidom element object with text child of data
	n = toNode('flash','fiction')
	n.toxml() = "<flash>fiction</flash>"
	"""
	text = minidom.Text()
	text.data=str(data)
	node = minidom.Element(nodeName)
	node.appendChild(text)
	return node

class Printer:
	"""Printer(element,newl="\n",forceindent=False)
	Returns a text representation of a minidom element. This is more
	readable than the minidom.element.toxml method and more compact than
	the minidom.element.toprettyxml method.
	Does not handle all types of xml.  Comments and processing instructions
	are ignored.
	
	"""
	def __init__(self,element,newl="\n"):
		import StringIO
		self.writer = StringIO.StringIO()
		self.newl = newl
		self.TopNodeName = element.nodeName
		
		self._print(element)
		
	def _print(self,sub,indent=""):
		"""sub is an xml element"""
		
		self.writer.write(indent)
		if sub.nodeName == "#text": 
			self.writer.seek(-len(indent),2) 
			self.writer.write ("%s" % sub.data.strip())
			
		elif sub.nodeType == sub.ELEMENT_NODE:
			if sub.parentNode:
				backup = sub.parentNode.nodeName == self.TopNodeName
			else:
				backup = True
			#if backup: self.writer.seek(-len(indent),2)

			self.writer.write("<%s" % (sub.nodeName))
			atts = sub._get_attributes()
			if atts: atts = ' '+' '.join(['%s="%s"' % (k,v) for k,v in atts.items()])
			else: atts = ''
			self.writer.write(atts)
			if sub.childNodes:
				self.writer.write(">")
				
				if len(sub.childNodes) > 1: self.writer.write(self.newl)
					
				#if backup : newindent = indent
				#else: newindent = indent+'\t'
				newindent = indent+'\t'
				for node in sub.childNodes: 
					self._print(node,newindent)
				### Possible comment out next two lines
				if len(sub.childNodes) > 1: self.writer.write(indent)
				
				#if backup: self.writer.seek(-(len(indent)),2)
				
				self.writer.write("</%s>%s" % (sub.nodeName,self.newl))
			else:
				self.writer.write("/>%s" % (self.newl))
	
	def write(self):
		return self.writer.getvalue()

### Story Class
class Story:
	def __init__(self,code,title,wordcount,status,file,*args,**kw):
		self.code = str(code).lower()
		self.title = str(title)
		self.wordcount = int(wordcount)
		if status in storyStatusCodes:
			self.status = status
			
		else:
			raise StoryError, "Invalid Status %s" % status
		self.file = str(file)
		self.workshops = []
		for a in args:
			if a in workshops: self.workshops.append(a)
			else: raise StoryError, "Workshop %s not recognized" % a
		self.pasttitles = []
		self.history = []
		self.submissions = []
		self.plot = ""
		for k,v in kw.items():
			if k == "pasttitles": self.pasttitles.extend(v)
			if k == "plot":self.plot = str(v)
	
	def addHistory(self,dobj,info):
		"""addHistory(dobj,info)
		Adds history with a date object and info string
		"""
		self.history.append('%s: %s' % (dobj.strftime(preffereddateformat),info))
	
	def newTitle(self,title):
		self.pasttitles.append(self.title)
		self.title=title
	
	def addSubmission(self,subcode):
		### Add the submission, and change status to OnMarket
		self.submissions.append(subcode)
		### Only change status if 'draft' or 'Sold'
		if self.status == 'Draft': self.status = 'OnMarket'
		if self.status == 'Sold' : self.status = 'Reprint'
		
	def toElement(self):
		e = minidom.Element('story')
		e.setAttribute('code',self.code)
		e.appendChild(toNode('title',self.title))
		for pt in self.pasttitles:
			e.appendChild(toNode('pasttitle',pt))
		e.appendChild(toNode('wordcount',self.wordcount))
		e.appendChild(toNode('status',self.status))
		for w in self.workshops:
			e.appendChild(minidom.Element(w))
		e.appendChild(toNode('file',self.file))
		h = minidom.Element('history')
		for item in self.history:
			h.appendChild(toNode('item',item))
		e.appendChild(h)
		for sub in self.submissions:
			e.appendChild(toNode('submission',sub))
		e.appendChild(toNode('plot',self.plot))
		return e
	
	def toXML(self,newl="\n"):
		p = Printer(self.toElement(),newl)
		return p.write()

def NodeToStory(node):
	code = node.getAttribute('code')
	title = node.getElementsByTagName('title')[0].firstChild.data.strip()
	pasttitles = [str(d.firstChild.data) for d in node.getElementsByTagName('pasttitle')]
	wordcount = node.getElementsByTagName('wordcount')[0].firstChild.data.strip()
	status = node.getElementsByTagName('status')[0].firstChild.data.strip()
	shops = filter(node.getElementsByTagName,workshops)

	file =  node.getElementsByTagName('file')[0].firstChild.data
	items = [str(d.firstChild.data) for d in node.getElementsByTagName('item')]
	submissions = [str(d.firstChild.data) for d in node.getElementsByTagName('submission')]
	plots = node.getElementsByTagName('plot')[0]
	if plots.childNodes: plot = plots.firstChild.data
	else: plot = ''
	
	filltitles = False
	if pasttitles:
		usefirst = pasttitles[0]
		pasttitles.pop(0)
		filltitles = True
	else:
		usefirst = title
		
	#code,title,wordcount,status,file,*args,**kw
	s = Story(code,usefirst,wordcount,status,file)
	if filltitles:
		for pt in pasttitles: s.newTitle(pt)
		s.newTitle(title)
	
	s.workshops = shops
	for item in items:
		s.addHistory(StringToDate(item[:12]),item[13:].strip())
	
	for sub in submissions:
		s.addSubmission(sub)
	s.plot = plot
	return s
	
MAIL = 'mail'
EMAIL = 'email'

			
class Submission:
	def __init__(self,story,storycode,market,date,method,cost,note=''):
		self.closed = False
		self.story =story
		self.storycode = storycode
		if market not in marketcodes:
			raise SubmissionError, "Unknown market %s" % market
		else:
			self.market = market
		self.datesent = StringToDate(date).strftime(preffereddateformat)
		self.code = "%s%s" % (marketdict[self.market],StringToDate(date).strftime('%Y%m%d'))
		if method not in submissionMethods:
			raise SubmissionError, "Unknown submission method %s" % method
		else:
			self.method = method
		self.cost = float(cost)
		if self.method == EMAIL and self.cost != 0:
			raise SubmissionError, "Cannot have cost to email submission"
		self.status = 'InMail'
		self.note = note
	
	def HowLong(self):
		"""Returns the number of days a submission was out (or is currently out)"""
		if hasattr(self,'daysout'): return self.daysout
		else:
			do = date.today()-StringToDate(self.datesent)
			return do.days
		
	def Close(self,date,status,note=""):
		db = StringToDate(date)
		ds = StringToDate(self.datesent)
		do = db-ds
		if do.days<0: raise SubmissionError, "Cannot close sub before sent date"
		if self.closed: raise SubmissionError, "Submission already closed"
		self.dateback = StringToDate(date).strftime(preffereddateformat)
		self.daysout = do.days
		if status not in submissionStatusCodes: 
			raise SubmissionError, "Unknown status code %s" % status
		else:
			self.status = status
		self.note += " %s" % (note)
		self.closed = True		
		
	def toElement(self):
		e = minidom.Element('submission')
		e.appendChild(toNode('code',self.code))
		e.appendChild(toNode('story',self.story))
		e.appendChild(toNode('storycode',self.storycode))
		e.appendChild(toNode('market',self.market))
		e.appendChild(toNode('datesent',self.datesent))
		if self.closed:
			e.appendChild(toNode('dateback',self.dateback))
			e.appendChild(toNode('daysout',self.daysout))
		e.appendChild(toNode('status',self.status))
		e.appendChild(toNode('method',self.method))
		e.appendChild(toNode('cost',"%.2f" % (self.cost)))
		if self.note: e.appendChild(toNode('note',self.note))
		return e



def NodeToSubmission(node):
	code = node.getElementsByTagName('code')[0].firstChild.data
	story = node.getElementsByTagName('story')[0].firstChild.data
	storycode = node.getElementsByTagName('storycode')[0].firstChild.data
	market = node.getElementsByTagName('market')[0].firstChild.data
	datesent = node.getElementsByTagName('datesent')[0].firstChild.data
	cost = node.getElementsByTagName('cost')[0].firstChild.data
	method = node.getElementsByTagName('method')[0].firstChild.data
	status = node.getElementsByTagName('status')[0].firstChild.data
	notes = node.getElementsByTagName('note')
	if notes: 
		if notes[0].childNodes: note = notes[0].firstChild.data
		else: note = ''
	else: note = ''
	closed = False
	if status != "InMail":
		### Then it is closed
		closed = True
		dateback = node.getElementsByTagName('dateback')[0].firstChild.data
		daysout = node.getElementsByTagName('daysout')[0].firstChild.data	
	
	s = Submission(story,storycode,market,datesent,method,cost,note)
	if closed:
		s.Close(dateback,status)
	return s



########## RED LINE OF DEATH #######################			
