import json
import os
from typing import List

import nltk
from flair.embeddings import DocumentPoolEmbeddings, WordEmbeddings
from flair.models import SequenceTagger
from flair.data import Sentence
from nltk.corpus import wordnet

# Stopwords list. We know that libraries like spicy or nltk have their own lists
# of stop words but we prefered doing it in this way in line with the work
# developed by https://hal.archives-ouvertes.fr/hal-02933266
stop_words = [
    "i",
    "me",
    "my",
    "myself",
    "we",
    "our",
    "ours",
    "ourselves",
    "you",
    "your",
    "yours",
    "yourself",
    "yourselves",
    "he",
    "him",
    "his",
    "himself",
    "she",
    "her",
    "hers",
    "herself",
    "it",
    "its",
    "itself",
    "they",
    "them",
    "their",
    "theirs",
    "themselves",
    "what",
    "which",
    "who",
    "whom",
    "this",
    "that",
    "these",
    "those",
    "am",
    "is",
    "are",
    "was",
    "were",
    "be",
    "been",
    "being",
    "have",
    "has",
    "had",
    "having",
    "do",
    "does",
    "did",
    "doing",
    "a",
    "an",
    "the",
    "and",
    "but",
    "if",
    "or",
    "because",
    "as",
    "until",
    "while",
    "of",
    "at",
    "by",
    "for",
    "with",
    "about",
    "against",
    "between",
    "into",
    "through",
    "during",
    "before",
    "after",
    "above",
    "below",
    "to",
    "from",
    "up",
    "down",
    "in",
    "out",
    "on",
    "off",
    "over",
    "under",
    "again",
    "further",
    "then",
    "once",
    "here",
    "there",
    "when",
    "where",
    "why",
    "how",
    "all",
    "any",
    "both",
    "each",
    "few",
    "more",
    "most",
    "other",
    "some",
    "such",
    "no",
    "nor",
    "not",
    "only",
    "own",
    "same",
    "so",
    "than",
    "too",
    "very",
    "s",
    "t",
    "can",
    "will",
    "just",
    "don",
    "should",
    "now",
    "",
]

# Similar to the stopwords, these lists were created by
# https://hal.archives-ouvertes.fr/hal-02933266 and adapted to use json files
# instead of the original ones for developing reasons.

words_list_dir = os.path.join(os.path.dirname(__file__), "words_lists")

# List of speculative adverbs such as `absolutely`, `doubtlessly`, among others.
with open(os.path.join(words_list_dir, "speculative.json"), "rb") as f:
    speculative_list = json.load(f)

# List of scalar adverbs such as `barely`, `profounfly`, among others.
with open(os.path.join(words_list_dir, "scalarity.json"), "rb") as f:
    scalarity_list = json.load(f)

# Dictionary of words and possible replacements. For example: "supremacy":
# ["dominance", "superiority"].
with open(os.path.join(words_list_dir, "special_tokens.json"), "rb") as f:
    special_tokens = json.load(f)

# Dictionary of words for each NEs (named entities): PER, LOC, ORG, and MISC.
with open(os.path.join(words_list_dir, "entities_list.json"), "rb") as f:
    entities_list = json.load(f)


_tagger_pos = None
_tagger_ner = None
_fasttext_emb = None
_fasttext_doc_emb = None
_vectors_list_cache = None


def ensure_nltk():
    """Ensure required NLTK resources are available."""
    required = {
        "punkt": "tokenizers/punkt",
        "wordnet": "corpora/wordnet.zip",
        "stopwords": "corpora/stopwords",
        "averaged_perceptron_tagger": "taggers/averaged_perceptron_tagger",
    }

    for pkg, location in required.items():
        try:
            nltk.data.find(location)
        except LookupError:
            nltk.download(pkg)


def get_pos_tagger():
    global _tagger_pos
    if _tagger_pos is None:
        ensure_nltk()
        _tagger_pos = SequenceTagger.load("pos")
    return _tagger_pos


def get_ner_tagger():
    global _tagger_ner
    if _tagger_ner is None:
        ensure_nltk()
        _tagger_ner = SequenceTagger.load("ner")
    return _tagger_ner


def get_fasttext_embeddings():
    global _fasttext_emb, _fasttext_doc_emb
    if _fasttext_emb is None:
        _fasttext_emb = WordEmbeddings("en")
        _fasttext_doc_emb = DocumentPoolEmbeddings([_fasttext_emb])
    return _fasttext_emb, _fasttext_doc_emb


