#! /usr/bin/python
# -*- coding: utf-8 -*-

import numpy as np
import tensorflow as tf

import tensorlayer as tl
from tensorlayer import logging
from tensorlayer.decorators import deprecated_alias
from tensorlayer.layers.core import Module

# TODO: Need to update to version 3.0
__all__ = [
    'RNN',
    'SimpleRNN',
    'GRURNN',
    'LSTMRNN',
    'BiRNN',
    # 'ConvRNNCell',
    # 'BasicConvLSTMCell',
    # 'ConvLSTM',
    'retrieve_seq_length_op',
    'retrieve_seq_length_op2',
    'retrieve_seq_length_op3',
    'target_mask_op',
]


class RNN(Module):
    """
    The :class:`RNN` class is a fixed length recurrent layer for implementing simple RNN,
    LSTM, GRU and etc.

    Parameters
    ----------
    cell : TensorFlow cell function
        A RNN cell implemented by tf.keras
            - E.g. tf.keras.layers.SimpleRNNCell, tf.keras.layers.LSTMCell, tf.keras.layers.GRUCell
            - Note TF2.0+, TF1.0+ and TF1.0- are different

    return_last_output : boolean
        Whether return last output or all outputs in a sequence.

            - If True, return the last output, "Sequence input and single output"
            - If False, return all outputs, "Synced sequence input and output"
            - In other word, if you want to stack more RNNs on this layer, set to False

        In a dynamic model, `return_last_output` can be updated when it is called in customised forward().
        By default, `False`.
    return_seq_2d : boolean
        Only consider this argument when `return_last_output` is `False`

            - If True, return 2D Tensor [batch_size * n_steps, n_hidden], for stacking Dense layer after it.
            - If False, return 3D Tensor [batch_size, n_steps, n_hidden], for stacking multiple RNN after it.

        In a dynamic model, `return_seq_2d` can be updated when it is called in customised forward().
        By default, `False`.
    return_last_state: boolean
        Whether to return the last state of the RNN cell. The state is a list of Tensor.
        For simple RNN and GRU, last_state = [last_output]; For LSTM, last_state = [last_output, last_cell_state]

            - If True, the layer will return outputs and the final state of the cell.
            - If False, the layer will return outputs only.

        In a dynamic model, `return_last_state` can be updated when it is called in customised forward().
        By default, `False`.
    in_channels: int
        Optional, the number of channels of the previous layer which is normally the size of embedding.
        If given, the layer will be built when init.
        If None, it will be automatically detected when the layer is forwarded for the first time.
    name : str
        A unique layer name.

    Examples
    --------
    For synced sequence input and output, see `PTB example <https://github.com/tensorlayer/tensorlayer/blob/master/example/tutorial_ptb_lstm.py>`__

    A simple regression model below.

    >>> inputs = tl.layers.Input([batch_size, num_steps, embedding_size])
    >>> rnn_out, lstm_state = tl.layers.RNN(
    >>>     cell=tf.keras.layers.LSTMCell(units=hidden_size, dropout=0.1),
    >>>     in_channels=embedding_size,
    >>>     return_last_output=True, return_last_state=True, name='lstmrnn'
    >>> )(inputs)
    >>> outputs = tl.layers.Dense(n_units=1)(rnn_out)
    >>> rnn_model = tl.models.Model(inputs=inputs, outputs=[outputs, rnn_state[0], rnn_state[1]], name='rnn_model')
    >>> # If LSTMCell is applied, the rnn_state is [h, c] where h the hidden state and c the cell state of LSTM.

    A stacked RNN model.

    >>> inputs = tl.layers.Input([batch_size, num_steps, embedding_size])
    >>> rnn_out1 = tl.layers.RNN(
    >>>     cell=tf.keras.layers.SimpleRNNCell(units=hidden_size, dropout=0.1),
    >>>     return_last_output=False, return_seq_2d=False, return_last_state=False
    >>> )(inputs)
    >>> rnn_out2 = tl.layers.RNN(
    >>>     cell=tf.keras.layers.SimpleRNNCell(units=hidden_size, dropout=0.1),
    >>>     return_last_output=True, return_last_state=False
    >>> )(rnn_out1)
    >>> outputs = tl.layers.Dense(n_units=1)(rnn_out2)
    >>> rnn_model = tl.models.Model(inputs=inputs, outputs=outputs)

    An example if the sequences have different length and contain padding.
    Similar to the DynamicRNN in TL 1.x.

    If the `sequence_length` is provided in RNN's forwarding and both `return_last_output` and `return_last_state`
    are set as `True`, the forward function will automatically ignore the paddings. Note that if `return_last_output`
    is set as `False`, the synced sequence outputs will still include outputs which correspond with paddings,
    but users are free to select which slice of outputs to be used in following procedure.

    The `sequence_length` should be a list of integers which indicates the length of each sequence.
    It is recommended to
    `tl.layers.retrieve_seq_length_op3 <https://tensorlayer.readthedocs.io/en/latest/modules/layers.html#compute-sequence-length-3>`__
    to calculate the `sequence_length`.

    >>> data = [[[1], [2], [0], [0], [0]], [[1], [2], [3], [0], [0]], [[1], [2], [6], [1], [1]]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> class DynamicRNNExample(tl.models.Model):
    >>>     def __init__(self):
    >>>         super(DynamicRNNExample, self).__init__()
    >>>         self.rnnlayer = tl.layers.RNN(
    >>>             cell=tf.keras.layers.SimpleRNNCell(units=6, dropout=0.1), in_channels=1, return_last_output=True,
    >>>             return_last_state=True
    >>>         )
    >>>     def forward(self, x):
    >>>         z, s = self.rnnlayer(x, sequence_length=tl.layers.retrieve_seq_length_op3(x))
    >>>         return z, s
    >>> model = DynamicRNNExample()
    >>> model.eval()
    >>> output, state = model(data)


    Notes
    -----
    Input dimension should be rank 3 : [batch_size, n_steps, n_features], if no, please see layer :class:`Reshape`.

    """

    def __init__(
        self,
        cell,
        return_last_output=False,
        return_seq_2d=False,
        return_last_state=True,
        in_channels=None,
        name=None,  # 'rnn'
    ):

        super(RNN, self).__init__(name=name)

        self.cell = cell
        self.return_last_output = return_last_output
        self.return_seq_2d = return_seq_2d
        self.return_last_state = return_last_state

        if in_channels is not None:
            self.build((None, None, in_channels))
            self._built = True

        logging.info("RNN %s: cell: %s, n_units: %s" % (self.name, self.cell.__class__.__name__, self.cell.units))

    def __repr__(self):
        s = ('{classname}(cell={cellname}, n_units={n_units}')
        s += ', name=\'{name}\''
        s += ')'
        return s.format(
            classname=self.__class__.__name__, cellname=self.cell.__class__.__name__, n_units=self.cell.units,
            **self.__dict__
        )

    def build(self, inputs_shape):
        """
        Parameters
        ----------
        inputs_shape : tuple
            the shape of inputs tensor
        """
        # Input dimension should be rank 3 [batch_size, n_steps(max), n_features]
        if len(inputs_shape) != 3:
            raise Exception("RNN : Input dimension should be rank 3 : [batch_size, n_steps, n_features]")

        with tf.name_scope(self.name) as scope:
            self.cell.build(tuple(inputs_shape))

        if self._trainable_weights is None:
            self._trainable_weights = list()
        for var in self.cell.trainable_variables:
            self._trainable_weights.append(var)

    # @tf.function
    def forward(self, inputs, sequence_length=None, initial_state=None, **kwargs):
        """
        Parameters
        ----------
        inputs : input tensor
            The input of a network
        sequence_length: None or list of integers
            The actual length of each sequence in batch without padding.
            If provided, when `return_last_output` and `return_last_state` are `True`,
            the RNN will perform in the manner of a dynamic RNN, i.e.
            the RNN will return the actual last output / state without padding.
        initial_state : None or list of Tensor (RNN State)
            If None, `initial_state` is zero state.

        **kwargs: dict
            Some attributes can be updated during forwarding
            such as `return_last_output`, `return_seq_2d`, `return_last_state`.
        """
        if kwargs:
            for attr in kwargs:
                if attr in self.__dict__:
                    setattr(self, attr, kwargs[attr])

        batch_size = inputs.get_shape().as_list()[0]
        total_steps = inputs.get_shape().as_list()[1]

        # checking the type and values of sequence_length
        if sequence_length is not None:
            if isinstance(sequence_length, list):
                pass
            elif isinstance(sequence_length, tf.Tensor):
                pass
            elif isinstance(sequence_length, np.ndarray):
                sequence_length = sequence_length.tolist()
            else:
                raise TypeError(
                    "The argument sequence_length should be either None or a list of integers. "
                    "Type got %s" % type(sequence_length)
                )
            if (len(sequence_length) != batch_size):
                raise ValueError(
                    "The argument sequence_length should contain %d " % batch_size +
                    "elements indicating the initial length of each sequence, but got only %d. " % len(sequence_length)
                )
            for i in sequence_length:
                if not (type(i) is int or (isinstance(i, tf.Tensor) and i.dtype.is_integer)):
                    raise TypeError(
                        "The argument sequence_length should be either None or a list of integers. "
                        "One element of sequence_length has the type %s" % type(i)
                    )
                if i > total_steps:
                    raise ValueError(
                        "The actual length of a sequence should not be longer than "
                        "that of the longest sequence (total steps) in this mini-batch. "
                        "Total steps of this mini-batch %d, " % total_steps +
                        "but got an actual length of a sequence %d" % i
                    )

        sequence_length = tl.layers.retrieve_seq_length_op3(inputs)

        sequence_length = [i - 1 if i >= 1 else 0 for i in sequence_length]

        # set warning
        # if (not self.return_last_output) and sequence_length is not None:
        #     warnings.warn(
        #         'return_last_output is set as %s ' % self.return_last_output +
        #         'When sequence_length is provided, it is recommended to set as True. ' +
        #         'Otherwise, padding will be considered while RNN is forwarding.'
        #     )

        # return the last output, iterating each seq including padding ones. No need to store output during each
        # time step.
        if self.return_last_output and sequence_length is None:
            outputs = [-1]
        else:
            outputs = list()

        # initialize the states if provided
        states = initial_state if initial_state is not None else self.cell.get_initial_state(inputs)
        if not isinstance(states, list):
            states = [states]

        stored_states = list()

        # initialize the cell
        self.cell.reset_dropout_mask()
        self.cell.reset_recurrent_dropout_mask()

        # recurrent computation
        # FIXME: if sequence_length is provided (dynamic rnn), only iterate max(sequence_length) times.
        for time_step in range(total_steps):

            cell_output, states = self.cell.call(inputs[:, time_step, :], states, training=self.is_train)
            stored_states.append(states)

            if self.return_last_output and sequence_length is None:
                outputs[-1] = cell_output
            else:
                outputs.append(cell_output)

        # prepare to return results
        if self.return_last_output and sequence_length is None:
            outputs = outputs[-1]

        elif self.return_last_output and sequence_length is not None:
            outputs = tf.convert_to_tensor(outputs)
            outputs = tf.gather(outputs, sequence_length, axis=0)

            outputs_without_padding = []
            for i in range(batch_size):
                outputs_without_padding.append(outputs[i][i][:])
            outputs = tf.convert_to_tensor(outputs_without_padding)
        else:
            if self.return_seq_2d:
                # PTB tutorial: stack dense layer after that, or compute the cost from the output
                # 2D Tensor [batch_size * n_steps, n_hidden]
                outputs = tf.reshape(tf.concat(outputs, 1), [-1, self.cell.units])
            else:
                # <akara>: stack more RNN layer after that
                # 3D Tensor [batch_size, n_steps, n_hidden]
                outputs = tf.reshape(tf.concat(outputs, 1), [-1, total_steps, self.cell.units])

        if self.return_last_state and sequence_length is None:
            return outputs, states
        elif self.return_last_state and sequence_length is not None:

            stored_states = tf.convert_to_tensor(stored_states)
            stored_states = tf.gather(stored_states, sequence_length, axis=0)

            states = []
            for i in range(stored_states.shape[1]):
                states.append(tf.convert_to_tensor([stored_states[b, i, b, :] for b in range(batch_size)]))

            return outputs, states
        else:
            return outputs


