# Copyright 2021 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Loads dataset for the dual encoder (retrieval) task."""
import functools
import itertools
from typing import Iterable, Mapping, Optional, Tuple

import dataclasses
import tensorflow as tf
import tensorflow_hub as hub

from official.core import config_definitions as cfg
from official.core import input_reader
from official.nlp.data import data_loader
from official.nlp.data import data_loader_factory
from official.nlp.modeling import layers


@dataclasses.dataclass
class DualEncoderDataConfig(cfg.DataConfig):
  """Data config for dual encoder task (tasks/dual_encoder)."""
  # Either set `input_path`...
  input_path: str = ''
  # ...or `tfds_name` and `tfds_split` to specify input.
  tfds_name: str = ''
  tfds_split: str = ''
  global_batch_size: int = 32
  # Either build preprocessing with Python code by specifying these values...
  vocab_file: str = ''
  lower_case: bool = True
  # ...or load preprocessing from a SavedModel at this location.
  preprocessing_hub_module_url: str = ''

  left_text_fields: Tuple[str] = ('left_input',)
  right_text_fields: Tuple[str] = ('right_input',)
  is_training: bool = True
  seq_length: int = 128


@data_loader_factory.register_data_loader_cls(DualEncoderDataConfig)
class DualEncoderDataLoader(data_loader.DataLoader):
  """A class to load dataset for dual encoder task (tasks/dual_encoder)."""

  def __init__(self, params):
    if bool(params.tfds_name) == bool(params.input_path):
      raise ValueError('Must specify either `tfds_name` and `tfds_split` '
                       'or `input_path`.')
    if bool(params.vocab_file) == bool(params.preprocessing_hub_module_url):
      raise ValueError('Must specify exactly one of vocab_file (with matching '
                       'lower_case flag) or preprocessing_hub_module_url.')
    self._params = params
    self._seq_length = params.seq_length
    self._left_text_fields = params.left_text_fields
    self._right_text_fields = params.right_text_fields

    if params.preprocessing_hub_module_url:
      preprocessing_hub_module = hub.load(params.preprocessing_hub_module_url)
      self._tokenizer = preprocessing_hub_module.tokenize
      self._pack_inputs = functools.partial(
          preprocessing_hub_module.bert_pack_inputs,
          seq_length=params.seq_length)
    else:
      self._tokenizer = layers.BertTokenizer(
          vocab_file=params.vocab_file, lower_case=params.lower_case)
      self._pack_inputs = layers.BertPackInputs(
          seq_length=params.seq_length,
          special_tokens_dict=self._tokenizer.get_special_tokens_dict())

  def _decode(self, record: tf.Tensor):
    """Decodes a serialized tf.Example."""
    name_to_features = {
        x: tf.io.FixedLenFeature([], tf.string)
        for x in itertools.chain(
            *[self._left_text_fields, self._right_text_fields])
    }
    example = tf.io.parse_single_example(record, name_to_features)

    # tf.Example only supports tf.int64, but the TPU only supports tf.int32.
    # So cast all int64 to int32.
    for name in example:
      t = example[name]
      if t.dtype == tf.int64:
        t = tf.cast(t, tf.int32)
      example[name] = t

    return example

  def _bert_tokenize(
      self, record: Mapping[str, tf.Tensor],
      text_fields: Iterable[str]) -> Tuple[tf.Tensor, tf.Tensor, tf.Tensor]:
    """Tokenize the input in text_fields using BERT tokenizer.

    Args:
      record: A tfexample record contains the features.
      text_fields: A list of fields to be tokenzied.

    Returns:
      The tokenized features in a tuple of (input_word_ids, input_mask,
      input_type_ids).
    """
    segments_text = [record[x] for x in text_fields]
    segments_tokens = [self._tokenizer(s) for s in segments_text]
    segments = [tf.cast(x.merge_dims(1, 2), tf.int32) for x in segments_tokens]
    return self._pack_inputs(segments)

  def _bert_preprocess(
      self, record: Mapping[str, tf.Tensor]) -> Mapping[str, tf.Tensor]:
    """Perform the bert word piece tokenization for left and right inputs."""

    def _switch_prefix(string, old, new):
      if string.startswith(old): return new + string[len(old):]
      raise ValueError('Expected {} to start with {}'.format(string, old))

    def _switch_key_prefix(d, old, new):
      return {_switch_prefix(key, old, new): value for key, value in d.items()}

    model_inputs = _switch_key_prefix(
        self._bert_tokenize(record, self._left_text_fields),
        'input_', 'left_')
    model_inputs.update(_switch_key_prefix(
        self._bert_tokenize(record, self._right_text_fields),
        'input_', 'right_'))
    return model_inputs

  def load(self, input_context: Optional[tf.distribute.InputContext] = None):
    """Returns a tf.dataset.Dataset."""
    reader = input_reader.InputReader(
        params=self._params,
        # Skip `decoder_fn` for tfds input.
        decoder_fn=self._decode if self._params.input_path else None,
        dataset_fn=tf.data.TFRecordDataset,
        postprocess_fn=self._bert_preprocess)
    return reader.read(input_context)
