/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package org.apache.sysds.runtime.matrix.data;

import static jcuda.jcusparse.cusparseOperation.CUSPARSE_OPERATION_NON_TRANSPOSE;
import static jcuda.jcusparse.cusparseOperation.CUSPARSE_OPERATION_TRANSPOSE;
import static jcuda.runtime.JCuda.cudaMemcpy;
import static jcuda.runtime.cudaMemcpyKind.cudaMemcpyHostToDevice;
import jcuda.Pointer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sysds.api.DMLScript;
import org.apache.sysds.runtime.DMLRuntimeException;
import org.apache.sysds.runtime.controlprogram.caching.MatrixObject;
import org.apache.sysds.runtime.controlprogram.context.ExecutionContext;
import org.apache.sysds.runtime.instructions.gpu.context.CSRPointer;
import org.apache.sysds.runtime.instructions.gpu.context.GPUContext;
import org.apache.sysds.utils.Statistics;

import jcuda.jcusparse.cusparseHandle;
import jcuda.jcublas.cublasHandle;
import jcuda.jcublas.cublasOperation;
import jcuda.runtime.JCuda;

public class LibMatrixCuMatMult extends LibMatrixCUDA {

	private static final Log LOG = LogFactory.getLog(LibMatrixCuMatMult.class.getName());

	private static class CuMatMultParameters {
		/*
		 * For the operation, C = op(A) %*% op(B), the below parameters are used
		 * to invoke the corresponding kernels in CuBLAS and CuSPARSE.
		 * 
		 * All the below values have to be valid or else this class has to throw
		 * an exception. No special values like -1 for unknowns allowed.
		 */
		public int m; // number of rows of matrix op(A) and C.
		public int n; // number of columns of matrix op(B) and C.
		public int k; // number of columns of op(A) and rows of op(B).
		public int lda; // leading dimension of two-dimensional array used to
						// store the matrix A.
		public int ldb; // leading dimension of two-dimensional array used to
						// store matrix B.
		public int ldc; // leading dimension of a two-dimensional array used to
						// store the matrix C.
		public long leftNumRows; // number of rows of A
		public long leftNumCols; // number of cols of A
		public long rightNumRows; // number of rows of B
		public long rightNumCols; // number of cols of B
		private boolean isLeftTransposed; // is op(A) = t(A)
		private boolean isRightTransposed; // is op(B) = t(B)

		public CuMatMultParameters(long leftNumRows1, long leftNumCols1, long rightNumRows1, long rightNumCols1,
				boolean isLeftTransposed1, boolean isRightTransposed1) {
			leftNumRows = leftNumRows1;
			leftNumCols = leftNumCols1;
			rightNumRows = rightNumRows1;
			rightNumCols = rightNumCols1;
			isLeftTransposed = isLeftTransposed1;
			isRightTransposed = isRightTransposed1;
			setDimensions();
		}

		public void rowToColumnMajor() {
			// To compensate for the input matrices being in row-major format
			// instead of column-major (the way cublas expects)
			isRightTransposed = swap(isLeftTransposed, isLeftTransposed = isRightTransposed);
			rightNumCols = swap(leftNumRows, leftNumRows = rightNumCols);
			rightNumRows = swap(leftNumCols, leftNumCols = rightNumRows);
			setDimensions();
		}

		private void validate() {
			int k1 = toInt(isRightTransposed ? rightNumCols : rightNumRows);
			if (k != k1)
				throw new DMLRuntimeException("Dimension mismatch: " + k + " != " + k1 + " [" + leftNumRows + ","
						+ leftNumCols + "," + rightNumRows + "," + rightNumCols + "], " + isLeftTransposed + " "
						+ isRightTransposed);
		}

		private void setDimensions() {
			// Validate the dimensions
			m = toInt(isLeftTransposed ? leftNumCols : leftNumRows);
			n = toInt(isRightTransposed ? rightNumRows : rightNumCols);
			k = toInt(isLeftTransposed ? leftNumRows : leftNumCols);
			lda = isLeftTransposed ? k : m;
			ldb = isRightTransposed ? n : k;
			ldc = m;
			if (m == -1 || n == -1 || k == -1)
				throw new DMLRuntimeException("Incorrect dimensions");
		}
	}

