#!/usr/bin/env python

import re
import sys
import types
import os
from os import path
from optparse import OptionParser

parser = OptionParser("usage: %prog <options> [files]")

superVerbose = False
parseErrors = False

#{
#    IBClasses =     (
#                {
#            ACTIONS =             {
#                changeTransparency = id;
#            };
#            CLASS = Controller;
#            LANGUAGE = ObjC;
#            OUTLETS =             {
#                itsWindow = id;
#            };
#            SUPERCLASS = NSObject;
#        }
#    );
#}

# language regular expressions = (class decl expr, outlet expr, action expr)
# where
# class decl = expr to match class and superclass
# outlet = (match potentially multiple exprs on line, func to break match into array of individual outlets)
# action = (hit this first expr, this is the def expr, clean up func)

rubyRegularExpressions = (
#   class MyInspector < OSX::NSObject
#   class MyOpenGLView <  NSOpenGLView
    (re.compile('class\s+(?P<CLASS>\w+)\s*<\s*((\w+::(?P<PKGSUPERCLASS>\w+))|(?P<SUPERCLASS>\w+))'), None),

#   ib_outlets :mailboxTable, :emailTable, :previewPane, :emailStatusLine, :mailboxStatusLine
#   ib_outlet :stickiesController
    (re.compile('\s+ib_outlets\s+(?P<OUTLETS>:.*).*'), lambda outlets: [anOutlet.split(':',1)[1] for anOutlet in outlets.split(',')]),
    (None, re.compile('\s+ib_action\s*\(*:(?P<ACTION>\w+).*'), None)
)
pythonRegularExpressions = (
# class FooBarAppDelegate(NSObject):
# class FooBarAppDelegate(NSObject, MyOtherClass):
    (re.compile('class\s+(?P<CLASS>\w+)\s*\(\s*(?P<SUPERCLASS>\w+)'), None),
    (re.compile('\s*(?P<OUTLETS>\w+)\s*=\s*.*IBOutlet\s*\('), lambda outlet: [outlet]),
    (re.compile('\s+@[^\s]*IBAction'), re.compile('\s*def\s+(?P<ACTION>\w+)\s*\('), lambda action: action.rsplit("_",1)[0])
)
applescriptRegularExpressions = (
# script Foobar
#     property parent: class "NSObject"  ## optional; assume NSObject if not present.
	(re.compile('script\s+(?P<CLASS>\w+)'), re.compile('\s+property\s+parent\s*:\s*class\s+\"(?P<SUPERCLASS>\w+)\"')),
### This is kind of sneaky: AppleScript puts the class and superclass definitions on different lines, but
### we're processing the file one line at a time.  Therefore, recognize either kind of line, and sort it
### out in processPossibleClassLine().

#	property myoutlet: missing value
	(re.compile('\s+property\s+\|?(?P<OUTLETS>\w+)\|?\s*:\s*missing value'), lambda outlet: [outlet]),

#	on myAction_(sender)
	(None, re.compile('\s+(on|to)\s+(?P<ACTION>\w+_)\(\w+\)'), lambda action: action.rsplit("_",1)[0])
)

extensionsToExpressionsMap = {
".py" : pythonRegularExpressions,
"python" : pythonRegularExpressions,
".rb" : rubyRegularExpressions,
"ruby" : rubyRegularExpressions,
".applescript" : applescriptRegularExpressions,
"applescript" : applescriptRegularExpressions
}

def setupProcessingForFile(aFile, forcedMode = None):
    
    if forcedMode:
        if forcedMode not in extensionsToExpressionsMap:
            parser.error("mode should be one of %s" % extensionsToExpressionsMap.keys())
            sys.exit(1)
        classREPair, outletREPair, actionRE = extensionsToExpressionsMap[forcedMode]
    else:
        root, extension = path.splitext(aFile)
        if extension and (extension in extensionsToExpressionsMap):
            classREPair, outletREPair, actionRE = extensionsToExpressionsMap[extension]
        
    return fileReader(aFile), classREPair, outletREPair, actionRE

def fileReader(aFile):
    if type(aFile) == types.FileType:
        openedFile = aFile
    else:
        openedFile = file(aFile, 'r')
    for aLine in openedFile:
        trimmedLine = aLine.split("#", 1)[0]
        yield trimmedLine, aLine
    openedFile.close()

collectedClasses = []
currentClass = None
lookingForActionDefinition = False
lookingForSuperclassDefinition = False

