# Copyright 2008 John Keyes (john@keyes.ie)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Configuration classes to provide access to config files.

FileConfig parses a configuration file.
StringConfig parses a configuration string.

The config file format is name value pairs, where the names are
strings and the values are expressions.

Examples:
  first_name = 'John'
  second_name = Keyes
  full_name = ${first_name} ${second_name}
  age = 10
  cost = 99.99
  cost_per_year = ${cost} / ${age}
"""
import logging
import re

#logging.basicConfig(level=logging.DEBUG)

class Config(dict):
    """
    A Config class which provides access to the
    configuration variables either as attributes
    or as dictionary keys.
    """

    # regular expressions for matching variables
    VAR_STR = '\$\{(.*?)\}'
    RE_HAS_VAR_REF = re.compile('.*%s.*' % VAR_STR)
    RE_VAR_REF = re.compile(VAR_STR)

    def __init__(self, config_str):
        dict.__init__(self)
        self.config_str = config_str 
        if config_str:
            self._evaluated = []
            self._depends = {}
            self._globals = {'__builtins__': None}
            self._locals = {}
            self._parse_config()

    def _parse_config(self):
        """
        Parse the config string.
        """
        for line in self.config_str.split('\n'):
            line = line.strip()
            if line and line[0] != '#':
                key, value = line.split('=')
                key = key.strip()
                value = value.strip()
 
                if not Config.RE_HAS_VAR_REF.match(value):
                    # no variable references e.g. ${var}, so we evaluate
                    # the expression to get value into the correct 
                    # Python type
                    try:
                        value = eval(value.strip(), self._globals, self._locals)
                        # we evaluated it successfully so record it as such
                        self._evaluated.append(key)
                    except SyntaxError:
                        # if there is a syntax error we'll just use the 
                        # variable as a string
                        value = value.strip()
                self[key.strip()] = value

        self._check_dependencies()

        # finished parsing lets evaluate each variable
        while len(self.keys()) != len(self._evaluated):
            for var_name in self.keys():
                self._get_var_value(var_name)

    def _check_dependencies(self):
        """
        Make sure the dependency tree is valid
        """
        # build the variable dependency lists
        variable_names = self.keys()

        for name in variable_names:
            self._build_dependency_hierarchy(name)

        # now check for cyclical references and unknown variables
        for name in variable_names:
            self._assert_variable_references(name, name)

    def _build_dependency_hierarchy(self, name):
        """
        Creates a dependency hierarchy.
        """
        # every variable gets a dependency list even though
        # it may be empty
        if name not in self._depends:
            self._depends[name] = []

        # only process unevaluated variables
        if not self._is_evaluated(name):
            # get the value which will contain the ${} references
            value = self[name]
            # extract reference names from value
            for match in  Config.RE_VAR_REF.finditer(value):
                v_name = match.group(1)
                self._depends[name].append(v_name)

    def _assert_variable_references(self, root_variable, dependency):
        """
        If a cycle is detected in the variable dependencies raise
        a KeyError exception as no mapping for that variable can
        be resolved.  Also raise a KeyError If a variable is 
        referenced but cannot be found.
        """
        depends_on = self._depends[dependency]
        # self reference
        if root_variable in depends_on:
            raise KeyError, "A variable cannot refer to itself"

        # check each variable that root_variable depends on
        for var in depends_on:
            if not self._has_variable(var): 
                # no variable could be found for the reference
                raise KeyError, "No such variable %s" % var

            if root_variable in self._depends[var]:
                # found root_variable depends on itself
                raise KeyError, "Get a life, not a cyclical reference"

            # check to see if the variable  is also in the 
            # dependencies list
            for next_dependency in self._depends[var]:
                self._assert_variable_references(root_variable, next_dependency)

 
    def _get_var_value(self, name):
        """
        Returns the actual value of a variable i.e. with 
        variable references replaced by values.
        """
        # evaluate the variable value if required
        if not self._is_evaluated(name): 
            value = self._evaluate_var(name)
        else:
            value = self[name]
        return value

    def _evaluate_var(self, name):
        """
        Evaluate the named variable.
        """
        value = self[name]

        # this variable has dependencies which we need to resolve first
        if name in self._depends:
            depends = self._depends[name]
            # for each depends variable replace it's reference with 
            # it's actual value
            for ref_name in depends:
                ref_value = self[ref_name]
                value = value.replace('${%s}' % ref_name, str(ref_value))

        # if all of the dependencies have been resolved get the correct type
        if not _needs_expansion(value):
            try:
                value = eval(value.strip(), self._globals, self._locals)
            except SyntaxError:
                # just use the stripped value if there's an error in 
                # the expression
                value = value.strip()
            # mark this variable as being evaluated
            self._evaluated.append(name)
            # replace value
            self[name] = value
        return value

    def __getattr__(self, name):
        """
        Provide attribute access to configuration variables.
        """
        return self[name]

    def _is_evaluated(self, variable):
        """
        Returns whether the specified variable has been fully evaluated.
        """
        return variable in self._evaluated

    def _has_variable(self, variable):
        """
        Returns whether the specified variable is in this config.
        """
        return variable in self

class FileConfig(Config):
    def __init__(self, filepath):
        config_file = open(filepath)
        config_str = config_file.read()
        config_file.close()
        Config.__init__(self, config_str)

def _needs_expansion(value):
    """
    Returns whether the specified value needs expansion.
    """
    return Config.RE_HAS_VAR_REF.match(value) is not None


