import numpy as np
from scipy import sparse as sp
from scipy import linalg
from .utils import getArrayFromNumericTable, make_HomogenNumericTable
from ..utils import check_array, check_X_y, deprecated, as_float_array
from daal.algorithms.linear_regression import training, prediction
from ..linear_model.base import _rescale_data
from ..externals.joblib import Parallel, delayed
from ..utils.fixes import sparse_lsqr

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

def daal_fit(self, X, y):
    # Create Numeric Tables for training data and dependent variables
    y = make2d(y)
    trainData = make_HomogenNumericTable(X)
    trainDependentVariables = make_HomogenNumericTable(y)
    if X.dtype == np.float64:
        algorithm = training.Batch_Float64NormEqDense()
    elif X.dtype == np.float32:
        algorithm = training.Batch_Float32NormEqDense()
    # Create an algorithm object to train the multiple linear regression model with the normal equations method

    # Pass a training data set and dependent values to the algorithm
    algorithm.input.set(training.data, trainData)
    algorithm.input.set(training.dependentVariables, trainDependentVariables)

    # Build the multiple linear regression model and retrieve the algorithm results
    algorithm.parameter.interceptFlag = self.fit_intercept
    self.trainingResult_ = algorithm.compute()
    self.coef_ = getArrayFromNumericTable(self.trainingResult_.get(training.model).getBeta())
    if self.fit_intercept == True:
        self.intercept_ = self.coef_[:,0]
        self.coef_ = self.coef_[:,1:]
    if self.coef_.shape[0] == 1:
        self.coef_ = np.ravel(self.coef_)
        self.intercept_ = self.intercept_[0]
    return self


def daal_predict(self, X):
    # Create Numeric Tables for testing data and ground truth values
    X = make2d(X)
    testData = make_HomogenNumericTable(X)
    # Create an algorithm object to predict values of multiple linear regression
    algorithm = prediction.Batch()
    # Pass a testing data set and the trained model to the algorithm

    algorithm.input.setTable(prediction.data, testData)
    algorithm.input.setModel(prediction.model, self.trainingResult_.get(training.model))
    # Predict values of multiple linear regression and retrieve the algorithm results
    predictionResult = algorithm.compute()
    res = getArrayFromNumericTable(predictionResult.get(prediction.prediction))
    if res.shape[1] == 1:
        res = np.ravel(res)
    return res

def fit(self, X, y, sample_weight=None):
    """
    Fit linear model.

    Parameters
    ----------
    X : numpy array or sparse matrix of shape [n_samples,n_features]
        Training data

    y : numpy array of shape [n_samples, n_targets]
        Target values

    sample_weight : numpy array of shape [n_samples]
        Individual weights for each sample

        .. versionadded:: 0.17
           parameter *sample_weight* support to LinearRegression.

    Returns
    -------
    self : returns an instance of self.
    """

    n_jobs_ = self.n_jobs
    X, y = check_X_y(X, y, accept_sparse=['csr', 'csc', 'coo'],
                     y_numeric=True, multi_output=True)

    self.sample_weight_ = sample_weight
    self.fit_shape_good_for_daal_ = True if X.shape[0] > X.shape[1] else False
    if (self.fit_shape_good_for_daal_ and
            not sp.issparse(X) and
            self.fit_intercept and
            (X.dtype == np.float64 or X.dtype == np.float32) and
            sample_weight is None):
        daal_fit(self, X, y)
        return self

    if sample_weight is not None and np.atleast_1d(sample_weight).ndim > 1:
        raise ValueError("Sample weights must be 1D array or scalar")

    X, y, X_offset, y_offset, X_scale = self._preprocess_data(
        X, y, fit_intercept=self.fit_intercept, normalize=self.normalize,
        copy=self.copy_X, sample_weight=sample_weight)

    if sample_weight is not None:
        # Sample weight can be implemented via a simple rescaling.
        X, y = _rescale_data(X, y, sample_weight)

    if sp.issparse(X):
        if y.ndim < 2:
            out = sparse_lsqr(X, y)
            self.coef_ = out[0]
            self._residues = out[3]
        else:
            # sparse_lstsq cannot handle y with shape (M, K)
            outs = Parallel(n_jobs=n_jobs_)(
                delayed(sparse_lsqr)(X, y[:, j].ravel())
                for j in range(y.shape[1]))
            self.coef_ = np.vstack(out[0] for out in outs)
            self._residues = np.vstack(out[3] for out in outs)
    else:
        self.coef_, self._residues, self.rank_, self.singular_ = \
            linalg.lstsq(X, y)
        self.coef_ = self.coef_.T

    if y.ndim == 1:
        self.coef_ = np.ravel(self.coef_)
    self._set_intercept(X_offset, y_offset, X_scale)
    return self

def predict(self, X):
    """Predict using the linear model

    Parameters
    ----------
    X : {array-like, sparse matrix}, shape = (n_samples, n_features)
        Samples.

    Returns
    -------
    C : array, shape = (n_samples,)
        Returns predicted values.
    """
    X = np.asarray(X) if not sp.issparse(X) else X
    good_shape_for_daal = True if X.ndim <= 1 else True if X.shape[0] > X.shape[1] else False

    if (sp.issparse(X) or
            not self.fit_intercept or
            not self.fit_shape_good_for_daal_ or
            not good_shape_for_daal or
            not (X.dtype == np.float64 or X.dtype == np.float32) or
            (hasattr(self, 'sample_weight_') and self.sample_weight_ is not None)):
        return self._decision_function(X)
    else:
        X = check_array(X)
        return daal_predict(self, X)