class SimpleRNN(RNN):
    """
    The :class:`SimpleRNN` class is a fixed length recurrent layer for implementing simple RNN.

    Parameters
    ----------
    units: int
        Positive integer, the dimension of hidden space.
    return_last_output : boolean
        Whether return last output or all outputs in a sequence.
            - If True, return the last output, "Sequence input and single output"
            - If False, return all outputs, "Synced sequence input and output"
            - In other word, if you want to stack more RNNs on this layer, set to False

        In a dynamic model, `return_last_output` can be updated when it is called in customised forward().
        By default, `False`.
    return_seq_2d : boolean
        Only consider this argument when `return_last_output` is `False`
            - If True, return 2D Tensor [batch_size * n_steps, n_hidden], for stacking Dense layer after it.
            - If False, return 3D Tensor [batch_size, n_steps, n_hidden], for stacking multiple RNN after it.

        In a dynamic model, `return_seq_2d` can be updated when it is called in customised forward().
        By default, `False`.
    return_last_state: boolean
        Whether to return the last state of the RNN cell. The state is a list of Tensor.
        For simple RNN, last_state = [last_output]

            - If True, the layer will return outputs and the final state of the cell.
            - If False, the layer will return outputs only.

        In a dynamic model, `return_last_state` can be updated when it is called in customised forward().
        By default, `False`.
    in_channels: int
        Optional, the number of channels of the previous layer which is normally the size of embedding.
        If given, the layer will be built when init.
        If None, it will be automatically detected when the layer is forwarded for the first time.
    name : str
        A unique layer name.
    `**kwargs`:
        Advanced arguments to configure the simple RNN cell.
        Please check tf.keras.layers.SimpleRNNCell.

    Examples
    --------

    A simple regression model below.

    >>> inputs = tl.layers.Input([batch_size, num_steps, embedding_size])
    >>> rnn_out, lstm_state = tl.layers.SimpleRNN(
    >>>     units=hidden_size, dropout=0.1, # both units and dropout are used to configure the simple rnn cell.
    >>>     in_channels=embedding_size,
    >>>     return_last_output=True, return_last_state=True, name='simplernn'
    >>> )(inputs)
    >>> outputs = tl.layers.Dense(n_units=1)(rnn_out)
    >>> rnn_model = tl.models.Model(inputs=inputs, outputs=[outputs, rnn_state[0]], name='rnn_model')

    Notes
    -----
    Input dimension should be rank 3 : [batch_size, n_steps, n_features], if no, please see layer :class:`Reshape`.

    """

    def __init__(
        self,
        units,
        return_last_output=False,
        return_seq_2d=False,
        return_last_state=True,
        in_channels=None,
        name=None,  # 'simplernn'
        **kwargs
    ):
        super(SimpleRNN, self).__init__(
            cell=tf.keras.layers.SimpleRNNCell(units=units, **kwargs), return_last_output=return_last_output,
            return_seq_2d=return_seq_2d, return_last_state=return_last_state, in_channels=in_channels, name=name
        )


