#!/usr/bin/python3
# coding=utf-8
# Copyright © 2007–2009, 2011, 2013, 2014 Apple Inc. All rights reserved.

import re
import sys
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 expression keys:
# comment = list of strings that start line comments
# class = 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)

rubyExpressions = {
    "comment": ["#"],
#   class MyInspector < OSX::NSObject
#   class MyOpenGLView <  NSOpenGLView
    "class": (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
    "outlet": (re.compile('\s+ib_outlets\s+(?P<OUTLETS>:.*).*'), lambda outlets: [anOutlet.split(':',1)[1] for anOutlet in outlets.split(',')]),
    "action": (None, re.compile('\s+ib_action\s*\(*:(?P<ACTION>\w+).*'), None),
}
pythonExpressions = {
    "comment": ["#"],
# class FooBarAppDelegate(NSObject):
# class FooBarAppDelegate(NSObject, MyOtherClass):
    "class": (re.compile('class\s+(?P<CLASS>\w+)\s*\(\s*(?P<SUPERCLASS>\w+)'), None),
    "outlet": (re.compile('\s*(?P<OUTLETS>\w+)\s*=\s*.*IBOutlet\s*\('), lambda outlet: [outlet]),
    "action": (re.compile('\s+@[^\s]*IBAction'), re.compile('\s*def\s+(?P<ACTION>\w+)\s*\('), lambda action: action.rsplit("_",1)[0]),
}
applescriptExpressions = {
    "comment": ["#", "--"],
# script Foobar
#     property parent: class "NSObject"  ## optional; assume NSObject if not present.
	"class": (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
	"outlet": (re.compile('\s+property\s+\|?(?P<OUTLETS>\w+)\|?\s*:\s*missing value'), lambda outlet: [outlet]),

#	on myAction_(sender)
#	on myAction:sender
#	on |miAcción|:sender
### AppleScript turns my_cool_action:sender into my_cool_action_(sender), so don’t accept underscores in event names.
    "action": (None, re.compile('\s+(?:on|to)\s+(?P<ACTION>[A-Z][A-Z0-9]*|\|\w+\|)(?:_\s*\(\s*(?:[A-Z][A-Z0-9_]*|\|\w+\|)\s*\)|\s*:\s*(?:[A-Z][A-Z0-9_]*|\|\w+\|)\s*$)', re.I | re.U), lambda action: action.strip("|")),
}

extensionsToExpressionsMap = {
    ".py" : pythonExpressions,
    "python" : pythonExpressions,
    ".rb" : rubyExpressions,
    "ruby" : rubyExpressions,
    ".applescript" : applescriptExpressions,
    "applescript" : applescriptExpressions,
}

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

def fileReader(aFile, expressions):
    if not isinstance(aFile, (str, os.PathLike)):
        openedFile = aFile
    else:
        openedFile = open(aFile, 'r')
    for aLine in openedFile:
        trimmedLine = aLine
        for delimiter in expressions["comment"]:
            trimmedLine = trimmedLine.split(delimiter, 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, expressions = setupProcessingForFile(aFile, forcedMode)
    currentClass = None
    for linePair in fileByLine:
        if processPossibleClassLine(linePair, expressions["class"]): continue
        if processPossibleOutletLine(linePair, expressions["outlet"]): continue
        if processPossibleActionLine(linePair, expressions["action"]): continue
        if superVerbose: print("[   --   ] %s" % linePair[1].rstrip('\n'))

def dumpElem(anElement):
    elementType = type(anElement)
    if elementType in [list, tuple]:
        dumpArray(anElement)
    elif elementType is dict:
        dumpDict(anElement)
    elif elementType is str:
        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) == 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:])
