# Lazygal, a lazy static web gallery generator.
# Copyright (C) 2011-2012 Alexandre Rossi <alexandre.rossi@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty 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.


import os
import logging
import configparser
import collections
import functools
import json
import copy


USER_CONFIG_PATH = os.path.expanduser('~/.lazygal/config')
DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(__file__),
                                   'defaults.json')


class BetterConfigParser(configparser.RawConfigParser):

    def getint(self, section, option):
        try:
            if not self.getboolean(section, option):
                return False
            else:
                raise ValueError
        except (ValueError, AttributeError):
            return configparser.RawConfigParser.getint(self, section, option)

    def getstr(self, section, option):
        try:
            if not self.getboolean(section, option):
                return False
            else:
                raise ValueError
        except (ValueError, AttributeError):
            return configparser.RawConfigParser.get(self, section, option)

    def getlist(self, section, option):
        str_vlist = self.get(section, option)

        if str_vlist == '':
            return []

        if type(str_vlist) is not list:
            str_vlist = str_vlist.split(',')

        # handle the case several vals were given at once (separated by comas)
        vlist = []
        for v in str_vlist:
            multiple_v = v.split(',')
            for mv in multiple_v:
                vlist.append(mv)

        return vlist

    def load(self, other_config, init=False, sections=None):
        """
        Take another configuration object and overload values in this config
        object.
        """
        all_sections = False
        if sections is None:
            sections = other_config.sections()
            all_sections = True

        for section in sections:
            if all_sections or other_config.has_section(section):
                if not self.has_section(section):
                    if not init:
                        self.new_section_cb(section)
                    self.add_section(section)
                for option in other_config.options(section):
                    if not init and not self.has_option(section, option):
                        self.new_option_cb(section, option)
                    self.set(section, option, other_config.get(section, option))

    def new_section_cb(self, section):
        pass

    def new_option_cb(self, section, option):
        pass


class LazygalConfigDeprecated(BaseException): pass
class NoSectionError(BaseException): pass
class NoOptionError(BaseException): pass


class LazygalIniConfig(BetterConfigParser):

    def check_deprecation(self, config=None):
        if config is None: config = self

        if config.has_section('lazygal'):
            raise LazygalConfigDeprecated("'lazygal' section is deprecated")

    def read(self, filenames):
        conf = BetterConfigParser()
        conf.read(filenames)
        self.load(conf)
        self.check_deprecation()

    def load(self, other_config, init=False, sections=None):
        self.check_deprecation(other_config)
        BetterConfigParser.load(self, other_config, init, sections)

    def new_section_cb(self, section):
        if section != 'template-vars':
            logging.warning(_("  Ignoring unknown section '%s'."), section)

    def new_option_cb(self, section, option):
        if section != 'template-vars':
            logging.warning(_("  Ignoring unknown option '%s' in section '%s'."),
                            option, section)


def false_or(s, f=None):
    if s.lower() in ('no', 'false'):
        return False
    elif f:
        return f(s)
    else:
        return s


def get_bool(s):
    if s.lower() in ('true', 'yes', 'on', '1') or s == 1:
        return True
    if s.lower() in ('false', 'no', 'off', '0') or s == 0:
        return False
    raise ValueError(_("Unknown boolean value '%s'") % s)


def get_list(s):
    if s:
        return [i.strip() for i in s.split(',')]
    return []


def get_dict(s):
    d = {}
    for i in s.split(','):
        key, value = i.split('=')
        d[key.strip()] = value.strip()
    return d


def get_int(s):
    return int(s)


def get_order(s):
    try:
        order, reverse = s.split(':')
    except ValueError:
        order, reverse = s, False
    return {'order': order, 'reverse': reverse == 'reverse'}

def get_image_size(s):
    sizes_defs_str = get_list(s)
    sizes_defs = []
    first = True
    for size_defs in sizes_defs_str:
        try:
            name, defs = size_defs.split('=')
        except ValueError:
            raise ValueError(_("Sizes is a comma-separated list of size names and specs:\n\t e.g. \"small=640x480,medium=1024x768\"."))

        sizes_defs.append({'name': name, 'defs': defs, 'default': first})
        first = False
    return sizes_defs


