#!/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 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

# TODO:
# - make a nicer comandline interface including stdin /stdout
# - write cleaner code (variables, constants, structure)

import sys, string

##################### GLOBALS ############################################
VERSION="0.0.12"

CHARHIGH=26
preset_options = {"wordswap":"yes",
                  "rotate_labels":"no",
                  "sort_labels":"yes",
                  "generate_pinseq":"yes",
                  "sym_width":"1400",
                  "pinwidthvertikal":"400",
                  "pinwidthvertical":"400",
                  "pinwidthhorizontal":"400"}
allowed_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"]
P_NR, P_SEQ, P_TYPE, P_STYLE, P_POS, P_NET, P_LABEL = 0,1,2,3,4,5,6


################################# FUNCTIONS ##############################

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

## make the parser more tolerant to the input file
def type_translate(str):
    if str == "i/o":
        str="io"
    elif str == "i":
        str="in"
    elif str == "o":
        str="out"
    elif str == "p":
        str="pas"
    return str

## 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 line in content:
        linenr=linenr+1
        if line[0]=="[":        # find a section 
            section=string.split(line[1:],"]")[0]
            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)
            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:
                for j in xrange(len(element)):
                    element[j]=string.strip(element[j])
                    if j in [P_TYPE,P_POS,P_STYLE]:
                        element[j]= string.lower(element[j])
                    if j == P_TYPE:
                        element[j] = type_translate(element[j])
                while len(element) < 7:
                    element.append("")
                pins.append(element)
        else:
            print linenr, ": illegal section name: ", section
            sys.exit()
    return options, geda_attr, pins

def checkpins(pinlist):
    for pin in pinlist:
    	if pin[P_STYLE] == "spacer":
	    if pin[P_POS] == "":
		print "Error: There must be a position with a spacer.\n"
		sys.exit()
	    if pin[P_POS] not in poslist:
		print "Error: position is not allowed: \n", pin
		sys.exit()
    	    continue
        if pin[P_STYLE] != "none":
            if pin[P_SEQ].isdigit():
                string.atoi(pin[P_SEQ])
            else:
                print "pinseq needs to be a number: \n", pin
                sys.exit()
        if pin[P_TYPE] not in typelist:
            print "Pintype not allowed: \n", pin
            sys.exit()
        if pin[P_STYLE] not in stylelist:
            print "Style is not allowed: \n", pin
            sys.exit()
        if pin[P_POS] not in poslist:
            print "Position is not allowed: \n", pin
            sys.exit()
        if pin[P_POS] == "" and pin[P_NET] == "":
            print "There must be either position or a netlabel: \n", pin
            sys.exit()

def checkattr(attr):
    for attr,value in attr.items():
        attr,nr=attr
        if attr not in allowed_attr:
            print "this attribute is not allowed: ", attr, value
            sys.exit()

## makes a list out of a string: "3abc345x?" --> ["","3","abc","345","x?"]
def splitspecial(str):
    isletter=1
    list=[""]
    for letter in str:
        if letter not in string.digits:
            if isletter == 1:
                list[-1]=list[-1] + letter
            else:
                list.append(letter)
                isletter=1
        else:
            if isletter == 0:
                list[-1]=list[-1] + letter
            else:
                list.append(letter)
                isletter=0
    return list
    
def pinsort(pin1,pin2):
    ## rules: nets first, positions second, labels last
    ## net
    if pin1[P_NET] != "" and pin2[P_NET] != "":
        return 0
    elif pin2[P_NET] != "":
        return 1
    elif pin1[P_NET] != "":
        return -1
    ## position
    if pin1[P_POS] > pin2[P_POS]:
        return -1
    elif pin1[P_POS] < pin2[P_POS]:
        return 1
    ## pinlabel
    label1 = parselabel(pin1[P_LABEL])
    label2 = parselabel(pin2[P_LABEL])
    l1 = splitspecial(label1)
    l2 = splitspecial(label2)
    minlen = len(l1)
    if len(l2) < minlen:
        minlen = len(l2)
    for i in xrange(minlen):
        if i % 2 == 1:
            ret= sort(string.atoi(l1[i]),string.atoi(l2[i]))
        else:
            ret= sort(l1[i],l2[i])
        if ret != 0:
            return ret
    ret=sort(len(l1),len(l2))
    return ret

