# 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/.

"""
user preferences
"""

__all__ = ('PreferencesReadError', 'Preferences')

import json
import mozfile
import os
import re
import tokenize
from ConfigParser import SafeConfigParser as ConfigParser
from StringIO import StringIO

class PreferencesReadError(Exception):
    """read error for prefrences files"""


class Preferences(object):
    """assembly of preferences from various sources"""

    def __init__(self, prefs=None):
        self._prefs = []
        if prefs:
            self.add(prefs)

    def add(self, prefs, cast=False):
        """
        :param prefs:
        :param cast: whether to cast strings to value, e.g. '1' -> 1
        """
        # wants a list of 2-tuples
        if isinstance(prefs, dict):
            prefs = prefs.items()
        if cast:
            prefs = [(i, self.cast(j)) for i, j in prefs]
        self._prefs += prefs

    def add_file(self, path):
        """a preferences from a file

        :param path:
        """
        self.add(self.read(path))

    def __call__(self):
        return self._prefs

    @classmethod
    def cast(cls, value):
        """
        interpolate a preference from a string
        from the command line or from e.g. an .ini file, there is no good way to denote
        what type the preference value is, as natively it is a string

        - integers will get cast to integers
        - true/false will get cast to True/False
        - anything enclosed in single quotes will be treated as a string with the ''s removed from both sides
        """

        if not isinstance(value, basestring):
            return value # no op
        quote = "'"
        if value == 'true':
            return  True
        if value == 'false':
            return False
        try:
            return int(value)
        except ValueError:
            pass
        if value.startswith(quote) and value.endswith(quote):
            value = value[1:-1]
        return value


    @classmethod
    def read(cls, path):
        """read preferences from a file"""

        section = None # for .ini files
        basename = os.path.basename(path)
        if ':' in basename:
            # section of INI file
            path, section = path.rsplit(':', 1)

        if not os.path.exists(path) and not mozfile.is_url(path):
            raise PreferencesReadError("'%s' does not exist" % path)

        if section:
            try:
                return cls.read_ini(path, section)
            except PreferencesReadError:
                raise
            except Exception, e:
                raise PreferencesReadError(str(e))

        # try both JSON and .ini format
        try:
            return cls.read_json(path)
        except Exception, e:
            try:
                return cls.read_ini(path)
            except Exception, f:
                for exception in e, f:
                    if isinstance(exception, PreferencesReadError):
                        raise exception
                raise PreferencesReadError("Could not recognize format of %s" % path)


    @classmethod
    def read_ini(cls, path, section=None):
        """read preferences from an .ini file"""

        parser = ConfigParser()
        parser.readfp(mozfile.load(path))

        if section:
            if section not in parser.sections():
                raise PreferencesReadError("No section '%s' in %s" % (section, path))
            retval = parser.items(section, raw=True)
        else:
            retval = parser.defaults().items()

        # cast the preferences since .ini is just strings
        return [(i, cls.cast(j)) for i, j in retval]

    @classmethod
    def read_json(cls, path):
        """read preferences from a JSON blob"""

        prefs = json.loads(mozfile.load(path).read())

        if type(prefs) not in [list, dict]:
            raise PreferencesReadError("Malformed preferences: %s" % path)
        if isinstance(prefs, list):
            if [i for i in prefs if type(i) != list or len(i) != 2]:
                raise PreferencesReadError("Malformed preferences: %s" % path)
            values = [i[1] for i in prefs]
        elif isinstance(prefs, dict):
            values = prefs.values()
        else:
            raise PreferencesReadError("Malformed preferences: %s" % path)
        types = (bool, basestring, int)
        if [i for i in values
            if not [isinstance(i, j) for j in types]]:
            raise PreferencesReadError("Only bool, string, and int values allowed")
        return prefs

    @classmethod
    def read_prefs(cls, path, pref_setter='user_pref', interpolation=None):
        """
        Read preferences from (e.g.) prefs.js

        :param path: The path to the preference file to read.
        :param pref_setter: The name of the function used to set preferences
                            in the preference file.
        :param interpolation: If provided, a dict that will be passed
                              to str.format to interpolate preference values.
        """

        comment = re.compile('/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/', re.MULTILINE)

        marker = '##//' # magical marker
        lines = [i.strip() for i in mozfile.load(path).readlines() if i.strip()]
        _lines = []
        for line in lines:
            if line.startswith(('#', '//')):
                continue
            if '//' in line:
                line = line.replace('//', marker)
            _lines.append(line)
        string = '\n'.join(_lines)
        string = re.sub(comment, '', string)

        # skip trailing comments
        processed_tokens = []
        f_obj = StringIO(string)
        for token in tokenize.generate_tokens(f_obj.readline):
            if token[0] == tokenize.COMMENT:
                continue
            processed_tokens.append(token[:2]) # [:2] gets around http://bugs.python.org/issue9974
        string = tokenize.untokenize(processed_tokens)

        retval = []
        def pref(a, b):
            if interpolation and isinstance(b, basestring):
                b = b.format(**interpolation)
            retval.append((a, b))
        lines = [i.strip().rstrip(';') for i in string.split('\n') if i.strip()]

        _globals = {'retval': retval, 'true': True, 'false': False}
        _globals[pref_setter] = pref
        for line in lines:
            try:
                eval(line, _globals, {})
            except SyntaxError:
                print line
                raise

        # de-magic the marker
        for index, (key, value) in enumerate(retval):
            if isinstance(value, basestring) and marker in value:
                retval[index] = (key, value.replace(marker, '//'))

        return retval

    @classmethod
    def write(cls, _file, prefs, pref_string='user_pref(%s, %s);'):
        """write preferences to a file"""

        if isinstance(_file, basestring):
            f = file(_file, 'a')
        else:
            f = _file

        if isinstance(prefs, dict):
            # order doesn't matter
            prefs = prefs.items()

        # serialize -> JSON
        _prefs = [(json.dumps(k), json.dumps(v) )
                  for k, v in prefs]

        # write the preferences
        for _pref in _prefs:
            print >> f, pref_string % _pref

        # close the file if opened internally
        if isinstance(_file, basestring):
            f.close()
