#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates.
# All rights reserved.

# This source code is licensed under the license found in the
# LICENSE file in the root directory of this source tree.

from __future__ import annotations

import torch
from botorch.posteriors import Posterior
from botorch.sampling.samplers import MCSampler
from torch import Tensor


class RejectionSampler(MCSampler):
    """
    Samples from a posterior subject to the constraint that samples in constrained_idx
    should be >= 0.

    If not enough feasible samples are generated, will return the least violating
    samples.
    """

    def __init__(
        self, num_samples: int, num_rejection_samples: int, constrained_idx: Tensor
    ):
        """Initialize RejectionSampler

        Args:
            num_samples (int): Number of samples to return. Note that if fewer samples
                than this number are positive in the required dimension, the remaining
                samples returned will be the "least violating", i.e. closest to 0.
            num_rejection_samples (int): Number of samples to draw before rejecting.
            constrained_idx (Tensor): Indices of input dimensions that should be
                constrained positive.
        """
        self.num_samples = num_samples
        self.num_rejection_samples = num_rejection_samples
        self.constrained_idx = constrained_idx
        self._sample_shape = torch.Size([num_samples])
        super().__init__()

    def _get_base_sample_shape(self, posterior: Posterior) -> torch.Size:
        return torch.Size([])

    def _construct_base_samples(self, posterior: Posterior, shape: torch.Size) -> None:
        self.base_samples = None

    def forward(self, posterior: Posterior) -> Tensor:
        """Run the rejection sampler.

        Args:
            posterior (Posterior): The unconstrained GP posterior object
                to perform rejection samples on.

        Returns:
            Tensor: Kept samples.
        """
        samples = posterior.rsample(
            sample_shape=torch.Size([self.num_rejection_samples])
        )
        assert (
            samples.shape[-1] == 1
        ), "Batches not supported"  # TODO T68656582 handle batches later
        constrained_samps = samples[:, self.constrained_idx, 0]
        valid = (constrained_samps >= 0).all(dim=1)
        if valid.sum() < self.num_samples:
            worst_violation = constrained_samps.min(dim=1)[0]
            keep = torch.argsort(worst_violation, descending=True)[: self.num_samples]
        else:
            keep = torch.where(valid)[0][: self.num_samples]
        return samples[keep, :, :]
