/*
 * This file is a part of EpnTAPClient.
 * This program aims to provide EPN-TAP support for software clients, like CASSIS spectrum analyzer.
 * See draft specifications: https://voparis-confluence.obspm.fr/pages/viewpage.action?pageId=559861
 * Copyright (C) 2016 Institut de Recherche en Astrophysique et Planétologie.
 *
 * This program is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or (at your option) any later
 * version. This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE. See the GNU General Public License for more details. You should have received a copy of
 * the GNU General Public License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package eu.omp.irap.vespa.votable.votabledata;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.xml.bind.DatatypeConverter;

import eu.omp.irap.vespa.epntapclient.votable.model.DataType;
import eu.omp.irap.vespa.epntapclient.votable.model.Field;
import eu.omp.irap.vespa.epntapclient.votable.model.Stream;
import eu.omp.irap.vespa.votable.utils.Debug;
import eu.omp.irap.vespa.votable.votable.VOTableException.CantParseVOTableException;

/**
 * @author N. Jourdane
 */
public final class BinaryStreamParser implements DataParser {

	/** The logger for the class BinaryStreamParser. */
	private static final Logger LOGGER = Logger.getLogger(BinaryStreamParser.class.getName());

	/** The index of the current position in the bytes array. */
	private int cursor;

	/** The XML nodes of VOTable document, containing the column names. */
	private List<Field> fieldsNodes;

	/** The byte array stored in the VOTable Table stream (if any). */
	private int rowSize;

	/** The stream to parse, containing data (column rows) stored in the VOTable. */
	private ByteBuffer stream;


	/**
	 * Constructor of BinaryStreamParser.
	 *
	 * @param voStream The stream to parse, containing data (column rows) stored in the VOTable.
	 * @param fieldsNodes the field XML nodes in VOTable document, containing the column names.
	 * @see <a href="http://www.ivoa.net/documents/REC/VOTable/VOTable-20040811.html#ToC35">The
	 *      Binary serialization in the VOTable REC.</a>
	 */
	public BinaryStreamParser(Stream voStream, List<Field> fieldsNodes) {
	    cursor = 0;
            this.fieldsNodes = fieldsNodes;
            String strStream = voStream.getValue().replaceAll("(\\r|\\n)", "");

            //12325
            if (strStream.isEmpty()) {
                LOGGER.warning("Stream value is empty. Nothing to parse.");
                stream = ByteBuffer.allocate(0); // buffer vide
                return;
            }
            stream = ByteBuffer.wrap(DatatypeConverter.parseBase64Binary(strStream));
	               
	}

	/**
	 * Get the size of data block according to its type.
	 *
	 * @param dataType The type of the data block, as described in the VOTable REC. See
	 *            http://www.ivoa.net/documents/REC/VOTable/VOTable-20040811.html#ToC11
	 * @return The size of the block, in bytes.
	 */
	private static int getBlockSize(DataType dataType) {
		int blockSize;
		switch (dataType) {
		case BOOLEAN:
		case UNSIGNED_BYTE:
		case CHAR:
			blockSize = 1;
			break;
		case SHORT:
		case UNICODE_CHAR:
			blockSize = 2;
			break;
		case INT:
		case FLOAT:
			blockSize = 4;
			break;
		case LONG:
		case DOUBLE:
		case FLOAT_COMPLEX:
			blockSize = 8;
			break;
		case DOUBLE_COMPLEX:
			blockSize = 16;
			break;
		default:
			blockSize = 0;
			break;
		}
		return blockSize;
	}