class GRURNN(RNN):
    """
    The :class:`GRURNN` class is a fixed length recurrent layer for implementing RNN with GRU cell.

    Parameters
    ----------
    units: int
        Positive integer, the dimension of hidden space.
    return_last_output : boolean
        Whether return last output or all outputs in a sequence.
            - If True, return the last output, "Sequence input and single output"
            - If False, return all outputs, "Synced sequence input and output"
            - In other word, if you want to stack more RNNs on this layer, set to False

        In a dynamic model, `return_last_output` can be updated when it is called in customised forward().
        By default, `False`.
    return_seq_2d : boolean
        Only consider this argument when `return_last_output` is `False`
            - If True, return 2D Tensor [batch_size * n_steps, n_hidden], for stacking Dense layer after it.
            - If False, return 3D Tensor [batch_size, n_steps, n_hidden], for stacking multiple RNN after it.

        In a dynamic model, `return_seq_2d` can be updated when it is called in customised forward().
        By default, `False`.
    return_last_state: boolean
        Whether to return the last state of the RNN cell. The state is a list of Tensor.
        For GRU, last_state = [last_output]

            - If True, the layer will return outputs and the final state of the cell.
            - If False, the layer will return outputs only.

        In a dynamic model, `return_last_state` can be updated when it is called in customised forward().
        By default, `False`.
    in_channels: int
        Optional, the number of channels of the previous layer which is normally the size of embedding.
        If given, the layer will be built when init.
        If None, it will be automatically detected when the layer is forwarded for the first time.
    name : str
        A unique layer name.
    `**kwargs`:
        Advanced arguments to configure the GRU cell.
        Please check tf.keras.layers.GRUCell.

    Examples
    --------

    A simple regression model below.

    >>> inputs = tl.layers.Input([batch_size, num_steps, embedding_size])
    >>> rnn_out, lstm_state = tl.layers.GRURNN(
    >>>     units=hidden_size, dropout=0.1, # both units and dropout are used to configure the GRU cell.
    >>>     in_channels=embedding_size,
    >>>     return_last_output=True, return_last_state=True, name='grurnn'
    >>> )(inputs)
    >>> outputs = tl.layers.Dense(n_units=1)(rnn_out)
    >>> rnn_model = tl.models.Model(inputs=inputs, outputs=[outputs, rnn_state[0]], name='rnn_model')

    Notes
    -----
    Input dimension should be rank 3 : [batch_size, n_steps, n_features], if no, please see layer :class:`Reshape`.

    """

    def __init__(
        self,
        units,
        return_last_output=False,
        return_seq_2d=False,
        return_last_state=True,
        in_channels=None,
        name=None,  # 'grurnn'
        **kwargs
    ):
        super(GRURNN, self).__init__(
            cell=tf.keras.layers.GRUCell(units=units, **kwargs), return_last_output=return_last_output,
            return_seq_2d=return_seq_2d, return_last_state=return_last_state, in_channels=in_channels, name=name
        )


