"""
The class ``basesetup.SetupOperation`` can be used as a base-class of a setup
operation which may be called from operasetup.py or as a standalone script.

=================================
Creating your own setup operation
=================================

* Import this package ``basesetup``
* Define a class which is derived from ``basesetup.SetupOperation`` with at
  least two methods ``__init__`` and
  ``__call__(self, sourceRoot, outputRoot=None, quiet=True)``:
* Your ``__init__`` method should call ``SetupOperation.__init__`` with the
  argument message that describes your operation. This message is printed
  when calling ``self.endTiming()``
* Your ``__call__`` method should consist of three steps:

  1. call ``self.startTiming()``.
  2. perform your operation.
  3. return the result of the operation by calling
     ``return self.endTiming(result, quiet=quiet)``.

  The argument ``result`` of the call to ``endTiming()`` is expected to be one
  of the three values 0, 1, 2:

  * 1 shall be used in case of an error. In that case the operasetup process
    will be stopped with a generic error message. Your setup operation is
    expected to print more details about the error by calling
    ``self.error(message)`` (or by printing an error message to ``sys.stderr``)
    before calling ``endTiming()``.
  * 0 shall be used if no files have changed.
  * 2 shall be used if at least one file has changed.

  The return values 0 and 2 are only used to print a message to the user
  "XX modified" versus "No changes to the XX".

  If the argument ``quiet`` of the call to ``endTiming()`` is true, then the
  message for the result value 0 is suppressed to avoid unnecessary output.

Example:

    import basesetup

    class MySetupOperation(basesetup.SetupOperation):
        def __init__(self):
            basesetup.SetupOperation.__init__(self, message="my operation")

        def __call__(self, sourceRoot, outputRoot=None, quiet=True):
            self.startTiming()
            # do something
            if an error happend: result = 1
            elif no files were changed: result = 0
            else (i.e., some files were changed): result = 2
            return self.endTiming(result, quiet=quiet)

===============================
Generating files from templates
===============================

A setup operation is often used to generate some file from a template and some
input data. The module util.py offers a utility function for this:
``util.readTemplate()``. To use that function you should

1. Create a template file.
2. Create an action-handler class which defines a method
   ``__call__(self, action, output)``.
3. call ``util.readTemplate(sourcePath, destinationPath, action_handler)``

1. Template-file
----------------

A template file is simple text-file with embedded action-lines. An action-line
has the format ``// <...>``. Anything between the angle-brackets is an action
name. The action name may contain any characters (even another angle bracket).
The action name is passed to a call of the action-handler, which should write
suitable text to the output, as replacement for the action-line.

@note Only a complete line can be an action-line. It is not possible to tell the
  action-handler to replace a part of a line.

There are a few pre-defined template-actions:

- ``// <copyright header>`` inserts the default Opera copyright header.
- ``// <autogenerated ...>`` inserts the comment
  ``// Please don't modify this file. This file is automatically generated by ...``
- ``// <-*- Mode: ... -*->`` is discarded in processing.  It can be used as a
  mode line to ensure emacs handles the template itself sensibly, without
  duplicating the one that ``// <copyright header>`` adds (for a c++ mode) to
  the generated file.

Example:

  // <-*- Mode: c++; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*->
  // <copyright header>
  // <autogenerated my_script.py>
  #ifndef MY_HEADER_FILE_H
  #define MY_HEADER_FILE_H

  enum MyEnum {
  // <insert my enum values>
      MY_LAST_ENUM_VALUE
  };

  #endif // MY_HEADER_FILE_H

2. Template Action Handler
--------------------------

A template action handler is a class which defines a method
``__call__(self, action, output)``. That method is called for each action-line
that is found in the template file. The method can handle the action by writing
strings to the specified output. If the method handled the action, it should
return True. If the method did not handle the action, it should return False.
If your handler returned False, the default handler handles the action. This
allows your handler to override the pre-defined template-actions.

Example:

    class MyEnumTemplateActionHandler:
        def __init__(self, enum_dict):
            self.__enums = enum_dict

        def __call__(self, action, output):
            if action == "insert my enum values":
                for e in sorted(self.__enums.keys()):
                    output.write("\t%(key)s = %(value)d,\n" % { "key":e, "value":self.__enums[e] }
                return True
            return False

3. Using the template action handler
------------------------------------

Example:

    template_file = os.path.join(sourceRoot, "modules", "my_module", "my_enum_template.h")
    target_file = os.path.join(outputRoot, "modules", "my_module", "my_enum.h")
    enums = { MY_FOO : 1, MY_X : 2, MY_Y : 17 }
    changed = util.readTemplate(template_file, target_file,
                                MyEnumTemplateActionHandler(enums))

util.readTemplate() returns True if the target_file was changed and False if it
was not changed. The target file will only be touched if its content has
changed. Thus generated header files will not cause a recompilation if the
content was not changed.

==============================================
Embedding the setup operation in operasetup.py
==============================================

Currently there is no automated way for operasetup.py to find any setup
operations in different modules. So the setup operation has to be inserted
manually in operasetup.py. To do that:
- Import your python module,
- Optionally add an option to operasetup's option_parser to disable/enable
  calling your setup operation.
- Operasetup.py assigns a list of setup operations in a variable named
  ``system_functions``. Create an instance of your setup operation and append
  that instance to this list.


=============================================
Make your setup operation a standalone script
=============================================

You can easily make your script a standalone script (which can run independently
of operasetup.py). The class OperaSetupOption (provided in this module) offers
the most common command line options like ``--outroot``.

The standalone script should sys.exit() with the exit-code that is used by the
SetupOperation:
- 0 if no files were changed
- 1 in case of an error
- 2 if some files were changed.

However the OperaSetupOption provides an argument ``--make``, which indicates
that the standalone script is executed from a makefile. In that case returning
any value but 0 tells make to abort the make process. That is not wanted if some
files have changed. So if the ``--make`` option was specified and the
SetupOperation returned 2, then the script shall exit with exit-code 0.

For every file your script tries to open, even if only to check for
its existence, as well as for every directory where it enumerates
files, the script must call util.fileTracker.addInput(filename). The
file name can be absolute or relative to the current directory. This
ensures correct generation of the .d (dependency tracking) files when
--timestamp is specified. addInput returns a boolean value indicating
whether the file or directory exists, so you can use the return value
instead of chacking for its existence in your code. The readTemplate
function already calls addInput. At the end of the script, you should
finalize the fileTracker object as shown in the example below.

Example:

    import os.path
    import sys
    if __name__ == "__main__":
        sourceRoot = os.path.normpath(os.path.join(sys.path[0], "..", "..", ".."))
        sys.path.insert(1, os.path.join(sourceRoot, "hardcore", "scripts")

    # import from modules/hardcore/scripts/:
    import util
    import basesetup

    class MyEnumTemplateActionHandler
    ...

    class MySetupOperation(basesetup.SetupOperation):
    ...

    if __name__ == "__main__":
        option_parser = basesetup.OperaSetupOption(
            sourceRoot = sourceRoot,
            description = " ".join(
                ["My setup operation",
                 "The script exists with status 0 if none of the output files",
                 "was changed. The exist status is 2 if at least one of the",
                 "output files was changed (see also option '--make').",
                 "The exit status is 1 if an error was detected."]))
        (options, args) = option_parser.parse_args(args=sys.argv[1:])
        my_setup_operation = MySetupOperation()
        result = my_setup_operation(sourceRoot = sourceRoot,
                                    outputRoot = options.outputRoot,
                                    quiet = options.quiet)
        if options.timestampfile and result != 1:
            util.fileTracker.finalize(options.timestampfile)
        if options.make and result == 2: sys.exit(0)
        else: sys.exit(result)
"""

