from __future__ import print_function

import numpy as np

import daal.algorithms.svm as svm
from scipy import sparse as sp

from daal.algorithms import classifier, multi_class_classifier, kernel_function

from .utils import make_HomogenNumericTable, getArrayFromNumericTable
from ..utils import check_random_state, check_X_y


LIBSVM_IMPL = ['c_svc', 'nu_svc', 'one_class', 'epsilon_svr', 'nu_svr']


def make2d(X):
    if X.ndim == 0:
        X = X[np.newaxis, np.newaxis]
    elif X.ndim == 1:
        X = X[:, np.newaxis]
    return X


# Methods to extract coefficients
def group_indices_by_class(num_classes, sv_ind_by_clf, labels):
    sv_ind_counters = np.zeros(num_classes, dtype=np.intp)

    num_of_sv_per_class = np.bincount(labels[np.hstack(sv_ind_by_clf)])
    sv_ind_by_class = [np.empty(n, dtype=np.int32) for n in num_of_sv_per_class]

    for indices_per_clf in sv_ind_by_clf:
        for sv_index in indices_per_clf:
            sv_label = labels[sv_index]
            i = sv_ind_counters[sv_label]
            sv_ind_by_class[sv_label][i] = sv_index
            sv_ind_counters[sv_label] += 1

    return sv_ind_by_class


def map_sv_to_columns_in_dual_coef_matrix(sv_ind_by_class):
    from collections import defaultdict
    sv_ind_mapping = defaultdict(lambda: -1)
    p = 0
    for indices_per_class in sv_ind_by_class:
        indices_per_class.sort()
        for sv_index in indices_per_class:
            if sv_ind_mapping[sv_index] == -1:
                sv_ind_mapping[sv_index] = p
                p += 1
    return sv_ind_mapping


def map_to_lexicographic(n):
    """Returns permutation of reverse lexicographics to lexicographics orders for pairs of n consecutive integer indexes"""
    from itertools import (combinations, count)
    two_class_order_gen = ((j, i) for i in range(n) for j in range(i))
    reverse_lookup = { key:val for key,val in zip(two_class_order_gen, count(0))}
    perm_iter = (reverse_lookup[pair] for pair in combinations(range(n), 2))
    return np.fromiter(perm_iter, dtype=np.intp)


def permute_list(li, perm):
    "Rearrange `li` according to `perm`"
    return [ li[i] for i in perm ]


def extract_dual_coef(num_classes, sv_ind_by_clf, sv_coef_by_clf, labels):
    "Construct dual coefficients array in SKLearn peculiar layout, as well corresponding support vector indexes"
    sv_ind_by_class = group_indices_by_class(num_classes, sv_ind_by_clf, labels)
    sv_ind_mapping = map_sv_to_columns_in_dual_coef_matrix(sv_ind_by_class)

    num_unique_sv = len(sv_ind_mapping)
    dc_dt = sv_coef_by_clf[0].dtype

    dual_coef = np.zeros((num_classes - 1, num_unique_sv), dtype=dc_dt)
    support_ = np.empty((num_unique_sv,), dtype=np.int32)

    p = 0
    for i in range(0, num_classes):
        for j in range(i + 1, num_classes):
            sv_ind_i_vs_j = sv_ind_by_clf[p]
            sv_coef_i_vs_j = sv_coef_by_clf[p]
            p += 1

            for k, sv_index in enumerate(sv_ind_i_vs_j):
                label = labels[sv_index]
                col_index = sv_ind_mapping[sv_index]
                if j == label:
                    row_index = i
                else:
                    row_index = j - 1
                dual_coef[row_index, col_index] = sv_coef_i_vs_j[k]
                support_[col_index] = sv_index

    return dual_coef, support_