class LSTMRNN(RNN):
    """
    The :class:`LSTMRNN` class is a fixed length recurrent layer for implementing RNN with LSTM cell.

    Parameters
    ----------
    units: int
        Positive integer, the dimension of hidden space.
    return_last_output : boolean
        Whether return last output or all outputs in a sequence.
            - If True, return the last output, "Sequence input and single output"
            - If False, return all outputs, "Synced sequence input and output"
            - In other word, if you want to stack more RNNs on this layer, set to False

        In a dynamic model, `return_last_output` can be updated when it is called in customised forward().
        By default, `False`.
    return_seq_2d : boolean
        Only consider this argument when `return_last_output` is `False`
            - If True, return 2D Tensor [batch_size * n_steps, n_hidden], for stacking Dense layer after it.
            - If False, return 3D Tensor [batch_size, n_steps, n_hidden], for stacking multiple RNN after it.

        In a dynamic model, `return_seq_2d` can be updated when it is called in customised forward().
        By default, `False`.
    return_last_state: boolean
        Whether to return the last state of the RNN cell. The state is a list of Tensor.
        For LSTM, last_state = [last_output, last_cell_state]

            - If True, the layer will return outputs and the final state of the cell.
            - If False, the layer will return outputs only.

        In a dynamic model, `return_last_state` can be updated when it is called in customised forward().
        By default, `False`.
    in_channels: int
        Optional, the number of channels of the previous layer which is normally the size of embedding.
        If given, the layer will be built when init.
        If None, it will be automatically detected when the layer is forwarded for the first time.
    name : str
        A unique layer name.
    `**kwargs`:
        Advanced arguments to configure the LSTM cell.
        Please check tf.keras.layers.LSTMCell.

    Examples
    --------

    A simple regression model below.

    >>> inputs = tl.layers.Input([batch_size, num_steps, embedding_size])
    >>> rnn_out, lstm_state = tl.layers.LSTMRNN(
    >>>     units=hidden_size, dropout=0.1, # both units and dropout are used to configure the LSTM cell.
    >>>     in_channels=embedding_size,
    >>>     return_last_output=True, return_last_state=True, name='grurnn'
    >>> )(inputs)
    >>> outputs = tl.layers.Dense(n_units=1)(rnn_out)
    >>> rnn_model = tl.models.Model(inputs=inputs, outputs=[outputs, rnn_state[0]], name='rnn_model')

    Notes
    -----
    Input dimension should be rank 3 : [batch_size, n_steps, n_features], if no, please see layer :class:`Reshape`.

    """

    def __init__(
        self,
        units,
        return_last_output=False,
        return_seq_2d=False,
        return_last_state=True,
        in_channels=None,
        name=None,  # 'lstmrnn'
        **kwargs
    ):
        super(LSTMRNN, self).__init__(
            cell=tf.keras.layers.LSTMCell(units=units, **kwargs), return_last_output=return_last_output,
            return_seq_2d=return_seq_2d, return_last_state=return_last_state, in_channels=in_channels, name=name
        )


