# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
import i_need_py23

"""Gestion des arguments de la ligne de commande.
"""

__all__ = ('split_args', 'join_args', 'build_options', 'get_args',
           'Options',
           )

try: True, False
except: True, False = 1, 0

import sys, re
from getopt import gnu_getopt

from base import isstr, isbool, seqof, odict
from output import set_verbosity, VERBOSITY_OPTS
from input import set_interaction, INTERACTION_OPTS
from functions import apply_args

RE_SPACES = re.compile(r'[ \t\r\n]+')
RE_QUOTE = re.compile(r'"')
RE_QQUOTE = re.compile(r'\\"')
RE_SPACES_OR_QUOTES = re.compile(r'[ \t\r\n"]+')
RE_TOKEN = re.compile(r'[^ \t\r\n"]+')
RE_QTOKEN = re.compile(r'"((?:\\"|[^"])*)"?')

def has_spaces(cl):
    return RE_SPACES.match(cl) is not None
def skip_spaces(pcl):
    mo = RE_SPACES.match(pcl[0])
    if mo is not None:
        pcl[0] = pcl[0][mo.end(0):]
def get_token(pcl):
    token = None
    mo = RE_TOKEN.match(pcl[0])
    if mo is not None:
        token, pcl[0] = pcl[0][:mo.end(0)], pcl[0][mo.end(0):]
    return token
def get_qtoken(pcl):
    qtoken = None
    mo = RE_QTOKEN.match(pcl[0])
    if mo is not None:
        qtoken, pcl[0] = mo.group(1), pcl[0][mo.end(0):]
    return qtoken

def split_args(cl):
    """Lire une chaine, et la découper en plusieurs arguments, à utiliser par
    exemple avec getopt() ou get_args().

    Note: les arguments peuvent être entre quotes, mais pour le moment, seul "
    est supporté, pas '.
    XXX ajouter le support de ' comme quote.

    @return: la liste des arguments, ou None si cl==None
    @rtype: list
    """
    if cl is None: return None

    args = []
    pcl = [cl]
    while pcl[0]:
        if has_spaces(pcl[0]):
            skip_spaces(pcl)
        if not pcl[0]:
            break

        arg = ''
        while pcl[0] and not has_spaces(pcl[0]):
            if pcl[0][:1] == '"':
                arg = arg + RE_QQUOTE.sub('"', get_qtoken(pcl))
            else:
                arg = arg + get_token(pcl)

        args.append(arg)

    return args

def join_args(args):
    """L'opération inverse de split_args

    @return: une chaine, ou None si args==None
    """
    if args is None: return None
    i = 0
    for i in range(len(args)):
        arg = args[i]
        if not args or RE_SPACES_OR_QUOTES.search(arg) is not None:
            args[i] = '"%s"' % RE_QUOTE.sub(r'\"', arg)
    return ' '.join(args)

def build_options(argsdesc):
    """Construire une liste d'options pour utilisation avec get_args ou getopt.

    A partir d'une liste de termes (option, longoptions, desc), construire et
    retourner (options, longoptions), où options est un chaine et longoptions
    une liste, pour utilisation avec getopt() ou get_args().

    @return: (options, longoptions)
    @rtype: tuple
    """
    options = ''
    longoptions = []
    if argsdesc is not None:
        for argdesc in argsdesc:
            if argdesc[0:1] and argdesc[0] is not None:
                options += argdesc[0]
            if argdesc[1:2] and argdesc[1] is not None:
                longopts = argdesc[1]
                if isstr(longopts): longopts = (longopts,)
                longoptions.extend(filter(None, longopts))
    return options, longoptions

# options courtes à faire traiter par set_verbosity() ou set_interaction()
M_OPTIONS = {}
# options longues à faire traiter par set_verbosity() ou set_interaction()
M_LONGOPTIONS = {}
for _opt in VERBOSITY_OPTS:
    if _opt.startswith('--'): M_LONGOPTIONS[_opt] = False
    elif _opt.startswith('-'): M_OPTIONS[_opt] = False
for _opt in INTERACTION_OPTS:
    if _opt.startswith('--'): M_LONGOPTIONS[_opt] = False
    elif _opt.startswith('-'): M_OPTIONS[_opt] = False
