"""
parse utils
-----------

Functionality for tokenizing, featurizing, tagging, padding sequences, and loading taggers
in a résumé section-agnostic manner. Section subpackages use and build upon these utils
in their respective ``parse.py`` modules.
"""
import functools
import importlib
import logging
import string
import sys

import pycrfsuite
from toolz import itertoolz


LOGGER = logging.getLogger(__name__)
_PUNCT_CHARS = set(string.punctuation)


@functools.lru_cache(maxsize=16)
def load_tagger(fpath):
    """
    Args:
        fpath (str or :class:`pathlib.Path`)

    Returns:
        :class:`pycrfsuite.Tagger`
    """
    try:
        tagger = pycrfsuite.Tagger()
        tagger.open(str(fpath))
        return tagger
    except IOError:
        LOGGER.warning(
            "tagger model file '%s' is missing; have you trained one yet? "
            "if not, use the `label_parser_training_data.py` script to do so.",
            fpath,
        )
        raise


def get_token_features_base(token):
    """
    Get base per-token features, independent of other tokens within its sequence.
    Specific sections should add to / remove from these features, as needed.

    Args:
        token (:class:`spacy.tokens.Token`)

    Returns:
        Dict[str, obj]
    """
    text = token.text
    return {
        "idx": token.i,
        "len": len(token),
        "shape": token.shape_,
        "prefix": token.prefix_,
        "suffix": token.suffix_,
        "is_alpha": token.is_alpha,
        "is_digit": token.is_digit,
        "is_lower": token.is_lower,
        "is_upper": token.is_upper,
        "is_title": token.is_title,
        "is_punct": token.is_punct,
        "is_left_punct": token.is_left_punct,
        "is_right_punct": token.is_right_punct,
        "is_bracket": token.is_bracket,
        "is_quote": token.is_quote,
        "is_space": token.is_space,
        "like_num": token.like_num,
        "like_url": token.like_url,
        "like_email": token.like_email,
        "is_stop": token.is_stop,
        "is_alnum": text.isalnum(),
        "is_newline": all(char == "\n" for char in text),
        "is_partial_digit": any(c.isdigit() for c in text) and not all(c.isdigit() for c in text),
        "is_partial_punct": any(c in _PUNCT_CHARS for c in text) and not all(c in _PUNCT_CHARS for c in text),
    }


def pad_tokens_features(tokens_features, *, n_left=1, n_right=1):
    """
    Pad list of tokens' features on the left and/or right with a configurable number of
    dummy feature dicts.

    Args:
        tokens_features (List[Dict[str, obj]])
        n_left (int): Number of dummy feature dicts to prepend.
        n_right (int): Number of dummy feature dicts to append.

    Returns:
        List[Dict[str, obj]]
    """
    return (
        [{"_start": True} for _ in range(n_left)]
        + tokens_features
        + [{"_end": True} for _ in range(n_right)]
    )


def get_line_token_idxs(tokens_features):
    """
    Get the [start, stop) indexes for all lines in ``tokens_features``,
    such that ``tokens_features[start : stop]`` corresponds to one line
    of featurized tokens.

    Args:
        tokens_features (List[Dict[str, obj]]: Sequence of featurized tokens, as produced
            by :func:`get_token_features_base()`.

    Yields:
        Tuple[int, int]
    """
    idxs_newlines = [tf["idx"] for tf in tokens_features if tf["is_newline"]]
    if not idxs_newlines:
        yield (0, len(tokens_features))
    else:
        if idxs_newlines[0] != 0:
            idxs_newlines.insert(0, 0)
        if idxs_newlines[-1] != len(tokens_features):
            idxs_newlines.append(len(tokens_features))
        for idx1, idx2 in itertoolz.sliding_window(2, idxs_newlines):
            yield (idx1, idx2)


def tag(tokens, features, *, tagger):
    """
    Tag each token in ``tokens`` with a section-specific label based on its features.

    Args:
        tokens (List[:class:`spacy.tokens.Token`]): A tokenized line of text.
        features (List[Dict[str, obj]]): As output by a section-specific ``featurize()``.
        tagger (:class:`pycrfsuite.Tagger`): Trained, section-specific CRF tagger.

    Returns:
        List[Tuple[:class:`spacy.tokens.Token`, str]]: Ordered sequence of
        (token, tag) pairs, aka "labeled tokens".
    """
    tags = tagger.tag(features)
    return list(zip(tokens, tags))
