# Copyright (c) 2021 PaddlePaddle 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.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import paddle
import paddle.nn as nn
import paddle.nn.functional as F
import numpy as np
from paddlex.cv.nets.ppdet.core.workspace import register
from ..ops import bipartite_match, box_coder, iou_similarity

__all__ = ['SSDLoss']


@register
class SSDLoss(nn.Layer):
    """
    SSDLoss

    Args:
        match_type (str): The type of matching method, should be
           'bipartite' or 'per_prediction'. None ('bipartite') by default.
        overlap_threshold (float32, optional): If `match_type` is 'per_prediction',
            this threshold is to determine the extra matching bboxes based
            on the maximum distance, 0.5 by default.
        neg_pos_ratio (float): The ratio of negative samples / positive samples.
        neg_overlap (float): The overlap threshold of negative samples.
        loc_loss_weight (float): The weight of loc_loss.
        conf_loss_weight (float): The weight of conf_loss.
    """

    def __init__(self,
                 match_type='per_prediction',
                 overlap_threshold=0.5,
                 neg_pos_ratio=3.0,
                 neg_overlap=0.5,
                 loc_loss_weight=1.0,
                 conf_loss_weight=1.0):
        super(SSDLoss, self).__init__()
        self.match_type = match_type
        self.overlap_threshold = overlap_threshold
        self.neg_pos_ratio = neg_pos_ratio
        self.neg_overlap = neg_overlap
        self.loc_loss_weight = loc_loss_weight
        self.conf_loss_weight = conf_loss_weight

    def _label_target_assign(self,
                             gt_label,
                             matched_indices,
                             neg_mask=None,
                             mismatch_value=0):
        gt_label = gt_label.numpy()
        matched_indices = matched_indices.numpy()
        if neg_mask is not None:
            neg_mask = neg_mask.numpy()

        batch_size, num_priors = matched_indices.shape
        trg_lbl = np.ones((batch_size, num_priors, 1)).astype('int32')
        trg_lbl *= mismatch_value
        trg_lbl_wt = np.zeros((batch_size, num_priors, 1)).astype('float32')

        for i in range(batch_size):
            col_ids = np.where(matched_indices[i] > -1)
            col_val = matched_indices[i][col_ids]
            trg_lbl[i][col_ids] = gt_label[i][col_val]
            trg_lbl_wt[i][col_ids] = 1.0

        if neg_mask is not None:
            trg_lbl_wt += neg_mask[:, :, np.newaxis]

        return paddle.to_tensor(trg_lbl), paddle.to_tensor(trg_lbl_wt)

    def _bbox_target_assign(self, encoded_box, matched_indices):
        encoded_box = encoded_box.numpy()
        matched_indices = matched_indices.numpy()

        batch_size, num_priors = matched_indices.shape
        trg_bbox = np.zeros((batch_size, num_priors, 4)).astype('float32')
        trg_bbox_wt = np.zeros((batch_size, num_priors, 1)).astype('float32')

        for i in range(batch_size):
            col_ids = np.where(matched_indices[i] > -1)
            col_val = matched_indices[i][col_ids]
            for v, c in zip(col_val.tolist(), col_ids[0]):
                trg_bbox[i][c] = encoded_box[i][v][c]
            trg_bbox_wt[i][col_ids] = 1.0

        return paddle.to_tensor(trg_bbox), paddle.to_tensor(trg_bbox_wt)

    def _mine_hard_example(self,
                           conf_loss,
                           matched_indices,
                           matched_dist,
                           neg_pos_ratio=3.0,
                           neg_overlap=0.5):
        pos = (matched_indices > -1).astype(conf_loss.dtype)
        num_pos = pos.sum(axis=1, keepdim=True)
        neg = (matched_dist < neg_overlap).astype(conf_loss.dtype)

        conf_loss = conf_loss * (1.0 - pos) * neg
        loss_idx = conf_loss.argsort(axis=1, descending=True)
        idx_rank = loss_idx.argsort(axis=1)
        num_negs = []
        for i in range(matched_indices.shape[0]):
            cur_idx = loss_idx[i]
            cur_num_pos = num_pos[i]
            num_neg = paddle.clip(
                cur_num_pos * neg_pos_ratio, max=pos.shape[1])
            num_negs.append(num_neg)
        num_neg = paddle.stack(num_negs, axis=0).expand_as(idx_rank)
        neg_mask = (idx_rank < num_neg).astype(conf_loss.dtype)
        return neg_mask

    def forward(self, boxes, scores, gt_box, gt_class, anchors):
        boxes = paddle.concat(boxes, axis=1)
        scores = paddle.concat(scores, axis=1)
        prior_boxes = paddle.concat(anchors, axis=0)
        gt_label = gt_class.unsqueeze(-1)
        batch_size, num_priors = scores.shape[:2]
        num_classes = scores.shape[-1] - 1

        def _reshape_to_2d(x):
            return paddle.flatten(x, start_axis=2)

        # 1. Find matched bounding box by prior box.
        #   1.1 Compute IOU similarity between ground-truth boxes and prior boxes.
        #   1.2 Compute matched bounding box by bipartite matching algorithm.
        matched_indices = []
        matched_dist = []
        for i in range(gt_box.shape[0]):
            iou = iou_similarity(gt_box[i], prior_boxes)
            matched_indice, matched_d = bipartite_match(iou, self.match_type,
                                                        self.overlap_threshold)
            matched_indices.append(matched_indice)
            matched_dist.append(matched_d)
        matched_indices = paddle.concat(matched_indices, axis=0)
        matched_indices.stop_gradient = True
        matched_dist = paddle.concat(matched_dist, axis=0)
        matched_dist.stop_gradient = True

        # 2. Compute confidence for mining hard examples
        # 2.1. Get the target label based on matched indices
        target_label, _ = self._label_target_assign(
            gt_label, matched_indices, mismatch_value=num_classes)
        confidence = _reshape_to_2d(scores)
        # 2.2. Compute confidence loss.
        # Reshape confidence to 2D tensor.
        target_label = _reshape_to_2d(target_label).astype('int64')
        conf_loss = F.softmax_with_cross_entropy(confidence, target_label)
        conf_loss = paddle.reshape(conf_loss, [batch_size, num_priors])

        # 3. Mining hard examples
        neg_mask = self._mine_hard_example(
            conf_loss,
            matched_indices,
            matched_dist,
            neg_pos_ratio=self.neg_pos_ratio,
            neg_overlap=self.neg_overlap)

        # 4. Assign classification and regression targets
        # 4.1. Encoded bbox according to the prior boxes.
        prior_box_var = paddle.to_tensor(
            np.array(
                [0.1, 0.1, 0.2, 0.2], dtype='float32')).reshape(
                    [1, 4]).expand_as(prior_boxes)
        encoded_bbox = []
        for i in range(gt_box.shape[0]):
            encoded_bbox.append(
                box_coder(
                    prior_box=prior_boxes,
                    prior_box_var=prior_box_var,
                    target_box=gt_box[i],
                    code_type='encode_center_size'))
        encoded_bbox = paddle.stack(encoded_bbox, axis=0)
        # 4.2. Assign regression targets
        target_bbox, target_loc_weight = self._bbox_target_assign(
            encoded_bbox, matched_indices)
        # 4.3. Assign classification targets
        target_label, target_conf_weight = self._label_target_assign(
            gt_label,
            matched_indices,
            neg_mask=neg_mask,
            mismatch_value=num_classes)

        # 5. Compute loss.
        # 5.1 Compute confidence loss.
        target_label = _reshape_to_2d(target_label).astype('int64')
        conf_loss = F.softmax_with_cross_entropy(confidence, target_label)

        target_conf_weight = _reshape_to_2d(target_conf_weight)
        conf_loss = conf_loss * target_conf_weight * self.conf_loss_weight

        # 5.2 Compute regression loss.
        location = _reshape_to_2d(boxes)
        target_bbox = _reshape_to_2d(target_bbox)

        loc_loss = F.smooth_l1_loss(location, target_bbox, reduction='none')
        loc_loss = paddle.sum(loc_loss, axis=-1, keepdim=True)
        target_loc_weight = _reshape_to_2d(target_loc_weight)
        loc_loss = loc_loss * target_loc_weight * self.loc_loss_weight

        # 5.3 Compute overall weighted loss.
        loss = conf_loss + loc_loss
        loss = paddle.reshape(loss, [batch_size, num_priors])
        loss = paddle.sum(loss, axis=1, keepdim=True)
        normalizer = paddle.sum(target_loc_weight)
        loss = paddle.sum(loss / normalizer)

        return loss
