/*
 * 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.functionobjects;

import org.apache.sysds.runtime.DMLRuntimeException;
import org.apache.sysds.runtime.matrix.data.CTableMap;
import org.apache.sysds.runtime.matrix.data.MatrixBlock;
import org.apache.sysds.runtime.matrix.data.MatrixIndexes;
import org.apache.sysds.runtime.matrix.data.Pair;
import org.apache.sysds.runtime.util.CommonThreadPool;
import org.apache.sysds.runtime.util.LongLongDoubleHashMap;
import org.apache.sysds.runtime.util.UtilFunctions;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;

public class CTable extends ValueFunction 
{
	private static final long serialVersionUID = -5374880447194177236L;

	private static CTable singleObj = null;
	
	private CTable() {
		// nothing to do here
	}
	
	public static CTable getCTableFnObject() {
		if ( singleObj == null )
			singleObj = new CTable();
		return singleObj;
	}

	public void execute(double v1, double v2, double w, boolean ignoreZeros, CTableMap resultMap, MatrixBlock resultBlock) {
		if( resultBlock != null )
			execute(v1, v2, w, ignoreZeros, resultBlock);
		else
			execute(v1, v2, w, ignoreZeros, resultMap);
	}
	
	public void execute(double v1, double v2, double w, boolean ignoreZeros, CTableMap resultMap) {
		// If any of the values are NaN (i.e., missing) then 
		// we skip this tuple, proceed to the next tuple
		if ( Double.isNaN(v1) || Double.isNaN(v2) || Double.isNaN(w) ) {
			return;
		}
		
		// safe casts to long for consistent behavior with indexing
		long row = UtilFunctions.toLong( v1 );
		long col = UtilFunctions.toLong( v2 );
		
		// skip this entry as it does not fall within specified output dimensions
		if( ignoreZeros && row == 0 && col == 0 ) {
			return;
		}
		
		//check for incorrect ctable inputs
		if( row <= 0 || col <= 0 ) {
			throw new DMLRuntimeException("Erroneous input while computing the contingency table (one of the value <= zero): "+v1+" "+v2);
		} 
	
		//hash group-by for core ctable computation
		resultMap.aggregate(row, col, w);	
	}	

	public void execute(double v1, double v2, double w, boolean ignoreZeros, MatrixBlock ctableResult) 
	{
		// If any of the values are NaN (i.e., missing) then 
		// we skip this tuple, proceed to the next tuple
		if ( Double.isNaN(v1) || Double.isNaN(v2) || Double.isNaN(w) ) {
			return;
		}
		
		// safe casts to long for consistent behavior with indexing
		long row = UtilFunctions.toLong( v1 );
		long col = UtilFunctions.toLong( v2 );
		
		// skip this entry as it does not fall within specified output dimensions
		if( ignoreZeros && row == 0 && col == 0 ) {
			return;
		}
		
		//check for incorrect ctable inputs
		if( row <= 0 || col <= 0 ) {
			throw new DMLRuntimeException("Erroneous input while computing the contingency table (one of the value <= zero): "+v1+" "+v2);
		}
		
		// skip this entry as it does not fall within specified output dimensions
		if( row > ctableResult.getNumRows() || col > ctableResult.getNumColumns() ) {
			return;
		}
		
		//add value
		ctableResult.set((int)row-1, (int)col-1,
			ctableResult.get((int)row-1, (int)col-1) + w);
	}

	public int execute(int row, double v2, double w, int maxCol, int[] retIx, double[] retVals) 
	{
		// If any of the values are NaN (i.e., missing) then 
		// we skip this tuple, proceed to the next tuple
		if ( Double.isNaN(v2) || Double.isNaN(w) ) {
			return maxCol;
		}
		
		// safe casts to long for consistent behavior with indexing
		int col = UtilFunctions.toInt( v2 );
		if( col <= 0 ) {
			throw new DMLRuntimeException("Erroneous input while computing the contingency table (value <= zero): "+v2);
		} 
		
		//set weight as value (expand is guaranteed to address different cells)
		retIx[row - 1] = col - 1;
		retVals[row - 1] = w;
		
		//maintain max seen col 
		return Math.max(maxCol, col);
	}

	public Pair<MatrixIndexes,Double> execute( long row, double v2, double w ) 
	{
		// If any of the values are NaN (i.e., missing) then 
		// we skip this tuple, proceed to the next tuple
		if ( Double.isNaN(v2) || Double.isNaN(w) )
			return new Pair<>(new MatrixIndexes(-1,-1), w);
		// safe casts to long for consistent behavior with indexing
		long col = UtilFunctions.toLong( v2 );
		if( col <= 0 )
			throw new DMLRuntimeException("Erroneous input while computing the contingency table (value <= zero): "+v2);
		return new Pair<>(new MatrixIndexes(row, col), w);
	}

	/* Multithreaded CTable (F = ctable(A,B,W))
	 * Divide the input vectors into equal-sized blocks and assign each block to a task.
	 * All tasks concurrently build their own CTableMaps.
	 * Cascade merge the partial maps.
	 * TODO: Support other cases
	 */
	public void execute(MatrixBlock in1, MatrixBlock in2, MatrixBlock w, CTableMap resultMap, int k) {
		ExecutorService pool = CommonThreadPool.get(k);
		ArrayList<CTableMap> partialMaps = new ArrayList<>();
		try {
			// Assign an equal-sized blocks to each task
			List<Callable<Object>> tasks = new ArrayList<>();
			int[] blockSizes = UtilFunctions.getBlockSizes(in1.getNumRows(), k);
			// Each task builds a separate CTableMap in a lock-free manner
			for(int startRow = 0, i = 0; i < blockSizes.length; startRow += blockSizes[i], i++)
				tasks.add(getPartialCTableTask(in1, in2, w, startRow, blockSizes[i], partialMaps));

			for(var task : pool.invokeAll(tasks))
				task.get();


			ArrayList<CTableMap> newPartialMaps = new ArrayList<>();
			// Cascade-merge all the partial CTableMaps
			while(partialMaps.size() > 1) {
				newPartialMaps.clear();
				tasks = new ArrayList<>();
				int count;
				// Each task merges 2 maps and returns the merged map
				for (count=0; count+1<partialMaps.size(); count=count+2)
					tasks.add(getMergePartialCTMapsTask(partialMaps.get(count),
						partialMaps.get(count+1), newPartialMaps));

				for(var task : pool.invokeAll(tasks))
					task.get();
				
				// Copy the remaining maps to be merged in the future iterations
				if (count < partialMaps.size())
					newPartialMaps.add(partialMaps.get(count));
				partialMaps.clear();
				partialMaps.addAll(newPartialMaps);
			}
		}
		catch(Exception ex) {
			throw new DMLRuntimeException(ex);
		}
		finally{
			pool.shutdown();
		}
		// Deep copy the last merged map into the result map
		var map = partialMaps.get(0);
		Iterator<LongLongDoubleHashMap.ADoubleEntry> iter = map.getIterator();
		while(iter.hasNext()) {
			LongLongDoubleHashMap.ADoubleEntry e = iter.next();
			resultMap.aggregate(e.getKey1(), e.getKey2(), e.value);
		}
	}

	public Callable<Object> getPartialCTableTask(MatrixBlock in1, MatrixBlock in2, MatrixBlock w,
		int startInd, int blockSize, ArrayList<CTableMap> pmaps) {
		return new PartialCTableTask(in1, in2, w, startInd, blockSize, pmaps);
	}

	public Callable<Object> getMergePartialCTMapsTask(CTableMap map1, CTableMap map2, ArrayList<CTableMap> pmaps) {
		return new MergePartialCTMaps(map1, map2, pmaps);
	}

	private static class PartialCTableTask implements Callable<Object> {
		private final MatrixBlock _in1;
		private final MatrixBlock _in2;
		private final MatrixBlock _w;
		private final int _startInd;
		private final int _blockSize;
		private final ArrayList<CTableMap> _partialCTmaps;

		protected PartialCTableTask(MatrixBlock in1, MatrixBlock in2, MatrixBlock w,
			int startRow, int blockSize, ArrayList<CTableMap> pmaps) {
			_in1 = in1;
			_in2 = in2;
			_w = w;
			_startInd = startRow;
			_blockSize = blockSize;
			_partialCTmaps = pmaps;
		}

		@Override public Object call() throws Exception {
			CTable ctable = CTable.getCTableFnObject();
			CTableMap ctmap = new CTableMap(LongLongDoubleHashMap.EntryType.INT);
			int endInd = UtilFunctions.getEndIndex(_in1.getNumRows(), _startInd, _blockSize);
			for( int i=_startInd; i<endInd; i++ )
			{
				double v1 = _in1.get(i, 0);
				double v2 = _in2.get(i, 0);
				double w = _w.get(i, 0);
				ctable.execute(v1, v2, w, false, ctmap);
			}
			synchronized(_partialCTmaps) {
				_partialCTmaps.add(ctmap);
			}
			return null;
		}
	}

	private static class MergePartialCTMaps implements Callable<Object> {
		private final CTableMap _map1;
		private final CTableMap _map2;
		private final ArrayList<CTableMap> _partialCTmaps;

		protected MergePartialCTMaps(CTableMap map1, CTableMap map2, ArrayList<CTableMap> pmaps) {
			_map1 = map1;
			_map2 = map2;
			_partialCTmaps = pmaps;
		}

		private void mergeToFinal(CTableMap map, CTableMap finalMap) {
			Iterator<LongLongDoubleHashMap.ADoubleEntry> iter = map.getIterator();
			while(iter.hasNext()) {
				LongLongDoubleHashMap.ADoubleEntry e = iter.next();
				finalMap.aggregate(e.getKey1(), e.getKey2(), e.value);
			}
		}

		@Override public Object call() throws Exception {
			CTableMap mergedMap = new CTableMap(LongLongDoubleHashMap.EntryType.INT);
			mergeToFinal(_map1, mergedMap);
			mergeToFinal(_map2, mergedMap);
			synchronized(_partialCTmaps) {
				_partialCTmaps.add(mergedMap);
				return null;
			}
		}
	}
}
