import os
import re

def entitize(s):
    s = s.replace('&', '&amp;')
    s = s.replace('>', '&gt;')
    s = s.replace('<', '&lt;')
    s = s.replace('"', '&quot;')
    return s

class JSDocGen(object):
    """Generate documentation from JavaScript source code"""

    hide_private = True # hide methods starting with __
    hide_protected = True # hide methods starting with _

    def __init__(self, *args):
        self._storage = {} # nested mapping filename: {func_name: FunctionObj}
        self._current_func = None;

    reg_function = re.compile((
        '([^\s]*)\s+(=\s*)?function\s*([^\( ]*)\(([^\)]*)\)'
        '\s*\{(\s*)((\/\*)(.*?)(\*\/))?'), 
        re.S | re.M)
    def parse(self, filepath):
        """parse the file and add the results to our mapping"""
        self._current_func = None;
        # read the data
        fp = open(filepath)
        try: data = fp.read()
        finally: fp.close()
        # prepare the storage
        filename = os.path.basename(filepath)
        # now walk through the data until all function definitions are
        # processed
        while 1:
            match = self.reg_function.search(data)
            if not match:
                break
            # we have a match, get the data out of it
            name = match.group(3)
            # if there's no name after the 'function' keyword, it must be
            # before it (sorry, no anonymous function support here yet!)
            to_remove = match.group(0)
            if not name and match.group(1) and match.group(2):
                # before, so remove the part before the name, too
                name = match.group(1).strip()
            elif match.group(2):
                # remove only the function definition: the bit before it might
                # contain part of another func def
                to_remove = to_remove[len(match.group(2))]
            elif not name:
                # anonymous function?
                data = data.replace(to_remove, '')
                continue
            data = data.replace(to_remove, '')
            args = ', '.join([x.strip() for x in 
                                match.group(4).split('/')])
            doc = ''
            if match.group(8):
                doc = match.group(8).strip()
                # strip indentation from lines if necessary
                whitespace = match.group(5)
                if whitespace.find('\n') > -1:
                    whitespace = whitespace.split('\n')[-1]
                    if whitespace:
                        doc = doc.split('\n')
                        stripped = []
                        for line in doc:
                            if line.startswith(whitespace):
                                line = line[len(whitespace):]
                            stripped.append(line)
                        doc = '\n'.join(stripped)
            # continue in another function, mainly to make the source a bit
            # more readable... :\
            self._store_data(filename, name, args, doc)

    reg_methodname = re.compile('([^\.]+)\.prototype\.([^\.]+)')
    def _store_data(self, filename, name, args, doc):
        """store the data of a single function in the storage"""
        if not self._storage.has_key(filename):
            self._storage[filename] = {}
        storage = self._storage[filename]
        # see if we are a method or unbound function definition
        match_name = self.reg_methodname.match(name)
        if match_name:
            # method definition
            classname = match_name.group(1)
            funcname = match_name.group(2)
            if self.should_hide(funcname):
                return
            func_obj = FunctionInstance(funcname)
            if not storage.has_key(classname):
                storage[classname] = FunctionInstance(classname)
            klass = storage[classname]
            klass.type = 'prototype'
            klass.methods.append(func_obj)
        elif name.startswith('this.'):
            name = name[5:]
            if self.should_hide(name):
                return
            func_obj = FunctionInstance(name)
            if not self._current_func:
                win = FunctionInstance('this')
                storage['this'] = win
                win.type = 'prototype'
                storage['this'].methods.append(func_obj)
            else:
                self._current_func.type = 'prototype'
                self._current_func.methods.append(func_obj)
        else:
            # plain function (or classdef)
            if self.should_hide(name):
                return
            func_obj = storage.get(name)
            if func_obj is None:
                func_obj = FunctionInstance(name)
                storage[name] = func_obj
            self._current_func = func_obj
        # set the rest of the properties
        func_obj.arguments = args
        func_obj.docstring = doc

    def should_hide(self, name):
        should_hide = ((self.hide_private and name.startswith('__')) or
                (self.hide_protected and name.startswith('_')))
        return should_hide

    def report(self, reporter):
        """generate a report of our current data using the reporter"""
        keys = self._storage.keys()
        keys.sort()
        for filename in keys:
            functions = self._storage[filename]
            functionnames = functions.keys()
            functionnames.sort()
            for name in functionnames:
                func = functions[name]
                if func.type == 'prototype':
                    reporter.print_proto_def(filename, func.name, 
                                                func.arguments, 
                                                func.docstring)
                    for method in func.methods:
                        reporter.print_method_def(filename, 
                                                    func.name, 
                                                    method.name, 
                                                    method.arguments, 
                                                    method.docstring)
                else:
                    reporter.print_func_def(filename, func.name, 
                                                func.arguments, 
                                                func.docstring)