del _opt

RE_OPTION = re.compile(r'.:?')
def get_args(args=None, options=None, longoptions=None, **optdescs):
    """frontend pour getopt qui reconnait les options de set_verbosity et
    set_interaction(), et mets à jour les niveaux automatiquement.
    """
    if args is None: args = sys.argv[1:]
    if options is None: options = ''
    longoptions = seqof(longoptions, [])

    options = RE_OPTION.findall(options)
    longoptions = list(longoptions)

    def in_options(opt, options=options):
        """Retourner True si l'option opt est mentionnée dans options, sans
        tenir compte du fait qu'elle prend ou non un argument dans options.

        Si opt n'est pas mentionné dans options, l'y rajouter.
        opt doit être de la forme 'o' ou 'o:'
        """
        normopt = opt[:1]
        for option in options:
            normoption = option[:1]
            if normopt == normoption: return True
        options.append(opt)
        return False
    def in_longoptions(longopt, longoptions=longoptions):
        """Retourner True si l'option longue longopt est mentionnée dans
        longoptions, sans tenir compte du fait qu'elle prend ou non un argument
        dans longoptions.

        Si longopt n'est pas mentionné dans longoptions, l'y rajouter.
        longopt doit être de la forme 'longopt' ou 'longopt='
        """
        if longopt[-1:] == '=': normlongopt = longopt[:-1]
        else: normlongopt = longopt
        for longoption in longoptions:
            if longoption[-1:] == '=': normlongoption = longoption[:-1]
            else: normlongoption = longoption
            if normlongopt == normlongoption: return True
        longoptions.append(longopt)
        return False

    # déterminer quelles options seront reconnues par set_verbosity. il s'agit
    # de toutes celles qui ne sont pas traitées par l'utilisateur
    m_options = M_OPTIONS.copy()
    m_longoptions = M_LONGOPTIONS.copy()

    for m_option in m_options.keys():
        # m_option est de la forme '-o'
        if not in_options(m_option[1:]):
            m_options[m_option] = True
    for m_longoption in m_longoptions.keys():
        # m_longoption est de la forme '--longopt'
        if not in_longoptions(m_longoption[2:]):
            m_longoptions[m_longoption] = True

    # appliquer les options reconnues par set_verbosity
    options = ''.join(options)
    optvalues, args = gnu_getopt(args, options, longoptions)
    for i in range(len(optvalues)):
        opt, _ = optvalues[i]
        set_verbosity_or_interaction = False
        if m_longoptions.get(opt, False): # long options
            set_verbosity_or_interaction = True
        elif m_options.get(opt, False): # options
            set_verbosity_or_interaction = True
        if set_verbosity_or_interaction:
            if opt in VERBOSITY_OPTS:
                set_verbosity(opt)
            elif opt in INTERACTION_OPTS:
                set_interaction(opt)
            optvalues[i] = None

    # retourner les autres options qui n'ont pas été reconnues
    return filter(None, optvalues), args

################################################################################

_none = object()

RE_PREFIX = re.compile(r'^-*')
RE_SUFFIX = re.compile(r'[:=]$')
RE_STUFF = re.compile(r'[^a-zA-Z0-9]')
def opt2name(opt):
    """Obtenir un nom de variable dérivé d'un nom d'option

    Les tirets de début et les caractères : et = de fin sont supprimés, et les
    caractères spéciaux sont remplacés par '_'
    """
    name = RE_PREFIX.sub('', opt)
    name = RE_SUFFIX.sub('', name)
    name = RE_STUFF.sub('_', name)
    return name

