#!/usr/bin/python

# fixcc -- nitpick lily's c++ code

# TODO
#  * maintainable rules: regexp's using whitespace (?x) and match names
#    <identifier>)
#  * trailing `*' vs. function definition
#  * do not break/change indentation of fixcc-clean files
#  * check lexer, parser
#  * rewrite in elisp, add to cc-mode
#  * using regexes is broken by design
#  * ?
#  * profit

import __main__
import getopt
import os
import re
import string
import sys
import time

COMMENT = 'COMMENT'
STRING = 'STRING'
GLOBAL_CXX = 'GC++'
CXX = 'C++'
verbose_p = 0
indent_p = 0

rules = {
	GLOBAL_CXX:
	[
	# delete gratuitous block
	('''\n(    |\t)\s*{\n\s*(.*?)(?![{}]|\b(do|for|else|if|switch|while)\b);\n\s*}''',
	 '\n\\2;'),
	],
	CXX:
	[
	# space before parenthesis open
	('([^\( \]])[ \t]*\(', '\\1 ('),
	# space after comma
	("\([^'],\)[ \t]*", '\1 '),
	# delete gratuitous block
	('''\n(    |\t)\s*{\n\s*(.*?)(?![{}]|\b(do|for|else|if|switch|while)\b);\n\s*}''',
	 '\n\\2;'),
	# delete inline tabs
	('(\w)\t+', '\\1 '),
	# delete inline double spaces
	('   *', ' '),
	# delete space after parenthesis open
	('\([ \t]*', '('),
	# delete space before parenthesis close
	('[ \t]*\)', ')'),
	# delete spaces after prefix
	('(--|\+\+)[ \t]*([\w\)])', '\\1\\2'),
	# delete spaces before postfix
	('([\w\)\]])[ \t]*(--|\+\+)', '\\1\\2'),
	# delete space after parenthesis close
	#('\)[ \t]*([^\w])', ')\\1'),
	# delete space around operator
	# ('([\w\(\)\]])([ \t]*)(::|\.)([ \t]*)([\w\(\)])', '\\1\\3\\5'),
	('([\w\(\)\]])([ \t]*)(\.|->)([ \t]*)([\w\(\)])', '\\1\\3\\5'),
	# delete space after operator
	('(::)([ \t]*)([\w\(\)])', '\\1\\3'),
	# delete superflous space around operator
	('([\w\(\)\]])([ \t]+)(&&|\|\||<=|>=|!=|\|=|==|\+=|-=|\*=|/=|\?|<|>|\+|-|=|/|:|&|\||\*)([ \t]+)([\w\(\)])', '\\1 \\3 \\5'),
	# space around operator1
	('([\w\)\]]) *(&&|\|\||<=|>=|!=|\|=|==|\+=|-=|\*=|/=|\?|<|>|=|/|:|&|\||\*) *([\w\(])', '\\1 \\2 \\3'),
	# space around operator2
	('([\w\)\]]) *(&&|\|\||<=|>=|!=|\|=|==|\+=|-=|\*=|/=|\?|<|>|=|/|:|&|\||\*) ([^\w\s])', '\\1 \\2 \\3'),
	# space around operator3
	('([^\w\s]) (&&|\|\||<=|>=|!=|\|=|==|\+=|-=|\*=|/=|\?|<|[^-]>|=|/|:|&|\||\*) *([\w\(])', '\\1 \\2 \\3'),
	# space around operator4
	('([\w\(\)\]]) (\*|/|\+|-) *([-:])', '\\1 \\2 \\3'),
	# space around +/-; exponent
	('([\w\)\]])(\+|-)([_A-Za-z\(])', '\\1 \\2 \\3'),
	('([_\dA-Za-df-z\)\]])(\+|-)([\w\(])', '\\1 \\2 \\3'),
	# trailing operator
	(' (::|&&|\|\||<=|>=|!=|\|=|==|\+=|-=|\*=|/=|\?|<|>|\+|-|=|/|:|&XXX|\||\*XXX)[ \t]*\n([ \t]*)',	 '\n\\2\\1 '),
	# pointer
	##('(bool|char|const|delete|int|stream|unsigned|void|size_t|struct \w+|[A-Z]\w*|,|;|&&|<|[^-]>|\|\||-|\+)[ \t]*(\*|&)[ \t]*', '\\1 \\2'),
	('(bool|char|const|delete|int|stream|unsigned|void|size_t|struct \w+|[A-Z]\w*|,|;|:|=|\?\)|&&|<|[^-]>|\|\||-|\+)[ \t]*(\*|&)[ \t]*', '\\1 \\2'),
	#to#('(bool|char|const|delete|int|stream|unsigned|void|([A-Z]\w*)|[,])[ \n\t]*(\*|&)[ \t]*', '\\1 \\3'),
	# pointer with template
	('(( *((bool|char|const|delete|int|stream|unsigned|void|size_t|class[ \t]+\w*|[A-Z]\w*|\w+::\w+|[,])[ \*&],*)+)>) *(\*|&) *', '\\1 \\5'),
	#to#('(( *((bool|char|delete|int|stream|unsigned|void|(class[ \t]+\w*)|([A-Z]\w*)|[,])[ \*&],*)+)>)[ \t\n]*(\*|&) *', '\\1 \\7'),
	# unary pointer, minus, not
	('(return|=) (\*|&|-|!) ([\w\(])', '\\1 \\2\\3'),
	# space after `operator'
	('(\Woperator) *([^\w\s])', '\\1 \\2'),
	# dangling brace close
	('\n[ \t]*(\n[ \t]*})', '\\1'),
	# dangling newline
	('\n[ \t]*\n[ \t]*\n', '\n\n'),
	# dangling parenthesis open
	#('[ \t]*\n[ \t]*\([ \t]*\n', '('),
	('\([ \t]*\n', '('),
	# dangling parenthesis close
	('\n[ \t]*\)', ')'),
	# dangling comma
	('\n[ \t]*,', ','),
	# dangling semicolon
	('\n[ \t]*;', ';'),
	# brace open
	('(\w)[ \t]*([^\s]*){([ \t]*\n)', '\\1\\2\n{\n'),
	# brace open backslash
	('(\w[^\n]*){[ \t]*\\\\\n', '\\1\\\n{\\\n'),
	# brace close
	("}[ \t]*([^'\n]*\w[^\n\\\]*)\n", '}\n\\1\n'),
	# brace close backslash
	("}[ \t]*([^'\n]*\w[^\n\\\]*)", '\n}\n\\1'),
	# delete space after `operator'
	#('(\Woperator) (\W)', '\\1\\2'),
	# delete space after case, label
	('(\W(case|label) ([\w]+)) :', '\\1:'),
	# delete space before comma
	('[ \t]*,', ','),
	# delete space before semicolon
	('[ \t]*;', ';'),
	# delete space before eol-backslash
	('[ \t]*\\\\\n', '\\\n'),
	# delete trailing whitespace
	('[ \t]*\n', '\n'),

	## Deuglify code that also gets ugly by rules above.
	# delete newline after typedef struct
	('(typedef struct\s+([\w]*\s){([^}]|{[^}]*})*})\s*\n\s*(\w[\w\d]*;)', '\\1 \\4'),
	# delete spaces around template brackets
	#('(dynamic_cast|template|([A-Z]\w*))[ \t]*<[ \t]*(( *(bool|char|int|unsigned|void|(class[ \t]+\w*)|([A-Z]\w*)),?)+)[ \t]?(| [\*&])[ \t]*>', '\\1<\\3\\8>'),
	('(dynamic_cast|template|typedef|\w+::\w+|[A-Z]\w*)[ \t]*<[ \t]*(( *(bool|char|const|int|unsigned|void|size_t|class[ \t]+\w*|[A-Z]\w*)( *[\*&]?,|[\*&])*)+)[ \t]?(| [\*&])[ \t]*>', '\\1<\\2\\6>'),
	('(\w+::\w+|[A-Z]\w*) < ((\w+::\w+|[A-Z]\w*)<[A-Z]\w*>) >', '\\1<\\2 >'),
	('((if|while)\s+\(([^\)]|\([^\)]*\))*\))\s*;', '\\1\n;'),
	('(for\s+\(([^;]*;[^;]*;([^\)]|\([^\)]*\))*)\))\s*;', '\\1\n;'),
	# do {..} while
	('(}\s*while\s*)(\(([^\)]|\([^\)]*\))*\))\s*;', '\\1\\2;'),

	## Fix code that gets broken by rules above.
	##('->\s+\*', '->*'),
	# delete space before #define x()
	('#[ \t]*define (\w*)[ \t]*\(', '#define \\1('),
	# add space in #define x ()
	('#[ \t]*define (\w*)(\(([^\(\)]|\([^\(\)]*\))*\)\\n)',
	 '#define \\1 \\2'),
	# delete space in #include <>
	('#[ \t]*include[ \t]*<[ \t]*([^ \t>]*)[ \t]*(/?)[ \t]*([^ \t>]*)[ \t]*>',
	'#include <\\1\\2\\3>'),
	# delete backslash before empty line (emacs' indent region is broken)
	('\\\\\n\n', '\n\n'),
	],

	COMMENT:
	[
	# delete trailing whitespace
	('[ \t]*\n', '\n'),
	# delete empty first lines
	('(/\*\n)\n*', '\\1'),
	# delete empty last lines
	('\n*(\n\*/)', '\\1'),
	## delete newline after start?
	#('/(\*)\n', '\\1'),
	## delete newline before end?
	#('\n(\*/)', '\\1'),
	],
	}

