import json
import os
import sys
import re
import copy

import numpy as np

from data_profiler.labelers.base_model import BaseModel
from data_profiler.labelers.base_model import AutoSubRegistrationMeta

_file_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(_file_dir)


class RegexModel(BaseModel, metaclass=AutoSubRegistrationMeta):

    def __init__(self, label_mapping=None, parameters=None):
        """
        Regex Model Initializer.

        Example regex_patterns:
            regex_patterns = {
                "LABEL_1": [
                    "LABEL_1_pattern_1",
                    "LABEL_1_pattern_2",
                    ...
                ],
                "LABEL_2": [
                    "LABEL_2_pattern_1",
                    "LABEL_2_pattern_2",
                    ...
                ],
                ...
            }

        Example encapsulators:
            encapsulators = {
                'start': r'(?<![\w.\$\%\-])',
                'end': r'(?:(?=(\b|[ ]))|(?=[^\w\%\$]([^\w]|$))|$)',
            }

        :param label_mapping: maps labels to their encoded integers
        :type label_mapping: dict
        :param parameters: Contains all the appropriate parameters for the model.
            Possible parameters are:
                max_length, max_num_chars, dim_embed
        :type parameters: dict
        :return: None
        """

        # parameter initialization
        if not parameters:
            parameters = {}
        parameters.setdefault('regex_patterns', {})
        parameters.setdefault('encapsulators', {'start': '', 'end': ''})
        parameters.setdefault('ignore_case', True)
        parameters.setdefault('default_label', 'BACKGROUND')
        self._epoch_id = 0

        # initialize class
        self.set_label_mapping(label_mapping)
        self._validate_parameters(parameters)
        self._parameters = parameters

    def _validate_parameters(self, parameters):
        """
        Validate the parameters sent in. Raise error if invalid parameters are
        present.

        :param parameters: parameter dict containing the following parameters:
            regex_patterns: patterns associated with each label_mapping
                Example regex_patterns:
                    regex_patterns = {
                        "LABEL_1": [
                            "LABEL_1_pattern_1",
                            "LABEL_1_pattern_2",
                            ...
                        ],
                        "LABEL_2": [
                            "LABEL_2_pattern_1",
                            "LABEL_2_pattern_2",
                            ...
                        ],
                        ...
                    }
            encapsulators: regex to add to start and end of each regex
                (used to capture entities inside of text).
                Example encapsulators:
                    encapsulators = {
                        'start': r'(?<![\w.\$\%\-])',
                        'end': r'(?:(?=(\b|[ ]))|(?=[^\w\%\$]([^\w]|$))|$)',
                    }
            ignore_case: whether or not to set the regex ignore case flag
            default_label: default label to assign when no regex found
        :type parameters: dict
        :return: None
        """
        _retype = type(re.compile('pattern for py 3.6 & 3.7'))
        
        errors = []

        list_of_necessary_params = ['encapsulators', 'regex_patterns',
                                    'ignore_case', 'default_label']
        # Make sure the necessary parameters are present and valid.
        for param in parameters:
            value = parameters[param]
            if param == 'encapsulators' and (
                    not isinstance(value, dict)
                    or 'start' not in value
                    or 'end' not in value):
                errors.append(
                    "`{}` must be a dict with keys 'start' and 'end'".format(
                        param
                    ))
            elif param == 'regex_patterns':
                if not isinstance(value, dict):
                    errors.append('`{}` must be a dict of regex pattern lists.'.
                                  format(param))
                    continue
                for key in value:
                    if key not in self.label_mapping:
                        errors.append(
                            "`{}` was a regex pattern not found in the "
                            "label_mapping".format(key))
                    elif not isinstance(value[key], list):
                        errors.append(
                            "`{}` must be a list of regex patterns, i.e."
                            "[pattern_1, pattern_2, ...]".format(key))
                    else:
                        for i in range(len(value[key])):
                            if not isinstance(value[key][i], (_retype, str)):
                                errors.append(
                                    "`{}`, pattern `{}' was not a valid regex "
                                    "pattern (re.Pattern, str)".format(key, i))
                            elif isinstance(value[key][i], str):
                                try:
                                    re.compile(value[key][i])
                                except re.error as e:
                                    errors.append(
                                        "`{}`, pattern {} was not a valid regex"
                                        " pattern: {}".format(key, i, str(e)))
            elif param == 'ignore_case' \
                    and not isinstance(parameters[param], bool):
                errors.append("`{}` must be a bool.".format(param))
            elif param == 'default_label' \
                    and not isinstance(parameters[param], str):
                errors.append("`{}` must be a string.".format(param))
            elif param not in list_of_necessary_params:
                errors.append("`{}` is not an accepted parameter.".format(
                    param))
        if errors:
            raise ValueError('\n'.join(errors))

    def _construct_model(self):
        pass

    def _reconstruct_model(self):
        pass

    def _need_to_reconstruct_model(self):
        pass

    def reset_weights(self):
        pass

    def predict(self, data, batch_size=None, show_confidences=False,
                verbose=True):
        """
        Applies the regex patterns (within regex_model) to the input_string,
        create predictions for all matching patterns. Each pattern has an
        associated entity and the predictions of each character within the
        string are given a True or False identification for each entity. All
        characters not identified by ANY of the regex patterns in the
        pattern_dict are considered background characters, and are replaced with
        the default_label value.

        :param data: list of strings to predict upon
        :type data: iterator
        :param batch_size: does not impact this model and should be fixed to not
            be required.
        :type batch_size: N/A
        :param show_confidences: whether user wants prediction confidences
        :type show_confidences:
        :param verbose: Flag to determine whether to print status or not
        :type verbose: bool
        :return: char level predictions and confidences
        :rtype: dict
        """
        start_pattern = ''
        end_pattern = ''
        regex_patterns = self._parameters['regex_patterns']
        default_ind = self.label_mapping[self._parameters['default_label']]
        encapsulators = self._parameters['encapsulators']
        re_flags = re.IGNORECASE if self._parameters['ignore_case'] else 0

        if encapsulators:
            start_pattern = encapsulators['start']
            end_pattern = encapsulators['end']

        pre_compiled_patterns = copy.deepcopy(regex_patterns)
        for entity_label, entity_patterns in pre_compiled_patterns.items():
            for i in range(len(entity_patterns)):
                pattern = (start_pattern
                           + pre_compiled_patterns[entity_label][i]
                           + end_pattern)
                pre_compiled_patterns[entity_label][i] = re.compile(
                    pattern, flags=re_flags)

        # Construct array initial regex predictions where background is
        # predicted.
        predictions = [np.empty((0,))] * 100
        i = 0
        for i, input_string in enumerate(data):

            # Double array size
            if len(predictions) <= i:
                predictions.extend([np.empty((0,))] * len(predictions))

            pred = np.zeros((len(input_string), self.num_labels), dtype=int)
            pred[:, default_ind] = 1

            for entity_label, entity_patterns in pre_compiled_patterns.items():
                entity_id = self.label_mapping[entity_label]
                for re_pattern in entity_patterns:

                    for each_find in re_pattern.finditer(input_string):
                        indices = each_find.span(0)
                        pred[indices[0]:indices[1], default_ind] = 0
                        pred[indices[0]:indices[1], entity_id] = 1
            if verbose:
                sys.stdout.flush()
                sys.stdout.write(
                    "\rData Samples Processed: {:d}   ".format(i))
            predictions[i] = pred

        if verbose:
            print()

        # Trim array size to number of samples
        if len(predictions) > i+1:
            del predictions[i+1:]

        if show_confidences:
            conf = copy.deepcopy(predictions)
            for i in range(len(conf)):
                conf[i] = conf[i] / \
                          np.linalg.norm(conf[i], axis=1, ord=1, keepdims=True)
            return {"pred": predictions, 'conf': conf}
        return {"pred": predictions}

    @classmethod
    def load_from_disk(cls, dirpath):
        """
        Loads whole model from disk with weights

        :param dirpath: directory path where you want to load the model from
        :type dirpath: str
        :return: None
        """
        # load parameters
        model_param_dirpath = os.path.join(dirpath, "model_parameters.json")
        with open(model_param_dirpath, 'r') as fp:
            parameters = json.load(fp)

        # load label_mapping
        labels_dirpath = os.path.join(dirpath, "label_mapping.json")
        with open(labels_dirpath, 'r') as fp:
            label_mapping = json.load(fp)

        loaded_model = cls(label_mapping, parameters)
        return loaded_model

    def save_to_disk(self, dirpath):
        """
        Saves whole model to disk with weights.

        :param dirpath: directory path where you want to save the model to
        :type dirpath: str
        :return: None
        """
        if not os.path.isdir(dirpath):
            os.makedirs(dirpath)

        model_param_dirpath = os.path.join(dirpath, "model_parameters.json")
        with open(model_param_dirpath, 'w') as fp:
            json.dump(self._parameters, fp)

        labels_dirpath = os.path.join(dirpath, "label_mapping.json")
        with open(labels_dirpath, 'w') as fp:
            json.dump(self.label_mapping, fp)

