/*-
 *******************************************************************************
 * Copyright (c) 2011, 2014 Diamond Light Source Ltd.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Matthew Gerring - initial API and implementation and/or initial documentation
 *******************************************************************************/
package org.eclipse.dawnsci.hdf.object;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import hdf.object.Attribute;
import hdf.object.Dataset;
import hdf.object.Datatype;
import hdf.object.HObject;

public class HierarchicalDataUtils {


	/**
	 * Overwrites destination_file if it exists, creates new if not.
	 *
	 * @param source_file
	 * @param destination_file
	 * @throws IOException
	 */
	public final static void copy(final File source_file, final File destination_file) throws IOException {
		HierarchicalDataUtils.copy(source_file, destination_file, new byte[4096]);
	}

	/**
	 * Overwrites destination_file if it exists, creates new if not.
	 *
	 * @param source_file
	 * @param destination_file
	 * @param buffer
	 * @throws IOException
	 */
	public final static void copy(final File source_file, final File destination_file, final byte[] buffer) throws IOException {

		if (!source_file.exists()) {
			return;
		}

		final File parTo = destination_file.getParentFile();
		if (!parTo.exists()) {
			parTo.mkdirs();
		}
		if (!destination_file.exists()) {
			destination_file.createNewFile();
		}

		InputStream source = null;
		OutputStream destination = null;
		try {

			source = new BufferedInputStream(new FileInputStream(source_file));
			destination = new BufferedOutputStream(new FileOutputStream(destination_file));
			int bytes_read;
			while (true) {
				bytes_read = source.read(buffer);
				if (bytes_read == -1) {
					break;
				}
				destination.write(buffer, 0, bytes_read);
			}

		} finally {
			source.close();
			destination.close();
		}
	}


	/**
	 * @throws Exception
	 * @throws OutOfMemoryError
	 *
	 */
	public static boolean isDataType(Dataset set, int requiredType) throws OutOfMemoryError, Exception {

		if (requiredType<0) return true; // Numbers less than 0 are any dataset

        final int   type  = set.getDatatype().getDatatypeClass();
        if (type==Datatype.CLASS_FLOAT || type==Datatype.CLASS_INTEGER || type==Datatype.CLASS_CHAR) {
        	if (IHierarchicalDataFile.NUMBER_ARRAY==requiredType) {
        		long[]shape = getDims(set);
                if (shape==null) return true;
        		return shape.length>1 || shape[0]>1;
        	} else if (IHierarchicalDataFile.SCALAR==requiredType) {
        		long[]shape = getDims(set);
                if (shape==null) return true;
        		return shape.length==1 && shape[0]==1;
        	}
        }

        if (type==Datatype.CLASS_STRING) {
        	if (IHierarchicalDataFile.TEXT==requiredType) {
        		return true;
        	} else if (IHierarchicalDataFile.SCALAR==requiredType) {
        		long[]shape = getDims(set);
                if (shape==null) return true;
        		return shape.length==1 && shape[0]==1;
        	}
        }

        return requiredType==type;
	}


	/**
	 * Returns the link in another file if this is a linked file.
	 * @param set
	 * @param original - may be null
	 * @return
	 * @throws OutOfMemoryError
	 * @throws Exception
	 */
	public static HObject getDataLink(HObject set, HierarchicalDataFile original) {

		try {
			// To make things faster, we only check for Datasets
			if (!(set instanceof Dataset)) return null;

			List<?> attributes = set.getMetadata(); // Appears in stack traces of VM exists
			if (attributes==null || attributes.isEmpty()) return null;

			for (Object attribute : attributes) {
				if (attribute instanceof Attribute) {
					Attribute a = (Attribute)attribute;
					if (a.getName().equals(HierarchicalInfo.NAPIMOUNT)) {
						return  getDataLink(set, a, original);
					}
				}
			}
		} catch (Throwable ne) {
			return null;
		}

		return null;
	}

	private static final Pattern LINK_ATTR = Pattern.compile("nxfile\\:\\/\\/(.+)\\#(entry.+)");

	private static final HObject getDataLink(HObject set, Attribute a, HierarchicalDataFile original) throws Exception {

		final String[] vals = (String[])a.getValue();
		final Matcher matcher = LINK_ATTR.matcher(vals[0]);
		if (matcher.matches()) {

			final String path     = matcher.group(1);
			final String fullPath = matcher.group(2);

			File file = new File(path);
			if (!file.exists()) {
				// Look in same directory.
				final File parent = set.getFileFormat().getAbsoluteFile().getParentFile();
				file = new File(parent, file.getName());
			}

			if (!file.exists()) return null;

			@SuppressWarnings("resource")
			IHierarchicalDataFile hFile = original!=null
					                    ? original.getLinkedFile(file.getAbsolutePath())
					                    // Causes a memory leak I think:
					                    : HierarchicalDataFactory.getReader(file.getAbsolutePath());
			HObject link = (HObject)hFile.getData(fullPath);
			link.getMetadata(); // Ensure that meta data is read.
			return link;
		}

		return null;
	}