class BiRNN(Module):
    """
    The :class:`BiRNN` class is a fixed length Bidirectional recurrent layer.

    Parameters
    ----------
    fw_cell : TensorFlow cell function for forward direction
        A RNN cell implemented by tf.keras, e.g. tf.keras.layers.SimpleRNNCell, tf.keras.layers.LSTMCell, tf.keras.layers.GRUCell.
        Note TF2.0+, TF1.0+ and TF1.0- are different
    bw_cell: TensorFlow cell function for backward direction similar with `fw_cell`
    return_seq_2d : boolean.
        If True, return 2D Tensor [batch_size * n_steps, n_hidden], for stacking Dense layer after it.
        If False, return 3D Tensor [batch_size, n_steps, n_hidden], for stacking multiple RNN after it.
        In a dynamic model, `return_seq_2d` can be updated when it is called in customised forward().
        By default, `False`.
    return_last_state: boolean
        Whether to return the last state of the two cells. The state is a list of Tensor.
            - If True, the layer will return outputs, the final state of `fw_cell` and the final state of `bw_cell`.
            - If False, the layer will return outputs only.

        In a dynamic model, `return_last_state` can be updated when it is called in customised forward().
        By default, `False`.
    in_channels: int
        Optional, the number of channels of the previous layer which is normally the size of embedding.
        If given, the layer will be built when init.
        If None, it will be automatically detected when the layer is forwarded for the first time.
    name : str
        A unique layer name.

    Examples
    --------
    A simple regression model below.

    >>> inputs = tl.layers.Input([batch_size, num_steps, embedding_size])
    >>> # the fw_cell and bw_cell can be different
    >>> rnnlayer = tl.layers.BiRNN(
    >>>     fw_cell=tf.keras.layers.SimpleRNNCell(units=hidden_size, dropout=0.1),
    >>>     bw_cell=tf.keras.layers.SimpleRNNCell(units=hidden_size + 1, dropout=0.1),
    >>>     return_seq_2d=True, return_last_state=True
    >>> )
    >>> # if return_last_state=True, the final state of the two cells will be returned together with the outputs
    >>> # if return_last_state=False, only the outputs will be returned
    >>> rnn_out, rnn_fw_state, rnn_bw_state = rnnlayer(inputs)
    >>> # if the BiRNN is followed by a Dense, return_seq_2d should be True.
    >>> # if the BiRNN is followed by other RNN, return_seq_2d can be False.
    >>> dense = tl.layers.Dense(n_units=1)(rnn_out)
    >>> outputs = tl.layers.Reshape([-1, num_steps])(dense)
    >>> rnn_model = tl.models.Model(inputs=inputs, outputs=[outputs, rnn_out, rnn_fw_state[0], rnn_bw_state[0]])

    A stacked BiRNN model.

    >>> inputs = tl.layers.Input([batch_size, num_steps, embedding_size])
    >>> rnn_out1 = tl.layers.BiRNN(
    >>>     fw_cell=tf.keras.layers.SimpleRNNCell(units=hidden_size, dropout=0.1),
    >>>     bw_cell=tf.keras.layers.SimpleRNNCell(units=hidden_size + 1, dropout=0.1),
    >>>     return_seq_2d=False, return_last_state=False
    >>> )(inputs)
    >>> rnn_out2 = tl.layers.BiRNN(
    >>>     fw_cell=tf.keras.layers.SimpleRNNCell(units=hidden_size, dropout=0.1),
    >>>     bw_cell=tf.keras.layers.SimpleRNNCell(units=hidden_size + 1, dropout=0.1),
    >>>     return_seq_2d=True, return_last_state=False
    >>> )(rnn_out1)
    >>> dense = tl.layers.Dense(n_units=1)(rnn_out2)
    >>> outputs = tl.layers.Reshape([-1, num_steps])(dense)
    >>> rnn_model = tl.models.Model(inputs=inputs, outputs=outputs)

    Notes
    -----
    Input dimension should be rank 3 : [batch_size, n_steps, n_features]. If not, please see layer :class:`Reshape`.

    """

    def __init__(
        self,
        fw_cell,
        bw_cell,
        return_seq_2d=False,
        return_last_state=False,
        in_channels=None,
        name=None,  # 'birnn'
    ):
        super(BiRNN, self).__init__(name)

        self.fw_cell = fw_cell
        self.bw_cell = bw_cell
        self.return_seq_2d = return_seq_2d
        self.return_last_state = return_last_state

        if in_channels is not None:
            self.build((None, None, in_channels))
            self._built = True

        logging.info(
            "BiRNN %s: fw_cell: %s, fw_n_units: %s, bw_cell: %s, bw_n_units： %s" % (
                self.name, self.fw_cell.__class__.__name__, self.fw_cell.units, self.bw_cell.__class__.__name__,
                self.bw_cell.units
            )
        )

    def __repr__(self):
        s = (
            '{classname}(fw_cell={fw_cellname}, fw_n_units={fw_n_units}'
            ', bw_cell={bw_cellname}, bw_n_units={bw_n_units}'
        )
        s += ', name=\'{name}\''
        s += ')'
        return s.format(
            classname=self.__class__.__name__, fw_cellname=self.fw_cell.__class__.__name__,
            fw_n_units=self.fw_cell.units, bw_cellname=self.bw_cell.__class__.__name__, bw_n_units=self.bw_cell.units,
            **self.__dict__
        )

    def build(self, inputs_shape):
        """
        Parameters
        ----------
        inputs_shape : tuple
            the shape of inputs tensor
        """
        # Input dimension should be rank 3 [batch_size, n_steps(max), n_features]
        if len(inputs_shape) != 3:
            raise Exception("RNN : Input dimension should be rank 3 : [batch_size, n_steps, n_features]")

        with tf.name_scope(self.name) as scope:
            self.fw_cell.build(tuple(inputs_shape))
            self.bw_cell.build(tuple(inputs_shape))

        if self._trainable_weights is None:
            self._trainable_weights = list()
        for var in self.fw_cell.trainable_variables:
            self._trainable_weights.append(var)
        for var in self.bw_cell.trainable_variables:
            self._trainable_weights.append(var)

    # @tf.function
    def forward(self, inputs, fw_initial_state=None, bw_initial_state=None, **kwargs):
        """
        Parameters
        ----------
        inputs : input tensor
            The input of a network
        fw_initial_state : None or list of Tensor (RNN State)
            If None, `fw_initial_state` is zero state.
        bw_initial_state : None or list of Tensor (RNN State)
            If None, `bw_initial_state` is zero state.
        **kwargs: dict
            Some attributes can be updated during forwarding
            such as `return_last_output`, `return_seq_2d`, `return_last_state`.
        """

        if kwargs:
            for attr in kwargs:
                if attr in self.__dict__:
                    setattr(self, attr, kwargs[attr])

        fw_outputs = list()
        bw_outputs = list()

        fw_states = fw_initial_state if fw_initial_state is not None else self.fw_cell.get_initial_state(inputs)
        bw_states = bw_initial_state if bw_initial_state is not None else self.bw_cell.get_initial_state(inputs)

        if not isinstance(fw_states, list):
            fw_states = [fw_states]
        if not isinstance(bw_states, list):
            bw_states = [bw_states]

        total_steps = inputs.get_shape().as_list()[1]

        self.fw_cell.reset_dropout_mask()
        self.fw_cell.reset_recurrent_dropout_mask()
        self.bw_cell.reset_dropout_mask()
        self.bw_cell.reset_recurrent_dropout_mask()

        for time_step in range(total_steps):
            fw_cell_output, fw_states = self.fw_cell.call(inputs[:, time_step, :], fw_states, training=self.is_train)
            bw_cell_output, bw_states = self.bw_cell.call(
                inputs[:, -time_step - 1, :], bw_states, training=self.is_train
            )

            fw_outputs.append(fw_cell_output)
            bw_outputs.append(bw_cell_output)

        if self.return_seq_2d:
            # PTB tutorial: stack dense layer after that, or compute the cost from the output
            # 2D Tensor [batch_size * n_steps, n_hidden]
            fw_outputs = tf.reshape(tf.concat(fw_outputs, 1), [-1, self.fw_cell.units])
            bw_outputs = tf.reshape(tf.concat(bw_outputs, 1), [-1, self.bw_cell.units])
        else:
            # <akara>: stack more RNN layer after that
            # 3D Tensor [batch_size, n_steps, n_hidden]
            fw_outputs = tf.reshape(tf.concat(fw_outputs, 1), [-1, total_steps, self.fw_cell.units])
            bw_outputs = tf.reshape(tf.concat(bw_outputs, 1), [-1, total_steps, self.bw_cell.units])

        outputs = tf.concat([fw_outputs, bw_outputs], -1)

        if self.return_last_state:
            return outputs, fw_states, bw_states
        else:
            return outputs