class Option(object):
    """Un objet stockant la description d'une option unique

    optdef  définition de l'option, e.g. 'o', 'o:', 'long-option', ou
            'long-option='
    optname nom de l'option, e.g. 'o' ou 'long-option'
    short   est-ce une option courte?
    takes_value
            cette option prend-elle un argument?

    action  action associée à cette option.
    name    nom de la variable associée à l'option.
    """

    _short, short = None, property(lambda self: self._short)
    _optdef, optdef = None, property(lambda self: self._optdef)
    _optname, optname = None, property(lambda self: self._optname)
    _takes_value, takes_value = None, property(lambda self: self._takes_value)

    def __init(self, short, optdef, optname, takes_value):
        self._short = short
        self._optdef = optdef
        self._optname = optname
        self._takes_value = takes_value

    _action, action = None, property(lambda self: self._action)
    _name, name = None, property(lambda self: self._name)

    LONGOPTION_PATTERN = r'(([a-zA-Z0-9$*@!_][a-zA-Z0-9$*@!_-]*)=?)'
    RE_LONGOPTION0 = re.compile(r'--%s$' % LONGOPTION_PATTERN)
    RE_LONGOPTION1 = re.compile(r'%s$' % LONGOPTION_PATTERN)
    OPTION_PATTERN = r'(([a-zA-Z0-9$*@!_]):?)'
    RE_OPTION0 = re.compile(r'-%s$' % OPTION_PATTERN)
    RE_OPTION1 = re.compile(r'%s$' % OPTION_PATTERN)

    def __init__(self, optdef):
        if not optdef: raise ValueError("optdef is required")

        mo = self.RE_LONGOPTION0.match(optdef)
        if mo is not None:
            self.__init(False, mo.group(1), mo.group(2), mo.group(1) != mo.group(2))
        else:
            mo = self.RE_OPTION0.match(optdef)
            if mo is not None:
                self.__init(True, mo.group(1), mo.group(2), mo.group(1) != mo.group(2))
            else:
                mo = self.RE_OPTION1.match(optdef)
                if mo is not None:
                    self.__init(True, mo.group(1), mo.group(2), mo.group(1) != mo.group(2))
                else:
                    mo = self.RE_LONGOPTION1.match(optdef)
                    if mo is not None:
                        self.__init(False, mo.group(1), mo.group(2), mo.group(1) != mo.group(2))
                    else:
                        raise ValueError("Invalid option: %s" % optdef)

    def __str__(self):
        prefix = self._short and '-' or '--'
        return '%s%s' % (prefix, self._optname)
    str = __str__
    opt = property(__str__)

    def __repr__(self):
        option = self.__str__()
        if self._takes_value:
            if self._short: option += ':'
            else: option += '='
        return '%s(%s)' % (self.__class__.__name__, repr(option))
    repr = __repr__

    def same_optdef(self, other):
        return isinstance(other, Option) and self._optdef == other.optdef
    def same_optname(self, other):
        return isinstance(other, Option) and \
            self._optname == other.optname and \
            self._takes_value == other.takes_value
    def __eq__(self, other):
        if isstr(other):
            return self.__str__() == other
        elif isinstance(other, Option):
            return self._optdef == other.optdef
        else:
            return False

    def set_action(self, action, name=None):
        self._action = action
        self._name = name

class Action(object):
    """Une action associée à une option quand elle est rencontrée sur la ligne
    de commande.

    name    nom de la variable associée à l'option, None s'il faut le calculer
    initial si une valeur est associée à l'option, valeur initiale de cette
            option.

    Cet objet doit implémenter une méthode __call__() qui prend les arguments
    (option[, value[, options]])
    La méthode doit retourner False si elle veut indiquer qu'elle n'a pas pu
    mettre à jour la valeur. Tout autre valeur indique le succès.

    option est une instance de Option. value est la valeur associée à l'option,
    ou _none si l'option ne prend pas d'argument. options est l'instance de
    l'objet Options qui analyse les arguments.
    """

    name = property(lambda self: None)
    initial = property(lambda self: None)

    def __call__(self, option=None, value=_none, options=None):
        pass

