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


import org.apache.sysds.runtime.util.UtilFunctions;

import java.util.stream.IntStream;

/**
 * Dense Large Row Blocks have multiple 1D arrays (blocks), which contain complete rows.
 * Except the last block all blocks have the same size (size refers to the number of rows contained and space allocated).
 */
public abstract class DenseBlockLDRB extends DenseBlock
{
	private static final long serialVersionUID = -7519435549328146356L;

	// On same platforms, allocating arrays very close to INT_MAX runs into
	// a "java.lang.OutOfMemoryError: Requested array size exceeds VM limit"
	// Also, this normally does not happen because we allocate row-aligned
	// chunks, to be on the safe side, we keep a margin of 8 for padding.
	public static int MAX_ALLOC = Integer.MAX_VALUE - 8;
	
	protected int _blen;

	protected DenseBlockLDRB(int[] dims) {
		super(dims);
	}

	/**
	 * Create the internal array to store the blocks. Does not create
	 * storage space for a block yet, call allocate block for that.
	 *
	 * @param numBlocks the number of blocks to create
	 */
	protected abstract void allocateBlocks(int numBlocks);

	@Override
	public int blockSize() {
		return _blen;
	}

	@Override
	public int blockSize(int bix) {
		return Math.min(_blen, _rlen - bix * _blen);
	}

	@Override
	public void reset(int rlen, int[] odims, double v) {
		long dataLength = (long) rlen * odims[0];
		int newBlockSize = Math.min(rlen, MAX_ALLOC / odims[0]);
		int numBlocks = UtilFunctions.toInt(Math.ceil((double) rlen / newBlockSize));
		if (_blen == newBlockSize && dataLength <= capacity()) {
			IntStream.range(0, numBlocks)
					.forEach((bi) -> {
						int toIndex = (int)Math.min(newBlockSize, dataLength - bi * newBlockSize) * _odims[0];
						fillBlock(bi, 0, toIndex, v);
					});
		} else {
			int lastBlockSize = (newBlockSize == rlen ? newBlockSize : rlen % newBlockSize) * odims[0];
			allocateBlocks(numBlocks);
			IntStream.range(0, numBlocks)
					.forEach(i -> {
						int length = (i == numBlocks - 1 ? lastBlockSize : newBlockSize *  _odims[0]);
						allocateBlock(i, length);
						if (v != 0)
							fillBlock(i, 0, length, v);
					});
		}
		_blen = newBlockSize;
		_rlen = rlen;
		_odims = odims;
	}

	@Override
	public int pos(int[] ix) {
		int pos = pos(ix[0]);
		pos += ix[ix.length - 1];
		for(int i = 1; i < ix.length - 1; i++)
			pos += ix[i] * _odims[i];
		return pos;
	}

	@Override
	public boolean isContiguous(int rl, int ru) {
		return index(rl) == index(ru);
	}

	@Override
	public int size(int bix) {
		return blockSize(bix) * _odims[0];
	}

	@Override
	public int index(int r) {
		return r / blockSize();
	}

	@Override
	public int pos(int r) {
		return (r % blockSize()) * _odims[0];
	}

	@Override
	public int pos(int r, int c) {
		return (r % blockSize()) * _odims[0] + c;
	}

	@Override
	public long countNonZeros() {
		long nnz = 0;
		for (int i = 0; i < numBlocks() - 1; i++) {
			nnz += computeNnz(i, 0, blockSize() * _odims[0]);
		}
		return nnz + computeNnz(numBlocks() - 1, 0, blockSize(numBlocks() - 1) * _odims[0]);
	}

	@Override
	public int countNonZeros(int r) {
		return (int) computeNnz(index(r), pos(r), _odims[0]);
	}

	@Override
	public long countNonZeros(int rl, int ru, int cl, int cu) {
		long nnz = 0;
		boolean allColumns = cl == 0 && cu == _odims[0];
		int rb = pos(rl);
		int re = blockSize() * _odims[0];
		// loop over rows of blocks, and call computeNnz for the specified columns
		for (int bi = index(rl); bi <= index(ru - 1); bi++) {
			// loop complete block if not last one
			if (bi == index(ru - 1)) {
				re = pos(ru - 1) + _odims[0];
			}
			if (allColumns) {
				nnz += computeNnz(bi, rb, re - rb);
			} else {
				for (int ri = rb; ri < re; ri += _odims[0]) {
					nnz += computeNnz(bi, ri + cl, cu - cl);
				}
			}
			rb = 0;
		}
		return nnz;
	}

	@Override
	public DenseBlock set(double v) {
		for (int i = 0; i < numBlocks() - 1; i++) {
			fillBlock(i, 0, blockSize() * _odims[0], v);
		}
		fillBlock(numBlocks() - 1, 0, blockSize(numBlocks() - 1) * _odims[0], v);
		return this;
	}

	@Override
	public DenseBlock set(int rl, int ru, int cl, int cu, double v) {
		boolean allColumns = cl == 0 && cu == _odims[0];
		int rb = pos(rl);
		int re = blockSize() * _odims[0];
		for (int bi = index(rl); bi <= index(ru - 1); bi++) {
			if (bi == index(ru - 1)) {
				re = pos(ru - 1) + _odims[0];
			}
			if (allColumns) {
				fillBlock(bi, rb, re, v);
			}
			else {
				for (int ri = rb; ri < re; ri += _odims[0]) {
					fillBlock(bi, ri + cl, ri + cu, v);
				}
			}
			rb = 0;
		}
		return this;
	}

	@Override
	public DenseBlock set(int r, double[] v) {
		int bix = index(r);
		int offset = pos(r);
		IntStream.range(0, _odims[0])
				.forEach((i) -> setInternal(bix, offset + i, v[i]));
		return this;
	}

	@Override
	public DenseBlock set(DenseBlock db) {
		// ToDo: Optimize if dense block types match
		// ToDo: Performance
		
		// this implementation needs to be robust against rows in the input
		// stretching over multiple blocks in the output and vice versa
		boolean aligned = blockSize()*_odims[0] == db.blockSize()*db._odims[0];
		if( aligned ) {
			for (int ri = 0; ri < _rlen; ri += blockSize()) {
				int bix = ri / blockSize();
				double[] other = db.valuesAt(bix);
				IntStream.range(0, blockSize(bix) * _odims[0])
					.forEach(i -> setInternal(bix, i, other[i]));
			}
		}
		else {
			long globalPos = 0;
			int bsize = blockSize() * _odims[0];
			for( int bix = 0; bix < db.numBlocks(); bix++ ) {
				double[] other = db.valuesAt(bix);
				int blen = db.blockSize(bix) * db._odims[0];
				int bix2 = (int)(globalPos / bsize);
				int off2 = (int)(globalPos % bsize);
				int blen2 = size(bix2);
				for( int i = 0; i < Math.min(blen,blen2-off2); i++ )
					setInternal(bix2, off2+i, other[i]);
				if( blen2-off2 < blen )
					for( int i = blen2-off2; i < blen; i++ )
						setInternal(bix2+1, i-(blen2-off2), other[i]);
				globalPos += blen;
			}
		}
		return this;
	}
}
