/*-
 * Copyright 2016 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
 */

package org.eclipse.dawnsci.hdf5;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.dawnsci.nexus.NexusException;
import org.eclipse.january.DatasetException;
import org.eclipse.january.dataset.IDataset;
import org.eclipse.january.dataset.ILazyWriteableDataset;
import org.eclipse.january.dataset.SliceND;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class to hold state of a HDF5 file
 */
public class HDF5File {
	private static final Logger logger = LoggerFactory.getLogger(HDF5File.class);

	private String file;
	private long id;   // HDF5 low level ID
	private long time; // time of release
	private AtomicInteger count; // number of accessors
	private boolean writeable; // if true then can write
	private boolean canSWMR;

	private ThreadPoolExecutor service;

	private Map<String, long[]> datasetIDs;
	private boolean cacheIDs;

	/**
	 * 
	 * @param filePath
	 * @param id
	 * @param writeable
	 * @param canBeSWMR if true, can be a SWMR writer (must be writeable too)
	 */
	public HDF5File(String filePath, long id, boolean writeable, boolean canBeSWMR) {
		this.file = filePath;
		this.id = id;
		count = new AtomicInteger(1);
		this.writeable = writeable;
		this.canSWMR = canBeSWMR;
		if (!writeable && canBeSWMR) {
			logger.error("Only writeable files can be SWMR");
			throw new IllegalArgumentException("Only writeable files can be SWMR");
		}
		datasetIDs = new HashMap<>();
		cacheIDs = false;
	}

	public long getID() {
		return id;
	}

	/**
	 * @return release time
	 */
	public long getTime() {
		return time;
	}

	/**
	 * Set release time
	 * @param time
	 */
	public void setTime(long time) {
		this.time = time;
	}

	/**
	 * @return number of accessors of file
	 */
	public int getCount() {
		return count.get();
	}

	/**
	 * Increment number of accessors of file
	 * @return incremented value
	 */
	public int incrementCount() {
		return count.incrementAndGet();
	}

	/**
	 * Decrement number of accessors of file
	 * @return decremented value
	 */
	public int decrementCount() {
		return count.decrementAndGet();
	}

	/**
	 * @return true if file is writable
	 */
	public boolean isWriteable() {
		return writeable;
	}

	/**
	 * @return true if the switch to SWMR writer mode has succeeded
	 */
	public boolean canSwitchSWMR() {
		return canSWMR;
	}

	private class WriteJob implements Runnable {
		private ILazyWriteableDataset out;
		private final IDataset data;
		private final SliceND slice;
		public WriteJob(final ILazyWriteableDataset out, final IDataset data, final SliceND slice) {
			this.out = out;
			this.data = data;
			this.slice = slice;
		}

		@Override
		public void run() {
//			System.err.printf("Writing " + DatasetUtils.convertToDataset(data).toString(true) + " to " + slice.toString());
			try {
				out.setSliceSync(null, data, slice);
			} catch (DatasetException e) {
				throw new RuntimeException(e);
			}
//			System.err.printf("... end\n");
		}
	}

	/**
	 * Add write job
	 * @param destination
	 * @param data
	 * @param slice
	 * @return true if writeable
	 */
	public synchronized boolean addWriteJob(ILazyWriteableDataset destination, IDataset data, SliceND slice) {
		if (writeable) {
			if (service == null) {
				service = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
			}
			if (!service.isShutdown()) {
				try {
					service.submit(new WriteJob(destination, data, slice));
					return true;
				} catch (RejectedExecutionException e) {
				}
			}
		}
		return false;
	}

	/**
	 * Finish all writes (block until it is done)
	 */
	public synchronized void flushWrites() {
		if (service != null) {
			BlockingQueue<Runnable> queue = service.getQueue();
			final long milli = 10; // period to sleep between checking for empty queue
			while (!service.isTerminated() && queue.peek() != null) {
				try {
					Thread.sleep(milli);
				} catch (InterruptedException e) {
				}
			}
		}
		flushDatasets();
	}

	/**
	 * Finish with file
	 * @param milliseconds to wait before finishing
	 */
	public synchronized void finish(long milliseconds) {
		if (service != null) {
			service.shutdown();
			try {
				service.awaitTermination(milliseconds, TimeUnit.MILLISECONDS);
				if (!service.isTerminated()) {
					service.shutdownNow();
				}
			} catch (InterruptedException e) {
			}
		}

		Iterator<String> it = datasetIDs.keySet().iterator();
		while (it.hasNext()) {
			String dataPath = it.next();
			long[] ids = datasetIDs.get(dataPath);
			it.remove();
			if (ids != null) {
				try {
					HDF5Utils.closeDataset(ids);
				} catch (NexusException e) {
					logger.error("Could not close {} in {}", dataPath, file, e);
				}
			}

		}
	}

	@Override
	public String toString() {
		return file;
	}

	/**
	 * Set dataset IDs caching
	 * @param cacheIDs if true, then also do not close
	 */
	public void setDatasetIDsCaching(boolean cacheIDs) {
		this.cacheIDs = cacheIDs; 
	}

	/**
	 * Open dataset
	 * <p>
	 * This can use cached IDs and store them too if set to do so
	 * @param dataPath
	 * @return IDs of dataset and its data space
	 */
	public synchronized long[] openDataset(final String dataPath) {
		long[] ids = datasetIDs.get(dataPath);
		if (ids == null) {
			try {
				ids = HDF5Utils.openDataset(this, dataPath);
				if (cacheIDs) {
					datasetIDs.put(dataPath, ids);
				}
			} catch (NexusException e) {
				logger.error("Could not open {} in {}", dataPath, file, e);
			}
		}
		return ids;
	}

	/**
	 * Close dataset if its IDs were cached
	 * @param dataPath
	 */
	public synchronized void closeDataset(final String dataPath) {
		long[] ids = datasetIDs.remove(dataPath);
		if (ids != null) {
			try {
				HDF5Utils.closeDataset(ids);
			} catch (NexusException e) {
				logger.error("Could not close {} in {}", dataPath, file, e);
			}
		}
	}

	/**
	 * Flush dataset if it has been opened and cached
	 * @param dataPath
	 */
	public synchronized void flushDataset(final String dataPath) {
		long[] ids = datasetIDs.get(dataPath);
		if (ids != null) {
			try {
				HDF5Utils.flushDataset(ids);
			} catch (NexusException e) {
				logger.error("Could not flush {} in {}", dataPath, file, e);
			}
		}
	}

	/**
	 * Flush all datasets whose IDs have been opened and cached
	 */
	public synchronized void flushDatasets() {
		for (String dataPath: datasetIDs.keySet()) {
			flushDataset(dataPath);
		}
	}

	/**
	 * @param dataPath
	 * @return true if dataset IDs are cached
	 */
	public synchronized boolean containsDataset(final String dataPath) {
		return datasetIDs.containsKey(dataPath);
	}
}