# Copyright (C) 2017 The Meme Factory, Inc.  http://www.meme.com/
#
# 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.

# Karl O. Pinc <kop@meme.com>

'''
Start the program.  Load config files, parse rules, print header,
and compile file structure conventions.
'''

import configparser
import os.path
import sys
import time

from . import exceptions as ex
from . import get_rules
from . import header
from . import parse_rules


#
# Constants
#
CONFIG_PATH = '../enforcer/enforcer.ini'


#
# Classes
#
class ErrorManager(object):
    '''Report errors and keep track of error existance.

    - Helps in allowing multiple errors to be reported (without traceback).

    - Avoids globals. (Sorta; there is a global error manager instance.)
    '''

    def __init__(self):
        super(ErrorManager, self).__init__()
        self.errored = False

    def express(self, exception):
        '''Report an exception (without traceback)'''
        ex.express(exception)
        if isinstance(exception, ex.Error):
            self.errored = True

    def exit_if_errored(self):
        '''Exit early if an error occurred'''
        if self.errored:
            self.errored = False  # reset for regression tests
            sys.exit(1)


#
# Initialization
#
em = ErrorManager()


#
# Functions
#
def check_missing_config_declaration(cfg, cfile, key, hint):
    if key not in cfg:
        raise ex.MissingConfigDeclarationError(
            'No "{0}" declaration in the config "{1}"'.format(key, cfile),
            hint)


def require_config(cfg, unused_keys, cfile, key, hint=None):
    try:
        check_missing_config_declaration(cfg, cfile, key, hint)
        unused_keys.remove(key)
    except ex.MissingConfigDeclarationError as err:
        em.express(err)


def check_bad_max_depth(strvalue, config_path):
    try:
        val = int(strvalue)
    except ValueError:
        raise ex.BadMaxDepthError(
            'max_depth ("{0}") is not an integer in the config "{1}"'
            .format(strvalue, config_path))
    if val < 1:
        raise ex.BadMaxDepthError(
            'max_depth ("{0}") is less than 1 in the config "{1}"'
            .format(val, config_path))
    return val


def convert_max_depth(strvalue, config_path):
    try:
        return check_bad_max_depth(strvalue, config_path)
    except ex.BadMaxDepthError as err:
        em.express(err)


def warn_extra_declaration(unused_keys, key):
    if key in unused_keys:
        unused_keys.remove(key)
        raise ex.UnusedDeclarationWarning(key)


def report_extra_declaration(unused_keys, key):
    try:
        warn_extra_declaration(unused_keys, key)
    except ex.UnusedDeclarationWarning as warn:
        em.express(warn)


def raise_unknown_declaration(key):
    raise ex.UnknownDeclarationError(key)


def report_unknown_declaration(key):
    try:
        raise_unknown_declaration(key)
    except ex.UnknownDeclarationError as err:
        em.express(err)


def report_extra_declarations(unused_keys, rules_source):
    if rules_source == get_rules.FILE_SOURCE:
        report_extra_declaration(unused_keys, 'google_credentials')
        report_extra_declaration(unused_keys, 'spreadsheet_name')
    else:
        report_extra_declaration(unused_keys, 'rules_path')

    for key in unused_keys:
        report_unknown_declaration(key)


def optional_key(unused_keys, key):
    if key in unused_keys:
        unused_keys.remove(key)


def optional_config_set(config_dict, enforcer_sect, key):
    if key in enforcer_sect:
        config_dict[key] = set(
            enforcer_sect[key].split('\n'))
    else:
        config_dict[key] = set()


def parse_config(cfile, enforcer_sect):
    config_dict = {}
    unused_keys = list(enforcer_sect.keys())

    require_config(enforcer_sect, unused_keys, cfile, 'rules_source')
    require_config(enforcer_sect, unused_keys, cfile, 'begin_marker')
    require_config(enforcer_sect, unused_keys, cfile, 'max_depth')

    em.exit_if_errored()

    rules_source = enforcer_sect['rules_source']
    if rules_source == get_rules.GOOGLE_SOURCE:
        require_config(enforcer_sect, unused_keys, cfile, 'google_credentials',
                       ('google_credentials is required when '
                        'rules_source is "{0}"').format(
                            get_rules.GOOGLE_SOURCE))
        require_config(enforcer_sect, unused_keys, cfile, 'spreadsheet_name',
                       ('spreadsheet_name is required when '
                        'rules_source is "{0}"').format(
                            get_rules.GOOGLE_SOURCE))
    elif rules_source == get_rules.FILE_SOURCE:
        require_config(enforcer_sect, unused_keys, cfile, 'rules_path',
                       ('rules_path is required when '
                        'rules_source is "{0}"').format(get_rules.FILE_SOURCE))
    else:
        raise ex.BadRulesSourceError(
            'Not one of ({0}, {1})'.format(
                get_rules.GOOGLE_SOURCE, get_rules.FILE_SOURCE),
            'Found the value ({0})'.format(rules_source))

    optional_key(unused_keys, 'ignored_names')
    optional_key(unused_keys, 'stock_parens')
    report_extra_declarations(unused_keys, rules_source)
    em.exit_if_errored()

    config_dict['rules_source'] = rules_source
    if rules_source == get_rules.GOOGLE_SOURCE:
        config_dict['google_credentials'] = enforcer_sect['google_credentials']
        config_dict['spreadsheet_name'] = enforcer_sect['spreadsheet_name']
    else:
        config_dict['rules_path'] = enforcer_sect['rules_path']
    config_dict['begin_marker'] = enforcer_sect['begin_marker']
    config_dict['max_depth'] = \
        convert_max_depth(enforcer_sect['max_depth'], cfile)
    optional_config_set(config_dict, enforcer_sect, 'ignored_names')
    optional_config_set(config_dict, enforcer_sect, 'stock_parens')

    return config_dict


def read_config(cfile=CONFIG_PATH):
    '''Return only the "enforcer" section of the config file.'''
    if not os.path.exists(cfile):
        raise ex.NoConfigFileError(
            'The config file "{0}" was not found'.format(cfile))

    config = configparser.ConfigParser(empty_lines_in_values=False)
    try:
        config.read(cfile)    # Technically, there's a race condition here
    except configparser.Error as err:
        raise ex.ConfigParserError(
            'The ({0}) configuration file is not acceptable'.format(cfile),
            err)

    if 'enforcer' not in config:
        raise ex.NoEnforcerSectionError(
            'The config file "{0}" is missing a required section'
            .format(cfile))
    return parse_config(cfile, config['enforcer'])


def make_analyzer(cfile=CONFIG_PATH):
    start_time = time.time()
    try:
        config = read_config(cfile)
        em.exit_if_errored()
        (metainfo, rule_file) = get_rules.get_rule_file(config)
        em.exit_if_errored()
        header.print_header(start_time, metainfo)
        analyzer = parse_rules.compile(em, config, rule_file)
    except ex.Error as err:
        em.express(err)
    em.exit_if_errored()

    return (config, analyzer)
