"""
    Support for vocularies with support for
    - aliases
    - automatic generate regular expression from aliases
    - NLP stemming
"""


import configparser
import re
import sys
from logging import info
from typing import Any, Callable, Dict, List, Optional, Pattern, Text, Union

import addict
import nltk

import act.scio.aliasregex as aliasregex
from act.scio.alias import parse_aliases

DEFAULT_CONFIG = addict.Dict(
    {
        "alias": None,
        "default": None,
        "regexfromalias": False,
        "primary": False,
        "key_mod": "lower",
        "regexmanual": "",
        "object_type": None,
    }
)


def identity(x: Any) -> Any:
    """Identity function. Used as default to return same value as input"""
    return x


def from_config(config_filename: str) -> addict.Dict:
    """
    Load vocabularies from config and return Dict
    with vocabulary-name as key and Vocabulary as value
    """
    cparser = configparser.ConfigParser()
    cparser.read(str(config_filename))

    vocabularies = addict.Dict()

    for vocabulary_name in cparser.sections():
        section = cparser[vocabulary_name]

        # Config
        config = addict.Dict()
        config.alias = section.get("alias", DEFAULT_CONFIG.alias)
        config.default = section.get("default", DEFAULT_CONFIG.default)
        config.regexfromalias = section.getboolean("regexfromalias")
        config.primary = section.getboolean("primary")
        config.key_mod = section.get("key_mod", DEFAULT_CONFIG.key_mod)
        config.regexmanual = section.get("regexmanual", DEFAULT_CONFIG.regexmanual)
        config.object_type = section.get("object_type", DEFAULT_CONFIG.object_type)
        vocabularies[vocabulary_name] = Vocabulary(config)

    return vocabularies


class IllegalVocabularyKeyType(Exception):
    """Non existing Vocabulary Key Type"""

    pass


class Vocabulary:
    """
    Support for vocularies with support for
    - aliases
    - automatic generate regular expression from aliases
    - NLP stemming
    """

    def __init__(
        self, config: Union[addict.Dict, Dict[Text, Text], configparser.SectionProxy]
    ) -> None:
        """
        Args:
            config (addict.Dict):  addict.Dict with the following keys
                    * alias (str)            # Config filw with aliases
                    * primary (bool)         # Point aliases to primary name
                    * default (bool)         # Default value for if key does not exist
                    * regexfromalias (bool)  # Create regex from aliases?
                    * primary (bool)         # Use primary name as default return value
                    * key_mod (str)          # Default key_mod (default = "lower")
                    * regexmanual (str[])    # Extra regular expressions for regex search
                    * object_type (str)      # ACT object type applicable for this vocabulary
        """
        self.config = addict.Dict(DEFAULT_CONFIG)
        self.config.update(config)
        self.regex: List[Pattern[Text]] = []
        self.vocab: Dict[str, Dict[str, addict.Dict]] = addict.Dict(
            none=addict.Dict(),
            lower=addict.Dict(),
            stem=addict.Dict(),
            norm=addict.Dict(),
        )

        self.stemmer = nltk.stem.PorterStemmer().stem

        if self.config.alias:
            self.load_alias(self.config.alias)

        if self.config.regexmanual:
            self.regex = [
                re.compile(entry.strip(), re.I)
                for entry in self.config.regexmanual.split("\n")
                if entry.strip()
            ]
        else:
            self.regex = []

        if self.config.regexfromalias:
            for aliasre in aliasregex.get_reg_ex_set(self.config.alias):
                try:
                    self.regex.append(re.compile(aliasre, re.IGNORECASE))
                except re.error:
                    sys.stderr.write(f"ERROR in regex {aliasre}\n")
                    raise

    def load_alias(self, filename: str) -> None:
        """
        Load aliases from file.

        Args:
            filename (str): Filename to read aliases from
        """
        with open(filename, "r") as f:

            for line in f.readlines():
                # Remove comments (starting with '#'), unless they are escaped
                line = re.sub(r"(?<!\\)#.*", "", line)

                if not line.strip():
                    continue  # Skip empty lines

                # Get key and aliases
                primary, aliases = parse_aliases(line)

                for name in [primary] + aliases:
                    # Stor a separate entry for each type of key_mod
                    # For each entry, also store the value itself and the
                    # primary name

                    self.vocab["none"][name] = addict.Dict(value=name, primary=primary)
                    self.vocab["lower"][name.lower()] = addict.Dict(
                        value=name, primary=primary
                    )
                    self.vocab["stem"][self.stemmer(name)] = addict.Dict(
                        value=name, primary=primary
                    )

                    self.vocab["norm"][aliasregex.normalize(name)] = addict.Dict(
                        value=name, primary=primary
                    )

    def __getitem__(self, key: str) -> Optional[str]:
        """
        Return entry from vocabulary.
        """

        return self.get(key)

    def regex_search(
        self,
        text: Text,
        key_mod: Text = "DEFAULT",
        normalize_result: Callable[[Text], Text] = identity,
        debug: bool = False,
    ) -> List[str]:
        """
        Find all matches in vocabulary from regex search

        Args:
            text (str):       Input text
            key_mod (text):   Lookup using modifie key
        """

        key_mod = self.get_key_mod(key_mod)

        result = []
        for regex in self.regex:
            for match in regex.findall(text):
                if debug:
                    info("%s found by regex %s", match, regex)
                result.append(normalize_result(match))

        return result

    def get_key_mod(self, key_mod: Optional[str]) -> str:
        """Verify and get key_mod"""

        # Get key modifier from config
        if key_mod == "DEFAULT":
            key_mod = self.config.key_mod

        elif key_mod is None:  # Use "none" modifier (the value itself)
            key_mod = "none"

        if key_mod not in self.vocab:
            raise IllegalVocabularyKeyType("Illegal key: {}".format(key_mod))
        return key_mod

    def get(
        self,
        key: str,
        key_mod: str = "DEFAULT",
        primary: Optional[bool] = None,
        default: Optional[str] = None,
    ) -> Optional[str]:
        """
        Search for entry in vocabulary

        Args:
            key (str):           Key to search for
            key_mod (str):       Key modifier ("lower", "stem" or "none"). If not set,
                                 use default value form self.config.key_mod.
            primary (bool|None): If True, return primary name, if not return the
                                 value itself from the vocabulary
            default (str|None):  Default value if key is not found. Default == None
        """

        key_mod = self.get_key_mod(key_mod)

        # If primary is not set, get from config whether we should retrieve the primary name
        if primary is None:
            primary = self.config.primary

        # If default is None, get default value from config
        if default is None:
            default = self.config.default

        if key_mod == "stem":
            key = self.stemmer(key)
        elif key_mod == "lower":
            key = key.lower()
        elif key_mod == "norm":
            key = aliasregex.normalize(key)

        value = self.vocab[key_mod].get(key)

        if not value:
            return default

        if primary:  # Return primary value
            return value.primary  # type: ignore

        # Return value ifself
        return value.value  # type: ignore
