#!/usr/bin/env python
"""Calculate cyclomatic complexity metric of a C/C++ file"""

#When you have ready about the graph thoery behind CCM you would this code to
#be very complicated. It isn't because what it boils down is counting the
#number of "conditionals" (if, while ,,,), which can be done easily by a bit
#of preprocessing (comments, strings, ...)"

from __future__ import with_statement
import re

patterns=[
    "\\bif\\s*\\(",
    "\\bfor\\s*\\(",
    "\\bwhile\\s*\\(",
    "\\bswitch\\s*\\(",
    "\\case\\s*",
    "\\bvirtual\\s*",
    "\\bgoto\\s*"
]

compiled_patterns=[]
for p in patterns:
    compiled_patterns.append(re.compile(p))


def strip_comments_and_strings(l,in_multiline_comment):
    #remove comments and strings from line so they don't interfere with our patterns
    output_string=""
    pos=0
    in_string=False
    while pos<len(l):
        if in_multiline_comment:
            next_pos=l.find("*/",pos)
            if next_pos!=-1:
                pos=next_pos+2
                in_multiline_comment=False
            else:
                pos=len(l)
        elif in_string:
            if l[pos]=='"':
                in_string=False
                output_string+=l[pos]
                pos+=1
            elif l[pos:pos+2]=='\\"':
                output_string+=l[pos:pos+2]
                pos+=2
            else:
                #output_string+=l[pos]
                pos+=1
        else:
            if l[pos:pos+2]=="//":
                pos=len(l)
            elif l[pos:pos+2]=="/*":
                in_multiline_comment=True
                pos+=2
            elif l[pos]=='"':
                in_string=True
                output_string+=l[pos]
                pos+=1
            else:
                output_string+=l[pos]
                pos+=1
        
    return (output_string,in_multiline_comment)


def calculate_line_ccm(l):
    if len(l)==0:
        return 0
    ccm=0
    for p in compiled_patterns:
        if p.search(l)!=None:
            ccm+=1
    return ccm


def calculate_ccm(filename):
    """Calculate CCM for the given filename.
    Returns a tuple of (ccm,nonblank_line_count)
    If the file type is unsupported None is returned."""
    
    with open(filename) as file_handle:
        in_multiline_comment=False
        ccm=1
        nonblank_lines=0
        for l in file_handle.readlines():
            (l,in_multiline_comment)=strip_comments_and_strings(l,in_multiline_comment)
            if len(l.strip())!=0:
                nonblank_lines=nonblank_lines+1
            ccm += calculate_line_ccm(l)
    return (ccm,nonblank_lines)


import sys
if __name__ == "__main__":
    assert strip_comments_and_strings("",False) == ("",False)
    assert strip_comments_and_strings("abcd",False) == ("abcd",False)
    assert strip_comments_and_strings("",True) == ("",True)
    assert strip_comments_and_strings("abcd",True) == ("",True)
    
    assert strip_comments_and_strings("abcd/*efgh",False) == ("abcd",True)
    assert strip_comments_and_strings("abcd/*efgh*/ijkl",False) == ("abcdijkl",False)
    assert strip_comments_and_strings("abcdefgh*/ijkl",True) == ("ijkl",False)

    assert strip_comments_and_strings("abcd//efgh",False) == ("abcd",False)
    assert strip_comments_and_strings("abcd//*efgh",False) == ("abcd",False)

    assert strip_comments_and_strings('printf("hello world")',False) == ('printf("")',False)
    assert strip_comments_and_strings('printf("hello \\"world\\"")',False) == ('printf("\\"\\"")',False)

    assert strip_comments_and_strings('printf("hello /*world*/")',False) == ('printf("")',False)

    assert calculate_line_ccm("") == 0
    assert calculate_line_ccm("printf(\"Hello world\");") == 0
    assert calculate_line_ccm("for(int x=0; x<10; x++) {") == 1
    assert calculate_line_ccm("for (int x=0; x<10; x++) {") == 1
    assert calculate_line_ccm("while(true) {") == 1
    assert calculate_line_ccm("} while(true);") == 1
    assert calculate_line_ccm("while(true);") == 1
    assert calculate_line_ccm("if(true) {") == 1
    assert calculate_line_ccm("motif(true);") == 0

    #And a small useful command-line invokation
    if len(sys.argv)<2:
        print "usage: <file>"
        sys.exit(99)
    result = calculate_ccm(sys.argv[1])
    print result
    sys.exit(0)