class FunctionInstance(object):
    """Represents a single function"""

    def __init__(self, name):
        self.name = name
        self.arguments = ''
        self.docstring = ''
        self.type = 'function'
        self.methods = []

class HTMLReporter(object):
    """Generate HTML"""
    def __init__(self, template):
        self.tfp = open(template)
        # to remember which files we've already processed (read: added to the
        # filenames list in ezdata)
        self.processed_files = [] 
        self.templessdata = {
            'filenames': [],
            'functions': [],
            'show_sidebar': False,
        }

    def print_proto_def(self, filename, name, args, doc):
        fname = ''
        if not filename in self.processed_files:
            self.processed_files.append(filename)
            self.templessdata['filenames'].append({
                'fname': filename,
                'anchor': '#%s' % filename,
            })
            fname = filename
        self.templessdata['functions'].append({
            'filename': fname,
            'name': name,
            'args': args,
            'sig': '%s(%s)' % (name, args),
            'doc': doc,
            'type': 'class',
        })

    def print_method_def(self, filename, classname, name, args, doc):
        fname = ''
        if not filename in self.processed_files:
            self.processed_files.append(filename)
            self.templessdata['filenames'].append({
                'fname': filename,
                'anchor': '#%s' % filename,
            })
            fname = filename
        self.templessdata['functions'].append({
            'filename': fname,
            'name': '%s.%s' % (classname, name),
            'args': args,
            'sig': '%s.%s(%s)' % (classname, name, args),
            'doc': doc,
            'type': 'method',
        })

    def print_func_def(self, filename, name, args, doc):
        fname = ''
        if not filename in self.processed_files:
            self.processed_files.append(filename)
            self.templessdata['filenames'].append({
                'fname': filename,
                'anchor': '#%s' % filename,
            })
            fname = filename
        self.templessdata['functions'].append({
            'filename': fname,
            'name': name,
            'args': args,
            'sig': '%s(%s)' % (name, args),
            'doc': doc,
            'type': 'function',
        })

    def output(self):
        # see if we have processed more than one file, if so show sidebar
        self.templessdata['show_sidebar'] = len(self.processed_files) > 1
        import templess
        t = templess.template(self.tfp)
        html = t.render_to_string(self.templessdata)
        return html

class TextReporter(object):
    """Generate plain text"""
    def print_proto_def(self, filename, name, args, doc):
        print 'prototype definition function %s(%s)' % (name, args)
        print doc
        print

    def print_method_def(self, filename, protoname, name, args, doc):
        print '%s.%s = function(%s)' % (protoname, name, args)
        print doc
        print

    def print_func_def(self, filename, name, args, doc):
        print 'function %s(%s)' % (name, args)
        print doc
        print

if __name__ == '__main__':
    import sys, os
    if len(sys.argv) == 1:
        print 'usage: %s <paths to JavaScript files to process>' % sys.argv[0]
        sys.exit(1)
    docgen = JSDocGen()
    for filepath in sys.argv[1:]:
        docgen.parse(filepath)
    templatepath = '%s%stemplate.html' % (
        os.path.dirname(os.path.abspath(__file__)), os.path.sep)
    rep = HTMLReporter(templatepath)
    docgen.report(rep)
    print rep.output()

