#!/usr/bin/python

# copyright Pat Beirne 2003
# GNU license

# pretty printer for Java/C/C++/Python
# align assignment statements, member declarations and function parameter lists

# usage:
# pretty_assign.py file_in.c
# pretty_assign -
# pretty_assign -h

# Ver 2: added leading space counter

# changes this:
"""
public void foo(int a,
                StringBuffer b,
                char c) {
    int d = 12;
    String e = new String(b);
    int f;
    DatagramSocket g = new DatagramSocket();

    f = 23;
    d = 12;
    reallyLongVarName = 14;
    """
# into this
"""
public void foo(int          a,
                StringBuffer b,
                char         c) {
    int d    = 12;
    String e = new String(b);
    int f;
    DatagramSocket g = new DatagramSocket();

    f                 = 23;
    d                 = 12;
    reallyLongVarName = 14;
    """

VERSION = "1.1"

# type 1 function decl, with or without closing )
# type 2 member decl
# type 3 simple assignments

import sys, os
import re

##################PATTERNS#################

# no (, ending with ) or ,
# second & last line
# ^space-----not(-space-----notspace&not(-----.or)$
type1re = re.compile(r"^(\s*)([^\(]+ )([^ \t\(]+(,|\)).*)$")

# a ( early, no terminal )
# ^space-----any(any-space------not)-spaces-comma-not)$
type1are = re.compile(r"^(\s*)(.+\(.+ )([^\),]+,[^\)\n]*)$") # first line

# has at least a type, and an =
type2re = re.compile(r"^(\s*)([^\(\)]+ )(\S+ +)(=[^=].*)$")

# has a = and just a simple name on the left
type3re = re.compile(r"^(\s*)(\S+\s*)(=[^=].*)$")

# in each case, the group[1] is the indent space on the left
# group[1] is the left hand line, after the indents
# group[2] starts with the variable name or equal sign
# type 2 lines also have a group[3], which is the equal sign

############FUNCTIONS##################
def lineType(str):
    """classify a string"""
    type = 0
    m = type1re.match(str)
    if m:
        type = 1
    else:
        m = type1are.match(str)
        if m:
            type = 1
        else:
            m = type3re.match(str)
            if m:
                type = 3
            else:
                m = type2re.match(str)
                if m:
                    type = 2
    return type, m

############### the major subroutine ###################
def pa(data, fout):

    accum = ""
    accum_list = []
    last_line_type = 0
    last_line_indent = -1
    for lin in data:
        # classify
        type, matches = lineType(lin)
        if matches:
            line_indent = len(matches.group(1))
        #print type, matches and `matches.groups()`+'\n' or lin,

        # printing
        # a change of type or indent triggers a print 
        if accum_list and (last_line_type!=type or line_indent!=last_line_indent):
            min = 0
            min2 = 0
            # generate alignment gaps
            for a in accum_list:
                if min<len(a[0]):
                    min = len(a[0])
            if last_line_type==2:
                for a in accum_list:
                    if min2<len(a[1]):
                        min2 = len(a[1])

            for a in accum_list:
#            print a
#            accum += `last_line_type`+ a[0]+('@'*(min-len(a[0])))+a[1]
                accum += a[0]+(' '*(min-len(a[0]))) + a[1]
                if last_line_type==2:
                    accum += (' '*(min2-len(a[1]))) + a[2]
                accum += '\n'

            print >>fout,accum,
            accum= ""
            accum_list = []

        # print non-affected files
        if type==0:
            print >>fout,lin,
            line_indent = -1

        # build the accumulator
        # NOTE: the elements of accum_list[] have the first two matches glued together
        if type==1 or type==3:
            accum_list.append( (matches.group(1) + matches.group(2), matches.group(3)) )
        if type==2:        
            accum_list.append( (matches.group(1) + matches.group(2),
                                matches.group(3), matches.group(4)) )
        last_line_type = type
        last_line_indent = line_indent

##############main###################
if len(sys.argv)==1:
    print "pretty_align usage:"
    print "     pretty_align [-n] filename [other_filename....]"
    print " or  pretty_align -"
    print " or  pretty_align -h"
    print ""
    print "     -n means don't make backup file (defaults to filename~)"
    print "     - means take input from stdin, output to stdout"
    print "     -h print help"

    sys.exit(1)

if sys.argv[1] == "-h":
    print "Pretty Align"
    print "Pat Beirne, 2003, version " + VERSION
    print ""
    print "Synopsis:"
    print "  Adds small amounts of white space to code files, to align"
    print "  variables and declaration types."
    print ""
    print "Invoking:"
    print "  pretty_align file1 file2 file3...."
    print ""
    print "  As each file is modified, a backup is made (filename~)"
    print ""
    print "Comments:"
    print "  There are three types of lines that are modified:"
    print "    function parameter lists that span multiple lines"
    print "    static, class members and function local declarations"
    print "    small clusters of simple assignment statements"
    print ""
    print "  This app works well on Java, Python, C and C++ code"
    print ""
    print "  This app does not make any changes to the left hand side"
    print "  of the lines, so your indenting is preserved, tabs and all"
    print "  This app is not aware of strings and comments, so, unfortunately"
    print "  it operates on strings and comments. Check your results before"
    print "  accepting its results"
    sys.exit(1)

if sys.argv[1]=='-':
    fin = sys.stdin
    fout = sys.stdout
    d = fin.readlines()
    pa(d,fout)
    sys.exit(0)

bMakeBackup = 1
nFirstArg = 1
if sys.argv[1]=='-n':
    bMakeBackup = 0
    nFirstArg = 2
    
for f in sys.argv[nFirstArg:]:
    fin = open(f)
    data = fin.readlines()
    fin.close()
    if bMakeBackup:
        os.rename(f,f+'~')
    fout = open(f,"w")
    pa(data,fout)