#!/usr/bin/python
# coding: iso8859-1
############################################################################
#    tragesym  - create gEDA symbols out of structured textfiles
#    begin      : 2001-10-20
#    copyright  : (C) 2001,2002,2003,2004,2006,2007, 2008 by Werner Hoch
#    email      : werner.ho@gmx.de
############################################################################
#                                                                          #
#    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.                                   #
#                                                                          #
############################################################################

# FEATURES:
# - create pins and their elements
# - sort pins alphabetical
# - swap words of the pinlabels
# - negation lines if label is in "_","\" is for escape
# - rotate top and bottom pinlabels if wished
# - if the symbol's width specified is 0, then tragesym calculates the
#   symbol width based on the greater number of pins at the top or at the
#   bottom of the symbol

import getopt, os.path, re, string, sys

##################### GLOBALS ############################################
VERSION="0.0.15"

CHARHIGH=26
preset_options = {"wordswap":"yes",
				  "rotate_labels":"no",
				  "sort_labels":"yes",
				  "generate_pinseq":"yes",
				  "sym_width":"1400",
				  "sym_height":"0",
				  "pinwidthvertikal":"400",
				  "pinwidthvertical":"400",
				  "pinwidthhorizontal":"400"}
official_attr = ["version", "name", "device", "refdes", "footprint", "numslots",
				"slot", "slotdef","description", "comment", "author",
				"documentation","value","dist-license", "use-license"]
single_attr_warning = ["device", "footprint", "author", "documentation",
					   "description", "numslots","dist-license", "use-license"]
single_attr = ["slot"]
multiple_attr = ["slotdef", "comment"]
stylelist = ["line","dot","clk","dotclk","spacer","none"]
poslist = ["l","r","t","b",""]
typelist = ["in","out","io","oc","oe","pas","tp","tri","clk","pwr"]
translate_pintype = {"i/o":"io", "i":"in", "o":"out", "p":"pas"}
P_NR, P_SEQ, P_TYPE, P_STYLE, P_POS, P_NET, P_LABEL = 0,1,2,3,4,5,6
re_section_header = re.compile("^\s*\[(?P<name>.+)]\s*$")

################################## CLASSES ###############################

class Pin:
	'''Encapsulation for all data related to a pin.'''
	def __init__(self, element):
		
		element.extend(('', '', '', '', '', '', ''))
		self.nr = element[P_NR].strip()
		self.seq = element[P_SEQ].strip()
		pintype = element[P_TYPE].lower().strip()
		self.type = translate_pintype.get(pintype, pintype)
		self.style = element[P_STYLE].lower().strip()
		self.pos = element[P_POS].lower().strip()
		self.net = element[P_NET].strip()
		self.label = element[P_LABEL].strip()

	def __str__(self):
		str = "Pin object (nr:" + self.nr + " seq:" + self.seq + " type:" + self.type
		str += " style:" + self.style + " pos:" + self.pos + " net:" + self.net
		str += " label:" + self.label + ")"
		return str

	def __cmp__(self, other):
		"""
		Comparison function for the pin.
		* The sorting rule is to place the nets first.
		* The pin position is the second sorting key
		* The pin label is the third sorting key
		"""
		if not isinstance(other, Pin):
			return NotImplemented
		ret = cmp(self.net, other.net)
		if ret != 0:
			return ret
		ret = cmp(other.pos, self.pos)
		if ret != 0:
			return ret
		return cmp(splitspecial(parselabel(self.label)),
				   splitspecial(parselabel(other.label)))


	def check(self):
		if self.style=="spacer":
			if self.pos == "":
				print "Error: there must be a position with a spacer.\n"
				sys.exit()
		if self.pos not in poslist:
			print "Error: position is not allowed: \n", self
			sys.exit()
			return
		if self.style != "none":
			if self.seq.isdigit():
				string.atoi(self.seq)
			else:
				print "pinseq needs to be a number: \n", self
				sys.exit()
		if self.type not in typelist:
			print "Pintype not allowed: \n", self
			sys.exit()
		if self.style not in stylelist:
			print "Style is not allowed: \n", self
			sys.exit()
		if self.pos not in poslist:
			print "Position is not allowed: \n", self
			sys.exit()
		if self.pos == "" and self.net == "":
			print "There must be either position or a netlabel: \n", self
			sys.exit()
		
		
