#!/usr/local/bin/python2.7
# 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",
                  "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_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
    if numpleft < numpright:
        high=(numpright+1)*o_vdist 
    else:
        high=(numpleft+1)*o_vdist 
    topy = bottomlefty + high

    # Calculate the position of several items.
    prightx, prighty= bottomleftx + pinlength + o_symwidth, prighty + bottomlefty + o_vdist
    pleftx, plefty= bottomleftx - pinlength, plefty + bottomlefty + 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, (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)

