/*-
 *******************************************************************************
 * Copyright (c) 2011, 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
 *
 * Contributors:
 *    Matthew Gerring - initial API and implementation and/or initial documentation
 *******************************************************************************/
package org.eclipse.dawnsci.remotedataset.client;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.eclipse.january.DatasetException;
import org.eclipse.january.dataset.DataEvent;
import org.eclipse.january.dataset.Dataset;
import org.eclipse.january.dataset.IDataListener;
import org.eclipse.january.dataset.IDatasetConnector;
import org.eclipse.january.dataset.IDynamicDataset;
import org.eclipse.january.dataset.LazyWriteableDataset;
import org.eclipse.january.dataset.ShapeUtils;
import org.eclipse.january.metadata.DynamicConnectionInfo;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class manages a remote connection to data and
 * allows datasets to work seamlessly remotely as if
 * they were ILazyDatasets.
 * 
 * To do this they require to know the data server's name and port.
 * They also need a path on the server for the file which they are linked to.
 * 
 * The DataServer is a standard HTTP with web sockets for events. This makes
 * RemoteDataset able to pass over HTTP with port 80 open for external data 
 * viewing too.
 * 
 * 
<usage><code>
final IRemoteDatasetService service = ...
final IRemoteDataset data = service.createRemoteDataset("localhost", 8080);<br>
data.setPath(h5File.getAbsolutePath());
data.setDatasetName("image"); // We just get the first image in the PNG file.
data.connect();

try {
    // Use it the same way as ILazyDataset
} finally {
    data.disconnect();
}
</code></usage>
 * 
 * @author Matthew Gerring
 *
 */
class RemoteDataset extends LazyWriteableDataset implements IDatasetConnector {
	
	private static final Logger logger = LoggerFactory.getLogger(RemoteDataset.class);
	
	private final URLBuilder urlBuilder;
	
	// Web socket stuff
	private Session connection;

	private boolean dynamicShape = true;
	private int[] transShape;

	private Executor exec;

	private WebSocketClient client;

	/**
	 * 
	 */
	private static final long serialVersionUID = -9031675045219778735L;

	/**
	 * 
		<usage><code>
		final IRemoteDatasetService service = ...
		final IRemoteDataset data = service.createRemoteDataset("localhost", 8080);<br>
		data.setPath(h5File.getAbsolutePath());<br>
		data.setDataset("image"); // We just get the first image in the PNG file.<br>
		data.connect();<br>
		<br>
		try {<br>
		    // Use it the same way as ILazyDataset<br>
		} finally {<br>
		    data.disconnect();<br>
		}<br>
		</code></usage>
	 * @param serverName
	 * @param port
	 */
	public RemoteDataset(String serverName, int port, Executor exec) {
		super("unknown", Dataset.INT, new int[]{1}, new int[]{IDynamicDataset.UNLIMITED}, null, null);
		this.urlBuilder = new URLBuilder(serverName, port);
		urlBuilder.setWritingExpected(true);
		this.exec       = exec;
		this.loader = new RemoteLoader(urlBuilder);
	}

	@Override
	public String connect() throws DatasetException {
		return connect(500, TimeUnit.MILLISECONDS);
	}

	/**
	 * Call to read the dataset, set current shape and create event connnection for
	 * IDynamicDataset part of the dataset
	 */
	@Override
	public String connect(long time, TimeUnit unit) throws DatasetException {
		try {
			createInfo();
			if (eventDelegate.hasDataListeners()) {
				createFileListener();
			}
		} catch (Exception e) {
			throw new DatasetException(e);
		}
		
		// TODO Does this cause a memory leak?
		// If multiple connect/disconnect are called will this break things?
		addMetadata(new DynamicConnectionInfo() {
			private static final long serialVersionUID = 6220818379127865903L;
			public boolean isConnected() {
				return connection.isOpen();
			}
		});
		
		return null;
	}

	public void disconnect() throws DatasetException {

		eventDelegate.clear();
		try {
			if (connection != null && connection.isOpen()) {
				connection.getRemote().sendString("Disconnected from " + urlBuilder.getPath());
				connection.close();
			}
			if (client != null && client.isStarted()) {
				client.stop();
			}
		} catch (Exception e) {
			throw new DatasetException(e);
		}
	}
	
    @Override
    public boolean refreshShape() {
    	try {
			createInfo();
		} catch (Exception e) {
			return false;
		}
    	return true;
    }
    
	private void createFileListener() throws Exception {
		
        URI uri = URI.create(urlBuilder.getEventURL());

        this.client = new WebSocketClient(exec);
        client.start();
       
        final DataEventSocket clientSocket = new DataEventSocket();
        // Attempt Connect
        Future<Session> fut = client.connect(clientSocket, uri);

        // Wait for Connect
        connection = fut.get();

        // Send a message
        connection.getRemote().sendString("Connected to "+urlBuilder.getPath());
	}
	