################################# FUNCTIONS ##############################

def usage():
	'''Print a usage message.'''
	print "tragesym version " + VERSION
	print "(C) 2001,2002,2003,2004,2006,2007 by Werner Hoch <werner.ho@gmx.de>"
	print "Usage is: ", sys.argv[0] ,"<infile> <outfile>"


def parselabel(str):
	'''returns a stripped label without overbar markers "\_"'''
	slash, neg= 0, 0
	textout=""
	for letter in str:
		if letter == '\\' and slash == 0:
			slash=1
		elif slash == 1 and letter == '_':
			if neg == 0:
				neg = 1
			else:
				neg = 0
			slash = 0                
		else:
			textout=textout+letter
			slash = 0
			
	if slash == 1 or neg == 1:
		print '''unbalanced overbars or escapesequence: ''', str
		print '''the overbar starts and ends with "\_" example: \"\_enable\_'''
		print '''to write a "\" use "\\"'''
		sys.exit()
	return textout

## round *unsigned* integer x to closest r
def round_closest(x,r):
	return x-(x+r/2)%r+r/2

## returns the words in reverse order    
def swapwords(str): 
	list=string.split(str," ")
	back=list[0]
	for i in list[1:]:
		back=i+" "+back
	return back

## split a string at the first tab or equal char
def split_tab_equal(str,n=1):
	list_tab=string.split(str,'\t',n)
	list_equal=string.split(str,'=',n)
	if len(list_tab[0]) < len(list_equal[0]):
		return list_tab
	else:
		return list_equal

## returns 2 dicts: (options, attr) and 2 arrays: (devices, pins)
def readsrc(filename):
	geda_attr={}
	options={}
	pins=[]
	f = open(filename,"r")
	content= f.readlines()
	section=""
	linenr=0
	for lineraw in content:
		line = lineraw.rstrip()
		linenr=linenr+1
		if len(line) == 0:
			continue
		match = re_section_header.match(line)
		if match:                   # find a section 
			section=match.group('name')
			continue
		elif section=="" or line[0]=="#" \
			 or len(string.strip(line)) == 0:   # comment, empty line or no section
			continue
		if section=="options":
			element=split_tab_equal(line,1)
			if len(element) > 1:
				options[string.strip(element[0])]=string.strip(element[1])
		elif section=="geda_attr":
			element=split_tab_equal(line,1)
			if len(element) < 2 or len(element[1].strip()) == 0:
				print 'Warning: Empty attribute "%s" in the geda_attr section' % element[0]
				print '         The incomplete attribute will be dropped'
			else:
				nr=1
				while geda_attr.has_key((element[0],nr)):
					nr=nr+1
				geda_attr[(string.strip(element[0]),nr)]=string.strip(element[1])
		elif section=="pins":
			element=string.split(line,"\t")
			if len(element) > 2:
				pins.append(Pin(element))
		else:
			print linenr, ": illegal section name: ", section
			sys.exit()
	return options, geda_attr, pins


def splitspecial(str):
	"""
	makes a list out of a string:
	"3abc345x?" --> ["",3,"abc",345,"x?"]
	"""
	isletter=1
	list=[]
	current = ""
	for letter in str:
		if letter not in string.digits:
			if isletter == 1:
				current += letter
			else:
				list.append(int(current))
				current = letter
				isletter=1
		else:
			if isletter == 0:
				current += letter
			else:
				list.append(current)
				current = letter
				isletter=0
	if isletter == 0:
		list.append(int(current))
	else:
		list.append(current)
	return list

def writesym(filename,options,attr,pins):
	o_symwidth=string.atoi(options["sym_width"])
	o_symheight=string.atoi(options["sym_height"])
	o_hdist=string.atoi(options["pinwidthhorizontal"])
	
	# If pinwidthvertikal was defined, use it, else use pinwidthvertical
	# This keeps compatibility with older versions, while fixing the spell
	# bug
	if options["pinwidthvertikal"] != preset_options["pinwidthvertikal"]:
		o_vdist=string.atoi(options["pinwidthvertikal"])
	else:
		o_vdist=string.atoi(options["pinwidthvertical"])

	o_wordswap=options["wordswap"]
	o_rotate=options["rotate_labels"]
	o_sort=options["sort_labels"]

	pinlength = 300