def get_vectors_list():
    global _vectors_list_cache
    if _vectors_list_cache is not None:
        return _vectors_list_cache

    _, doc_emb = get_fasttext_embeddings()

    vectors = {}
    for key in entities_list.keys():
        vectors[key] = {}
        for word in entities_list[key]:
            sent = Sentence(word)
            doc_emb.embed(sent)
            vectors[key][word] = sent.embedding.to("cpu")

    _vectors_list_cache = vectors
    return _vectors_list_cache


def get_synonyms(word: str, pos: str = None, only_hyponyms: bool = False) -> List[str]:
    """
    Returns a list of synonyms for `word`. If `only_hyponyms` returns a list of
    hyponyms. Note that, if `word` has a certain part of speech P, the resultant
    synonyms might not belong to P. To keep only synonyms of a certain part of
    speech use `pos`.

    parameters:
        - word (str) word to find the synonyms or hyponyms.
        - pos (str) to get synoynms or hyponyms of a certain Part of Speech. The
          values for this parameter are:
            `n` noun, also tagged as `NN` by other pos taggers. `a` adjectives,
            also tagged as `JJ` by other pos taggers. `s` adjectives, also
            tagged as `JJ` by other pos taggers. `r` adverbs, also tagged as
            `RB` by other pos taggers. `v` verbs, also tagged as `VB` by other
            pos taggers.
          if `pos` is not one of the values mentioned above, `get_synonyms` will
          return the empty list. if `pos` is None will `get_synonyms` will
          return synonyms or hyponyms belonging to any pos type.
        - only_hyponyms (bool) flag to return only hyponyms instead of synonyms.

    returns:
        synonyms (List[str]) list of synonyms or hyponyms.
    """
    synonyms = set()

    # Possible values for `pos`
    pos_types = ["n", "a", "s", "r", "v"]
    
    ensure_nltk()
    for syn in wordnet.synsets(word):
        # If pos is None this if is always true and includes all the candidates
        # returned by wordnet. Otherwise, it checks pos types.
        if (pos is None) or ((syn.pos() in pos_types) and (syn.pos() == pos)):
            for l_ in syn.hyponyms() if only_hyponyms else syn.lemmas():
                # Hyphens and underscores are replaced by spaces.
                synonym = l_.name().replace("_", " ").replace("-", " ").lower()

                # Keep only utf-8 chars.
                synonym = "".join([char for char in synonym if char in " qwertyuiopasdfghjklzxcvbnm"])

                # Add synonym or hyponym
                synonyms.add(synonym)

    # Remove the word introduced as input if wordnet suggested it as synonym or
    # hyponym.
    if word in synonyms:
        synonyms.remove(word)
    return list(synonyms)


def replace_exp_to_sent(sent: str, exp: str, start_pos: int, end_pos: int) -> str:
    """
    Given a sentence `sent`, an expression `exp`, and offsets `start_pos`,
    `end_pos`, the function replaces the expression of `sent` that is in the
    range [start_pos, end_pos] with `exp`.

    parameters:
        - `sent` (str) sentence to expand.
        - `exp` (str) expression to add.
        - `start_pos` (int) index of `sent`.
        - `end_pos` (int) index of `sent`.

    returns:
        example (str) modification of `sent` with the replacement of `exp`.
    """
    return sent[:start_pos] + exp + sent[end_pos:]

def add_exp_to_sent(sent: str,
                    exp: str,
                    start_pos: int,
                    end_pos: int,
                    where: str = "before") -> str:
    """
    Given a sentence `sent`, an expression `exp`, and offsets `start_pos`,
    `end_pos`, the function adds `exp` to `sent` before `start_pos` if `where ==
    'before'` or after `end_pos` if `where == 'after'`.

    paramters:
        - `sent` (str) sentence to expand.
        - `exp` (str) expression to add.
        - `start_pos` (int) index of `sent`.
        - `end_pos` (int) index of `sent`.
        - `where (str) values "before" or "after".

    returns:
        example (str) modification of `sent` with the addition of `exp`.
    """
    if where == "before":
        example = (sent[:start_pos] + exp + " " + sent[start_pos:])
    elif where == "after":
        example = (sent[:end_pos] + " " + exp + " " + sent[end_pos:])
    return example