# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import ntpath
import os
import posixpath
import re

from os.path import relpath

from Preprocessor import Preprocessor

from ..util import (
    ensureParentDir,
    FileAvoidWrite,
)


RE_SHELL_ESCAPE = re.compile('''([ \t`#$^&*(){}\\|;'"<>?\[\]])''')


def shell_escape(s):
    """Escape some characters with a backslash, and double dollar signs."""
    return RE_SHELL_ESCAPE.sub(r'\\\1', str(s)).replace('$', '$$')


class ConfigEnvironment(object):
    """Perform actions associated with a configured but bare objdir.

    The purpose of this class is to preprocess files from the source directory
    and output results in the object directory.

    There are two types of files: config files and config headers,
    each treated through a different member function.

    Creating a ConfigEnvironment requires a few arguments:
      - topsrcdir and topobjdir are, respectively, the top source and
        the top object directory.
      - defines is a list of (name, value) tuples. In autoconf, these are
        set with AC_DEFINE and AC_DEFINE_UNQUOTED
      - non_global_defines are a list of names appearing in defines above
        that are not meant to be exported in ACDEFINES and ALLDEFINES (see
        below)
      - substs is a list of (name, value) tuples. In autoconf, these are
        set with AC_SUBST.

    ConfigEnvironment automatically defines two additional substs variables
    from all the defines not appearing in non_global_defines:
      - ACDEFINES contains the defines in the form -DNAME=VALUE, for use on
        preprocessor command lines. The order in which defines were given
        when creating the ConfigEnvironment is preserved.
      - ALLDEFINES contains the defines in the form #define NAME VALUE, in
        sorted order, for use in config files, for an automatic listing of
        defines.
    and another additional subst variable from all the other substs:
      - ALLSUBSTS contains the substs in the form NAME = VALUE, in sorted
        order, for use in autoconf.mk. It includes ACDEFINES, but doesn't
        include ALLDEFINES.

    ConfigEnvironment expects a "top_srcdir" subst to be set with the top
    source directory, in msys format on windows. It is used to derive a
    "srcdir" subst when treating config files. It can either be an absolute
    path or a path relative to the topobjdir.
    """

    def __init__(self, topsrcdir, topobjdir, defines=[], non_global_defines=[],
            substs=[]):

        self.defines = dict(defines)
        self.substs = dict(substs)
        self.topsrcdir = topsrcdir
        self.topobjdir = topobjdir
        global_defines = [name for name, value in defines
            if not name in non_global_defines]
        self.substs['ACDEFINES'] = ' '.join(['-D%s=%s' % (name,
            shell_escape(self.defines[name])) for name in global_defines])
        self.substs['ALLSUBSTS'] = '\n'.join(sorted(['%s = %s' % (name,
            self.substs[name]) for name in self.substs]))
        self.substs['ALLDEFINES'] = '\n'.join(sorted(['#define %s %s' % (name,
            self.defines[name]) for name in global_defines]))

    def get_relative_srcdir(self, file):
        '''Returns the relative source directory for the given file, always
        using / as a path separator.
        '''
        assert(isinstance(file, basestring))
        dir = posixpath.dirname(relpath(file, self.topobjdir).replace(os.sep, '/'))
        if dir:
            return dir
        return '.'

    def get_top_srcdir(self, file):
        '''Returns a normalized top_srcdir for the given file: if
        substs['top_srcdir'] is a relative path, it is relative to the
        topobjdir. Adjust it to be relative to the file path.'''
        top_srcdir = self.substs['top_srcdir']
        if posixpath.isabs(top_srcdir) or ntpath.isabs(top_srcdir):
            return top_srcdir
        return posixpath.normpath(posixpath.join(self.get_depth(file), top_srcdir))

    def get_file_srcdir(self, file):
        '''Returns the srcdir for the given file, where srcdir is in msys
        format on windows, thus derived from top_srcdir.
        '''
        dir = self.get_relative_srcdir(file)
        top_srcdir = self.get_top_srcdir(file)
        return posixpath.normpath(posixpath.join(top_srcdir, dir))

    def get_depth(self, file):
        '''Returns the DEPTH for the given file, that is, the path to the
        object directory relative to the directory containing the given file.
        Always uses / as a path separator.
        '''
        return relpath(self.topobjdir, os.path.dirname(file)).replace(os.sep, '/')

    def get_input(self, file):
        '''Returns the input file path in the source tree that can be used
        to create the given config file or header.
        '''
        assert(isinstance(file, basestring))
        return os.path.normpath(os.path.join(self.topsrcdir, "%s.in" % relpath(file, self.topobjdir)))

    def create_config_file(self, path):
        '''Creates the given config file. A config file is generated by
        taking the corresponding source file and replacing occurences of
        "@VAR@" by the value corresponding to "VAR" in the substs dict.

        Additional substs are defined according to the file being treated:
            "srcdir" for its the path to its source directory
            "relativesrcdir" for its source directory relative to the top
            "DEPTH" for the path to the top object directory
        '''
        input = self.get_input(path)
        pp = Preprocessor()
        pp.context.update(self.substs)
        pp.context.update(top_srcdir = self.get_top_srcdir(path))
        pp.context.update(srcdir = self.get_file_srcdir(path))
        pp.context.update(relativesrcdir = self.get_relative_srcdir(path))
        pp.context.update(DEPTH = self.get_depth(path))
        pp.do_filter('attemptSubstitution')
        pp.setMarker(None)
        with FileAvoidWrite(path) as pp.out:
            pp.do_include(input)

    def create_config_header(self, path):
        '''Creates the given config header. A config header is generated by
        taking the corresponding source file and replacing some #define/#undef
        occurences:
            "#undef NAME" is turned into "#define NAME VALUE"
            "#define NAME" is unchanged
            "#define NAME ORIGINAL_VALUE" is turned into "#define NAME VALUE"
            "#undef UNKNOWN_NAME" is turned into "/* #undef UNKNOWN_NAME */"
            Whitespaces are preserved.
        '''
        with open(self.get_input(path), 'rU') as input:
            ensureParentDir(path)
            output = FileAvoidWrite(path)
            r = re.compile('^\s*#\s*(?P<cmd>[a-z]+)(?:\s+(?P<name>\S+)(?:\s+(?P<value>\S+))?)?', re.U)
            for l in input:
                m = r.match(l)
                if m:
                    cmd = m.group('cmd')
                    name = m.group('name')
                    value = m.group('value')
                    if name:
                        if name in self.defines:
                            if cmd == 'define' and value:
                                l = l[:m.start('value')] + str(self.defines[name]) + l[m.end('value'):]
                            elif cmd == 'undef':
                                l = l[:m.start('cmd')] + 'define' + l[m.end('cmd'):m.end('name')] + ' ' + str(self.defines[name]) + l[m.end('name'):]
                        elif cmd == 'undef':
                           l = '/* ' + l[:m.end('name')] + ' */' + l[m.end('name'):]

                output.write(l)
            output.close()

