import string, UserList
from distutils.fancy_getopt import wrap_text

class Options(UserList.UserList):
    def __init__(self, data=None):
        UserList.UserList.__init__(self, data)
            
        # Sanity check the supplied options
        shorts = []
        longs = []
        for opt in self:
            if not isinstance(opt, BaseOption):
                raise TypeError("Options %s is not of BaseOption" % opt)
            (short, long) = opt.getForGetOpt({}, {})
            for s in short:
                if s != ':' and s in shorts:
                    raise Exception('Duplicate short option in %s' % str(self))
                shorts.append(s)
            for l in long:
                if l in longs:
                    raise Exception('Duplicate long option in %s' % str(self))
                longs.append(l)
                
    def findMaxOption(self, level=0):
        max_opt = 0
        for option in self:
            l = option.displayLength()

            # add two spaces for each level of sub-options
            l = l + 2*level

            if hasattr(option, 'subOptions'):
                sublen = option.subOptions.findMaxOption(level+1)
                if sublen > l:
                    l = sublen
                
            if l > max_opt:
                max_opt = l

        return max_opt

    def generate_help(self, level=1, max_opt=0):
        """Generate help text (a list of strings, one per suggested line of
        output) from the option table for this FancyGetopt object.
        """
        # If max_opt is > 0, this is help for a sub-option
        # maximum width has already been determined.
        if max_opt > 0:
            # The indent for sub-options is included in option length
            opt_width = max_opt - 2*(level-1)
        else:
            opt_width = max_opt = self.findMaxOption()

        # room for indent + dashes + longest option + gutter
        col_width = 2*level + 2 + max_opt + 2
            
        # Typical help block looks like this:
        #   --foo       controls foonabulation
        # Help block for longest option looks like this:
        #   --flimflam  set the flim-flam level
        # and with wrapped text:
        #   --flimflam  set the flim-flam level (must be between
        #               0 and 100, except on Tuesdays)
        # Options with short names will have the short name shown (but
        # it doesn't contribute to max_opt):
        #   --foo (-f)  controls foonabulation
        # If adding the short option would make the left column too wide,
        # we push the explanation off to the next line
        #   --flimflam (-l)
        #               set the flim-flam level
        # Important parameters:
        #   - 2 spaces before option block start lines
        #   - 2 dashes for each long option name
        #   - min. 2 spaces between option and explanation (gutter)

        # Now generate lines of help text.
        line_width = 78
        text_width = line_width - col_width
        indent = '  ' * level
        big_indent = ' ' * (col_width)
        lines = []

        for option in self:
            if isinstance(option, ExclusiveOptions):
                lines.extend(option.choices.generate_help(level, max_opt))
                continue

            text = wrap_text(option.description, text_width)

            if option.takesArg:
                long_opt = '%s=<%s>' % (option.longName, option.argName)
                short_opt = '%s<%s>' % (option.shortName, option.argName)
            else:
                long_opt = option.longName
                short_opt = option.shortName

            # If there is a short option, display it above the long opt
            if option.shortName:
                lines.append('%s-%s' % (indent, short_opt))

            line = "%s--%-*s" % (indent, opt_width, long_opt)
            if text:
                lines.append('%s  %s' % (line, text[0]))
            else:
                lines.append(line)

            # Add any description that didn't fit on the first line
            for line in text[1:]:
                lines.append(big_indent + line)

            if isinstance(option, TypedOption):
                for (val, desc) in option.allowed:
                    text = wrap_text(desc, text_width)
                    lines.append('%s    %-*s%s' % (indent, opt_width, val, text[0]))
                    for line in text[1:]:
                        lines.append(big_indent + line)
                        
            if hasattr(option, 'subOptions'):
                lines.extend(option.subOptions.generate_help(level + 1, max_opt))

        return lines


    def generate_html_help(self,parentTable):
        """Generate help html
        """
        for option in self:
            option.generate_html_help(parentTable)


class BaseOption:

    def validate(self):
        return

    def getForGetOpt(self, short2long, takes_arg):
        raise NotImplementedError('subclass must override')

    def displayLength(self):
        raise NotImplementedError('subclass %s must override' % self.__class__)

    def gen_command_line(self):
        raise NotImplementedError('subclass %s must override' % self.__class__)

    def gen_description(self):
        raise NotImplementedError('subclass %s must override' % self.__class__)

    def apply_options(self):
        raise NotImplementedError('subclass %s must override' % self.__class__)

    def isApplied(self):
        raise NotImplementedError('subclass %s must override' % self.__class__)

    def getName(self):
        raise NotImplementedError('subclass %s must override' % self.__class__)

    def __str__(self):
        return self.gen_command_line()
    __repr__ = __str__