def processPossibleClassLine(linePair, classREPair):
    global currentClass, collectedClasses, lookingForActionDefinition, lookingForSuperclassDefinition
    trimmedLine, originalLine = linePair
    classRE, superclassRE = classREPair

    m = classRE.match(trimmedLine)
    if m:
        if superVerbose: print "[ class  ] %s" % originalLine.rstrip('\n')

        lookingForActionDefinition = False
        currentClass = m.groupdict()
        if 'SUPERCLASS' not in currentClass:
            currentClass['SUPERCLASS'] = 'NSObject'
            lookingForSuperclassDefinition = True
            # special for AppleScript: can't define superclasses, so presume NSObject.

        if 'PKGSUPERCLASS' in currentClass:
            if currentClass['PKGSUPERCLASS'] is not None:
                currentClass['SUPERCLASS'] = currentClass['PKGSUPERCLASS']
            del currentClass['PKGSUPERCLASS']
		
        currentClass['LANGUAGE'] = 'ObjC'
        currentClass['ACTIONS'] = []
        currentClass['OUTLETS'] = []
        collectedClasses.append(currentClass)
        return True

    if lookingForSuperclassDefinition:
        m = superclassRE.match(trimmedLine)
        if m:
            if superVerbose: print "[ parent ] %s" % originalLine.rstrip('\n')
            currentClass['SUPERCLASS'] = m.group('SUPERCLASS')
            lookingForSuperclassDefinition = False
            return True

def processPossibleOutletLine(linePair, outletREPair):
    global currentClass, parseErrors
    trimmedLine, originalLine = linePair
    outletRE, outletSplitter = outletREPair
    m = outletRE.match(trimmedLine)
    if m is None:
        return False
    else:
        if superVerbose: print "[ outlet ] %s" % originalLine.rstrip('\n')

    outlets = outletSplitter(m.group('OUTLETS'))
    if superVerbose:
        print "outlets " % outlets

    if not currentClass:
        parseErrors = True
        if superVerbose:
            print "***error: found outlets without a current class bucket to shove it into!! BOO!! -- ", outlets
        return True

    for anOutlet in outlets:
        currentClass['OUTLETS'].append( {anOutlet : 'id'} )
    return True

def processPossibleActionLine(linePair, actionRETuple):
    global currentClass, lookingForActionDefinition, parseErrors
    trimmedLine, originalLine = linePair
    contextExpr, defExpr, cleanupFunc = actionRETuple
    if not contextExpr:
        lookingForActionDefinition = True

    if not lookingForActionDefinition:
        m = contextExpr.match(trimmedLine)
        if m is None:
            return False
        else:
            if superVerbose: print "[ action ] %s" % originalLine.rstrip('\n')
        lookingForActionDefinition = True
        return True
        
    else:
        m = defExpr.match(trimmedLine)
        if m is None:
            return False
        else:
            if superVerbose: print "[ action ] %s" % originalLine.rstrip('\n')
    
        action = m.group('ACTION')
    
        if not currentClass:
            parseErrors = True
            if superVerbose:
                print "***error: found actions without a current class bucket to shove it into!! BOO!! -- ", action
            return True
        
        if cleanupFunc:
            action = cleanupFunc(action)
    
        currentClass['ACTIONS'].append( {action : 'id'} )
        lookingForActionDefinition = False
        return True

def processFile(aFile, forcedMode = None):
    global currentClass
    fileByLine, classREPair, outletREPair, actionRETuple = setupProcessingForFile(aFile, forcedMode)
    currentClass = None
    for linePair in fileByLine:
        if processPossibleClassLine(linePair, classREPair): continue
        if processPossibleOutletLine(linePair, outletREPair): continue
        if processPossibleActionLine(linePair, actionRETuple): continue
        if superVerbose: print "[   --   ] %s" % linePair[1].rstrip('\n')

def dumpElem(anElement):
    elementType = type(anElement)
    if elementType in [types.ListType, types.TupleType]:
        dumpArray(anElement)
    elif elementType is types.DictType:
        dumpDict(anElement)
    elif elementType is types.StringType:
        dumpString(anElement)
    else:
        if superVerbose:
            print "BARF: ", elementType

def dumpString(anElement):
    print "<string>%s</string>" % anElement

def dumpDict(aDict):
    print "<dict>"
    for k in aDict:
        print "<key>%s</key>" % k
        dumpElem(aDict[k])
    print "</dict>"

def dumpArray(anArray):
    print "<array>"
    for e in anArray:
        dumpElem(e)
    print "</array>"

def dumpAsClassesNibPListGoop(collectedClasses):
    print """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IBClasses</key>"""
    dumpElem(collectedClasses)
    print """<key>IBVersion</key>
	<string>1</string>
</dict>
</plist>"""

def main(argv):
    global parseErrors
    parser.add_option('-m', '--mode',
                      dest="mode",
                      type="string",
                      action="store",
                      help="Lock the mode.  Required when parsing from stdin.")
    (globalOptions, args) = parser.parse_args(argv)
    if len(args) is 0:
        if not globalOptions.mode:
            parser.print_usage()
            sys.exit(1)
        processFile(sys.stdin, globalOptions.mode)
    else:
        for aFile in argv:
            processFile(aFile, globalOptions.mode)
    
    dumpAsClassesNibPListGoop(collectedClasses)
    if parseErrors:
        System.exit(1)

if __name__ == "__main__":
    main(sys.argv[1:])