def daal_fit(self, X, y, kernel):

    if self.C <= 0:
        raise ValueError("C <= 0")

    y = make2d(y)
    num_classes = len(self.classes_)

    if num_classes == 2:
        # Intel(R) DAAL requires binary classes to be 1 and -1. sklearn normalizes
        # the classes to 0 and 1, so we temporarily replace the 0s with -1s.
        y[y == 0] = -1

    train_data = make_HomogenNumericTable(X)
    train_ground_truth = make_HomogenNumericTable(y)

    dtype = X.dtype
    training_batch = svm.training.Batch(fptype=dtype)

    training_batch.parameter.C = self.C
    # Convert cache_size from MB to bytes.
    training_batch.parameter.cacheSize = int(self.cache_size * 1024 * 1024)
    training_batch.parameter.doShrinking = self.shrinking
    training_batch.parameter.accuracyThreshold = self.tol
    training_batch.parameter.tau = self.tol * (1e-3)

    # Default to 2**30 iterations if self.max_iter is -1 (unlimited)
    training_batch.parameter.maxIterations = self.max_iter if self.max_iter > 0 else 2**30

    if kernel == 'rbf':
        sigma_value = np.sqrt(0.5/self._gamma)
        kf_rbf_algo1 = kernel_function.rbf.Batch(fptype=dtype)
        kf_rbf_algo1.parameter.sigma = sigma_value
        training_batch.parameter.kernel = kf_rbf_algo1
    else:
        training_batch.parameter.kernel = kernel_function.linear.Batch(fptype=dtype)

    if num_classes == 2:
        # Binary classifier
        algorithm = training_batch
    else:
        # Multi-class classifier
        algorithm = multi_class_classifier.training.Batch(num_classes, fptype=dtype)
        algorithm.parameter.training = training_batch

    algorithm.input.set(classifier.training.data, train_data)
    algorithm.input.set(classifier.training.labels, train_ground_truth)

    training_result = algorithm.compute()

    model = training_result.get(classifier.training.model)
    self._daal_model = model

    if num_classes == 2:
        two_class_sv_ind_ = getArrayFromNumericTable(model.getSupportIndices())
        two_class_sv_ind_ = two_class_sv_ind_.ravel()

        # support indexes need permutation to arrange them into the same layout as that of Scikit-Learn
        tmp = np.empty(two_class_sv_ind_.shape, dtype=np.dtype([('label', y.dtype), ('ind', two_class_sv_ind_.dtype)]))
        tmp['label'][:] = y[two_class_sv_ind_].ravel()
        tmp['ind'][:] = two_class_sv_ind_
        perm = np.argsort(tmp, order=['label', 'ind'])
        del tmp
        self.support_ = two_class_sv_ind_[perm]
        self.support_vectors_ = X[self.support_]

        self.dual_coef_ = getArrayFromNumericTable(model.getClassificationCoefficients())
        self.dual_coef_ = self.dual_coef_[perm].T
        self.intercept_ = np.array([model.getBias()])
    else:
        intercepts = []
        coefs = []
        num_models = model.getNumberOfTwoClassClassifierModels()
        sv_ind_by_clf = []
        label_indexes = []

        model_id = 0
        for i1 in range(num_classes):
            label_indexes.append(np.where( y == i1 )[0])
            for i2 in range(i1):
                classifier_model = model.getTwoClassClassifierModel(model_id)
                svm_model = svm.Model.downCast(classifier_model)

                # Indices correspond to input features with label i1 followed by input features with label i2
                two_class_sv_ind_ = getArrayFromNumericTable(svm_model.getSupportIndices())
                # Map these indexes to indexes of the training data
                sv_ind = np.take(np.hstack((label_indexes[i1], label_indexes[i2])), two_class_sv_ind_.ravel())
                sv_ind_by_clf.append(sv_ind)

                # svs_ = getArrayFromNumericTable(svm_model.getSupportVectors())
                # assert np.array_equal(svs_, X[sv_ind])

                intercepts.append(-svm_model.getBias())
                coefs.append(-getArrayFromNumericTable(svm_model.getClassificationCoefficients()))
                model_id += 1

        # permute solutions to lexicographic ordering
        to_lex_perm = map_to_lexicographic(num_classes)
        sv_ind_by_clf = permute_list(sv_ind_by_clf, to_lex_perm)
        sv_coef_by_clf = permute_list(coefs, to_lex_perm)
        intercepts = permute_list(intercepts, to_lex_perm)

        self.dual_coef_, self.support_ = extract_dual_coef(
            num_classes,    # number of classes
            sv_ind_by_clf,  # support vector indexes by two-class classifiers
            sv_coef_by_clf, # classification coefficients by two-class classifiers
            y.squeeze().astype(np.intp, copy=False)   # integer labels
        )
        self.support_vectors_ = X[self.support_]
        self.intercept_ = np.array(intercepts)


    indices = y.take(self.support_, axis=0)
    if num_classes == 2:
        self.n_support_ = np.array([np.sum(indices == -1), np.sum(indices == 1)], dtype=np.int32)
    else:
        self.n_support_ = np.array([np.sum(indices == c) for c in self.classes_], dtype=np.int32)

    self.probA_ = np.empty(0)
    self.probB_ = np.empty(0)

    return training_result


