#!/usr/bin/env python

import os
import subprocess
import tempfile
import sys
import filecmp
import optparse
import shutil
import difflib
import re

def hg_modified_files():
    files = os.popen ('hg st -nma')
    return [filename.strip() for filename in files]

def copy_file(filename):
    [tmp,pathname] = tempfile.mkstemp()
    src = open(filename, 'r')
    dst = open(pathname, 'w')
    for line in src:
        dst.write(line)
    dst.close()
    src.close()
    return pathname

# generate a temporary configuration file
def uncrustify_config_file(level):
    level2 = """
nl_collapse_empty_body=False
nl_if_brace=Add
nl_brace_else=Add
nl_elseif_brace=Add
nl_else_brace=Add
nl_while_brace=Add
nl_do_brace=Add
nl_for_brace=Add
nl_brace_while=Add
nl_switch_brace=Add
nl_after_case=True
nl_namespace_brace=Remove
nl_after_brace_open=True
nl_class_leave_one_liners=False
nl_enum_leave_one_liners=False
nl_func_leave_one_liners=False
nl_if_leave_one_liners=False
nl_class_colon=Ignore
nl_after_access_spec=1
nl_after_semicolon=True
pos_class_colon=Lead
pos_class_comma=Trail
pos_bool=Lead
nl_class_init_args=Add
nl_template_class=Add
nl_class_brace=Add
# does not work very well
nl_func_type_name=Ignore
nl_func_scope_name=Ignore
nl_func_type_name_class=Ignore
nl_func_proto_type_name=Ignore
# function\\n(
nl_func_paren=Remove
nl_fdef_brace=Add
nl_struct_brace=Add
nl_enum_brace=Add
nl_union_brace=Add
mod_full_brace_do=Add
mod_full_brace_for=Add
mod_full_brace_if=Add
mod_full_brace_while=Add
mod_full_brace_for=Add
mod_remove_extra_semicolon=True
# max code width
#code_width=128
#ls_for_split_full=True
#ls_func_split_full=True
"""
    level1 = """
# extra spaces here and there
sp_brace_typedef=Add
sp_enum_assign=Add
sp_before_sparen=Add
sp_after_semi_for=Add
sp_arith=Add
sp_assign=Add
sp_compare=Add
sp_func_class_paren=Add
sp_after_type=Add
sp_type_func=Add
sp_angle_paren=Add
"""
    level0 = """
sp_func_proto_paren=Add
sp_func_def_paren=Add
sp_func_call_paren=Add
sp_after_semi_for=Ignore
sp_before_sparen=Ignore
sp_type_func=Ignore
sp_after_type=Ignore
nl_class_leave_one_liners=True
nl_enum_leave_one_liners=True
nl_func_leave_one_liners=True
nl_assign_leave_one_liners=True
#nl_collapse_empty_body=False
nl_getset_leave_one_liners=True
nl_if_leave_one_liners=True
nl_fdef_brace=Ignore
# finally, indentation configuration
indent_with_tabs=0
indent_namespace=false
indent_columns=2
indent_brace=2
indent_case_brace=2
indent_class=true
indent_class_colon=True
# alignment
indent_align_assign=False
align_left_shift=True
# comment reformating disabled
cmt_reflow_mode=1 # do not touch comments at all
cmt_indent_multi=False # really, do not touch them
"""
    [tmp,pathname] = tempfile.mkstemp()
    dst = open(pathname, 'w')
    dst.write(level0)
    if level >= 1:
        dst.write(level1)
    if level >= 2:
        dst.write(level2)
    dst.close()
    return pathname