# Recognize special sequences in the input.
#
#   (?P<name>regex) -- Assign result of REGEX to NAME.
#   *? -- Match non-greedily.
#   (?m) -- Multiline regex: Make ^ and $ match at each line.
#   (?s) -- Make the dot match all characters including newline.
#   (?x) -- Ignore whitespace in patterns.
no_match = 'a\ba'
snippet_res = {
	CXX: {
	'multiline_comment':
	r'''(?sx)
	(?P<match>
	(?P<code>
	[ \t]*/\*.*?\*/))''',
	
	'singleline_comment':
	r'''(?mx)
	^.*
	(?P<match>
	(?P<code>
	[ \t]*//([ \t][^\n]*|)\n))''',

	'string':
	r'''(?x)
	(?P<match>
	(?P<code>
	"([^\"\n](\")*)*"))''',
	
	'char':
	r'''(?x)
	(?P<match>
	(?P<code>
	'([^']+|\')))''',
	   
	   'include':
	   r'''(?x)
	   (?P<match>
	   (?P<code>
	   "#[ \t]*include[ \t]*<[^>]*>''',
	   },
	  }

class Chunk:
	def replacement_text (self):
		return ''

	def filter_text (self):
		return self.replacement_text ()

class Substring (Chunk):
	def __init__ (self, source, start, end):
		self.source = source
		self.start = start
		self.end = end

	def replacement_text (self):
		s = self.source[self.start:self.end]
		if verbose_p:
			sys.stderr.write ('CXX Rules')
		for i in rules[CXX]:
			if verbose_p:
				sys.stderr.write ('.')
				#sys.stderr.write ('\n\n***********\n')
				#sys.stderr.write (i[0])
				#sys.stderr.write ('\n***********\n')
				#sys.stderr.write ('\n=========>>\n')
				#sys.stderr.write (s)
				#sys.stderr.write ('\n<<=========\n')
			s = re.sub (i[0], i[1], s)
		if verbose_p:
			sys.stderr.write ('done\n')
		return s
		