import optparse
import platform
import os.path
import os
import time
import sys

# importing from modules/hardcore/scripts
import util

class SetupOperation:
    def __init__(self, message, perl=None):
        """Initialises the SetupOperation.

        message -- a text that is printed by endTiming()
        """
        self.__update_operasetup_onchange = False
        self.__start = None
        self.setMessage(message)
        self.__perl = perl

    def logmessage(self, quiet, message):
        if not quiet: print message
    def error(self, message): print >>sys.stderr, "error:", message
    def warning(self, message): print >>sys.stderr, "warning:", message

    def updateOperasetupOnchange(self):
        self.__update_operasetup_onchange = True

    def setMessage(self, message):
        """Update the message endTiming() shall print."""
        self.__message = message

    def findPerl(self, quiet=True):
        """Find a perl executable, if possible.

        Single optional parameter, quiet, defaults to True. If a false value is
        passed, some messages are logged.

        On success, returns the full path of the perl executable found.
        This result is cached, making it cheap to call this method again later.
        On failure, None is returned.
        """
        if self.__perl is None:
            if "PERL" in os.environ and os.access(os.environ["PERL"], os.F_OK | os.X_OK):
                self.__perl = os.environ["PERL"]
                self.logmessage(quiet, "use perl from environment PERL=%s" % self.__perl)

            else:
                # search for a perl executable in the PATH environment
                if platform.system() in ("Windows", "Microsoft"):
                    perl_executable = "perl.exe"
                else:
                    perl_executable = "perl"
                path_env = os.environ.get("PATH", "")
                for path in filter(None, map(str.strip, path_env.split(os.path.pathsep))):
                    if os.access(os.path.join(path, perl_executable), os.F_OK | os.X_OK):
                        self.logmessage(quiet, "found perl in path '%s'" % path)
                        self.__perl = os.path.join(path, perl_executable)
                        break
                else:
                    self.error("can't find suitable perl executable; set the environment variable PERL to the perl executable or adjust your PATH (current value='%s') to include the path to the perl executable or use --perl argument" % path_env)
                    return None

        elif os.access(self.__perl, os.F_OK | os.X_OK):
            self.logmessage(quiet, "use perl from commandline argument --perl=%s" % self.__perl)

        else:
            self.error("invalid --perl argument; path is not an executable file")
            return None

        return self.__perl

    def hasOperaSetup(self, sourceRoot, outputRoot):
        """Test for presence of operasetup.h.

        Requires two parameters:
            sourceRoot -- path to your source tree's root
            outputRoot -- None or path to your output tree's root

        Checks for modules/hardcore/base/operasetup.h under outputRoot, if not
        None, else sourceRoot.  Returns true precisely if it exists.
        """
        opera_setup_h = os.path.join(outputRoot or sourceRoot, 'modules', 'hardcore', 'base', 'operasetup.h')
        return os.path.exists(opera_setup_h)

    def writeOperaSetup(self, sourceRoot, outputRoot):
        """
        Writes a small header file with the current time to
        {outputRoot}/modules/hardcore/base/operasetup.h
        That file's sole purpose is to help dependency resolving. If
        the platform needs it, it should be included from a
        platform-specific header file.

        If outputRoot equals sourceRoot, then modules/hardcore/module.generated
        is updated with the name of the generated file: "base/operasetup.h".
        """
        if outputRoot is None:
            outputRoot = sourceRoot
        opera_setup_h = os.path.join(outputRoot, 'modules', 'hardcore', 'base', 'operasetup.h')
        util.makedirs(os.path.dirname(opera_setup_h))
        f = None
        try:
            f = open(opera_setup_h, "w")
            print >>f, "#ifndef MODULES_HARDCORE_OPERA_OPERASETUP_H"
            print >>f, "#define MODULES_HARDCORE_OPERA_OPERASETUP_H"
            print >>f, "// Modified by operasetup.py %s" % time.ctime()
            print >>f, "#endif // !MODULES_HARDCORE_OPERA_OPERASETUP_H"
        finally:
            if f: f.close()
        if outputRoot == sourceRoot:
            util.updateModuleGenerated(os.path.join(sourceRoot, 'modules', 'hardcore'),
                                       ["base/operasetup.h"])

    def startTiming(self):
        """Should be called at the start of the method ``__call__``.

        This stores a timestamp for the start of the operation.
        See endTiming()
        """
        self.__start = time.time()

    def endTiming(self, result, sourceRoot=None, outputRoot=None, quiet=None):
        """Should be called at the end of the method ``__call__``.

        This prints the message that was specified in the constructor with
        information about the result of the operation.

        The specified result is returned by this function, so you can call it as

            return self.endTiming(result)

        Arguments:
        result --  specifies the result of the operation:
          * Shall be 0 if no files have been changed by the operation. In this
            case "No change to the XX" is printed.
          * Shall be 2 if some files have been changed by the operation. In this
            case "XX modified" is printed.
          * Shall be 1 if the operation could not be completed because of an
            error. In this case "Errors encountered while processing the XX" is
            printed.
          The specified result is returned to the caller.

        sourceRoot -- is only needed if updateOperasetupOnchange() was
          called. This specifies the source-root of the Opera sources. If no
          outputRoot is specified, then the file
          modules/hardcore/base/operasetup.h is created relative to this
          directory. See writeOperaSetup().

        outputRoot -- is only needed if updateOperasetupOnchange() was
          called. This specifies the output-root in which to generate the file
          modules/hardcore/base/operasetup.h. If None, then the file is
          generated relative to the sourceRoot. See writeOperaSetup().

        quiet -- if a false value is passed, report outcome even if nothing was
          modified. If True (the default) nothing is reported unless files have
          changed or an error occurred.
        """
        if self.__start is None: return result
        timing = "%.2gs" % (time.time() - self.__start)
        if result == 2:
            if self.__update_operasetup_onchange: self.writeOperaSetup(sourceRoot, outputRoot)
            print "%s modified (%s)." % (self.__message.capitalize() if self.__message[0].islower() else self.__message, timing)
        elif result == 0:
            if not quiet:
                print "No changes to the %s (%s)." % (self.__message, timing)
        else:
            print "Errors encountered while processing the %s (%s)." % (
                self.__message, timing)
        self.__start = None
        return result

    def callWithTiming(self, sourceRoot=None, feature_def=None, outputRoot=None, quiet=None):
        self.startTiming()
        if feature_def is not None:
            # call with arguments sourceRoot, feature_def, outputRoot, quiet
            result = self(sourceRoot=sourceRoot, feature_def=feature_def, outputRoot=outputRoot, quiet=quiet)
        elif outputRoot is not None:
            # call with argument sourceRoot, outputRoot, quiet
            result = self(sourceRoot=sourceRoot, outputRoot=outputRoot, quiet=quiet)
        else:
            # call with argument sourceRoot, quiet
            result = self(sourceRoot=sourceRoot, quiet=quiet)
        return self.endTiming(result, sourceRoot, outputRoot, quiet)

