# -*- coding:utf-8 -*-
# Author: hankcs
# Date: 2020-05-03 14:44
import logging
import os
from abc import ABC, abstractmethod
from typing import Tuple, Union, List

from hanlp_common.constant import EOS, PAD
from hanlp_common.structure import SerializableDict
from hanlp_common.configurable import Configurable
from hanlp.common.vocab import Vocab
from hanlp.utils.io_util import get_resource
from hanlp_common.io import load_json
from hanlp_common.reflection import classpath_of, str_to_type
from hanlp.utils.string_util import ispunct


class ToIndex(ABC):

    def __init__(self, vocab: Vocab = None) -> None:
        super().__init__()
        if vocab is None:
            vocab = Vocab()
        self.vocab = vocab

    @abstractmethod
    def __call__(self, sample):
        pass

    def save_vocab(self, save_dir, filename='vocab.json'):
        vocab = SerializableDict()
        vocab.update(self.vocab.to_dict())
        vocab.save_json(os.path.join(save_dir, filename))

    def load_vocab(self, save_dir, filename='vocab.json'):
        save_dir = get_resource(save_dir)
        vocab = SerializableDict()
        vocab.load_json(os.path.join(save_dir, filename))
        self.vocab.copy_from(vocab)


class FieldToIndex(ToIndex):

    def __init__(self, src, vocab: Vocab, dst=None) -> None:
        super().__init__(vocab)
        self.src = src
        if not dst:
            dst = f'{src}_id'
        self.dst = dst

    def __call__(self, sample: dict):
        sample[self.dst] = self.vocab(sample[self.src])
        return sample

    def save_vocab(self, save_dir, filename=None):
        if not filename:
            filename = f'{self.dst}_vocab.json'
        super().save_vocab(save_dir, filename)

    def load_vocab(self, save_dir, filename=None):
        if not filename:
            filename = f'{self.dst}_vocab.json'
        super().load_vocab(save_dir, filename)


class VocabList(list):

    def __init__(self, *fields) -> None:
        super().__init__()
        for each in fields:
            self.append(FieldToIndex(each))

    def append(self, item: Union[str, Tuple[str, Vocab], Tuple[str, str, Vocab], FieldToIndex]) -> None:
        if isinstance(item, str):
            item = FieldToIndex(item)
        elif isinstance(item, (list, tuple)):
            if len(item) == 2:
                item = FieldToIndex(src=item[0], vocab=item[1])
            elif len(item) == 3:
                item = FieldToIndex(src=item[0], dst=item[1], vocab=item[2])
            else:
                raise ValueError(f'Unsupported argument length: {item}')
        elif isinstance(item, FieldToIndex):
            pass
        else:
            raise ValueError(f'Unsupported argument type: {item}')
        super(self).append(item)

    def save_vocab(self, save_dir):
        for each in self:
            each.save_vocab(save_dir, None)

    def load_vocab(self, save_dir):
        for each in self:
            each.load_vocab(save_dir, None)