	@Override
	public List<Object[]> parse() throws CantParseVOTableException {
		List<Object[]> data = new ArrayList<>();
		int nbColumns = fieldsNodes.size();
		BinaryStreamParser.LOGGER
				.fine("Parsing data (" + nbColumns + " columns) in BINARY stream...");               
		Object[] row = new Object[nbColumns];

		int nValue = 0;
		while (stream.hasRemaining()) {
			int colId = nValue % nbColumns;
			Field column = fieldsNodes.get(colId);

			rowSize = getRowSize(column);

			// May be useful: if arraySize == 0, continue

			if (colId == 0) {
				row = new Object[nbColumns];
			}

			row[colId] = processDataBlock(column.getDatatype());

			if (colId == nbColumns - 1) {
				data.add(row);
			}
			nValue++;
		}

		Debug.writeObject("voTableData", data);
		return data;
	}

	/**
	 * @param column The XML node of the VOTable data, containing informations about the column,
	 *            like the data primitive type.
	 * @return The size of the row, in bytes.
	 * @throws BufferUnderflowException Can not get the array size because there is no bytes to read
	 *             in the stream.
	 */
	private int getRowSize(Field column) {
		DataType dataType = column.getDatatype();
		int blockSize = getBlockSize(dataType);
		int arraySize;
		if (column.getArraysize() == null) {
			arraySize = blockSize;
		} else if ("*".equals(column.getArraysize())) {
			if (!dataType.equals(DataType.DOUBLE)) {
				try {
					arraySize = stream.getInt() * blockSize;
				} catch (BufferUnderflowException e) {
					LOGGER.log(Level.SEVERE, "No space left on stream, can not get array size", e);
					throw new BufferUnderflowException();
				}
			}
			// GFI - add this to get around wrong APIS column definition 
			else { 
//				int position = stream.position();
//				int size = 0;
//				for (int i=0; stream.position() < stream.limit(); i++) {
//					if (stream.get() != 0) {
//						size = i;
//						break;
//					}
//				}
//				try {
//					stream.position(position);
//				} catch (Exception e) {
//					e.printStackTrace();
//				}
				column.setDatatype(DataType.CHAR);
				dataType = column.getDatatype();
				arraySize = 4;
			}
		} else {
			arraySize = Integer.parseInt(column.getArraysize()) * blockSize;
		}
		if (arraySize != blockSize && !(dataType.equals(DataType.CHAR)
				|| dataType.equals(DataType.UNICODE_CHAR))) {
			throw new UnsupportedOperationException("Numeric data as array are not supported.");
		}
		return arraySize;
	}

	/**
	 * @param dataType The type of the data block.
	 * @return An object representing the data. It can be any of the primitive type defined in the
	 *         VOtableREC. See http://www.ivoa.net/documents/REC/VOTable/VOTable-20040811.html#ToC11
	 */
	private Object processDataBlock(DataType dataType) {
		Object dataBlock;
		if (dataType.equals(DataType.BOOLEAN)) {
			dataBlock = new Boolean(stream.getInt() == 0 ? false : true);
		} else if (dataType.equals(DataType.UNSIGNED_BYTE)) {
			dataBlock = stream.get();
		} else if (dataType.equals(DataType.SHORT)) {
			dataBlock = stream.getShort();
		} else if (dataType.equals(DataType.INT)) {
			dataBlock = stream.getInt();
		} else if (dataType.equals(DataType.LONG)) {
			dataBlock = stream.getLong();
		} else if (dataType.equals(DataType.CHAR)) {
			String value = new String();
			for (int i = 0; i < rowSize && cursor < stream.capacity()
					&& stream.position() < stream.limit(); i++) {
				value += (char) stream.get();
			}
			dataBlock = value.trim();
		} else if (dataType.equals(DataType.UNICODE_CHAR)) {
			String value = new String();
			for (int i = 0; i < rowSize && cursor < stream.capacity(); i += 2) {
				value += stream.getChar();
			}
			dataBlock = value.trim();
		} else if (dataType.equals(DataType.FLOAT)) {
			dataBlock = stream.getFloat();
		} else if (dataType.equals(DataType.DOUBLE)) {
			dataBlock = stream.getDouble();
		} else {
			BinaryStreamParser.LOGGER.warning("Data type " + dataType + " is not supported.");
			dataBlock = null;
		}
		return dataBlock;
	}
}