	/**
	 * Matrix multiply on GPU Examines sparsity and shapes and routes call to
	 * appropriate method from cuBLAS or cuSparse C = op(A) x op(B)
	 *
	 * The user is expected to call
	 * ec.releaseMatrixOutputForGPUInstruction(outputName);
	 *
	 * @param ec
	 *            Current {@link ExecutionContext} instance
	 * @param gCtx
	 *            a valid {@link GPUContext}
	 * @param instName
	 *            name of the invoking instruction to record{@link Statistics}.
	 * @param left
	 *            Matrix A
	 * @param right
	 *            Matrix B
	 * @param outputName
	 *            Name of the output matrix C (in code generated after LOP
	 *            layer)
	 * @param isLeftTransposed
	 *            op for A, transposed or not
	 * @param isRightTransposed
	 *            op for B, tranposed or not
	 * @return output of matrix multiply
	 */
	public static MatrixObject matmult(ExecutionContext ec, GPUContext gCtx, String instName, MatrixObject left,
			MatrixObject right, String outputName, boolean isLeftTransposed, boolean isRightTransposed) {
		boolean isM1Sparse = isInSparseFormat(gCtx, left);
		boolean isM2Sparse = isInSparseFormat(gCtx, right);
		MatrixObject output = ec.getMatrixObject(outputName);
		long outRLen = isLeftTransposed ? left.getNumColumns() : left.getNumRows();
		long outCLen = isRightTransposed ? right.getNumRows() : right.getNumColumns();

		CuMatMultParameters params = new CuMatMultParameters(left.getNumRows(), left.getNumColumns(),
				right.getNumRows(), right.getNumColumns(), isLeftTransposed, isRightTransposed);

		if (isM1Sparse && isM2Sparse) {
			// -------------------------------------------------------------------------------------
			// sparse-sparse matrix multiplication
			params.validate();
			int transa = cusparseOp(isLeftTransposed);
			int transb = cusparseOp(isRightTransposed);

			// Step 1: Allocate output => sparse format
			ec.allocateGPUMatrixObject(outputName, outRLen, outCLen);

			// Step 2: Get the handles to sparse/dense pointers for left, right
			// and output
			CSRPointer A = left.getGPUObject(gCtx).getJcudaSparseMatrixPtr();
			CSRPointer B = right.getGPUObject(gCtx).getJcudaSparseMatrixPtr();
			CSRPointer C = CSRPointer.allocateForMatrixMultiply(gCtx, getCusparseHandle(gCtx), A, transa, B, transb,
					params.m, params.n, params.k);
		
			// Step 3: Invoke the kernel
			cudaSupportFunctions.cusparsecsrgemm(getCusparseHandle(gCtx), transa, transb, params.m, params.n, params.k, A.descr,
					(int) A.nnz, A.val, A.rowPtr, A.colInd, B.descr, (int) B.nnz, B.val, B.rowPtr, B.colInd, C.descr,
					C.val, C.rowPtr, C.colInd);
			output.getGPUObject(gCtx).setSparseMatrixCudaPointer(C);
			// -------------------------------------------------------------------------------------
		} else if (!isM1Sparse && isM2Sparse) {
			// -------------------------------------------------------------------------------------
			// dense-sparse matrix multiplication
			// Step 1: Allocate output => dense format
			getDenseMatrixOutputForGPUInstruction(ec, instName, outputName, outRLen, outCLen);

			// Step 2: Get the handles to sparse/dense pointers for left, right
			// and output
			Pointer A = getDensePointer(gCtx, left, instName);
			CSRPointer B = right.getGPUObject(gCtx).getJcudaSparseMatrixPtr();
			Pointer C = getDensePointer(gCtx, output, instName);

			// Step 3: Invoke the kernel
			denseSparseMatMult(getCusparseHandle(gCtx), instName, C, A, B, params);
			// -------------------------------------------------------------------------------------
		} else if (isM1Sparse && !isM2Sparse) {
			// -------------------------------------------------------------------------------------
			// sparse-dense matrix multiplication
			// Step 1: Allocate output => dense format
			getDenseMatrixOutputForGPUInstruction(ec, instName, outputName, outRLen, outCLen);

			// Step 2: Get the handles to sparse/dense pointers for left, right
			// and output
			CSRPointer A = left.getGPUObject(gCtx).getJcudaSparseMatrixPtr();
			Pointer B = getDensePointer(gCtx, right, instName);
			Pointer C = getDensePointer(gCtx, output, instName);

			// Step 3: Invoke the kernel
			sparseDenseMatMult(gCtx, instName, C, A, B, left.getNumRows(), left.getNumColumns(), right.getNumRows(),
					right.getNumColumns(), outRLen, outCLen, isLeftTransposed, isRightTransposed);
			// -------------------------------------------------------------------------------------
		} else {
			// -------------------------------------------------------------------------------------
			// dense-dense matrix multiplication
			// Step 1: Allocate output => dense format
			getDenseMatrixOutputForGPUInstruction(ec, instName, outputName, outRLen, outCLen);

			// Step 2: Get the handles to sparse/dense pointers for left, right
			// and output
			Pointer A = getDensePointer(gCtx, left, instName);
			Pointer B = getDensePointer(gCtx, right, instName);
			Pointer C = getDensePointer(gCtx, output, instName);

			// Step 3: Invoke the kernel
			denseDenseMatMult(getCublasHandle(gCtx), instName, C, A, B, params);
			// -------------------------------------------------------------------------------------
		}
		return output;
	}