## PatchChunkLine class
class PatchChunkLine:
    ## @var __type
    #  type
    ## @var __line
    #  line
    ## @var SRC
    #  Source
    SRC = 1
    ## @var DST
    #  Destination
    DST = 2
    ## @var BOTH
    #  Both
    BOTH = 3
    def __init__(self):
        """! Initializer
        @param self The current class
        @return none
        """
        self.__type = 0
        self.__line = ''
    def set_src(self,line):
        """! Set source
        @param self The current class
        @param line source line
        @return none
        """
        self.__type = self.SRC
        self.__line = line
    def set_dst(self,line):
        """! Set destination
        @param self The current class
        @param line destination line
        @return none
        """
        self.__type = self.DST
        self.__line = line
    def set_both(self,line):
        """! Set both 
        @param self The current class
        @param line
        @return none
        """
        self.__type = self.BOTH
        self.__line = line
    def append_to_line(self, s):
        """! Append to line
        @param self The current class
        @param s line to append
        @return none
        """
        self.__line = self.__line + s
    def line(self):
        """! Get line
        @param self The current class
        @return line
        """
        return self.__line
    def is_src(self):
        """! Is source
        @param self The current class
        @return true if type is source
        """
        return self.__type == self.SRC or self.__type == self.BOTH
    def is_dst(self):
        """! Is destination
        @param self The current class
        @return true if type is destination
        """
        return self.__type == self.DST or self.__type == self.BOTH
    def write(self, f):
        """! Write to file
        @param self The current class
        @param f file
        @return exception if invalid type 
        """
        if self.__type == self.SRC:
            f.write('-%s\n' % self.__line)
        elif self.__type == self.DST:
            f.write('+%s\n' % self.__line)
        elif self.__type == self.BOTH:
            f.write(' %s\n' % self.__line)
        else:
            raise Exception('invalid patch')
    
## PatchChunk class
class PatchChunk:
    ## @var __lines
    #  list of lines
    ## @var __src_pos
    #  source position
    ## @var __dst_pos
    #  destination position
    ## @var src
    #  source
    ## @var dst
    #  destination
    def __init__(self, src_pos, dst_pos):
        """! Initializer
        @param self:  this object
        @param src_pos: source position
        @param dst_pos: destination position
        @return none
        """
        self.__lines = []
        self.__src_pos = int(src_pos)
        self.__dst_pos = int(dst_pos)
    def src_start(self):
        """! Source start function
        @param self this object
        @return source position
        """
        return self.__src_pos
    def add_line(self,line):
        """! Add line function
        @param self The current class
        @param line line to add
        @return none
        """
        self.__lines.append(line)
    def src(self):
        """! Get source lines
        @param self The current class
        @return the source lines
        """
        src = []
        for line in self.__lines:
            if line.is_src():
                src.append(line)
        return src
    def dst(self):
        """! Get destination lines
        @param self The current class
        @return the destination lines
        """
        dst = []
        for line in self.__lines:
            if line.is_dst():
                dst.append(line)
        return dst
    def src_len(self):
        """! Get number of source lines
        @param self The current class
        @return number of source lines
        """
        return len(self.src())
    def dst_len(self):
        """! Get number of destinaton lines
        @param self The current class
        @return number of destination lines
        """
        return len(self.dst())
    def write(self,f):
        """! Write lines to file
        @param self The current class
        @param f: file to write to
        @return none
        """
        f.write('@@ -%d,%d +%d,%d @@\n' % (self.__src_pos, self.src_len(),
                                           self.__dst_pos, self.dst_len()))
        for line in self.__lines:
            line.write(f)

## Patch class
class Patch:
    ## @var __src
    #  source
    ## @var __dst
    #  destination
    ## @var __chunks
    #  chunks
    def __init__(self):
        """! Initializer
        @param self The current class
        @return none
        """
        self.__src = ''
        self.__dst = ''
        self.__chunks = []
    def add_chunk(self, chunk):
        """! Add chunk
        @param self this object
        @param chunk chunk
        @return none
        """
        self.__chunks.append(chunk)
    def chunks(self):
        """! Get the chunks
        @param self The current class
        @return the chunks
        """
        return self.__chunks
    def set_src(self,src):
        """! Set source
        @param self this object
        @param src source
        @return none
        """
        self.__src = src
    def set_dst(self,dst):
        """! Set destination
        @param self this object
        @param dst destintion
        @return none
        """
        self.__dst = dst
    def apply(self,filename):
        """! Apply function
        @param self The current class
        @param filename file name
        @return none
        """
        # XXX: not implemented
        return
    def write(self,f):
        """! Write to file
        @param self The current class
        @param f the file
        @return none
        """
        f.write('--- %s\n' % self.__src )
        f.write('+++ %s\n' % self.__dst )
        for chunk in self.__chunks:
            chunk.write(f)

