#!/usr/bin/python
# Copyright 2005, Michael O. McCracken

# Version 1.3 - July 26 2005
# Changes since 1.2:
#  - works on more nibs (ones that cause nibtool to produce single
#    line iBDeclaredKeys arrays)
#  - Is smarter about which files it should try to read,and will back
#  up one level if you gave it a nib file instead of the whole nib
#  wrapper.
# - cleaner display
# - cleans up temporary file.
# - fails more gracefully if you don't have pyobjc.

# 1.2 was June 29 2005.
# Changes since 1.1: Tries harder to get useful names for objects.
# Changes since 1.0: bindings are sorted by bindingName in output.

# This work is licensed under the Creative Commons
# Attribution-ShareAlike License. To view a copy of this license,
# visit http://creativecommons.org/licenses/by-sa/2.5/ or send a
# letter to Creative Commons, 559 Nathan Abbott Way, Stanford,
# California 94305, USA.

# Although it is not a requirement of the license, please send
# improvements back to Mike at michael_mccracken@mac.com

import os, sys
try:
    from Foundation import *
except:
    print "This script requires the PyObjC package from http://pyobjc.sf.net/"
    sys.exit(-1)

def getNibDictFromFileName(fileName):
    """
    Mostly just a regex- based sanitizer for nibtool output, this
    function will hopefully not be necessary for long.
    I filed bug 4159819 about nibtool, and it was marked as duplicate recently.
    """
    import sre
    nibtoolcmd = "nibtool -jx"

    f = os.popen(nibtoolcmd + " " + fileName)
    lines = f.readlines()

    shortFileName = os.path.split(fileName)[-1].split('.')[0] + ".nibtool.plist"

    outF = open(shortFileName, "w")

    singleLineParenRE = sre.compile(u"iBDeclaredKeys = \"\((.*)\)\";")
    openParenRE = sre.compile(u"iBDeclaredKeys = \"\(")
    closeParenRE = sre.compile(u"\W+\)\";")

    singleLineBracketRE = sre.compile(u"Options = \"[{](.*)[}]\";")
    openBracketRE = sre.compile(u"Options = \"{")
    closeBracketRE = sre.compile(u"}\";")

    i = 0
    while i < len(lines):
        skipLine = False
        line = lines[i].decode('utf-16') # why does nibtool output utf16?

        (newline, nsubs) = singleLineParenRE.subn(r'iBDeclaredKeys = (\1);', line)
        if nsubs > 0:
            i+= 1
            outF.write(newline.encode('utf-8'))
            continue
        (newline, nsubs) = openParenRE.subn(u"iBDeclaredKeys = (", newline)

        if nsubs != 0:
            outF.write(newline.encode('utf-8'))
            # we are now going through that array
            nsubsInArray = 0
            while nsubsInArray == 0:
                i += 1
                line = unicode(lines[i], 'utf-16')
                (newline, nsubsInArray) = closeParenRE.subn(u");", line)
                outF.write(newline.encode('utf-8'))
                if nsubsInArray:
                    skipLine = True

        (newline, nsubs) = singleLineBracketRE.subn(r'Options = {\1};', line)
        if nsubs > 0:
            i += 1
            outF.write(newline.encode("utf-8"))
            continue
        (newline, nsubs) = openBracketRE.subn(u"Options = {", line)
        if nsubs != 0:
            outF.write(newline.encode('utf-8'))
             # we are now going through that dict
            nsubsInDict = 0
            while nsubsInDict == 0:
                i += 1
                line = unicode(lines[i], 'utf-16')
                (newline, nsubsInDict) = closeBracketRE.subn(u"};", line)
                outF.write(newline.encode('utf-8'))
                if nsubsInDict:
                    skipLine = True

        if not skipLine:
            outF.write(newline.encode("utf-8"))
        # end while i < len(lines):
        i += 1

    outF.close()
    try:
        d = NSDictionary.alloc().initWithContentsOfFile_(shortFileName)
    finally:
        os.unlink(shortFileName)
    return d

class ObjectInfo:
    def __init__(self, dict):
        self.className = dict['Class']
        self.name = self.getName(dict)
        self.declaredKeys = dict.setdefault('iBDeclaredKeys', ())

    def getName(self, d):
        name = ""
        if d.has_key('identifier'):
            name += d['identifier']
        if d.has_key('iBTitle'):
            name += d['iBTitle']
        if d.has_key('title'):
            if d.has_key('Name'):
                name += d['Name']
            else:
                name += d['title']
        if d.has_key('stringValue'):
            name += d['stringValue']

        if name != "":
            return self.className + " \"" + name + "\""
        else:
            # "d['Name'] is usually bad, but we can use it as a last resort
            if d.has_key('Name'):
                return self.className + " \"" + d['Name'] +"\""
            else:
                return "Unnamed %s" % self.className

    def __repr__(self):
        return self.name

class BindingInfo:
    def __init__(self, dict, objects):
        self.dict = dict
        self.controller = objects[dict['Controller']]
        self.object = objects[dict['Object']]
        self.bindingName = dict['Binding']
        self.keyPath = dict['KeyPath']
        self.options = dict.setdefault('Options', {})
        if self.options.has_key('NSDisplayPattern'):
            self.displayPattern = self.options['NSDisplayPattern']
        else:
            self.displayPattern = None
            
    def __repr__(self):
        rep =  "%s -> %s: %s" %\
              (self.bindingName, self.controller.name, self.keyPath)
        return rep

def getBindingsFromDict(d):
    objects = {}
    bindings = {}

    for okey in d['Objects'].keys():
        numberKey = okey.split()[1]
        objects[numberKey] = ObjectInfo(d['Objects'][okey])

    for bkey in d['Connections'].keys():
        numberKey = bkey.split()[1]
        conn = d['Connections'][bkey]
        if conn['Class'] == "NSNibBindingConnector":
            bindings[numberKey] = BindingInfo(conn, objects)

    return bindings

def printInfoForBindings(bindings):
    bindingsByObjectName = {}
    for b in bindings.values():
        bindingsByObjectName.setdefault(b.object.name, [])
        bindingsByObjectName[b.object.name].append(b)

    for objName in bindingsByObjectName.keys():
        bindingsForObj = bindingsByObjectName[objName]
        bindingsToSort = [(b.bindingName, b) for b in bindingsForObj]
        bindingsToSort.sort()
        bindingsForObj = [b[1] for b in bindingsToSort]
        
        print objName, ":"
        for binding in bindingsForObj:
            print " " * 2, binding
        print

   
if __name__ == "__main__":
    if len(sys.argv) < 2:
        print "usage: %s filename(s)" % sys.argv[0]
    for fileName in sys.argv[1:]:

        # warn about non-nib files.
        if os.path.split(fileName)[-1].split('.')[-1] != "nib":
            print '*', fileName, "is not a nib"
            continue

        # be smart about nib files versus whole nibs.
        if not os.path.isdir(fileName):
            print '*', fileName, "is not a nib wrapper."

            enclosingDir = os.path.split(fileName)[0]
            if enclosingDir.split('.')[-1] == "nib":
                fileName = enclosingDir
                print "** using", enclosingDir, "instead"
            else:
                continue

        # ignore backup nibs
        if os.path.split(fileName)[-1].split('.')[0].find("~") != -1:
            continue

        try:
            d = getNibDictFromFileName(fileName)
            bindings = getBindingsFromDict(d)
            if len(bindings) > 0:
                print '*', fileName, ":"
                printInfoForBindings(bindings)
            else:
                print '*', fileName, "has no bindings."
        except Exception, e:
            print "Error:", e
            raise e