'''
class ConvRNNCell(object):
    """Abstract object representing an Convolutional RNN Cell."""

    def __call__(self, inputs, state, scope=None):
        """Run this RNN cell on inputs, starting from the given state."""
        raise NotImplementedError("Abstract method")

    @property
    def state_size(self):
        """size(s) of state(s) used by this cell."""
        raise NotImplementedError("Abstract method")

    @property
    def output_size(self):
        """Integer or TensorShape: size of outputs produced by this cell."""
        raise NotImplementedError("Abstract method")

    def zero_state(self, batch_size):  #, dtype=LayersConfig.tf_dtype):
        """Return zero-filled state tensor(s).
        Args:
          batch_size: int, float, or unit Tensor representing the batch size.
        Returns:
          tensor of shape '[batch_size x shape[0] x shape[1] x num_features]
          filled with zeros

        """
        dtype = LayersConfig.tf_dtype
        shape = self.shape
        num_features = self.num_features
        # TODO : TypeError: 'NoneType' object is not subscriptable
        zeros = tf.zeros([batch_size, shape[0], shape[1], num_features * 2], dtype=dtype)
        return zeros


class BasicConvLSTMCell(ConvRNNCell):
    """Basic Conv LSTM recurrent network cell.

    Parameters
    -----------
    shape : tuple of int
        The height and width of the cell.
    filter_size : tuple of int
        The height and width of the filter
    num_features : int
        The hidden size of the cell
    forget_bias : float
        The bias added to forget gates (see above).
    input_size : int
        Deprecated and unused.
    state_is_tuple : boolen
        If True, accepted and returned states are 2-tuples of the `c_state` and `m_state`.
        If False, they are concatenated along the column axis. The latter behavior will soon be deprecated.
    act : activation function
        The activation function of this layer, tanh as default.

    """

    def __init__(
            self, shape, filter_size, num_features, forget_bias=1.0, input_size=None, state_is_tuple=False,
            act=tf.nn.tanh
    ):
        """Initialize the basic Conv LSTM cell."""
        # if not state_is_tuple:
        # logging.warn("%s: Using a concatenated state is slower and will soon be "
        #             "deprecated.  Use state_is_tuple=True.", self)
        if input_size is not None:
            logging.warn("%s: The input_size parameter is deprecated.", self)
        self.shape = shape
        self.filter_size = filter_size
        self.num_features = num_features
        self._forget_bias = forget_bias
        self._state_is_tuple = state_is_tuple
        self._activation = act

    @property
    def state_size(self):
        """State size of the LSTMStateTuple."""
        return (LSTMStateTuple(self._num_units, self._num_units) if self._state_is_tuple else 2 * self._num_units)

    @property
    def output_size(self):
        """Number of units in outputs."""
        return self._num_units

    def __call__(self, inputs, state, scope=None):
        """Long short-term memory cell (LSTM)."""
        with tf.compat.v1.variable_scope(scope or type(self).__name__):  # "BasicLSTMCell"
            # Parameters of gates are concatenated into one multiply for efficiency.
            if self._state_is_tuple:
                c, h = state
            else:
                # print state
                # c, h = tf.split(3, 2, state)
                c, h = tf.split(state, 2, 3)
            concat = _conv_linear([inputs, h], self.filter_size, self.num_features * 4, True)

            # i = input_gate, j = new_input, f = forget_gate, o = output_gate
            # i, j, f, o = tf.split(3, 4, concat)
            i, j, f, o = tf.split(concat, 4, 3)

            new_c = (c * tf.nn.sigmoid(f + self._forget_bias) + tf.nn.sigmoid(i) * self._activation(j))
            new_h = self._activation(new_c) * tf.nn.sigmoid(o)

            if self._state_is_tuple:
                new_state = LSTMStateTuple(new_c, new_h)
            else:
                new_state = tf.concat([new_c, new_h], 3)
            return new_h, new_state


def _conv_linear(args, filter_size, num_features, bias, bias_start=0.0, scope=None):
    """convolution:

    Parameters
    ----------
    args : tensor
        4D Tensor or a list of 4D, batch x n, Tensors.
    filter_size : tuple of int
        Filter height and width.
    num_features : int
        Nnumber of features.
    bias_start : float
        Starting value to initialize the bias; 0 by default.
    scope : VariableScope
        For the created subgraph; defaults to "Linear".

    Returns
    --------
    - A 4D Tensor with shape [batch h w num_features]

    Raises
    -------
    - ValueError : if some of the arguments has unspecified or wrong shape.

    """
    # Calculate the total size of arguments on dimension 1.
    total_arg_size_depth = 0
    shapes = [a.get_shape().as_list() for a in args]
    for shape in shapes:
        if len(shape) != 4:
            raise ValueError("Linear is expecting 4D arguments: %s" % str(shapes))
        if not shape[3]:
            raise ValueError("Linear expects shape[4] of arguments: %s" % str(shapes))
        else:
            total_arg_size_depth += shape[3]

    dtype = [a.dtype for a in args][0]

    # Now the computation.
    with tf.compat.v1.variable_scope(scope or "Conv"):
        matrix = tf.compat.v1.get_variable(
            "Matrix", [filter_size[0], filter_size[1], total_arg_size_depth, num_features], dtype=dtype
        )
        if len(args) == 1:
            res = tf.nn.conv2d(args[0], matrix, strides=[1, 1, 1, 1], padding='SAME')
        else:
            res = tf.nn.conv2d(tf.concat(args, 3), matrix, strides=[1, 1, 1, 1], padding='SAME')
        if not bias:
            return res
        bias_term = tf.compat.v1.get_variable(
            "Bias", [num_features], dtype=dtype,
            initializer=tf.compat.v1.initializers.constant(bias_start, dtype=dtype)
        )
    return res + bias_term


class ConvLSTM(Module):
    """A fixed length Convolutional LSTM layer.

    See this `paper <https://arxiv.org/abs/1506.04214>`__ .

    Parameters
    ----------
    prev_layer : :class:`Module`
        Previous layer
    cell_shape : tuple of int
        The shape of each cell width * height
    filter_size : tuple of int
        The size of filter width * height
    cell_fn : a convolutional RNN cell
        Cell function like :class:`BasicConvLSTMCell`
    feature_map : int
        The number of feature map in the layer.
    initializer : initializer
        The initializer for initializing the parameters.
    n_steps : int
        The sequence length.
    initial_state : None or ConvLSTM State
        If None, `initial_state` is zero state.
    return_last : boolean
        Whether return last output or all outputs in each step.
            - If True, return the last output, "Sequence input and single output".
            - If False, return all outputs, "Synced sequence input and output".
            - In other word, if you want to stack more RNNs on this layer, set to False.

    return_seq_2d : boolean
        Only consider this argument when `return_last_output` is `False`
            - If True, return 2D Tensor [n_example, n_hidden], for stacking DenseLayer after it.
            - If False, return 3D Tensor [n_example/n_steps, n_steps, n_hidden], for stacking multiple RNN after it.

    name : str
        A unique layer name.

    Attributes
    ----------
    outputs : tensor
        The output of this RNN. return_last_output = False, outputs = all cell_output, which is the hidden state.
        cell_output.get_shape() = (?, h, w, c])

    final_state : tensor or StateTuple
        The finial state of this layer.
            - When state_is_tuple = False, it is the final hidden and cell states,
            - When state_is_tuple = True, You can get the final state after each iteration during training, then feed it to the initial state of next iteration.

    initial_state : tensor or StateTuple
        It is the initial state of this ConvLSTM layer, you can use it to initialize
        your state at the beginning of each epoch or iteration according to your
        training procedure.

    batch_size : int or tensor
        Is int, if able to compute the batch_size, otherwise, tensor for ``?``.

    """

    @deprecated_alias(layer='prev_layer', end_support_version=1.9)  # TODO remove this line for the 1.9 release
    def __init__(
            self,
            prev_layer,
            cell_shape=None,
            feature_map=1,
            filter_size=(3, 3),
            cell_fn=BasicConvLSTMCell,
            initializer=tf.compat.v1.initializers.random_uniform(-0.1, 0.1),
            n_steps=5,
            initial_state=None,
            return_last=False,
            return_seq_2d=False,
            name='convlstm',
    ):
        super(ConvLSTM, self).__init__(prev_layer=prev_layer, name=name)

        logging.info(
            "ConvLSTM %s: feature_map: %d, n_steps: %d, "
            "in_dim: %d %s, cell_fn: %s " %
            (self.name, feature_map, n_steps, self.inputs.get_shape().ndims, self.inputs.get_shape(), cell_fn.__name__)
        )
        # You can get the dimension by .get_shape() or ._shape, and check the
        # dimension by .with_rank() as follow.
        # self.inputs.get_shape().with_rank(2)
        # self.inputs.get_shape().with_rank(3)

        # Input dimension should be rank 5 [batch_size, n_steps(max), h, w, c]
        try:
            self.inputs.get_shape().with_rank(5)
        except Exception:
            raise Exception(
                "RNN : Input dimension should be rank 5 : [batch_size, n_steps, input_x, "
                "input_y, feature_map]"
            )

        fixed_batch_size = self.inputs.get_shape().with_rank_at_least(1)[0]

        if fixed_batch_size.value:
            batch_size = fixed_batch_size.value
            logging.info("     RNN batch_size (concurrent processes): %d" % batch_size)

        else:
            batch_size = array_ops.shape(self.inputs)[0]
            logging.info("     non specified batch_size, uses a tensor instead.")
        self.batch_size = batch_size
        outputs = []
        self.cell = cell = cell_fn(shape=cell_shape, filter_size=filter_size, num_features=feature_map)

        if initial_state is None:
            self.initial_state = cell.zero_state(batch_size, dtype=LayersConfig.tf_dtype)
        else:
            self.initial_state = initial_state

        state = self.initial_state

        # with tf.variable_scope("model", reuse=None, initializer=initializer):
        with tf.compat.v1.variable_scope(name, initializer=initializer) as vs:
            for time_step in range(n_steps):
                if time_step > 0: tf.compat.v1.get_variable_scope().reuse_variables()
                (cell_output, state) = cell(self.inputs[:, time_step, :, :, :], state)
                outputs.append(cell_output)

            # Retrieve just the RNN variables.
            # rnn_variables = [v for v in tf.all_variables() if v.name.startswith(vs.name)]
            rnn_variables = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.VARIABLES, scope=vs.name)

            logging.info(" n_params : %d" % (len(rnn_variables)))

            if return_last:
                # 2D Tensor [batch_size, n_hidden]
                self.outputs = outputs[-1]
            else:
                if return_seq_2d:
                    # PTB tutorial: stack dense layer after that, or compute the cost from the output
                    # 4D Tensor [n_example, h, w, c]
                    self.outputs = tf.reshape(tf.concat(outputs, 1), [-1, cell_shape[0] * cell_shape[1] * feature_map])
                else:
                    # <akara>: stack more RNN layer after that
                    # 5D Tensor [n_example/n_steps, n_steps, h, w, c]
                    self.outputs = tf.reshape(
                        tf.concat(outputs, 1), [-1, n_steps, cell_shape[0], cell_shape[1], feature_map]
                    )

        self.final_state = state

        self._add_layers(self.outputs)
        self._add_params(rnn_variables)

'''


