/*
 * 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 
 * <p>
 * Equals library for MatrixBlocks:
 * </p>
 * 
 * <p>
 * The implementations adhere to the properties of equals of:
 * </p>
 * 
 * <ul>
 * <li>Reflective</li>
 * <li>Symmetric</li>
 * <li>Transitive</li>
 * <li>Consistent</li>
 * </ul>
 * 
 * <p>
 * The equals also is valid if the metadata of number of non zeros are unknown in either input. An unknown number of non
 * zero values is indicated by a negative nonzero count in the input matrices.
 * </p>
 */
public class LibMatrixEquals {

	/** Logger for Equals library */
	private static final Log LOG = LogFactory.getLog(LibMatrixEquals.class.getName());

	/** first block */
	private final MatrixBlock a;
	/** second block */
	private final MatrixBlock b;
	/** Epsilon allowed between the blocks */
	private final double eps;

	/**
	 * Private instance of a comparison, constructed to reduce the arguments to method calls.
	 * 
	 * @param a first block
	 * @param b second block
	 */
	private LibMatrixEquals(MatrixBlock a, MatrixBlock b) {
		this.a = a;
		this.b = b;
		this.eps = Double.MIN_VALUE * 1024;
	}

	/**
	 * Private instance of a comparison, constructed to reduce the arguments to method calls.
	 * 
	 * @param a   first block
	 * @param b   second block
	 * @param eps epsilon allowed
	 */
	private LibMatrixEquals(MatrixBlock a, MatrixBlock b, double eps) {
		this.a = a;
		this.b = b;
		this.eps = eps;
	}

	/**
	 * <p>
	 * Analyze if the two matrix blocks are equivalent, this functions even if the underlying allocation and data
	 * structure varies.
	 * </p>
	 * 
	 * <p>
	 * The implementations adhere to the properties of equals of:
	 * </p>
	 * 
	 * <ul>
	 * <li>Reflective</li>
	 * <li>Symmetric</li>
	 * <li>Transitive</li>
	 * <li>Consistent</li>
	 * </ul>
	 * 
	 * @param a Matrix Block a to compare
	 * @param b Matrix Block b to compare
	 * @return If the block are equivalent.
	 */
	public static boolean equals(MatrixBlock a, MatrixBlock b) {
		// Same object
		if(a == b)
			return true;
		return new LibMatrixEquals(a, b).exec();
	}

	/**
	 * <p>
	 * Analyze if the two matrix blocks are equivalent, this functions even if the underlying allocation and data
	 * structure varies.
	 * </p>
	 * 
	 * <p>
	 * The implementations adhere to the properties of equals of:
	 * </p>
	 * 
	 * <ul>
	 * <li>Reflective</li>
	 * <li>Symmetric</li>
	 * <li>Transitive</li>
	 * <li>Consistent</li>
	 * </ul>
	 * 
	 * @param a   Matrix Block a to compare
	 * @param b   Matrix Block b to compare
	 * @param eps Epsilon to allow between values
	 * @return If the block are equivalent.
	 */
	public static boolean equals(MatrixBlock a, MatrixBlock b, double eps) {
		// Same object
		if(a == b)
			return true;
		return new LibMatrixEquals(a, b, eps).exec();
	}

	/**
	 * Execute the comparison
	 * 
	 * @return if the blocks are equivalent
	 */
	private boolean exec() {

		if(isMetadataDifferent())
			return false;
		else if(a.isEmpty() && b.nonZeros != -1)
			return b.isEmpty();
		else if(b.isEmpty() && a.nonZeros != -1)
			return false;
		else if(a.denseBlock != null && b.denseBlock != null)
			return a.denseBlock.equals(b.denseBlock, eps);
		else if(a.sparseBlock != null && b.sparseBlock != null)
			return a.sparseBlock.equals(b.sparseBlock, eps);
		else if(a.sparseBlock != null && b.denseBlock != null && b.denseBlock.isContiguous())
			return a.sparseBlock.equals(b.denseBlock.values(0), b.getNumColumns(), eps);
		else if(b.sparseBlock != null && a.denseBlock != null && a.denseBlock.isContiguous())
			return b.sparseBlock.equals(a.denseBlock.values(0), a.getNumColumns(), eps);

		return genericEquals();
	}

	/**
	 * Compare metadata, and return true if metadata is different
	 * 
	 * @param a MatrixBlock a
	 * @param b MatrixBlock b
	 * @return If the metadata was comparable
	 */
	private boolean isMetadataDifferent() {
		boolean diff = false;

		diff |= a.getNumRows() != b.getNumRows();
		diff |= a.getNumColumns() != b.getNumColumns();
		final long nnzA = a.getNonZeros();
		final long nnzB = b.getNonZeros();
		diff |= nnzA != -1 && nnzB != -1 && nnzA != nnzB;

		return diff;
	}

	/**
	 * Generic implementation to cover all cases. But it is slow in most.
	 * 
	 * @return if the matrices are equivalent.
	 */
	private boolean genericEquals() {
		LOG.warn("Using generic equals, potential optimizations are possible");
		final int rows = a.getNumRows();
		final int cols = a.getNumColumns();
		for(int i = 0; i < rows; i++)
			for(int j = 0; j < cols; j++)
				if(Math.abs(a.get(i, j) - b.get(i, j)) > eps)
					return false;
		return true;
	}
}