class OperaSetupOption(optparse.OptionParser):
    """A base class for opera-setup commandline arguments.

    By default the following options are available:

    --outroot=PATH
    --q, --quiet, --no-quiet
    --make

    On creating an instance the options

    --mainline_configuration=[current|next]
    --perl=PATH_TO_EXECUTABLE

    can be enabled.
    """
    def __init__(self, sourceRoot, description=None, withMainlineConfiguration=False, withPerl=False):
        """Initialises the OptionParser instance and adds some default options.

        Arguments:
        sourceRoot -- the root directory to the opera sources.
        description -- (optional) a short description of the script. The
            description is printed when the script is executed with the
            --help argument.
        withMainlineConfiguration -- (optional) can be set to True to add the
            option --mainline_configuration
        withPerl -- (optional) can be set to True to add the option --perl
        """
        optparse.OptionParser.__init__(self, description=description)

        self.add_option(
            "--outroot", metavar="PATH", dest="outputRoot",
            help=" ".join(
                ["Base directory in which to put output files. The default",
                 "value is the same as the source-directory '%default'. You",
                 "can e.g. specify a path which contains the name of the",
                 "configuration you are compiling for, e.g.",
                 "\"../../../opera/profiles/debug_tv/include\".",
                 "The same path should be specified as include path on",
                 "compiling the sources, because some #include files like",
                 "\"modules/hardcore/base/tweaks_and_apis.h\" will be",
                 "generated in that directory."]))

        if withMainlineConfiguration:
            self.add_option(
                "--mainline_configuration", metavar="CONFIGURATION",
                choices=["current", "next"],
                help = " ".join(
                    ["Use the specified mainline configuration. Valid",
                     "configurations are \"next\" and \"current\". If no",
                     "configuration is specified, \"%default\" is assumed.",
                     "The file modules/hardcore/version.ini defines a version",
                     "number for the specified mainline configuration."]))
            self.set_defaults(mainline_configuration="current")

        if withPerl:
            self.add_option(
                "--perl", metavar="PATH_TO_EXECUTABLE",
                help=" ".join(
                    ["Specifies the full path to the Perl executable to use on",
                     "running perl-scripts. If the option is not specified,",
                     "perl is expected to be found where the PERL environment",
                     "variable points to, or in one of the directories",
                     "specified in the PATH environment variable."]))

        self.add_option(
            "-q", "--quiet", dest="quiet", action="store_true")

        self.add_option(
            "--no-quiet", dest="quiet", action="store_false")

        self.add_option(
            "--make", dest="make", action="store_true",
            help=" ".join(
                ["Don't exit with status 2 even if the output file was",
                 "changed. An exit status 0 is used for both cases. This",
                 "indicates that the script was successful and make can",
                 "continue."]))

        self.add_option(
            "--timestamp", metavar="FILE", dest="timestampfile",
            help=" ".join(
                ["Generate two files as a side-effect. FILE will be an empty",
                 "timestamp file which is always updated, even if there are",
                 "no changes to any other generated files. It can be used in",
                 "makefile rules because it will always be newer than the",
                 "prerequisites. Also, FILE.d will be a Makefile snippet",
                 "describing the dependencies of the timestamp file on the",
                 "input files used by the script."]))

        self.set_defaults(outputRoot=sourceRoot, quiet=True, make=False, timestampfile=None)