class Snippet (Chunk):
	def __init__ (self, type, match, format):
		self.type = type
		self.match = match
		self.hash = 0
		self.options = []
		self.format = format

	def replacement_text (self):
		return self.match.group ('match')

	def substring (self, s):
		return self.match.group (s)

	def __repr__ (self):
		return `self.__class__` + ' type = ' + self.type

class Multiline_comment (Snippet):
	def __init__ (self, source, match, format):
		self.type = type
		self.match = match
		self.hash = 0
		self.options = []
		self.format = format

	def replacement_text (self):
		s = self.match.group ('match')
		if verbose_p:
			sys.stderr.write ('COMMENT Rules')
		for i in rules[COMMENT]:
			if verbose_p:
				sys.stderr.write ('.')
			s = re.sub (i[0], i[1], s)
		return s

snippet_type_to_class = {
	'multiline_comment': Multiline_comment,
#	'string': Multiline_comment,
#	'include': Include_snippet,
}

def find_toplevel_snippets (s, types):
	if verbose_p:
		sys.stderr.write ('Dissecting')

	res = {}
	for i in types:
		res[i] = re.compile (snippet_res[format][i])

	snippets = []
	index = 0
	## found = dict (map (lambda x: (x, None),
	##		      types))
	## urg python2.1
	found = {}
	map (lambda x, f = found: f.setdefault (x, None),
	     types)

	# We want to search for multiple regexes, without searching
	# the string multiple times for one regex.
	# Hence, we use earlier results to limit the string portion
	# where we search.
	# Since every part of the string is traversed at most once for
	# every type of snippet, this is linear.

	while 1:
		if verbose_p:
			sys.stderr.write ('.')
		first = None
		endex = 1 << 30
		for type in types:
			if not found[type] or found[type][0] < index:
				found[type] = None
				m = res[type].search (s[index:endex])
				if not m:
					continue

				cl = Snippet
				if snippet_type_to_class.has_key (type):
					cl = snippet_type_to_class[type]
				snip = cl (type, m, format)
				start = index + m.start ('match')
				found[type] = (start, snip)

			if found[type] \
			   and (not first \
				or found[type][0] < found[first][0]):
				first = type

				# FIXME.

				# Limiting the search space is a cute
				# idea, but this *requires* to search
				# for possible containing blocks
				# first, at least as long as we do not
				# search for the start of blocks, but
				# always/directly for the entire
				# @block ... @end block.

				endex = found[first][0]

		if not first:
			snippets.append (Substring (s, index, len (s)))
			break

		(start, snip) = found[first]
		snippets.append (Substring (s, index, start))
		snippets.append (snip)
		found[first] = None
		index = start + len (snip.match.group ('match'))

	return snippets