class Option(BaseOption):
    def __init__(self, shortName, longName, description, subOptions=None):
        # Type- and value-check the option names
        if len(longName) < 2:
            raise SyntaxError('invalid long option: ' + longName)
        if shortName is not None and len(shortName) != 1:
            raise SyntaxError('invalid short option: ' + shortName)

        self.shortName = shortName or ''
        i = string.find(longName, '=')
        if i > 0:
            self.takesArg = 1
            argName = longName[i+1:]
            longName = longName[:i]
            self.argName = argName or longName
        else:
            self.takesArg = 0
        self.longName = longName
        self.description = description
        
        if not isinstance(subOptions, Options):
            subOptions = Options(subOptions)
        self.subOptions = subOptions

    def getForGetOpt(self, short2long, takes_arg):
        short_opts = self.shortName
        if self.takesArg:
            if short_opts:
                short_opts = short_opts + ':'
            long_opts = [self.longName + '=']
        else:
            long_opts = [self.longName]

        takes_arg[self.longName] = self.takesArg
        if self.shortName:
            short2long[self.shortName] = self.longName
        
        # get getopt options for any sub-options
        for option in self.subOptions:
            (short, long) = option.getForGetOpt(short2long, takes_arg)
            short_opts = short_opts + short
            long_opts.extend(long)

        return (short_opts, long_opts)

    def displayLength(self):
        l = len(self.longName)
        if self.takesArg:
            # add the "=<argName>" length
            l = l + 3 + len(self.argName)
        return l

    def validate(self):
        for option in self.subOptions:
            option.validate()

        for option in self.subOptions:
            if option.isApplied() and not self.applied:
                raise CommandLineUtil.ArgumentError("%s specified without %s" % (option.getName(),self.longName))
        return

    def apply_options(self, options):
        self.applied = options.has_key(self.longName)
        for option in self.subOptions:
            option.apply_options(options)
        return


    def isApplied(self):
        return self.applied

    def getName(self):
        return self.longName

    def gen_command_line(self):
        cl = '[--%s' % self.longName
        if self.takesArg:
            cl = cl + '=<%s>' % self.argName
        if self.subOptions:
            sub = map(lambda s: s.gen_command_line(), self.subOptions)
            if len(sub) > 1:
                cl = cl + ' [%s]' % string.join(sub, ' ')
            else:
                cl = cl + ' ' + sub[0]
        return cl + ']'



    def generate_html_help(self,parentTable):
        doc = parentTable.ownerDocument
        tr = doc.createElement('TR')
        parentTable.appendChild(tr)

        if self.takesArg:
            long_opt = '%s=<%s>' % (self.longName, self.argName)
            short_opt = '%s<%s>' % (self.shortName, self.argName)
        else:
            long_opt = self.longName
            short_opt = self.shortName

        # If there is a short self, display it above the long opt
        if self.shortName:
            td = doc.createElement('TD')
            td.appendChild(doc.createTextNode("-%s"%short_opt))
            tr.appendChild(td)
            td = doc.createElement('TD')
            td.rowSpan = '2'
            td.appendChild(doc.createTextNode(self.description))
            tr.appendChild(td)

            tr = doc.createElement('TR')
            parentTable.appendChild(tr)
            td = doc.createElement('TD')
            td.appendChild(doc.createTextNode("--%s"%long_opt))
            tr.appendChild(td)
        else:
            td = doc.createElement('TD')
            td.appendChild(doc.createTextNode("--%s"%long_opt))
            tr.appendChild(td)
            td = doc.createElement('TD')
            td.appendChild(doc.createTextNode(self.description))
            tr.appendChild(td)




class TypedOption(Option):
    def __init__(self, shortName, longName, description, allowed, subOptions=None):
        Option.__init__(self, shortName, longName, description, subOptions)
        self.allowedValues = map(lambda (value, desc): value, allowed)
        self.allowed = allowed

    def apply_options(self, options):
        self.applied = options.get(self.longName)
        for option in self.subOptions:
            option.apply_options(options)
        return

    def validate(self):
        if self.applied and self.applied not in self.allowedValues:
            import string
            expected = string.join(self.allowedValues, ', ')
            raise SyntaxError('option %s: expected %s, got %s' %
                              (self.longName, expected, self.applied))

        Option.validate(self)
        return

    def gen_command_line(self):
        sub = ''
        for option in self.subOptions:
            sub = sub + '%s ' % option.gen_command_line()
        av = '['
        for a in self.allowedValues:
            av = av + a
            if a != self.allowedValues[-1]:
                av = av + '|'
        av =av + ']'
        if sub:
            return '[--%s=%s [%s]]' % (self.longName,av,sub)
        else:
            return '[--%s=%s]' % (self.longName,av)


class ExclusiveOptions(BaseOption):
    def __init__(self, choices):
        if not isinstance(choices, Options):
            choices = Options(choices)
        self.choices = choices

    def getForGetOpt(self, short2long, takes_arg):
        short_opts = ''
        long_opts = []
        # get getopt options for all choices
        for option in self.choices:
            (short, long) = option.getForGetOpt(short2long, takes_arg)
            short_opts = short_opts + short
            long_opts.extend(long)

        return (short_opts, long_opts)

    def displayLength(self):
        return self.choices.findMaxOption()

    def validate(self):
        # make sure only one of our choices is in the options

        applied = 0
        for opt in self.choices:
            if opt.isApplied():
                if applied:
                    opts = string.join(map(lambda x:x.getName(),self.choices),',')
                    raise CommandLineUtil.ArgumentError("Only one of %s allowed" % opts)
                applied = opt
        if applied:
            #NOTE, this allows some sub commands to sneak in
            applied.validate()
        else:
            #Validate them all
            for option in self.choices:
                option.validate()

    def apply_options(self, options):
        for option in self.choices:
            option.apply_options(options)
        return

    def isApplied(self):
        for option in self.choices:
            if option.isApplied(): return 1
        return 0 
        

    def getName(self):
        return '(' + string.join(map(lambda x:x.getName(),self.choices),',') + ')'


    def gen_command_line(self):
        cl = '['
        first = 1
        for c in self.choices:
            if not first:
                cl = cl + ' | '
            else:
                first = 0
            cl = cl + c.gen_command_line()
        return cl + ']'


    def generate_html_help(self,parentTable):
        for c in self.choices:
            c.generate_html_help(parentTable)