class Options(object):
    """Une classe permettant de traiter des arguments en ligne de commande.

    Son objectif est d'offrir une solution plus flexible que les fonctions
    build_options et get_args()

    Avec le constructeur et la méthode add_option(), il est possible de
    construire la liste des options valides.

    Ensuite, la méthode parse() permet d'analyser la ligne de commande. Par
    défaut, si une méthode n'est pas définie pour une option, ou si la méthode
    définie retourne False, initialiser une variable nommée d'après l'option, en
    remplaçant sa valeur (si l'option prend un argument) ou lui ajoutant 1 (si
    l'option ne prend pas d'argument).
    """

    class SetValue(Action):
        """Mettre à jour une variable

        value   valeur qu'il faut forcer, ou _none s'il faut prendre la valeur par
                défaut. Si l'option prend un argument, la valeur par défaut est la
                valeur spécifiée sur la ligne de commande. Sinon, il s'agit d'une
                valeur incrémentée représentant le nombre de fois que l'option
                apparait.
        name    nom de la variable à initialiser, ou None s'il faut dériver le nom
                de la variable à partir du nom de l'option.
        initial valeur initiale de la variable
        """

        _value = None
        _name, name = None, property(lambda self: self._name)
        _initial, initial = None, property(lambda self: self._initial)

        def __init__(self, value=_none, name=None, initial=None):
            self._value = value
            self._name = name
            self._initial = initial

        def __call__(self, option=None, value=_none, options=None):
            # nom: celui qui est spécifié dans le constructeur, ou un nom dérivé du
            # nom de l'option
            name = self._name
            if name is None: name = opt2name(option.optname)
            # valeur: celle qui est spécifiée dans le constructeur, ou alors laisser
            # options sans charger
            if self._value is not _none: value = self._value

            # mettre à jour la valeur
            options.update_value(option, value)

    class CallMethod(Action):
        _method = None

        def __init__(self, method=None):
            self._method = method

        def __call__(self, option=None, value=None, options=None):
            return apply_args(self._method, option, value, options)

    # type d'analyse: '+' pour s'arrêter à la première non option, '' sinon
    _parseopt = None

    # liste d'options courtes, instances de Option
    _soptions = None

    # liste d'options longues, instances de Option
    _loptions = None

    # valeurs stockées dans cet objet
    _values = None

    # dictionnaire des options définies, avec chacune une instance de Option
    # associée
    _options = None

    ############################################################################
    # Constructeur

    def __init__(self, *optdescs):
        """Initialiser l'objet avec un ensemble d'argument de la forme

            (options, longoptions, desc)

        où options est une chaine avec des lettres de la forme 'o' ou 'o:',
        longoptions une liste de chaines de la forme 'option' ou 'option=', et
        desc une chaine quelconque.

        Ce format est pour assurer la compatibilité avec la fonction
        build_options()
        """
        super(Options, self).__init__()
        object.__setattr__(self, '_parseopt', '')
        object.__setattr__(self, '_soptions', [])
        object.__setattr__(self, '_loptions', [])
        object.__setattr__(self, '_values', {})
        object.__setattr__(self, '_options', {})

        self.add_option(VERBOSITY_OPTS, set_verbosity)
        self.add_option(INTERACTION_OPTS, set_interaction)
        for optdesc in optdescs:
            options = filter(None, optdesc[:2])
            desc = optdesc[2:3] and optdesc[2] or None
            self.add_option(options, None, desc)

    def __option(self, opt):
        """Obtenir l'instance de Option correspondant à l'argument
        """
        if isinstance(opt, Option): return opt
        if not opt.startswith('-'):
            if len(opt) == 1: opt = '-' + opt
            else: opt = '--' + opt
        option = self._options.get(opt, None)
        if option is None: raise ValueError("Unknown option: %s" % opt)
        return option

    def add_option(self, options=None, action=None, desc=None):
        """Ajouter une option

        options peut être une chaine de l'une des formes suivantes:

        '+'    arrêter l'analyse à la première non-option (configuration de gnu_getopt)
        'o', '-o', 'o:', '-o:'
               option courte sans et avec argument
        'longo', '--longo', 'longo=', '--longo='
               option longue sans et avec argument

        options peut aussi être une liste de ces chaines
        """
        default_name = None
        for opt in filter(None, seqof(options, ())):
            # traiter la configuration de l'analyse '+'
            if opt.startswith('+'):
                self._parseopt = '+'
                opt = opt[1:]
                if not opt: continue

            # nom par défaut
            if default_name is None:
                default_name = opt2name(opt)

            # option
            option = Option(opt)

            # action
            if isinstance(action, Action):
                # action déjà spécifiée
                pass
            elif action is None:
                # pas d'action: mettre à jour la variable d'après le nom de la
                # première option
                action = Options.SetValue(name=default_name)
            elif isstr(action):
                # mettre à jour la variable nommée d'après l'action
                action = Options.SetValue(name=action)
            elif callable(action):
                # appeler l'action
                action = Options.CallMethod(action)
            else:
                raise ValueError("Unsupported action: %s" % repr(action))

            name = action.name
            if name is None: name = default_name

            option.set_action(action, name)

            # si une précédente option est définie, il faut la remplacer
            self._soptions = filter(lambda soption: not soption.same_optname(option), self._soptions)
            self._loptions = filter(lambda loption: not loption.same_optname(option), self._loptions)

            # nouvelle option
            if option.short: self._soptions.append(option)
            else: self._loptions.append(option)
            self._options[option.opt] = option

            # valeur initiale
            # ne spécifier la valeur initiale que si elle n'existe pas déjà
            if not self.has_value(option):
                self.set_value(option, action.initial)

        return self

    ############################################################################
    # Gestion des valeurs

    def __getitem__(self, key):
        return self._values[key]
    def __setitem__(self, key, value):
        self._values[key] = value
    def __delitem__(self, key):
        del self._values[key]
    def get(self, key, default=None):
        return self._values.get(key, default)
    def __getattr__(self, key, default=_none):
        try:
            if default is _none: return self._values[key]
            else: return self._values.get(key, default)
        except KeyError: raise AttributeError(key)
    def __setattr__(self, key, value):
        if self._values.has_key(key): self._values[key] = value
        else: return super(Options, self).__setattr__(key, value)
    def __delattr__(self, key):
        try: del self._values[key]
        except KeyError: raise AttributeError(key)

    def get_value(self, option, default=_none):
        """Obtenir la valeur correspondant à l'option
        """
        option = self.__option(option)
        return self.get(option.name, default)
    def has_value(self, option):
        option = self.__option(option)
        return self._values.has_key(option.name)
    def set_value(self, option, value):
        """Spécifier la valeur correspondant à l'option
        """
        option = self.__option(option)
        self._values[option.name] = value
        return True

    def update_value(self, option, value=_none):
        option = self.__option(option)
        if value is _none:
            if option.takes_value:
                raise ValueError("Required value")
            else:
                value = self.get_value(option, None)
                if value is None: value = 0
                self.set_value(option, value + 1)
        else:
            self.set_value(option, value)

    ############################################################################
    # Exploitation

    def get_args(self, args=None):
        """Analyser les arguments à la recherche des options valides. Si
        args==None, prendre sys.argv[1:]

        @return (optvalues, args)

        optvalues est une liste de tuple (opt, value) correspondant à toutes les
        options qui ont été analysées par gnu_getopt(). args est la liste des
        arguments qui ne sont pas des options.
        """
        if args is None: args = sys.argv[1:]
        soptions = self._parseopt + ''.join([option.optdef for option in self._soptions])
        loptions = [option.optdef for option in self._loptions]
        optvalues, args = gnu_getopt(args, soptions, loptions)
        return filter(None, optvalues), args

    _parsed_names = None

    def parse(self, args=None, optvalues=None):
        """Traiter les options analysées par get_args(). Si optvalues==None,
        analyser les arguments de args avec get_args() d'abord.

        @return (roptvalues, args)

        optvalues est une liste de tuple (opt, value) correspondant à toutes les
        options qui ont été analysées, mais n'ont pas pu être traitées par cet
        objet.

        args est la liste des arguments qui ne sont pas des options.
        """
        self._parsed_names = {}
        if optvalues is None: optvalues, args = self.get_args(args)
        roptvalues = []
        for opt, value in optvalues:
            option = self.__option(opt)
            self._parsed_names[option.name] = True
            if not option.takes_value: value = _none
            if option.action(option, value, self) == False:
                roptvalues.append((opt, value))
                self.update_value(option, value)
        return roptvalues, args

    def was_parsed(self, name):
        """Indiquer si une option correspondant à la variable name a été
        mentionnée sur la ligne de commande.
        """
        if self._parsed_names is None: return False
        return self._parsed_names.has_key(name)