STRING_TO_JSON = collections.OrderedDict({
    'runtime': {
        'quiet'               : get_bool,
        'debug'               : get_bool,
        'check-all-dirs'      : get_bool,
    },
    'global': {
        'force-gen-pages'     : get_bool,
        'clean-destination'   : get_bool,
        'preserve'            : get_list,
        'dir-flattening-depth': functools.partial(false_or, f=get_int),
        'puburl'              : false_or,
        'exclude'             : get_list,
        'preserve_args'       : get_list,
        'exclude_args'        : get_list,
    },
    'webgal': {
        'image-size'          : get_image_size,
        'thumbs-per-page'     : get_int,
        'filter-by-tag'       : get_list,
        'sort-medias'         : get_order,
        'sort-subgals'        : get_order,
        'original'            : get_bool,
        'original-baseurl'    : false_or,
        'original-symlink'    : get_bool,
        'dirzip'              : get_bool,
        'jpeg-quality'        : get_int,
        'jpeg-optimize'       : get_bool,
        'jpeg-progressive'    : get_bool,
        'publish-metadata'    : get_bool,
        'keep-gps'            : get_bool,
    },
})


class LazygalConfig(object):
    valid_sections = ('runtime', 'global', 'webgal', 'template-vars', )

    def __init__(self, load_defaults=False):
        self.c = collections.OrderedDict()
        self.valid_options = {s:[] for s in self.valid_sections}

        self.load(self.load_file(DEFAULT_CONFIG_PATH),
                  defaults=True, setvalue=load_defaults)
        self.files = [DEFAULT_CONFIG_PATH]

    def has_section(self, section):
        return section in self.c

    def add_section(self, section):
        if not self.has_section(section):
            if section in self.valid_sections:
                self.c[section] = collections.OrderedDict()
            else:
                raise ValueError("section '%s' is not a valid section name"
                                 % section)

    def options(self, section):
        return self.c[section].keys()

    def get(self, section, option):
        if not self.has_section(section):
            raise NoSectionError(section)
        if option in self.c[section]:
            return self.c[section][option]
        else:
            raise NoOptionError(_('%s in section %s') % (option, section))

    def set(self, section, option, value):
        self.add_section(section)

        if option in self.valid_options[section] or section == 'template-vars':
            if type(value) is str\
            and section in STRING_TO_JSON\
            and option in STRING_TO_JSON[section]:
                value = STRING_TO_JSON[section][option](value)
            self.c[section][option] = copy.deepcopy(value)
        else:
            raise ValueError(_("option '%s' is not valid in section '%s'")
                                % (option, section))

    def load(self, newconf, setvalue=True, defaults=False):
        for section, s in newconf.items():
            if defaults and section not in self.valid_sections:
                continue # ignore default section loading

            for k, v in s.items():
                if defaults:
                    self.valid_options[section].append(k)
                if not defaults or (defaults and setvalue):
                    try:
                        self.set(section, k, v)
                    except ValueError as e:
                        logging.warning(_('Ignoring option: %s') % e.args[0])

    def load_file(self, path):
        newconf = None
        with open(path, 'r') as json_fp:
            newconf = json.load(json_fp)
        return newconf

    def load_inifile(self, path):
        iniconf = LazygalIniConfig(defaults=False)
        with open(path) as fp:
            iniconf.read_file(fp)

        for s in iniconf.sections():
            for key, value in iniconf.items(s):
                try:
                    self.set(s, key, value)
                except ValueError as e:
                    logging.warning(_('Ignoring option: %s') % e.args[0])

    def load_any(self, path):
        if not os.path.isfile(path):
            logging.debug(_('Cannot load non-existent config %s.') % path)
            return

        try:
            self.load(self.load_file(path))
        except ValueError:
            try:
                self.load_inifile(path)
            except Exception:
                raise
            else:
                logging.warning(_('INI-style config file format is deprecated.'))
        else:
            self.files.append(path)

    def __str__(self):
        return json.dumps(self.c)

    def items(self):
        return self.c.items()

    def __getitem__(self, key):
        return self.c[key]


class LazygalWebgalConfig(LazygalConfig):
    valid_sections = ('webgal', 'template-vars', )


# vim: ts=4 sw=4 expandtab