### Count the number of pins in each side

	numpleft=0
	numpright=0
	numpbottom=0
	numptop = 0
	for pin in pins:
		if pin.pos == "l": # left pin
			numpleft=numpleft+1
		elif pin.pos == "r": #right pin
			numpright=numpright+1
		elif pin.pos == "b": #right pin
			numpbottom=numpbottom+1
		elif pin.pos == "t": #right pin
			numptop=numptop+1

	# Calculate the position of the pins in the left and right side.    
	plefty, prighty = 0, 0
	if numpleft >  numpright:
		plefty=plefty+(numpleft-1)*o_vdist
		prighty = plefty
	else :
		prighty=prighty+(numpright-1)*o_vdist
		plefty = prighty

	# Calculate the bottom left of the box
	bottomleftx, bottomlefty = pinlength + 100, 100
	if numpbottom > 0:
		bottomlefty += pinlength

	# Calculate the minimum symwidth and increase it if necessary
	calculated_top_symwidth=(numptop-1)*o_hdist+2*o_hdist
	calculated_bottom_symwidth=(numpbottom-1)*o_hdist+2*o_hdist
	
	calculated_symwidth = max(calculated_bottom_symwidth,
							  calculated_top_symwidth)

	if (numptop + numpbottom > 0):
		print "Note: use sym_width to adjust symbol width if texts overlap."

	if o_symwidth == 0:
		o_symwidth = calculated_symwidth

	# Calculate the symbol's high
	calculated_symheight = 0
	high = 0
	if numpleft < numpright:
		calculated_symheight=(numpright+1)*o_vdist 
	else:
		calculated_symheight=(numpleft+1)*o_vdist 
	
	if o_symheight == 0:
		high = calculated_symheight
	else: high = o_symheight
	topy = bottomlefty + high
	
	pleftrighty = bottomlefty + (high - calculated_symheight) / 2

	# Calculate the position of several items.
	prightx, prighty= bottomleftx + pinlength + o_symwidth, prighty + pleftrighty + o_vdist
	pleftx, plefty= bottomleftx - pinlength, plefty + pleftrighty + o_vdist
	ptopx, ptopy= bottomleftx + o_hdist, bottomlefty + high + pinlength
	pbottomx, pbottomy = bottomleftx + o_hdist, bottomlefty - pinlength   

	# Lets add some pad if sym_width was defined
	ptopx = ptopx + (o_symwidth - calculated_top_symwidth) / 2
	pbottomx = pbottomx + (o_symwidth - calculated_bottom_symwidth) / 2
	
	

	ptopx = round_closest(ptopx, 100)
	pbottomx = round_closest(pbottomx, 100)
	
	f = open(filename, "w")

### Draw the symbol version
	if attr.has_key(("version",1)):
		value=attr[("version",1)]
		if re.match("[0-9]{8}$", value):
			f.write("v " + value + " 1\n")
		elif re.match("[0-9]{8} 1$", value):
			f.write("v " + value + "\n")
		else:
			print "error: version string format invalid: [%s]" % value
			sys.exit()
	else:
		print "error: version attribut missing"
		sys.exit()
   
	if o_sort == "yes":
		pins.sort()

	for pin in pins:
		if pin.style == "none": #
			continue
		if pin.style=="spacer":
			if o_sort == "yes":
				print "Warning: spacers are not supported when sorting labels"
				continue
			elif pin.pos == "l": #left pin
				plefty=plefty - o_vdist  #where to draw the _next_ pin
			elif pin.pos == "r": #right pin
				prighty=prighty - o_vdist
			elif pin.pos == "b": # bottom pin
				pbottomx=pbottomx + o_hdist
			elif pin.pos == "t": # top pin
				ptopx=ptopx + o_hdist
			continue
		