class VocabDict(SerializableDict):

    def __init__(self, *args, **kwargs) -> None:
        """A dict holding :class:`hanlp.common.vocab.Vocab` instances. When used as a transform, it transforms the field
        corresponding to each :class:`hanlp.common.vocab.Vocab` into indices.

        Args:
            *args: A list of vocab names.
            **kwargs: Names and corresponding :class:`hanlp.common.vocab.Vocab` instances.
        """
        vocabs = dict(kwargs)
        for each in args:
            vocabs[each] = Vocab()
        super().__init__(vocabs)

    def save_vocabs(self, save_dir, filename='vocabs.json'):
        """Save vocabularies to a directory.

        Args:
            save_dir: The directory to save vocabularies.
            filename:  The name for vocabularies.
        """
        vocabs = SerializableDict()
        for key, value in self.items():
            if isinstance(value, Vocab):
                vocabs[key] = value.to_dict()
        vocabs.save_json(os.path.join(save_dir, filename))

    def load_vocabs(self, save_dir, filename='vocabs.json', vocab_cls=Vocab):
        """Load vocabularies from a directory.

        Args:
            save_dir: The directory to load vocabularies.
            filename:  The name for vocabularies.
        """
        save_dir = get_resource(save_dir)
        vocabs = SerializableDict()
        vocabs.load_json(os.path.join(save_dir, filename))
        self._load_vocabs(self, vocabs, vocab_cls)

    @staticmethod
    def _load_vocabs(vd, vocabs: dict, vocab_cls=Vocab):
        """

        Args:
            vd:
            vocabs:
            vocab_cls: Default class for the new vocab
        """
        for key, value in vocabs.items():
            if 'idx_to_token' in value:
                cls = value.get('type', None)
                if cls:
                    cls = str_to_type(cls)
                else:
                    cls = vocab_cls
                vocab = cls()
                vocab.copy_from(value)
                vd[key] = vocab
            else:  # nested Vocab
                # noinspection PyTypeChecker
                vd[key] = nested = VocabDict()
                VocabDict._load_vocabs(nested, value, vocab_cls)

    def lock(self):
        """
        Lock each vocab.
        """
        for key, value in self.items():
            if isinstance(value, Vocab):
                value.lock()

    def unlock(self):
        """
        Unlock each vocab.
        """
        for key, value in self.items():
            if isinstance(value, Vocab):
                value.unlock()

    @property
    def mutable(self):
        status = [v.mutable for v in self.values() if isinstance(v, Vocab)]
        return len(status) == 0 or any(status)

    def __call__(self, sample: dict):
        for key, value in self.items():
            if isinstance(value, Vocab):
                field = sample.get(key, None)
                if field is not None:
                    sample[f'{key}_id'] = value(field)
        return sample

    def __getattr__(self, key):
        if key.startswith('__'):
            return dict.__getattr__(key)
        return self.__getitem__(key)

    def __setattr__(self, key, value):
        return self.__setitem__(key, value)

    def __getitem__(self, k: str) -> Vocab:
        return super().__getitem__(k)

    def __setitem__(self, k: str, v: Vocab) -> None:
        super().__setitem__(k, v)

    def summary(self, logger: logging.Logger = None):
        """Log a summary of vocabs using a given logger.

        Args:
            logger: The logger to use.
        """
        for key, value in self.items():
            if isinstance(value, Vocab):
                report = value.summary(verbose=False)
                if logger:
                    logger.info(f'{key}{report}')
                else:
                    print(f'{key}{report}')

    def put(self, **kwargs):
        """Put names and corresponding :class:`hanlp.common.vocab.Vocab` instances into self.

        Args:
            **kwargs: Names and corresponding :class:`hanlp.common.vocab.Vocab` instances.
        """
        for k, v in kwargs.items():
            self[k] = v


class NamedTransform(ABC):
    def __init__(self, src: str, dst: str = None) -> None:
        if dst is None:
            dst = src
        self.dst = dst
        self.src = src

    @abstractmethod
    def __call__(self, sample: dict) -> dict:
        return sample


class ConfigurableTransform(Configurable, ABC):
    @property
    def config(self):
        return dict([('classpath', classpath_of(self))] +
                    [(k, v) for k, v in self.__dict__.items() if not k.startswith('_')])

    @classmethod
    def from_config(cls, config: dict):
        """

        Args:
          config: 
          kwargs: 
          config: dict: 

        Returns:

        
        """
        cls = config.get('classpath', None)
        assert cls, f'{config} doesn\'t contain classpath field'
        cls = str_to_type(cls)
        config = dict(config)
        config.pop('classpath')
        return cls(**config)


class ConfigurableNamedTransform(NamedTransform, ConfigurableTransform, ABC):
    pass


class EmbeddingNamedTransform(ConfigurableNamedTransform, ABC):

    def __init__(self, output_dim: int, src: str, dst: str) -> None:
        super().__init__(src, dst)
        self.output_dim = output_dim


class RenameField(NamedTransform):

    def __call__(self, sample: dict):
        sample[self.dst] = sample.pop(self.src)
        return sample


class CopyField(object):
    def __init__(self, src, dst) -> None:
        self.dst = dst
        self.src = src

    def __call__(self, sample: dict) -> dict:
        sample[self.dst] = sample[self.src]
        return sample


class FilterField(object):
    def __init__(self, *keys) -> None:
        self.keys = keys

    def __call__(self, sample: dict):
        sample = dict((k, sample[k]) for k in self.keys)
        return sample


class TransformList(list):
    """Composes several transforms together.

    Args:
      transforms(list of ``Transform`` objects): list of transforms to compose.
    Example:

    Returns:

    >>> transforms.TransformList(
        >>>     transforms.CenterCrop(10),
        >>>     transforms.ToTensor(),
        >>> )
    """

    def __init__(self, *transforms) -> None:
        super().__init__()
        self.extend(transforms)

    def __call__(self, sample):
        for t in self:
            sample = t(sample)
        return sample

    def index_by_type(self, t):
        for i, trans in enumerate(self):
            if isinstance(trans, t):
                return i


class LowerCase(object):
    def __init__(self, src, dst=None) -> None:
        if dst is None:
            dst = src
        self.src = src
        self.dst = dst

    def __call__(self, sample: dict) -> dict:
        src = sample[self.src]
        if isinstance(src, str):
            sample[self.dst] = src.lower()
        elif isinstance(src, list):
            sample[self.dst] = [x.lower() for x in src]
        return sample


class LowerCase3D(LowerCase):

    def __call__(self, sample: dict) -> dict:
        src = sample[self.src]
        sample[self.dst] = [[y.lower() for y in x] for x in src]
        return sample