	/**
	 * Internal method to invoke the appropriate CuSPARSE kernel for matrix
	 * multiplication for operation: C = op(A) * op(B) This assumes B and C are
	 * allocated in dense row-major format and A is sparse.
	 * 
	 * Other than input and output, this method requires additional memory =
	 * outRLen * outCLen * sizeOfDataType
	 * 
	 * @param gCtx
	 *            a valid {@link GPUContext}
	 * @param instName
	 *            name of the invoking instruction to record{@link Statistics}.
	 * @param C
	 *            output matrix pointer
	 * @param A
	 *            left matrix pointer
	 * @param B
	 *            right matrix pointer
	 * @param leftNumRows
	 *            number of rows of A
	 * @param leftNumColumns
	 *            number of cols of A
	 * @param rightNumRows
	 *            number of rows of B
	 * @param rightNumColumns
	 *            number of cols of B
	 * @param outRLen
	 *            number of rows of C
	 * @param outCLen
	 *            number of cols of C
	 * @param isLeftTransposed
	 *            is op(A) = t(A)
	 * @param isRightTransposed
	 *            is op(B) = t(B)
	 */
	static void sparseDenseMatMult(GPUContext gCtx, String instName, Pointer C, CSRPointer A, Pointer B,
			long leftNumRows, long leftNumColumns, long rightNumRows, long rightNumColumns, long outRLen, long outCLen,
			boolean isLeftTransposed, boolean isRightTransposed) {
		// t(C) = t(B) %*% t(A)
		Pointer output = null;
		if (outRLen != 1 && outCLen != 1) {
			output = gCtx.allocate(instName, outRLen * outCLen * sizeOfDataType, false);
		} else {
			// no transpose required for vector output
			output = C;
		}
		CuMatMultParameters params = new CuMatMultParameters(rightNumRows, rightNumColumns, leftNumRows,
				leftNumColumns, !isRightTransposed, !isLeftTransposed);
		denseSparseMatMult(getCusparseHandle(gCtx), instName, output, B, A, params);
		if (outRLen != 1 && outCLen != 1) {
			// Transpose: C = t(output)
			cudaSupportFunctions.cublasgeam(gCtx.getCublasHandle(), cublasOperation.CUBLAS_OP_T, cublasOperation.CUBLAS_OP_T,
					toInt(outCLen), toInt(outRLen), one(), output, toInt(outRLen), zero(), new Pointer(),
					toInt(outRLen), C, toInt(outCLen));
			if (!DMLScript.EAGER_CUDA_FREE)
				JCuda.cudaDeviceSynchronize();
			gCtx.cudaFreeHelper(instName, output, DMLScript.EAGER_CUDA_FREE);
		}
	}