def nitpick_file (outdir, file):
	s = open (file).read ()

	for i in rules[GLOBAL_CXX]:
		s = re.sub (i[0], i[1], s)

	# FIXME: Containing blocks must be first, see
	#        find_toplevel_snippets.
	#        We leave simple strings be part of the code
	snippet_types = (
		'multiline_comment',
		'singleline_comment',
		'string',
#		'char',
		)

	chunks = find_toplevel_snippets (s, snippet_types)
	#code = filter (lambda x: is_derived_class (x.__class__, Substring),
	#	       chunks)

	t = string.join (map (lambda x: x.filter_text (), chunks), '')
	fixt = file
	if s != t:
		if not outdir:
			os.system ('mv %s %s~' % (file, file))
		else: 
			fixt = os.path.join (outdir,
					     os.path.basename (file))
		h = open (fixt, "w")
		h.write (t)
		h.close ()
	if s != t or indent_p:
		indent_file (fixt)

def indent_file (file):
	emacs = '''emacs\
	--no-window-system\
	--batch\
	--no-site-file\
	--no-init-file\
	%(file)s\
	--eval '(let ((error nil)
		      (version-control nil))
		 (load-library "cc-mode")
		 (c++-mode)
		 (indent-region (point-min) (point-max))
		 (if (buffer-modified-p (current-buffer))
		  (save-buffer)))' ''' % vars ()
	emacsclient = '''emacsclient\
	--socket-name=%(socketdir)s/%(socketname)s\
        --no-wait\
	--eval '(let ((error nil)
		      (version-control nil))
		 (load-library "cc-mode")
                 (find-file "%(file)s")
		 (c++-mode)
		 (indent-region (point-min) (point-max))
		 (if (buffer-modified-p (current-buffer))
		  (save-buffer)))' ''' \
		  % { 'file': file,
		      'socketdir' : socketdir,
		      'socketname' : socketname, }
	if verbose_p:
		sys.stderr.write (emacs)
		sys.stderr.write ('\n')
	os.system (emacs)


def usage ():
	sys.stdout.write (r'''
Usage:
fixcc [OPTION]... FILE...

Options:
   --help
   --indent   reindent, even if no changes
   --verbose
   --test

Typical use with LilyPond:

   fixcc $(find flower kpath-guile lily -name '*cc' -o -name '*hh' | grep -v /out)

This script is licensed under the GNU GPL
''')