	public static long[] getDims(final IHierarchicalDataFile file, String setPath) throws Exception {

		HObject obj = (HObject)file.getData(setPath);
		if (!(obj instanceof Dataset)) return null;
		Dataset set = (Dataset)obj;
        if (set.getDims()==null) {
        	set.getMetadata();
        }
        return set.getDims();
	}

	public static long[] getDims(final Dataset set) throws Exception {

        if (set.getDims()==null) {
        	set.getMetadata();
        }
        return set.getDims();
	}

	public static long getSize(final Dataset set) throws Exception {

		long[] shape;
		try {
			shape = HierarchicalDataUtils.getDims((Dataset)set);
		} catch (Exception e) {
			return -1;
		}
		if (shape==null) return -1;

		long size = shape[0];
		for (int i = 1; i < shape.length; i++) size*=shape[i];

		final int bpi  = set.getDatatype().getDatatypeSize();
        return  bpi*size;


	}

	/**
	 * Extract the value of an attribute.
	 * @param value
	 * @return
	 */
	public static String extractValue(Object value) {
		if (value == null) {
			return "";

		} else if (value.getClass().isArray()) {
			return toString(value);

		} else {
			return value.toString();
		}
	}

	/**
	 * Deals with primitive arrays
	 * @param value
	 */
	private static String toString(Object value) {

		if (value==null) return null;

        if (value instanceof short[]) {
        	return Arrays.toString((short[])value);

        } else if  (value instanceof int[]) {
        	return Arrays.toString((int[])value);

        } else if  (value instanceof long[]) {
        	return Arrays.toString((long[])value);

        } else if  (value instanceof char[]) {
        	return Arrays.toString((char[])value);

        } else if  (value instanceof float[]) {
        	return Arrays.toString((float[])value);

        } else if  (value instanceof double[]) {
        	return Arrays.toString((double[])value);

        } else if  (value instanceof boolean[]) {
        	return Arrays.toString((boolean[])value);

        } else if  (value instanceof byte[]) {
        	return Arrays.toString((byte[])value);

        } else if  (value instanceof Object[]) {
        	return Arrays.toString((Object[])value);
        }

        return value.toString();
	}

	/**
	 * Convert the size == 1 array to a scalar (extract value[0]). Works on
	 * primitive type arrays, String[] and Number[]
	 *
	 * @param value
	 *            a {@link Dataset} value (i.e. an array, the result of calling
	 *            {@link Dataset#read()})
	 * @return the scalar value, or <code>null</code> if no scalar can be
	 *         extracted
	 */
	public static Object extractScalar(Object value) {

		if (value == null)
			return null;

		if (value instanceof short[]) {
			short[] valueArray = (short[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof int[]) {
			int[] valueArray = (int[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof long[]) {
			long[] valueArray = (long[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof char[]) {
			char[] valueArray = (char[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof float[]) {
			float[] valueArray = (float[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof double[]) {
			double[] valueArray = (double[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof boolean[]) {
			boolean[] valueArray = (boolean[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof byte[]) {
			byte[] valueArray = (byte[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof String[]) {
			String[] valueArray = (String[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;

		} else if (value instanceof Number[]) {
			Number[] valueArray = (Number[]) value;
			if (valueArray.length == 1)
				return valueArray[0];
			return null;
		}

		return null;
	}

	/**
	 * Compares the two scalar objects if they are the same type and Comparable
	 * using their compareTo method, else compares the toString value.
	 *
	 * @see Comparable#compareTo(Object)
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static int compareScalars(Object a, Object b) {
		if (a instanceof Comparable && b instanceof Comparable) {
			Comparable ca = (Comparable) a;
			Comparable cb = (Comparable) b;
			if (a.getClass() == b.getClass())
				return ca.compareTo(cb);
		}
		return a.toString().compareTo(b.toString());
	}

	/**
	 * Compare object a as a scalar to String b by first converting b to a type
	 * that matches a.
	 *
	 * @see Comparable#compareTo(Object)
	 * @throws NumberFormatException
	 *             if b can not be converted to the same type as a
	 */
	public static int compareScalarToString(Object a, String b)
			throws NumberFormatException {
		if (a instanceof Short) {
			return ((Short) a).compareTo(Short.parseShort(b));
		}
		if (a instanceof Integer) {
			return ((Integer) a).compareTo(Integer.parseInt(b));
		}
		if (a instanceof Long) {
			return ((Long) a).compareTo(Long.parseLong(b));
		}
		if (a instanceof Character) {
			return ((Character) a).toString().compareTo(b);
		}
		if (a instanceof Float) {
			return ((Float) a).compareTo(Float.parseFloat(b));
		}
		if (a instanceof Double) {
			return ((Double) a).compareTo(Double.parseDouble(b));
		}
		if (a instanceof Boolean) {
			return ((Boolean) a).compareTo(Boolean.parseBoolean(b));
		}
		if (a instanceof Byte) {
			return ((Byte) a).compareTo(Byte.valueOf(b));
		}
		if (a instanceof String) {
			return ((String) a).compareTo(b);
		}
		String name;
		if (a == null) {
			name = "null";
		} else {
			name = "a.getClass().getName()";
		}
		throw new NumberFormatException("a has unknown type for conversion: "
				+ name);
	}
}