	/**
	 * Internal method to invoke the appropriate CuSPARSE kernel for matrix
	 * multiplication for operation: C = op(A) * op(B) This assumes B and C are
	 * allocated in dense row-major format and A is sparse.
	 * 
	 * @param handle
	 *            cusparse handle
	 * @param instName
	 *            name of the invoking instruction to record{@link Statistics}.
	 * @param C
	 *            output matrix pointer
	 * @param A
	 *            left matrix pointer
	 * @param B
	 *            right matrix pointer
	 * @param param
	 *            BLAS parameters
	 */
	private static void denseSparseMatMult(cusparseHandle handle, String instName, Pointer C, Pointer A, CSRPointer B,
			CuMatMultParameters param) {
		// Ignoring sparse vector dense matrix multiplication and dot product
		boolean isVector = (param.leftNumRows == 1 && !param.isLeftTransposed)
				|| (param.leftNumCols == 1 && param.isLeftTransposed);
		if (isVector) {
			LOG.debug(" GPU Sparse-Dense Matrix Vector ");
			int m = toInt(param.rightNumRows);
			int n = toInt(param.rightNumCols);
			int transa = reverseCusparseOp(cusparseOp(param.isLeftTransposed));
			cudaSupportFunctions.cusparsecsrmv(handle, transa, m, n, toInt(B.nnz), one(), B.descr, B.val, B.rowPtr, B.colInd, A,
					zero(), C);
		} else {
			int m = toInt(param.rightNumRows);
			int k = toInt(param.rightNumCols);
			param.rowToColumnMajor();
			param.validate();
			int transa = reverseCusparseOp(cusparseOp(param.isLeftTransposed));
			int transb = cusparseOp(param.isRightTransposed);
			LOG.debug(" GPU Sparse-Dense Matrix Multiply (rhs transpose) ");
			cudaSupportFunctions.cusparsecsrmm2(handle, transa, transb, m, param.n, k, toInt(B.nnz), one(), B.descr, B.val,
					B.rowPtr, B.colInd, A, param.ldb, zero(), C, param.ldc);
		}
	}

