# -*- coding: utf-8 -*-
"""Histogram-based Outlier Detection (HBOS)
"""
# Author: Yue Zhao <zhaoy@cmu.edu>
# License: BSD 2 clause

import numpy as np
import torch

from .base import BaseDetector
from .basic_operators import histt


class HBOS(BaseDetector):
    """Histogram-based outlier detection (HBOS) is an efficient unsupervised
    method. It assumes the feature independence and calculates the degree
    of outlyingness by building histograms. See :cite:`goldstein2012histogram`
    for details.


    Parameters
    ----------
    n_bins : int or string, optional (default=10)
        The number of bins. "auto" uses the birge-rozenblac method for
        automatic selection of the optimal number of bins for each feature.

    alpha : float in (0, 1), optional (default=0.1)
        The regularizer for preventing overflow.

    contamination : float in (0., 0.5), optional (default=0.1)
        The amount of contamination of the data set,
        i.e. the proportion of outliers in the data set. Used when fitting to
        define the threshold on the decision function.

    batch_size : integer, optional (default = None)
        Number of samples to process per batch.

    device : str, optional (default = 'cpu')
        Valid device id, e.g., 'cuda:0' or 'cpu'

    Attributes
    ----------
    bin_edges_ : numpy array of shape (n_bins + 1, n_features )
        The edges of the bins.

    hist_ : numpy array of shape (n_bins, n_features)
        The density of each histogram.

    decision_scores_ : numpy array of shape (n_samples,)
        The outlier scores of the training data.
        The higher, the more abnormal. Outliers tend to have higher
        scores. This value is available once the detector is fitted.

    threshold_ : float
        The threshold is based on ``contamination``. It is the
        ``n_samples * contamination`` most abnormal samples in
        ``decision_scores_``. The threshold is calculated for generating
        binary outlier labels.

    labels_ : int, either 0 or 1
        The binary labels of the training data. 0 stands for inliers
        and 1 for outliers/anomalies. It is generated by applying
        ``threshold_`` on ``decision_scores_``.
    """

    def __init__(self, contamination=0.1, n_bins=10, alpha=0.1,
                 device='cuda:0'):
        super(HBOS, self).__init__(contamination=contamination)
        self.n_bins = n_bins
        self.alpha = alpha
        self.device = device

    def fit(self, X, y=None, return_time=False):
        """Fit detector. y is ignored in unsupervised methods.

        Parameters
        ----------
        X : numpy array of shape (n_samples, n_features)
            The input samples.

        y : Ignored
            Not used, present for API consistency by convention.

        return_time : boolean (default=True)
            If True, set self.gpu_time to the measured GPU time.

        Returns
        -------
        self : object
            Fitted estimator.
        """
        # todo: add one for pytorch tensor
        # X = check_array(X)
        self._set_n_classes(y)

        if self.device != 'cpu' and return_time:
            start = torch.cuda.Event(enable_timing=True)
            end = torch.cuda.Event(enable_timing=True)
            start.record()

        X = X.to(self.device)
        n_samples, n_features = X.shape[0], X.shape[1]

        # initialize containers for calculation
        hist_ = torch.zeros([self.n_bins, n_features]).to(self.device)
        bin_edges = torch.zeros([self.n_bins + 1, n_features]).to(self.device)
        outlier_scores = torch.zeros([n_samples, n_features]).to(self.device)

        for i in range(n_features):
            hist_[:, i], bin_edges[:, i] = histt(X[:, i], bins=self.n_bins,
                                                 device=self.device)

        hist_ = hist_.contiguous()
        bin_edges = bin_edges.contiguous()

        # conduct feature-wise binning
        for i in range(n_features):
            bin_inds = torch.bucketize(X[:, i], bin_edges[:, i])
            out_score_i = torch.log2(hist_[:, i] + self.alpha)

            bin_inds[bin_inds == 0] = 1
            bin_inds[bin_inds == self.n_bins + 1] = self.n_bins
            outlier_scores[:, i] = out_score_i[bin_inds - 1]

        if self.device != 'cpu' and return_time:
            end.record()
            torch.cuda.synchronize()

        self.decision_scores_ = (
                torch.sum(outlier_scores, dim=1) * -1).cpu().numpy()

        self._process_decision_scores()

        # return GPU time in seconds
        if self.device != 'cpu' and return_time:
            self.gpu_time = start.elapsed_time(end) / 1000

        return self

    def decision_function(self, X):
        """Predict raw anomaly score of X using the fitted detector.
         For consistency, outliers are assigned with larger anomaly scores.
        Parameters
        ----------
        X : numpy array of shape (n_samples, n_features)
            The training input samples. Sparse matrices are accepted only
            if they are supported by the base estimator.
        Returns
        -------
        anomaly_scores : numpy array of shape (n_samples,)
            The anomaly score of the input samples.
        """
        # use multi-thread execution
        if hasattr(self, 'X_train'):
            original_size = X.shape[0]
            X = np.concatenate((self.X_train, X), axis=0)

        # return decision_scores_.ravel()
