#
# Chris Lumens <clumens@redhat.com>
#
# Copyright 2005, 2006, 2007 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use, modify,
# copy, or redistribute it subject to the terms and conditions of the GNU
# General Public License v.2.  This program is distributed in the hope that it
# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  Any Red Hat
# trademarks that are incorporated in the source code or documentation are not
# subject to the GNU General Public License and may only be used or replicated
# with the express permission of Red Hat, Inc. 
#
"""
Specialized option handling.

This module exports two classes:

    KSOptionParser - A specialized subclass of OptionParser to be used
                     in BaseHandler subclasses.

    KSOption - A specialized subclass of Option.
"""
import warnings
from copy import copy
from optparse import *

from constants import *
from errors import *
from version import *

import gettext
_ = lambda x: gettext.ldgettext("pykickstart", x)

class KSOptionParser(OptionParser):
    """A specialized subclass of optparse.OptionParser to handle extra option
       attribute checking, work error reporting into the KickstartParseError
       framework, and to turn off the default help.
    """
    def exit(self, status=0, msg=None):
        pass

    def error(self, msg):
        if self.lineno != None:
            raise KickstartParseError(formatErrorMsg(self.lineno, msg=msg))
        else:
            raise KickstartParseError(msg)

    def keys(self):
        retval = []

        for opt in self.option_list:
            if opt not in retval:
                retval.append(opt.dest)

        return retval

    def _init_parsing_state (self):
        OptionParser._init_parsing_state(self)
        self.option_seen = {}

    def check_values (self, values, args):
        def seen(self, option):
            return self.option_seen.has_key(option)

        def usedTooNew(self, option):
            return option.introduced and option.introduced > self.version

        def usedDeprecated(self, option):
            return option.deprecated

        def usedRemoved(self, option):
            return option.removed and option.removed <= self.version

        for option in filter(lambda o: isinstance(o, Option), self.option_list):
            if option.required and not seen(self, option):
                raise KickstartValueError(formatErrorMsg(self.lineno, _("Option %s is required") % option))
            elif seen(self, option) and usedTooNew(self, option):
                mapping = {"option": option, "intro": versionToString(option.introduced),
                           "version": versionToString(self.version)}
                self.error(_("The %(option)s option was introduced in version %(intro)s, but you are using kickstart syntax version %(version)s.") % mapping)
            elif seen(self, option) and usedRemoved(self, option):
                mapping = {"option": option, "removed": versionToString(option.removed),
                           "version": versionToString(self.version)}

                if option.removed == self.version:
                    self.error(_("The %(option)s option is no longer supported.") % mapping)
                else:
                    self.error(_("The %(option)s option was removed in version %(removed)s, but you are using kickstart syntax version %(version)s.") % mapping)
            elif seen(self, option) and usedDeprecated(self, option) and self.version >= option.deprecated:
                mapping = {"lineno": self.lineno, "option": option}
                warnings.warn(_("Ignoring deprecated option on line %(lineno)s:  The %(option)s option has been deprecated and no longer has any effect.  It may be removed from future releases, which will result in a fatal error from kickstart.  Please modify your kickstart file to remove this option.") % mapping, DeprecationWarning)

        return (values, args)

    def parse_args(self, *args, **kwargs):
        if kwargs.has_key("lineno"):
            self.lineno = kwargs.pop("lineno")

        return OptionParser.parse_args(self, **kwargs)

    def __init__(self, mapping=None, version=None):
        """Create a new KSOptionParser instance.  Each KickstartCommand
           subclass should create one instance of KSOptionParser, providing
           at least the lineno attribute.  mapping and version are not required.
           Instance attributes:

           mapping -- A mapping from option strings to different values.
           version -- The version of the kickstart syntax we are checking
                      against.
        """
        OptionParser.__init__(self, option_class=KSOption,
                              add_help_option=False,
                              conflict_handler="resolve")
        if mapping is None:
            self.map = {}
        else:
            self.map = mapping

        self.lineno = None
        self.option_seen = {}
        self.version = version

def _check_ksboolean(option, opt, value):
    if value.lower() in ("on", "yes", "true", "1"):
        return True
    elif value.lower() in ("off", "no", "false", "0"):
        return False
    else:
        mapping = {"opt": opt, "value": value}
        raise OptionValueError(_("Option %(opt)s: invalid boolean value: %(value)r") % mapping)

def _check_string(option, opt, value):
    if len(value) > 2 and value.startswith("--"):
        mapping = {"opt": opt, "value": value}
        raise OptionValueError(_("Option %(opt)s: invalid string value: %(value)r") % mapping)
    else:
        return value

# Creates a new Option class that supports several new attributes:
# - required:  any option with this attribute must be supplied or an exception
#              is thrown
# - introduced:  the kickstart syntax version that this option first appeared
#                in - an exception will be raised if the option is used and
#                the specified syntax version is less than the value of this
#                attribute
# - deprecated:  the kickstart syntax version that this option was deprecated
#                in - a DeprecationWarning will be thrown if the option is
#                used and the specified syntax version is greater than the
#                value of this attribute
# - removed:  the kickstart syntax version that this option was removed in - an
#             exception will be raised if the option is used and the specified
#             syntax version is greated than the value of this attribute
# Also creates a new type:
# - ksboolean:  support various kinds of boolean values on an option
# And two new actions:
# - map :  allows you to define an opt -> val mapping such that dest gets val
#          when opt is seen
# - map_extend:  allows you to define an opt -> [val1, ... valn] mapping such
#                that dest gets a list of vals built up when opt is seen
class KSOption (Option):
    ATTRS = Option.ATTRS + ['introduced', 'deprecated', 'removed', 'required']
    ACTIONS = Option.ACTIONS + ("map", "map_extend",)
    STORE_ACTIONS = Option.STORE_ACTIONS + ("map", "map_extend",)

    TYPES = Option.TYPES + ("ksboolean", "string")
    TYPE_CHECKER = copy(Option.TYPE_CHECKER)
    TYPE_CHECKER["ksboolean"] = _check_ksboolean
    TYPE_CHECKER["string"] = _check_string

    def _check_required(self):
        if self.required and not self.takes_value():
            raise OptionError(_("Required flag set for option that doesn't take a value"), self)

    # Make sure _check_required() is called from the constructor!
    CHECK_METHODS = Option.CHECK_METHODS + [_check_required]

    def process (self, opt, value, values, parser):
        Option.process(self, opt, value, values, parser)
        parser.option_seen[self] = 1

    # Override default take_action method to handle our custom actions.
    def take_action(self, action, dest, opt, value, values, parser):
        if action == "map":
            values.ensure_value(dest, parser.map[opt.lstrip('-')])
        elif action == "map_extend":
            values.ensure_value(dest, []).extend(parser.map[opt.lstrip('-')])
        else:
            Option.take_action(self, action, dest, opt, value, values, parser)

    def takes_value(self):
        # Deprecated options don't take a value.
        return Option.takes_value(self) and not self.deprecated

    def __init__(self, *args, **kwargs):
        self.deprecated = False
        self.required = False
        Option.__init__(self, *args, **kwargs)