	/**
	 * Internal method to invoke the appropriate CuBLAS kernel for matrix
	 * multiplication for operation: C = op(A) * op(B) This assumes A, B and C
	 * are allocated in dense format. The caller is expected to invoke
	 * params.rowToColumnMajor().
	 * 
	 * @param handle
	 *            cublas handle
	 * @param instName
	 *            name of the invoking instruction to record{@link Statistics}.
	 * @param C
	 *            output matrix pointer
	 * @param A
	 *            left matrix pointer
	 * @param B
	 *            right matrix pointer
	 * @param param
	 *            BLAS parameters
	 */
	private static void denseDenseMatMult(cublasHandle handle, String instName, Pointer C, Pointer A, Pointer B,
			CuMatMultParameters param) {
		param.rowToColumnMajor();
		param.validate();
		int transa = cublasOp(param.isLeftTransposed);
		int transb = cublasOp(param.isRightTransposed);
		B = swap(A, A = B);
		if (param.m == 1 && param.n == 1) {
			// Vector product
			LOG.debug(" GPU Dense-dense Vector Product");
			double[] result = { 0 };
			cudaSupportFunctions.cublasdot(handle, param.k, A, 1, B, 1, Pointer.to(result));
			// By default in CuBlas V2, cublas pointer mode is set to
			// CUBLAS_POINTER_MODE_HOST.
			// This means that scalar values passed are on host (as opposed to
			// on device).
			// The result is copied from the host back to the device so that the
			// rest of
			// infrastructure can treat it uniformly.
			cudaMemcpy(C, Pointer.to(result), 1 * sizeOfDataType, cudaMemcpyHostToDevice);
		} else if (param.m == 1) {
			// Vector-matrix multiply
			LOG.debug(" GPU Dense Vector-Matrix Multiply");
			transb = reverseCublasOp(transb);
			int rightNumRows = (transb == CUSPARSE_OPERATION_TRANSPOSE) ? param.k : param.n;
			int rightNumCols = (transb == CUSPARSE_OPERATION_TRANSPOSE) ? param.n : param.k;
			cudaSupportFunctions.cublasgemv(handle, transb, rightNumRows, rightNumCols, one(), B, param.ldb, A, 1, zero(), C, 1);
		} else if (param.n == 1) {
			// Matrix-vector multiply
			LOG.debug(" GPU Dense Matrix-Vector Multiply");
			int leftNumRows = (transa == CUSPARSE_OPERATION_NON_TRANSPOSE) ? param.m : param.k;
			int leftNumCols = (transa == CUSPARSE_OPERATION_NON_TRANSPOSE) ? param.k : param.m;
			cudaSupportFunctions.cublasgemv(handle, transa, leftNumRows, leftNumCols, one(), A, param.lda, B, 1, zero(), C, 1);
		} else {
			LOG.debug(" GPU Dense-Dense Matrix Multiply ");
			cudaSupportFunctions.cublasgemm(handle, transa, transb, param.m, param.n, param.k, one(), A, param.lda, B, param.ldb,
					zero(), C, param.ldc);
		}
	}

	// Convenient methods to swap two values
	// Usage: y = swap(x, x=y);
	private static long swap(long x, long y) {
		return x;
	}

	private static boolean swap(boolean x, boolean y) {
		return x;
	}

	private static Pointer swap(Pointer x, Pointer y) {
		return x;
	}

	/**
	 * Convenient wrapper to return appropriate cuSPARSE trans value
	 * 
	 * @param isTransposed
	 *            is op(input) = t(input)
	 * @return CUSPARSE_OPERATION_TRANSPOSE or CUSPARSE_OPERATION_NON_TRANSPOSE
	 */
	private static int cusparseOp(boolean isTransposed) {
		return isTransposed ? CUSPARSE_OPERATION_TRANSPOSE : CUSPARSE_OPERATION_NON_TRANSPOSE;
	}

	/**
	 * Convenient wrapper to return appropriate cuBLAS trans value
	 * 
	 * @param isTransposed
	 *            is op(input) = t(input)
	 * @return CUBLAS_OP_T or CUBLAS_OP_N
	 */
	private static int cublasOp(boolean isTransposed) {
		return isTransposed ? cublasOperation.CUBLAS_OP_T : cublasOperation.CUBLAS_OP_N;
	}

	/**
	 * Flips the cuBLAS trans value
	 * 
	 * @param trans
	 *            can be CUBLAS_OP_T or CUBLAS_OP_N
	 * @return CUBLAS_OP_N if trans is CUBLAS_OP_T else CUBLAS_OP_T
	 */
	private static int reverseCublasOp(int trans) {
		return trans == cublasOperation.CUBLAS_OP_T ? cublasOperation.CUBLAS_OP_N : cublasOperation.CUBLAS_OP_T;
	}

	/**
	 * Flips the cuSPARSE trans value
	 * 
	 * @param trans
	 *            can be CUSPARSE_OPERATION_NON_TRANSPOSE or
	 *            CUSPARSE_OPERATION_TRANSPOSE
	 * @return CUSPARSE_OPERATION_NON_TRANSPOSE if trans is
	 *         CUSPARSE_OPERATION_TRANSPOSE else CUSPARSE_OPERATION_TRANSPOSE
	 */
	private static int reverseCusparseOp(int trans) {
		return trans == CUSPARSE_OPERATION_TRANSPOSE ? CUSPARSE_OPERATION_NON_TRANSPOSE : CUSPARSE_OPERATION_TRANSPOSE;
	}
}