class ToChar(object):
    def __init__(self, src, dst='char', max_word_length=None, min_word_length=None, pad=PAD) -> None:
        if dst is None:
            dst = src
        self.src = src
        self.dst = dst
        self.max_word_length = max_word_length
        self.min_word_length = min_word_length
        self.pad = pad

    def __call__(self, sample: dict) -> dict:
        src = sample[self.src]
        if isinstance(src, str):
            sample[self.dst] = self.to_chars(src)
        elif isinstance(src, list):
            sample[self.dst] = [self.to_chars(x) for x in src]
        return sample

    def to_chars(self, word: str):
        chars = list(word)
        if self.min_word_length and len(chars) < self.min_word_length:
            chars = chars + [self.pad] * (self.min_word_length - len(chars))
        if self.max_word_length:
            chars = chars[:self.max_word_length]
        return chars


class AppendEOS(NamedTransform):

    def __init__(self, src: str, dst: str = None, eos=EOS) -> None:
        super().__init__(src, dst)
        self.eos = eos

    def __call__(self, sample: dict) -> dict:
        sample[self.dst] = sample[self.src] + [self.eos]
        return sample


class WhitespaceTokenizer(NamedTransform):

    def __call__(self, sample: dict) -> dict:
        src = sample[self.src]
        if isinstance(src, str):
            sample[self.dst] = self.tokenize(src)
        elif isinstance(src, list):
            sample[self.dst] = [self.tokenize(x) for x in src]
        return sample

    @staticmethod
    def tokenize(text: str):
        return text.split()


class NormalizeDigit(object):
    def __init__(self, src, dst=None) -> None:
        if dst is None:
            dst = src
        self.src = src
        self.dst = dst

    @staticmethod
    def transform(word: str):
        new_word = ""
        for char in word:
            if char.isdigit():
                new_word += '0'
            else:
                new_word += char
        return new_word

    def __call__(self, sample: dict) -> dict:
        src = sample[self.src]
        if isinstance(src, str):
            sample[self.dst] = self.transform(src)
        elif isinstance(src, list):
            sample[self.dst] = [self.transform(x) for x in src]
        return sample


class Bigram(NamedTransform):

    def __init__(self, src: str, dst: str = None) -> None:
        if not dst:
            dst = f'{src}_bigram'
        super().__init__(src, dst)

    def __call__(self, sample: dict) -> dict:
        src: List = sample[self.src]
        dst = src + [EOS]
        dst = [dst[i] + dst[i + 1] for i in range(len(src))]
        sample[self.dst] = dst
        return sample


class FieldLength(NamedTransform):

    def __init__(self, src: str, dst: str = None, delta=0) -> None:
        self.delta = delta
        if not dst:
            dst = f'{src}_length'
        super().__init__(src, dst)

    def __call__(self, sample: dict) -> dict:
        sample[self.dst] = len(sample[self.src]) + self.delta
        return sample


class BMESOtoIOBES(object):
    def __init__(self, field='tag') -> None:
        self.field = field

    def __call__(self, sample: dict) -> dict:
        sample[self.field] = [self.convert(y) for y in sample[self.field]]
        return sample

    @staticmethod
    def convert(y: str):
        if y.startswith('M-'):
            return 'I-'
        return y


class NormalizeToken(ConfigurableNamedTransform):

    def __init__(self, mapper: Union[str, dict], src: str, dst: str = None) -> None:
        super().__init__(src, dst)
        self.mapper = mapper
        if isinstance(mapper, str):
            mapper = get_resource(mapper)
        if isinstance(mapper, str):
            self._table = load_json(mapper)
        elif isinstance(mapper, dict):
            self._table = mapper
        else:
            raise ValueError(f'Unrecognized mapper type {mapper}')

    def __call__(self, sample: dict) -> dict:
        src = sample[self.src]
        if self.src == self.dst:
            sample[f'{self.src}_'] = src
        if isinstance(src, str):
            src = self.convert(src)
        else:
            src = [self.convert(x) for x in src]
        sample[self.dst] = src
        return sample

    def convert(self, token) -> str:
        return self._table.get(token, token)


class PunctuationMask(ConfigurableNamedTransform):
    def __init__(self, src: str, dst: str = None) -> None:
        """Mask out all punctuations (set mask of punctuations to False)

        Args:
          src:
          dst:

        Returns:

        """
        if not dst:
            dst = f'{src}_punct_mask'
        super().__init__(src, dst)

    def __call__(self, sample: dict) -> dict:
        src = sample[self.src]
        if isinstance(src, str):
            dst = not ispunct(src)
        else:
            dst = [not ispunct(x) for x in src]
        sample[self.dst] = dst
        return sample


class NormalizeCharacter(NormalizeToken):
    def convert(self, token) -> str:
        return ''.join([NormalizeToken.convert(self, c) for c in token])