# @tf.function
def retrieve_seq_length_op(data):
    """An op to compute the length of a sequence from input shape of [batch_size, n_step(max), n_features],
    it can be used when the features of padding (on right hand side) are all zeros.

    Parameters
    -----------
    data : tensor
        [batch_size, n_step(max), n_features] with zero padding on right hand side.

    Examples
    -----------
    Single feature

    >>> data = [[[1],[2],[0],[0],[0]],
    >>>         [[1],[2],[3],[0],[0]],
    >>>         [[1],[2],[6],[1],[0]]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> length = tl.layers.retrieve_seq_length_op(data)
    [2 3 4]

    Multiple features

    >>> data = [[[1,2],[2,2],[1,2],[1,2],[0,0]],
    >>>          [[2,3],[2,4],[3,2],[0,0],[0,0]],
    >>>          [[3,3],[2,2],[5,3],[1,2],[0,0]]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> length = tl.layers.retrieve_seq_length_op(data)
    [4 3 4]

    References
    ------------
    Borrow from `TFlearn <https://github.com/tflearn/tflearn/blob/master/tflearn/layers/recurrent.py>`__.

    """
    with tf.name_scope('GetLength'):
        used = tf.sign(tf.reduce_max(input_tensor=tf.abs(data), axis=2))
        length = tf.reduce_sum(input_tensor=used, axis=1)

        return tf.cast(length, tf.int32)