def parse_patchset(generator):
    src_file = re.compile('^--- (.*)$')
    dst_file = re.compile('^\+\+\+ (.*)$')
    chunk_start = re.compile('^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@')
    src = re.compile('^-(.*)$')
    dst = re.compile('^\+(.*)$')
    both = re.compile('^ (.*)$')
    patchset = []
    current_patch = None
    for line in generator:
        m = src_file.search(line)
        if m is not None:
            current_patch = Patch()
            patchset.append(current_patch)
            current_patch.set_src(m.group(1))
            continue
        m = dst_file.search(line)
        if m is not None:
            current_patch.set_dst(m.group(1))
            continue
        m = chunk_start.search(line)
        if m is not None:
            current_chunk = PatchChunk(m.group(1), m.group(3))
            current_patch.add_chunk(current_chunk)
            continue
        m = src.search(line)
        if m is not None:
            l = PatchChunkLine()
            l.set_src(m.group(1))
            current_chunk.add_line(l)
            continue
        m = dst.search(line)
        if m is not None:
            l = PatchChunkLine()
            l.set_dst(m.group(1))
            current_chunk.add_line(l)
            continue
        m = both.search(line)
        if m is not None:
            l = PatchChunkLine()
            l.set_both(m.group(1))
            current_chunk.add_line(l)
            continue
        raise Exception()
    return patchset

def remove_trailing_whitespace_changes(patch_generator):
    whitespace = re.compile('^(.*)([ \t]+)$')
    patchset = parse_patchset(patch_generator)
    for patch in patchset:
        for chunk in patch.chunks():
            src = chunk.src()
            dst = chunk.dst()
            try:
                for i in range(0,len(src)):
                    s = src[i]
                    d = dst[i]
                    m = whitespace.search(s.line())
                    if m is not None and m.group(1) == d.line():
                        d.append_to_line(m.group(2))
            except:
                return patchset
    return patchset


def indent(source, debug, level):
    output = tempfile.mkstemp()[1]
    # apply uncrustify
    cfg = uncrustify_config_file(level)
    if debug:
        sys.stderr.write('original file=' + source + '\n')
        sys.stderr.write('uncrustify config file=' + cfg + '\n')
        sys.stderr.write('temporary file=' + output + '\n')
    try:
        uncrust = subprocess.Popen(['uncrustify', '-c', cfg, '-f', source, '-o', output],
                                   stdin = subprocess.PIPE,
                                   stdout = subprocess.PIPE,
                                   stderr = subprocess.PIPE)
        (out, err) = uncrust.communicate('')
        if debug:
            sys.stderr.write(out)
            sys.stderr.write(err)
    except OSError:
        raise Exception ('uncrustify not installed')
    # generate a diff file
    src = open(source, 'r')
    dst = open(output, 'r')
    diff = difflib.unified_diff(src.readlines(), dst.readlines(), 
                                fromfile=source, tofile=output)
    src.close()
    dst.close()
    if debug:
        initial_diff = tempfile.mkstemp()[1]
        sys.stderr.write('initial diff file=' + initial_diff + '\n')
        tmp = open(initial_diff, 'w')
        tmp.writelines(diff)
        tmp.close()
    final_diff = tempfile.mkstemp()[1]
    if level < 3:
        patchset = remove_trailing_whitespace_changes(diff);
        dst = open(final_diff, 'w')
        if len(patchset) != 0:
            patchset[0].write(dst)
        dst.close()
    else:
        dst = open(final_diff, 'w')
        dst.writelines(diff)
        dst.close()
            
            
    # apply diff file
    if debug:
        sys.stderr.write('final diff file=' + final_diff + '\n')
    shutil.copyfile(source,output)
    patch = subprocess.Popen(['patch', '-p1', '-i', final_diff, output],
                             stdin = subprocess.PIPE,
                             stdout = subprocess.PIPE,
                             stderr = subprocess.PIPE)
    (out, err) = patch.communicate('')
    if debug:
        sys.stderr.write(out)
        sys.stderr.write(err)
    return output
 