def do_options ():
	global indent_p, outdir, verbose_p
	(options, files) = getopt.getopt (sys.argv[1:], '',
					  ['help', 'indent', 'outdir=',
					   'test', 'verbose'])
	for (o, a) in options:
		if o == '--help':
			usage ()
			sys.exit (0)
		elif o == '--indent':
			indent_p = 1
		elif o == '--outdir':
			outdir = a
		elif o == '--verbose':
			verbose_p = 1
		elif o == '--test':
			test ()
			sys.exit (0)
		else:
			assert unimplemented
	if not files:
		usage ()
		sys.exit (2)
	return files


outdir = 0
format = CXX
socketdir = '/tmp/fixcc'
socketname = 'fixcc%d' % os.getpid ()

def setup_client ():
	#--no-window-system\
	#--batch\
	os.unlink (os.path.join (socketdir, socketname))
	os.mkdir (socketdir, 0700)
	emacs='''emacs\
		--no-site-file\
		--no-init-file\
		--eval '(let ((error nil)
			      (version-control nil))
			 (load-library "server")
			 (setq server-socket-dir "%(socketdir)s")
			 (setq server-name "%(socketname)s")
			 (server-start)
			 (while t) (sleep 1000))' ''' \
			 % { 'socketdir' : socketdir,
			     'socketname' : socketname, }
			     
	if not os.fork ():
		os.system (emacs)
		sys.exit (0)
	while not os.path.exists (os.path.join (socketdir, socketname)):
		time.sleep (1)

def main ():
	#emacsclient should be faster, but this does not work yet
	#setup_client ()
	files = do_options ()
	if outdir and not os.path.isdir (outdir):
		os.makedirs (outdir)
	for i in files:
                sys.stderr.write ('%s...\n' % i)
		nitpick_file (outdir, i)


## TODO: make this compilable and check with g++
TEST = '''
#include <libio.h>
#include <map>
class
ostream ;

class Foo {
public: static char* foo ();
std::map<char*,int>* bar (char, char) { return 0; }
};
typedef struct
{
  Foo **bar;
} String;

ostream &
operator << (ostream & os, String d);

typedef struct _t_ligature
{
  char *succ, *lig;
  struct _t_ligature * next;
}  AFM_Ligature;
  
typedef std::map < AFM_Ligature const *, int > Bar;

  /**
  (c) 1997--2006 Han-Wen Nienhuys <hanwen@cs.uu.nl>
  */
  
/*      ||
 *      vv
 * !OK  OK
 */
/*     ||
       vv
  !OK  OK
 */
char *
Foo:: foo ()
{
int
i
;
  char* a= &++ i ;
  a [*++ a] = (char*) foe (*i, &bar) *
  2;
  int operator double ();
  std::map<char*,int> y =*bar(-*a ,*b);
  Interval_t<T> & operator*= (T r);
  Foo<T>*c;
  int compare (Pqueue_ent < K, T > const& e1, Pqueue_ent < K,T> *e2);
  delete *p;
  if (abs (f)*2 > abs (d) *FUDGE)
    ;
  while (0);
  for (; i<x foo(); foo>bar);
  for (; *p && > y;
       foo > bar)
;
  do {
  ;;;
  }
  while (foe);

  squiggle. extent;
  1 && * unsmob_moment (lf);
  line_spanner_ = make_spanner ("DynamicLineSpanner", rq ? rq->*self_scm
(): SCM_EOL);
  case foo: k;

  if (0) {a=b;} else {
   c=d;
  }

  cookie_io_functions_t Memory_out_stream::functions_ = {
    Memory_out_stream::reader,
    ...
  };

  int compare (Array < Pitch> *, Array < Pitch> *);
  original_ = (Grob *) & s;
  Drul_array< Link_array<Grob> > o;
}

  header_.char_info_pos = (6 + header_length) * 4;
  return ly_bool2scm (*ma < * mb);

  1 *::sign(2);

  (shift) *-d;

  a = 0 ? *x : *y;

a = "foo() 2,2,4";
{
  if (!span_)
    {
      span_ = make_spanner ("StaffSymbol", SCM_EOL);
    }
}
{
  if (!span_)
    {
      span_ = make_spanner (StaffSymbol, SCM_EOL);
    }
}
'''

def test ():
	test_file = 'fixcc.cc'
	open (test_file, 'w').write (TEST)
	nitpick_file (outdir, test_file)
	sys.stdout.write (open (test_file).read ())

if __name__ == '__main__':
	main ()

