# Copyright (c) 2019 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.
import time
import glob
import os

import numpy as np

import pgl
import paddle.fluid as F
import paddle.fluid.layers as L


class Loss(object):

    def __init__(self, config):
        self.config = config

    @classmethod
    def factory(cls, config):
        loss_type = config.loss_type
        if loss_type == "hinge":
            return HingeLoss(config)
        elif loss_type == "global_hinge":
            return GlobalHingeLoss(config)
        elif loss_type == "softmax_with_cross_entropy":
            return lambda logits, label: L.reduce_mean(L.softmax_with_cross_entropy(logits, label))
        else:
            raise ValueError


class HingeLoss(Loss):

    def __call__(self, user_feat, pos_item_feat, neg_item_feat):
        pos = L.reduce_sum(user_feat * pos_item_feat, -1, keep_dim=True) # [B, 1]
        neg = L.matmul(user_feat, neg_item_feat, transpose_y=True) # [B, B]
        loss = L.reduce_mean(L.relu(neg - pos + self.config.margin))
        return loss


def all_gather(X):
    trainer_id = int(os.getenv("PADDLE_TRAINER_ID", "0"))
    trainer_num = int(os.getenv("PADDLE_TRAINERS_NUM", "0"))
    if trainer_num == 1:
        copy_X = X * 1
        copy_X.stop_gradient=True
        return copy_X

    Xs = []
    for i in range(trainer_num):
        copy_X = X * 1
        copy_X =  L.collective._broadcast(copy_X, i, True)
        copy_X.stop_gradient=True
        Xs.append(copy_X)

    if len(Xs) > 1:
        Xs=L.concat(Xs, 0)
        Xs.stop_gradient=True
    else:
        Xs = Xs[0]
    return Xs


class GlobalHingeLoss(Loss):

    def __call__(self, user_feat, pos_item_feat, neg_item_feat):
        pos = L.reduce_sum(user_feat * pos_item_feat, -1, keep_dim=True) # [B, 1]
        all_pos = all_gather(pos) # [B * n, 1]
        all_neg_item_feat = all_gather(neg_item_feat) # [B * n, 1]
        all_user_feat = all_gather(user_feat) # [B * n, 1]

        neg1 = L.matmul(user_feat, all_neg_item_feat, transpose_y=True) # [B, B * n]
        neg2 = L.matmul(all_user_feat, neg_item_feat, transpose_y=True) # [B *n, B]

        loss1 = L.reduce_mean(L.relu(neg1 - pos + self.config.margin))
        loss2 = L.reduce_mean(L.relu(neg2 - all_pos + self.config.margin))

        loss = loss1 + loss2
        return loss