def indent_files(files, diff=False, debug=False, level=0, inplace=False):
    output = []
    for f in files:
        dst = indent(f, debug=debug, level=level)
        output.append([f,dst])

    # First, copy to inplace
    if inplace:
        for src,dst in output:
            shutil.copyfile(dst,src)
        return True

    # now, compare
    failed = []
    for src,dst in output:
        if filecmp.cmp(src,dst) == 0:
            failed.append([src, dst])
    if len(failed) > 0:
        if not diff:
            print('Found %u badly indented files:' % len(failed))
            for src,dst in failed:
                print('  ' + src)
        else:
            for src,dst in failed:
                s = open(src, 'r').readlines()
                d = open(dst, 'r').readlines()
                for line in difflib.unified_diff(s, d, fromfile=src, tofile=dst):
                    sys.stdout.write(line)
        return False
    return True

def run_as_hg_hook(ui, repo, **kwargs):
    # hack to work around mercurial < 1.3 bug
    from mercurial import lock, error
    lock.LockError = error.LockError
    # actually do the work
    files = hg_modified_files()
    if not indent_files(files, inplace=False):
        return True
    return False

def run_as_main():
    parser = optparse.OptionParser()
    parser.add_option('--debug', action='store_true', dest='debug', default=False,
                      help='Output some debugging information')
    parser.add_option('-l', '--level', type='int', dest='level', default=0,
                      help="Level of style conformance: higher levels include all lower levels. "
                      "level=0: re-indent only. level=1: add extra spaces. level=2: insert extra newlines and "
                      "extra braces around single-line statements. level=3: remove all trailing spaces")
    parser.add_option('--check-hg-hook', action='store_true', dest='hg_hook', default=False, 
                      help='Get the list of files to check from mercurial\'s list of modified '
                      'and added files and assume that the script runs as a pretxncommit mercurial hook')
    parser.add_option('--check-hg', action='store_true', dest='hg', default=False,
                      help="Get the list of files to check from mercurial\'s list of modified and added files")
    parser.add_option('-f', '--check-file', action='store', dest='file', default='',
                      help="Check a single file")
    parser.add_option('--diff', action='store_true', dest='diff', default=False,
                      help="Generate a diff on stdout of the indented files")
    parser.add_option('-i', '--in-place', action='store_true', dest='in_place', default=False,
                      help="Indent the input files in-place")
    (options,args) = parser.parse_args()
    debug = options.debug
    if options.hg_hook:
        files = hg_modified_files()
        if not indent_files(files, debug=options.debug,
                            level=options.level,
                            inplace=False):
            sys.exit(1)
    elif options.hg:
        files = hg_modified_files()
        indent_files(files, diff=options.diff, 
                     debug=options.debug,
                     level=options.level,
                     inplace=options.in_place)
    elif options.file != '':
        file = options.file
        if not os.path.exists(file) or \
                not os.path.isfile(file):
            print('file %s does not exist' % file)
            sys.exit(1)
        indent_files([file], diff=options.diff, 
                     debug=options.debug,
                     level=options.level,
                     inplace=options.in_place)
    sys.exit(0)

if __name__ == '__main__':
#    try:
        run_as_main()
#    except Exception, e:
#        sys.stderr.write(str(e) + '\n')
#        sys.exit(1)