def sort(item1,item2):
    if item1 < item2:
        return -1
    elif item1 > item2:
        return 1
    return 0

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[P_POS] == "l": # left pin
    		numpleft=numpleft+1
    	elif pin[P_POS] == "r": #right pin
    		numpright=numpright+1
    	elif pin[P_POS] == "b": #right pin
    		numpbottom=numpbottom+1
    	elif pin[P_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  ## bottom left of the box
    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)]
        f.write("v " + value + " 1\n")
    else:
        print "error: version attribut missing"
        sys.exit()
   
    if o_sort == "yes":
        pins.sort(pinsort)

    for pin in pins:
        if pin[P_STYLE] == "none": #
            continue
        if pin[P_STYLE]=="spacer":
            if o_sort == "yes":
                print "warning: spacers are not supported when sorting labels"
                continue
            elif pin[P_POS] == "l": #left pin
                plefty=plefty - o_vdist  #where to draw the _next_ pin
            elif pin[P_POS] == "r": #right pin
                prighty=prighty - o_vdist
            elif pin[P_POS] == "b": # bottom pin
                pbottomx=pbottomx + o_hdist
            elif pin[P_POS] == "t": # top pin
                ptopx=ptopx + o_hdist
            continue
### decide which pindirection to use
        ## TODO: put all constants into a dictionary         
        if pin[P_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)  # """"
            negl=(0, CHARHIGH*4+20,0,0) # dx,dy,vert,rotate (base is label)
            swap=0   # swap words in label ?
            plefty=plefty - o_vdist  #where to draw the _next_ pin
        elif pin[P_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)
            negl=(0, CHARHIGH*4+20,2,0)
            swap=1
            prighty=prighty - o_vdist
        elif pin[P_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)
                negl=(CHARHIGH*4+20,0,0,1)
            else:
                pint=(50,100,0,0)
                pinl=(0,350,3,0)
                pina=(0,500,3,0)
                pinq=(50,100,2,0)
                negl=(0,CHARHIGH*4+20,1,0)
            swap=0
            pbottomx=pbottomx + o_hdist
        elif pin[P_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)
                negl=(CHARHIGH*4+20,0,2,1)
                swap=1
            else:
                pint=(50,-200,0,0)
                pinl=(0,-350,5,0)
                pina=(0,-500,5,0)
                pinq=(50,-200,2,0)
                negl=(0,20,1,0)
                swap=0
            ptopx=ptopx + o_hdist
### draw the pin
        if (pin[P_STYLE]=="dot" or  #short pin and dot?
            pin[P_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
        if pin[P_POS] == "t": # top pin
	    y += 50
        f.write("T %i"%x+" %i"%y+" 5 8 1 1 %i"%pintr+" %i 1\n"%pinta)
        f.write("pinnumber="+pin[P_NR]+"\n")
### draw pinseq
        pintx, pinty, pinta, pintr=pinq
        x=basex+pintx
        y=basey+pinty
        if pin[P_POS] == "t": # top pin
	    y += 50
        f.write("T %i"%x+" %i"%y+" 5 8 0 1 %i"%pintr+" %i 1\n"%pinta)
        f.write("pinseq="+pin[P_SEQ]+"\n")
### draw pinlabel and pintype
        pinlx, pinly, pinla, pinlr=pinl
        pinax, pinay, pinaa, pinar=pina
        if (pin[P_STYLE]=="clk" or  #move label if clocksign
            pin[P_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[P_LABEL])
        else:
            label=pin[P_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[P_TYPE]+"\n")
        f.write("}\n")
### draw the negation bubble
        if (pin[P_STYLE]=="dot" or pin[P_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[P_STYLE]=="clk" or
            pin[P_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/bottom pins
    if numptop + numpbottom > 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

    nets={}
    for pin in pins:
        if pin[P_STYLE] == "none":
            if not nets.has_key(pin[P_NET]):
                nets[pin[P_NET]] = pin[P_NR]
            else:
                nets[pin[P_NET]] = nets[pin[P_NET]] + ","+ pin[P_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][P_STYLE] not in ["none","spacer"]:
            pins[nr][P_SEQ] = "%i"%seq
            seq = seq + 1
    return pins

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

## TODO: use getopt
try:
    file_in=sys.argv[1]
    file_out=sys.argv[2]
except:
    print "tragesym version " + VERSION
    print "Bad arguments, usage is: ", sys.argv[0] ,"infile outfile"
    sys.exit()
    
## read sourcefile
opts,attr,pins=readsrc(file_in)

options=mergeoptions(opts,preset_options)

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

checkpins(pins)
checkattr(attr)

writesym(file_out,options,attr,pins)