# @tf.function
def retrieve_seq_length_op2(data):
    """An op to compute the length of a sequence, from input shape of [batch_size, n_step(max)],
    it can be used when the features of padding (on right hand side) are all zeros.

    Parameters
    -----------
    data : tensor
        [batch_size, n_step(max)] with zero padding on right hand side.

    Examples
    -----------
    >>> data = [[1,2,0,0,0],
    >>>         [1,2,3,0,0],
    >>>         [1,2,6,1,0]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> length = tl.layers.retrieve_seq_length_op2(data)
    tensor([2 3 4])

    """
    return tf.reduce_sum(input_tensor=tf.cast(tf.greater(data, tf.zeros_like(data)), tf.int32), axis=1)


# @tf.function
def retrieve_seq_length_op3(data, pad_val=0):
    """An op to compute the length of a sequence, the data shape can be [batch_size, n_step(max)] or
    [batch_size, n_step(max), n_features].

    If the data has type of tf.string and pad_val is assigned as empty string (''), this op will compute the
    length of the string sequence.

    Parameters
    -----------
    data : tensor
        [batch_size, n_step(max)] or [batch_size, n_step(max), n_features] with zero padding on the right hand side.
    pad_val:
        By default 0. If the data is tf.string, please assign this as empty string ('')

    Examples
    -----------
    >>> data = [[[1],[2],[0],[0],[0]],
    >>>         [[1],[2],[3],[0],[0]],
    >>>         [[1],[2],[6],[1],[0]]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> length = tl.layers.retrieve_seq_length_op3(data)
    tensor([2, 3, 4])
    >>> data = [[[1,2],[2,2],[1,2],[1,2],[0,0]],
    >>>         [[2,3],[2,4],[3,2],[0,0],[0,0]],
    >>>         [[3,3],[2,2],[5,3],[1,2],[0,0]]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> length = tl.layers.retrieve_seq_length_op3(data)
    tensor([4, 3, 4])
    >>> data = [[1,2,0,0,0],
    >>>         [1,2,3,0,0],
    >>>         [1,2,6,1,0]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> length = tl.layers.retrieve_seq_length_op3(data)
    tensor([2, 3, 4])
    >>> data = [['hello','world','','',''],
    >>>         ['hello','world','tensorlayer','',''],
    >>>         ['hello','world','tensorlayer','2.0','']]
    >>> data = tf.convert_to_tensor(data, dtype=tf.string)
    >>> length = tl.layers.retrieve_seq_length_op3(data, pad_val='')
    tensor([2, 3, 4])

    """
    data_shape_size = data.get_shape().ndims
    if data_shape_size == 3:
        return tf.reduce_sum(
            input_tensor=tf.cast(tf.reduce_any(input_tensor=tf.not_equal(data, pad_val), axis=2), dtype=tf.int32),
            axis=1
        )
    elif data_shape_size == 2:
        return tf.reduce_sum(input_tensor=tf.cast(tf.not_equal(data, pad_val), dtype=tf.int32), axis=1)
    elif data_shape_size == 1:
        raise ValueError("retrieve_seq_length_op3: data has wrong shape! Shape got ", data.get_shape().as_list())
    else:
        raise ValueError(
            "retrieve_seq_length_op3: handling data with num of dims %s hasn't been implemented!" % (data_shape_size)
        )


def target_mask_op(data, pad_val=0):
    """ Return the mask of the input sequence data based on the padding values.

    Parameters
    -----------
    data : tf.Tensor
        A tensor with 2 or 3 dimensions.
    pad_val: int, float, string, etc
        The value that represent padding. By default, 0. For tf.string, you may use empty string.

    Examples
    -----------
    >>> data = [['hello', 'world', '', '', ''],
    >>>         ['hello', 'world', 'tensorlayer', '', ''],
    >>>         ['hello', 'world', 'tensorlayer', '2.0', '']]
    >>> data = tf.convert_to_tensor(data, dtype=tf.string)
    >>> mask = tl.layers.target_mask_op(data, pad_val='')
    >>> print(mask)
    tf.Tensor(
    [[1 1 0 0 0]
     [1 1 1 0 0]
     [1 1 1 1 0]], shape=(3, 5), dtype=int32)
    >>> data = [[[1], [0], [0], [0], [0]],
    >>>         [[1], [2], [3], [0], [0]],
    >>>         [[1], [2], [0], [1], [0]]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> mask = tl.layers.target_mask_op(data)
    >>> print(mask)
    tf.Tensor(
    [[1 0 0 0 0]
     [1 1 1 0 0]
     [1 1 0 1 0]], shape=(3, 5), dtype=int32)
    >>> data = [[[0,0],[2,2],[1,2],[1,2],[0,0]],
    >>>         [[2,3],[2,4],[3,2],[1,0],[0,0]],
    >>>         [[3,3],[0,1],[5,3],[1,2],[0,0]]]
    >>> data = tf.convert_to_tensor(data, dtype=tf.float32)
    >>> mask = tl.layers.target_mask_op(data)
    >>> print(mask)
    tf.Tensor(
    [[0 1 1 1 0]
     [1 1 1 1 0]
     [1 1 1 1 0]], shape=(3, 5), dtype=int32)
    """

    if not isinstance(data, tf.Tensor):
        raise AttributeError("target_mask_op: the type of input data should be tf.Tensor but got %s." % type(data))
    data_shape_size = data.get_shape().ndims
    if data_shape_size == 3:
        return tf.cast(tf.reduce_any(input_tensor=tf.not_equal(data, pad_val), axis=2), dtype=tf.int32)
    elif data_shape_size == 2:
        return tf.cast(tf.not_equal(data, pad_val), dtype=tf.int32)
    elif data_shape_size == 1:
        raise ValueError(
            "target_mask_op: data_shape %s is not supported. "
            "The shape of data should have 2 or 3 dims." % (data.get_shape())
        )
    else:
        raise ValueError(
            "target_mask_op: handling data_shape %s hasn't been implemented! "
            "The shape of data should have 2 or 3 dims" % (data.get_shape())
        )