### decide which pindirection to use
		## TODO: put all constants into a dictionary         
		if pin.pos == "l": #left pin
			basex, basey= pleftx, plefty  #where to draw this pin
			xf, yf= 1, 0  # orientation factors  
			pint=(200,50,6,0) # dx, dy, alignment, angle
			pinl=(350,0,0,0)  # """"
			pina=(350,0,2,0)  # """"
			pinq=(200,-50,8,0)  # """"
			swap=0   # swap words in label ?
			plefty=plefty - o_vdist  #where to draw the _next_ pin
		elif pin.pos == "r": #right pin
			basex, basey = prightx, prighty 
			xf, yf= -1, 0
			pint=(-200,50,0,0)
			pinl=(-350,0,6,0)
			pina=(-350,0,8,0)
			pinq=(-200,-50,2,0)
			swap=1
			prighty=prighty - o_vdist
		elif pin.pos == "b": # bottom pin
			basex, basey=pbottomx, pbottomy
			xf, yf= 0, 1
			if o_rotate == "yes": # bottom pin with 90 text
				pint=(-50,200,6,90)
				pinl=(0,350,0,90)
				pina=(0,350,2,90)
				pinq=(50,200,8,90)
			else:
				pint=(50,200,2,0)
				pinl=(0,350,3,0)
				pina=(0,500,3,0)
				pinq=(-50,200,8,0)
			swap=0
			pbottomx=pbottomx + o_hdist
		elif pin.pos == "t": # top pin
			basex, basey=ptopx, ptopy
			xf, yf= 0, -1
			if o_rotate == "yes": # with 90 text
				pint=(-50,-200,0,90)
				pinl=(0,-350,6,90)
				pina=(0,-350,8,90)
				pinq=(50,-200,2,90)
				swap=1
			else:
				pint=(50,-200,0,0)
				pinl=(0,-350,5,0)
				pina=(0,-500,5,0)
				pinq=(-50,-200,6,0)
				swap=0
			ptopx=ptopx + o_hdist
### draw the pin
		if (pin.style=="dot" or  #short pin and dot?
			pin.style=="dotclk"):
			x=basex + xf*200
			y=basey + yf*200
		else:
			x=basex + xf*300
			y=basey + yf*300
		f.write("P %i"%basex+" %i"%basey+" %i"%x + " %i"%y+ " 1 0 0\n")
		f.write("{\n")
### draw pinnumber
		pintx, pinty, pinta, pintr=pint
		x=basex+pintx
		y=basey+pinty
		f.write("T %i"%x+" %i"%y+" 5 8 1 1 %i"%pintr+" %i 1\n"%pinta)
		f.write("pinnumber="+pin.nr+"\n")
### draw pinseq
		pintx, pinty, pinta, pintr=pinq
		x=basex+pintx
		y=basey+pinty
		f.write("T %i"%x+" %i"%y+" 5 8 0 1 %i"%pintr+" %i 1\n"%pinta)
		f.write("pinseq="+pin.seq+"\n")
### draw pinlabel and pintype
		pinlx, pinly, pinla, pinlr=pinl
		pinax, pinay, pinaa, pinar=pina
		if (pin.style=="clk" or  #move label if clocksign
			pin.style=="dotclk"):
			pinlx=pinlx + xf*75
			pinly=pinly + yf*75
			pinax=pinax + xf*75
			pinay=pinay + yf*75
		pinlx=pinlx + basex
		pinly=pinly + basey 
		pinax=pinax + basex
		pinay=pinay + basey
		if o_wordswap=="yes" and swap==1:
			label=swapwords(pin.label)
		else:
			label=pin.label
		f.write("T %i"%pinlx+" %i"%pinly+" 9 8 1 1 %i"%pinlr+" %i 1\n"%pinla)
		f.write("pinlabel="+label+"\n")
		f.write("T %i"%pinax+" %i"%pinay+" 5 8 0 1 %i"%pinar+" %i 1\n"%pinaa)
		f.write("pintype="+pin.type+"\n")
		f.write("}\n")
### draw the negation bubble
		if (pin.style=="dot" or pin.style=="dotclk"):
			x=basex + xf*250
			y=basey + yf*250
			f.write("V %i"%x+" %i"%y +" 50 6 0 0 0 -1 -1 0 -1 -1 -1 -1 -1\n")
### draw the clocksign
		if (pin.style=="clk" or
			pin.style=="dotclk"):
			x1=basex+ xf*400
			y1=basey+ yf*400
			x2=x1- xf*100 +yf*75
			y2=y1- yf*100 +xf*75
			x3=x1- xf*100 -yf*75
			y3=y1- yf*100 -xf*75
			f.write("L %i"%x1+" %i"%y1+" %i"%x2+" %i"%y2 + " 3 0 0 0 -1 -1\n")
			f.write("L %i"%x1+" %i"%y1+" %i"%x3+" %i"%y3 + " 3 0 0 0 -1 -1\n")