[((13, 0, 13, 26), 'sys.path.append', 'sys.path.append', ({(13, 16, 13, 25): '_file_dir'}, {}), '(_file_dir)', False, 'import sys\n'), ((12, 28, 12, 53), 'os.path.abspath', 'os.path.abspath', ({(12, 44, 12, 52): '__file__'}, {}), '(__file__)', False, 'import os\n'), ((201, 32, 201, 61), 'copy.deepcopy', 'copy.deepcopy', ({(201, 46, 201, 60): 'regex_patterns'}, {}), '(regex_patterns)', False, 'import copy\n'), ((262, 30, 262, 76), 'os.path.join', 'os.path.join', ({(262, 43, 262, 50): 'dirpath', (262, 52, 262, 75): '"""model_parameters.json"""'}, {}), "(dirpath, 'model_parameters.json')", False, 'import os\n'), ((267, 25, 267, 68), 'os.path.join', 'os.path.join', ({(267, 38, 267, 45): 'dirpath', (267, 47, 267, 67): '"""label_mapping.json"""'}, {}), "(dirpath, 'label_mapping.json')", False, 'import os\n'), ((285, 30, 285, 76), 'os.path.join', 'os.path.join', ({(285, 43, 285, 50): 'dirpath', (285, 52, 285, 75): '"""model_parameters.json"""'}, {}), "(dirpath, 'model_parameters.json')", False, 'import os\n'), ((289, 25, 289, 68), 'os.path.join', 'os.path.join', ({(289, 38, 289, 45): 'dirpath', (289, 47, 289, 67): '"""label_mapping.json"""'}, {}), "(dirpath, 'label_mapping.json')", False, 'import os\n'), ((99, 23, 99, 61), 're.compile', 're.compile', ({(99, 34, 99, 60): '"""pattern for py 3.6 & 3.7"""'}, {}), "('pattern for py 3.6 & 3.7')", False, 'import re\n'), ((245, 19, 245, 45), 'copy.deepcopy', 'copy.deepcopy', ({(245, 33, 245, 44): 'predictions'}, {}), '(predictions)', False, 'import copy\n'), ((264, 25, 264, 38), 'json.load', 'json.load', ({(264, 35, 264, 37): 'fp'}, {}), '(fp)', False, 'import json\n'), ((269, 28, 269, 41), 'json.load', 'json.load', ({(269, 38, 269, 40): 'fp'}, {}), '(fp)', False, 'import json\n'), ((282, 15, 282, 37), 'os.path.isdir', 'os.path.isdir', ({(282, 29, 282, 36): 'dirpath'}, {}), '(dirpath)', False, 'import os\n'), ((283, 12, 283, 32), 'os.makedirs', 'os.makedirs', ({(283, 24, 283, 31): 'dirpath'}, {}), '(dirpath)', False, 'import os\n'), ((287, 12, 287, 43), 'json.dump', 'json.dump', ({(287, 22, 287, 38): 'self._parameters', (287, 40, 287, 42): 'fp'}, {}), '(self._parameters, fp)', False, 'import json\n'), ((291, 12, 291, 45), 'json.dump', 'json.dump', ({(291, 22, 291, 40): 'self.label_mapping', (291, 42, 291, 44): 'fp'}, {}), '(self.label_mapping, fp)', False, 'import json\n'), ((207, 57, 208, 44), 're.compile', 're.compile', (), '', False, 'import re\n'), ((212, 23, 212, 37), 'numpy.empty', 'np.empty', ({(212, 32, 212, 36): '(0,)'}, {}), '((0,))', True, 'import numpy as np\n'), ((232, 16, 232, 34), 'sys.stdout.flush', 'sys.stdout.flush', ({}, {}), '()', False, 'import sys\n'), ((248, 26, 248, 79), 'numpy.linalg.norm', 'np.linalg.norm', (), '', True, 'import numpy as np\n'), ((218, 36, 218, 50), 'numpy.empty', 'np.empty', ({(218, 45, 218, 49): '(0,)'}, {}), '((0,))', True, 'import numpy as np\n'), ((138, 36, 138, 61), 're.compile', 're.compile', ({(138, 47, 138, 60): 'value[key][i]'}, {}), '(value[key][i])', False, 'import re\n')]