	public class DataEventSocket extends WebSocketAdapter {
		@Override
	    public void onWebSocketText(String data) {
			
	        super.onWebSocketText(data);			
	        try {
				DataEvent evt = DataEvent.decode(data);
				if (evt.getShape()!=null) {
					if (dynamicShape) {
						resize(evt.getShape());
						setMax(evt.getShape());
						
						eventDelegate.fire(evt);
					} else {
						RemoteDataset.this.transShape =  evt.getShape();
					}
				}
			} catch (Exception ne) {
				logger.error("Cannot set shape of dataset!", ne);
			}
		}
	}
	
	public void setShapeDynamic(boolean isDyn) {
		dynamicShape  = isDyn;
		if (dynamicShape && transShape!=null) {
		    this.shape = transShape;
		    setMax(shape);
		    transShape = null;
		}
	}

	private void setMax(int[] shape) {
		int[] max = new int[shape.length];
		for (int i = 0; i < max.length; i++) max[i] = -1;
		setMaxShape(max);
	}

	private void createInfo() throws Exception {
		
		List<String> info = getInfo();
		
		if (this.name == null) this.name  = info.get(0);
		this.shape = toIntArray(info.get(1));
		if (this.shape.length == 1 && shape[0] == -1) {
			System.err.println();
			
		}
		setMax(shape);
		this.oShape = shape;
		this.dtype = Integer.parseInt(info.get(2));
		this.isize = Integer.parseInt(info.get(3));
		try {
			size = ShapeUtils.calcLongSize(shape);
		} catch (IllegalArgumentException e) {
			size = Long.MAX_VALUE; // this indicates that the entire dataset cannot be read in! 
		}

		if (info.size() > 4) {
			setMaxShape(toIntArray(info.get(4)));
			setChunking(toIntArray(info.get(5)));
		}
	}

	private int[] toIntArray(String array) {
		// array is null, or of the form [1,2,3,4]
		if (array.equals("null"))
			return null;
		if (array.length() <= 2) {
			return new int[0]; // special case of scalar dataset
		}

		final String[] split = array.substring(1, array.length()-1).split(",");
		final int[]    ret   = new int[split.length];
		for (int i = 0; i < split.length; i++) {
			ret[i] = Integer.parseInt(split[i].trim());
 		}
		return ret;
	}

	private List<String> getInfo() throws Exception {
		
		final List<String> ret = new ArrayList<String>();
		final URL url = new URL(urlBuilder.getInfoURL());
		URLConnection  conn = url.openConnection();

		final BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
		try {
			String line = null;
			while((line = reader.readLine())!=null) ret.add(line);
		} finally {
			reader.close();
		}
		return ret;
	}

	@Override
	public void addDataListener(IDataListener l) {
		// If we are not already web socket client and connect has been called, create the listener.
		try {
		    if (this.connection==null && loader!=null) createFileListener();
		} catch (Exception ne) {
			throw new IllegalArgumentException(ne);
		}
		eventDelegate.addDataListener(l);
	}

	public String getPath() {
		return urlBuilder.getPath();
	}

	public void setPath(String path) {
		urlBuilder.setPath(path);
	}

	public String getDatasetName() {
		return urlBuilder.getDataset();
	}

	public void setDatasetName(String dataset) {
		urlBuilder.setDataset(dataset);
	}

	public boolean isWritingExpected() {
		return urlBuilder.isWritingExpected();
	}

	public void setWritingExpected(boolean writingExpected) {
		urlBuilder.setWritingExpected(writingExpected);
	}

	@Override
	public IDynamicDataset getDataset() {
		return this;
	}
	
	@Override
	public RemoteDataset clone() {
		RemoteDataset ret = new RemoteDataset(urlBuilder.getServerName(),urlBuilder.getPort(),this.exec);
		ret.urlBuilder.setDataset(this.urlBuilder.getDataset());
		ret.urlBuilder.setPath(this.urlBuilder.getPath());
		ret.client = this.client;
		ret.connection = this.connection;
		ret.loader = this.loader;
		ret.shape = shape;
		ret.size = size;
		ret.maxShape = maxShape;
		ret.oShape = oShape;
		ret.prepShape = prepShape;
		ret.postShape = postShape;
		ret.begSlice = begSlice;
		ret.delSlice = delSlice;
		ret.map = map;
		ret.base = base;
		ret.metadata = copyMetadata();
		ret.oMetadata = oMetadata;
		ret.eventDelegate = eventDelegate;
		ret.name = this.name;
		return ret;
	}
}