### draw a box 
	f.write("B %i"%bottomleftx+" %i"%bottomlefty+" %i"%o_symwidth+" %i"%high+
			" 3 0 0 0 -1 -1 0 -1 -1 -1 -1 -1\n")

### draw the attributes
	urefx, urefy = bottomleftx+o_symwidth, bottomlefty + high + 100

	# Center name if we have top pins
	if numptop > 0:
		namex, namey = (bottomleftx + o_symwidth) / 2 - 200, (bottomlefty + high) / 2 + 100
	else:
		namex, namey = bottomleftx, bottomlefty+high+100

	textx = namex
	texty = namey + 200
	if numptop > 0:
		texty += 100
	
	## special attribute format
	if attr.has_key(("refdes",1)):
		f.write("T %i"% urefx +" %i"% urefy +" 8 10 1 1 0 6 1\n")
		f.write("refdes=" + attr[("refdes",1)] + "\n")
	else:
		print "Warning: refdes attribut missing"

	if attr.has_key(("name",1)):
		f.write("T %i" %namex + " %i"% namey + " 9 10 1 0 0 0 1\n")
		f.write(attr[("name",1)] + "\n")
	else:
		print "Warning: name attribut missing"

	## attributes with same format and warnings
	for a in single_attr_warning:
		if attr.has_key((a,1)):
			f.write("T %i" %textx + " %i"% texty + " 5 10 0 0 0 0 1\n")
			f.write(a + "=" + attr[(a,1)] + "\n")
			texty=texty+200
		else:
			print "Warning: " + a + " attribut missing"

	## attributes without warning
	for a in single_attr:
		if attr.has_key((a,1)):
			f.write("T %i" %textx + " %i"% texty + " 5 10 0 0 0 0 1\n")
			f.write(a + "=" + attr[(a,1)] + "\n")
			texty=texty+200

	## attributes with more than one equal name
	for a in multiple_attr:
		i = 1
		while attr.has_key((a,i)):
			f.write("T %i" %textx + " %i"% texty + " 5 10 0 0 0 0 1\n")
			f.write(a + "=" + attr[(a,i)] + "\n")
			texty=texty+200
			i = i + 1

	## unknown attributes
	for (name, number),value in attr.items():
		if name not in official_attr:
			f.write("T %i" %textx + " %i"% texty + " 5 10 0 0 0 0 1\n")
			f.write(name + "=" + value + "\n")
			texty=texty+200
			print 'Warning: The attribute "%s=%s" is not official' %(name, value)

	nets={}
	for pin in pins:
		if pin.style == "none":
			if not nets.has_key(pin.net):
				nets[pin.net] = pin.nr
			else:
				nets[pin.net] = nets[pin.net] + ","+ pin.nr
	for key,value in nets.items():
		f.write("T %i" %textx + " %i"% texty + " 5 10 0 0 0 0 1\n")
		f.write("net=" + key + ":" + value + "\n")
		texty=texty+200
 
	return 0

def mergeoptions(source_opt,pre_opt):
	ret=pre_opt
	for item in source_opt.keys():
		if ret.has_key(item):
			ret[item]=source_opt[item]
		else:
			print "This option is not allowed:", item
			sys.exit()
	return ret

def generate_pinseq(pins):
	seq=1
	for nr in xrange(len(pins)):
		if pins[nr].style not in ["none","spacer"]:
			pins[nr].seq = "%i"%seq
			seq = seq + 1
	return pins

###################### MAIN #################################################

## parse command line options
try:
	opts, args = getopt.getopt(sys.argv[1:], "h", ["help"])
except:
	usage()
	sys.exit()

## handle command line options
for o, a in opts:
	if o in ("-h", "--help"):
		usage()
		sys.exit()

## get files
if len(args) != 2:
	usage()
	sys.exit()

file_in=args[0]
file_out=args[1]
if not os.path.exists(file_in):
	print "Input file " + file_in + " not found."
	sys.exit()

## read sourcefile
opts,attr,pins=readsrc(file_in)

options=mergeoptions(opts,preset_options)

if options["generate_pinseq"] == "yes":
	pins=generate_pinseq(pins)

for pin in pins:
	pin.check()

writesym(file_out,options,attr,pins)