def fit(self, X, y, sample_weight=None):
        """Fit the SVM model according to the given training data.
        Parameters
        ----------
        X : {array-like, sparse matrix}, shape (n_samples, n_features)
            Training vectors, where n_samples is the number of samples
            and n_features is the number of features.
            For kernel="precomputed", the expected shape of X is
            (n_samples, n_samples).
        y : array-like, shape (n_samples,)
            Target values (class labels in classification, real numbers in
            regression)
        sample_weight : array-like, shape (n_samples,)
            Per-sample weights. Rescale C per sample. Higher weights
            force the classifier to put more emphasis on these points.
        Returns
        -------
        self : object
            Returns self.
        Notes
        ------
        If X and y are not C-ordered and contiguous arrays of np.float64 and
        X is not a scipy.sparse.csr_matrix, X and/or y may be copied.
        If X is a dense array, then the other methods will not support sparse
        matrices as input.
        """
        rnd = check_random_state(self.random_state)

        sparse = sp.isspmatrix(X)
        if sparse and self.kernel == "precomputed":
            raise TypeError("Sparse precomputed kernels are not supported.")
        self._sparse = sparse and not callable(self.kernel)

        X, y = check_X_y(X, y, dtype=np.float64, order='C', accept_sparse='csr')
        y = self._validate_targets(y)

        sample_weight = np.asarray([]
                                   if sample_weight is None
                                   else sample_weight, dtype=np.float64)
        solver_type = LIBSVM_IMPL.index(self._impl)

        # input validation
        if solver_type != 2 and X.shape[0] != y.shape[0]:
            raise ValueError("X and y have incompatible shapes.\n" +
                             "X has %s samples, but y has %s." %
                             (X.shape[0], y.shape[0]))

        if self.kernel == "precomputed" and X.shape[0] != X.shape[1]:
            raise ValueError("X.shape[0] should be equal to X.shape[1]")

        if sample_weight.shape[0] > 0 and sample_weight.shape[0] != X.shape[0]:
            raise ValueError("sample_weight and X have incompatible shapes: "
                             "%r vs %r\n"
                             "Note: Sparse matrices cannot be indexed w/"
                             "boolean masks (use `indices=True` in CV)."
                             % (sample_weight.shape, X.shape))

        if self.gamma == 'auto':
            self._gamma = 1.0 / X.shape[1]
        else:
            self._gamma = self.gamma

        kernel = self.kernel
        if callable(kernel):
            kernel = 'precomputed'

        fit = self._sparse_fit if self._sparse else self._dense_fit
        if self.verbose:  # pragma: no cover
            print('[LibSVM]', end='')

        # see comment on the other call to np.iinfo in this file
        seed = rnd.randint(np.iinfo('i').max)

        if ( not sparse and not self.probability and
             sample_weight.size == 0 and self.class_weight is None and kernel in ['linear', 'rbf']):

            self._daal_fit = True
            daal_fit(self, X, y, kernel)
        else:
            self._daal_fit = False
            fit(X, y, sample_weight, solver_type, kernel, random_seed=seed)


        self.shape_fit_ = X.shape

        # In binary case, we need to flip the sign of coef, intercept and
        # decision function. Use self._intercept_ and self._dual_coef_ internally.
        if not self._daal_fit:
            self._intercept_ = self.intercept_.copy()
            self._dual_coef_ = self.dual_coef_.copy()
        else:
            self._intercept_ = self.intercept_.copy()
            self._dual_coef_ = self.dual_coef_.copy()
            if len(self.classes_) == 2:
                np.negative(self._dual_coef_, out=self._dual_coef_)

        if not self._daal_fit and len(self.classes_) == 2 and self._impl in ['c_svc', 'nu_svc']:
            self.intercept_ *= -1
            self.dual_coef_ = -self.dual_coef_

        return self

try:
    # corresponds to SKLearn's prediction based on scores
    multi_class_pmethod = multi_class_classifier.prediction.voteBased
except AttributeError:
    multi_class_pmethod = multi_class_classifier.prediction.multiClassClassifierWu

def daal_predict(self, X):

    dtype = X.dtype
    num_classes = len(self.classes_)
    nt = make_HomogenNumericTable(X)

    prediction_batch = svm.prediction.Batch(fptype=dtype)

    if self.kernel == 'rbf':
        sigma_value = np.sqrt(0.5/self._gamma)
        prediction_batch.parameter.kernel = kernel_function.rbf.Batch(fptype=dtype, sigma=sigma_value)
    else:
        prediction_batch.parameter.kernel = kernel_function.linear.Batch(fptype=dtype)

    if num_classes == 2:
        algorithm = prediction_batch
    else:
        algorithm = multi_class_classifier.prediction.Batch(num_classes, fptype=dtype,
                                                            pmethod=multi_class_pmethod)
        algorithm.parameter.prediction = prediction_batch
        algorithm.parameter.training = svm.training.Batch(fptype=dtype)

    algorithm.input.setTable(classifier.prediction.data, nt)
    algorithm.input.setModel(classifier.prediction.model, self._daal_model)

    prediction_result = algorithm.compute()

    res = getArrayFromNumericTable(prediction_result.get(classifier.prediction.prediction))
    res = res.ravel()

    if num_classes == 2:
        # Convert from Intel(R) DAAL format back to original classes
        np.greater(res, 0, out=res)

    return res


def predict(self, X):
    """Perform regression on samples in X.

    For an one-class model, +1 (inlier) or -1 (outlier) is returned.

    Parameters
    ----------
    X : {array-like, sparse matrix}, shape (n_samples, n_features)
        For kernel="precomputed", the expected shape of X is
        (n_samples_test, n_samples_train).

    Returns
    -------
    y_pred : array, shape (n_samples,)
    """
    X = self._validate_for_predict(X)
    if self._daal_fit:
        y = daal_predict(self, X)
    else:
        predict = self._sparse_predict if self._sparse else self._dense_predict
        y = predict(X)

    return self.classes_.take(np.asarray(y, dtype=np.intp))
