/*
 * Copyright (C) 2006  Mike Murray
 *
 *  All rights reserved.
 *  This document may not be reused without
 *  express written permission from Mike Murray.
 *
 */

package net.sf.j3d.network.resources;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.sf.j3d.network.Resource;
import net.sf.j3d.network.Server.IOStreams;
import net.sf.j3d.network.db.Client;
import net.sf.j3d.network.db.DatabaseConnection;
import net.sf.j3d.network.db.OutputServer;
import net.sf.j3d.network.db.Query;

/**
 * A DistributedResource object represents a file in the distributed content repository.
 * The content repository is designed to break up an array of bytes into uniform size
 * chunks of data that can be moved around and mirrored to store the contents of some
 * resource across many running clients. A DistributedResource object may have some
 * number of these chunks stored in an internal (in memory) cache. When the getData method is
 * called, as many chunks of data as possible are loaded from the cache. Other data is
 * then requested from a ResourceDistributionTask, if one is currently running on the
 * currently running Server. The ResourceDistributionTask stores chunks of data in a
 * hard disk cache. If more chunks of data are required for the resource to be completely
 * loaded and returned, the DistributedResource object will request this data from peers
 * that are connected to the currently running Server. These peers should respond with
 * a URI for a resource server. When the resource server is contacted, it is informed of
 * which chunks of data need to be loaded and the process of loading a DistributedResource
 * begins again on the peer machine (a call to the getData method is made). This process
 * is identical to the one just presented here. This process is repeated until the resource
 * is fully loaded.
 * 
 * @author Mike Murray
 */
public class DistributedResource implements Resource {
	public static boolean verbose = true;
	public static boolean ioVerbose = false;
	
	private int chunkSize = 500000;
	private boolean commitAtEnd = false;
	
	private String uri, exclude;
	private long tot;
	private int size;
	private boolean loaded[];
	private long toa[];
	private byte data[][];
	
	public DistributedResource(String uri) {
		this.uri = uri;
		this.size = -1;
		this.loaded = null;
		this.toa = null;
		this.data = null;
	}
	
	public DistributedResource(String uri, long size) {
		this.uri = uri;
		
		this.tot = size;
		this.size = (int) (size / this.chunkSize);
		this.loaded = new boolean[this.size];
		this.toa = new long[this.size];
		this.data = new byte[this.size][0];
	}
	
	public DistributedResource(Resource res) {
		this.uri = res.getURI();
		
		Object o = res.getData();
		if (o instanceof byte[][] == false)
			throw new IllegalArgumentException("Resource data is not byte[][].");
		
		this.data = (byte[][]) res.getData();
		
		for (int i = 0; i < this.data.length - 1; i++) {
			if (this.data[i].length != this.chunkSize)
				throw new IllegalArgumentException("Improper dimensions for resource data.");
		}
		
		this.size = this.data.length;
		this.tot = this.chunkSize * (this.size - 1) +
					this.data[this.data.length - 1].length;
		this.loaded = new boolean[this.size];
		for (int i = 0; i < this.loaded.length; i++) this.loaded[i] = true;
		this.toa = new long[this.size];
		this.toa[0] = System.currentTimeMillis();
		for (int i = 1; i < this.toa.length; i++) this.toa[i] = this.toa[i - 1] + 2;
	}
	
	public boolean isLoaded() {
		if (this.data == null) return false;
		if (this.loaded == null) return false;
		for (int i = 0; i < this.loaded.length; i++)
			if (!this.loaded[i]) return false;
		
		return true;
	}
	
	public int getSize() { return this.size; }
	public long getTotalBytes() {
		if (this.verbose) {
			String loaded;
			
			if (this.isLoaded())
				loaded = "(loaded).";
			else
				loaded = "(not loaded).";
			
			System.out.println("DistributedResource (" + this.uri +
								"): Total bytes = " + this.tot +
								loaded);
		}
		
		return this.tot;
	}
	
	public void setData(int index, byte d[]) {
		if (this.verbose) {
			String msg = "DistributedResource (" + this.uri +
								"): Set data[" + index + "] to ";
			if (d == null)
				System.out.println(msg + "null.");
			else
				System.out.println(msg + (d.length / 1000.0) +
									" kilobyte chunk.");
		}
		
		this.data[index] = d;
		this.loaded[index] = this.data != null;
	}
	
	public Object getData() { return this.data; }
	
	public synchronized byte[] getData(int index, boolean load) {
		return this.getData(index, load, load);
	}
	
	public synchronized byte[] getData(int index, boolean load, boolean fromRes) {
		if (this.data == null) {
			this.loadFromLocalDB();
			load = false;
		}
		
		if (index < this.data.length && this.data[index].length > 0)
			return this.data[index];
		
		if (load && index < this.toa.length && this.toa[index] > 0) {
			byte b[] = this.loadFromLocalDB(index);
			
			if (b != null) {
				this.data[index] = b;
				this.loaded[index] = true;
				return b;
			}
		}
		
		if (fromRes) {
			try {
				this.loadFromResourceServer();
			} catch (IOException ioe) {
				System.out.println("DistributedResource(" + this.uri +
									"): IO error while loading from resource server -- " +
									ioe.getMessage());
			}
		}
		
		if (index >= this.data.length) {
			System.out.println("DistributedResource: Data is " + this.data.length +
								" chunks. Chunk " + index + " does not exist.");
			return null;
		} else if (index >= this.loaded.length) {
			System.out.println("DistributedResource: Loaded list knows " +
								this.loaded.length + " chunks. Data has " +
								this.data.length + " chunks. Chunk " + index +
								" was requested.");
			return null;
		}
		
		if (this.loaded[index])
			return this.data[index];
		else
			return null;
	}
	
	protected void clear() {
		this.clearCache();
		this.data = null;
	}
	
	protected void clearCache() {
		if (this.tot > 0)
			ResourceDistributionTask.getCurrentTask().subtractCache(this.tot);
		
		if (this.verbose)
			System.out.println("DistributedResource (" + this.uri +
								"): Cleared cache.");
	}
	
	protected void checkCache() {
		ResourceDistributionTask task = ResourceDistributionTask.getCurrentTask();
		
		if (this.tot > 0) {
			task.addCache(this.tot);
			task.checkFull();
		}
	}
	
	protected void loadFromLocalDB() {
		this.clearCache();
		
		OutputServer s = OutputServer.getCurrentServer();
		if (s == null) return;
		
		System.out.println("DistributedResource (" + this.uri + "): Loading from local DB...");
		
		String table = s.getDatabaseConnection().getTable();
		Query q = new Query(table, DatabaseConnection.indexColumn,
							DatabaseConnection.dataColumn,
							DatabaseConnection.uriColumn + " = '" +
							this.uri + "'");
		Query tq = new Query(table, DatabaseConnection.indexColumn,
							DatabaseConnection.toaColumn,
							DatabaseConnection.uriColumn + " = '" +
							this.uri + "'");
		Hashtable h = s.getDatabaseConnection().executeQuery(q);
		Hashtable th = s.getDatabaseConnection().executeQuery(tq);
		this.data = new byte[h.size()][this.chunkSize];
		this.toa = new long[this.data.length];
		this.loaded = new boolean[this.data.length];
		this.size = h.size();
		this.tot = (this.size - 1) * this.chunkSize;
		
		Iterator itr = h.entrySet().iterator();
		w: while (itr.hasNext()) {
			Map.Entry e = (Map.Entry) itr.next();
			String si = e.getKey().toString();
			int i = Integer.parseInt(si);
			
			this.data[i] = (byte[]) e.getValue();
			
			if (this.data[i] == null) {
				System.out.println("DistributedResource (" + this.uri +
									"): Query result contained null row at " + i);
				continue w;
			}
			
			this.loaded[i] = true;
			this.toa[i] = Long.parseLong((String) th.get(si));
			
			if (i == this.data.length - 1)
				this.tot += this.data[i].length;
		}
		
		this.checkCache();
	}
	
	protected byte[] loadFromLocalDB(int index) {
		System.out.println("DistributedResource (" + this.uri + "): Loading chunk " +
							index + " from local DB...");
		
		OutputServer s = OutputServer.getCurrentServer();
		if (s == null) return null;
		
		String table = s.getDatabaseConnection().getTable();
		Query q = new Query(table, DatabaseConnection.uriColumn,
							DatabaseConnection.dataColumn,
							DatabaseConnection.indexColumn +
							" = " + index);
		q.setValue(0, this.uri);
		Hashtable h = s.getDatabaseConnection().executeQuery(q);
		if (h == null) return null;
		
		byte b[] = (byte[]) h.get(this.uri);
		return b;
	}
	
	protected void setExcludeHost(String host) { this.exclude = host; }
	
	protected void loadFromResourceServer() throws IOException {
		System.out.println("DistributedResource (" + this.uri +
							"): Loading data from resource server...");
		
		Client.getCurrentClient().getServer().loadResource(this, this.exclude, true);
		this.exclude = null;
	}
	
	protected void commitToLocalDB() {
		OutputServer s = OutputServer.getCurrentServer();
		
		if (s == null) {
			System.out.println("DistributedResource (" + this.uri +
								"): Unable to commit (no local DB)");
			return;
		}
		
		if (DistributedResource.verbose)
			System.out.println("DistributedResource (" + this.uri +
								"): Deleting from local DB...");
		
		s.getDatabaseConnection().deleteUri(this.uri);
		
		if (DistributedResource.verbose)
			System.out.println("DistributedResource (" + this.uri +
								"): Commiting data to local DB...");
		
		int tot = 0;
		
		i: for (int i = 0; i < this.data.length; i++) {
			if (!this.loaded[i]) continue i;
			
			s.getDatabaseConnection().storeOutput(this.toa[i], this.data[i], this.uri, i);
			tot++;
		}
		
		if (DistributedResource.verbose)
			System.out.println("DistributedResource (" + this.uri +
								"): Commited " + tot + " chunks to local DB");
	}
	
	protected void commitToLocalDB(int index) {
		this.commitToLocalDB(this.toa[index], this.data[index], index);
	}
	
	protected void commitToLocalDB(long toa, byte data[], int index) {
		OutputServer s = OutputServer.getCurrentServer();
		
		if (DistributedResource.verbose)
			System.out.println("DistributedResource (" + this.uri +
					"): Deleting " + index + " from local DB...");
		
		s.getDatabaseConnection().deleteIndex(uri, index);
		
		if (DistributedResource.verbose)
			System.out.println("DistributedResource (" + this.uri +
								"): Commiting " + index + " to local DB...");
		
		s.getDatabaseConnection().storeOutput(toa, data, this.uri, index);
	}
	
	public synchronized void loadFromStream(InputStream in) throws IOException {
		this.clearCache();
		
		if (DistributedResource.ioVerbose) {
			System.out.println("DistributedResource (" + this.uri +
								"): Loading from stream...");
			if (this.data != null)
				System.out.println("DistributedResource (" + this.uri +
									"): Current data will be flushed.");
		}
		
		int t = Integer.MAX_VALUE;
		
		this.data = null;
		List l = null;
		
		if (this.data == null) {
			l = new ArrayList();
		} else {
			t = this.loaded.length;
		}
		
		int r = -1;
		byte latest[] = new byte[0];
		
		long lastToa = 0;
		
		i: for (int i = 0; i < t; i++) {
			if (l == null)
				this.data[i] = new byte[this.chunkSize];
			else
				latest = new byte[this.chunkSize];
			
			long toa = System.currentTimeMillis();
			if (i > 0 && toa == lastToa) toa = lastToa + 1;
			lastToa = toa;
			
			for (int j = 0; j < this.chunkSize; j++) {
				r = in.read();
				
				if (DistributedResource.ioVerbose && j == 0 && i == 0)
					System.out.println("DistributedResource (" + this.uri +
										"): Read first byte from in stream.");
				
				if (r < 0) {
					if (j == 0 && i == 0) return;
					
					byte d[];
					
					if (l == null) {
						d = this.data[i];
						this.data[i] = new byte[j];
						System.arraycopy(d, 0, this.data[i], 0, j);
					} else {
						d = latest;
						latest = new byte[j];
						System.arraycopy(d, 0, latest, 0, j);
						
						if (DistributedResource.ioVerbose)
							System.out.println("DistributedResource (" + this.uri +
												"): Loaded " + (latest.length / 1000.0) +
												" kilobytes from stream.");
						l.add(latest);
					}
					
					if (!this.commitAtEnd) this.commitToLocalDB(toa, latest, i);
					break i;
				}
				
				if (l == null)
					this.data[i][j] = (byte) r;
				else
					latest[j] = (byte) r;
			}
			
			if (l == null) {
				this.loaded[i] = true;
				this.toa[i] = toa;
			} else {
				if (DistributedResource.ioVerbose)
					System.out.println("DistributedResource: Loaded " +
										(latest.length / 1000.0) +
										" kilobytes from stream.");
				l.add(latest);
			}
			
			if (!this.commitAtEnd) this.commitToLocalDB(toa, latest, i);
		}
		
		if (l != null) {
			long s = l.size();
			if (s != 0)
				s = (s - 1) * chunkSize + ((byte[])l.get((int) s - 1)).length;
			this.tot = s;
			this.size = (int) (s / this.chunkSize);
			if (s % chunkSize != 0) this.size++;
			this.loaded = new boolean[this.size];
			this.toa = new long[this.size];
			this.data = new byte[this.size][0];
			
			if (this.verbose) {
				System.out.println("DistributedResource: tot = " + this.tot);
				System.out.println("DistributedResource: size = " + this.size);
			}
			
			Iterator itr = l.iterator();
			
			byte b[];
			
			for (int i = 0; itr.hasNext(); i++) {
				b = (byte[]) itr.next();
				
				this.data[i] = b;
				this.loaded[i] = true;
				this.toa[i] = System.currentTimeMillis();
			}
		}
		
		this.checkCache();
		
		if (this.commitAtEnd) this.commitToLocalDB();
	}
	
	public InputStream getInputStream() {
		InputStream in = new InputStream() {
			long total;
			int chunk, index;
			byte b[];
			
			public int read() throws IOException {
				if (chunk == 0 && this.index == 0 && DistributedResource.ioVerbose)
					System.out.println("DistributedResource (" +
										DistributedResource.this.uri +
										"): Begin read");
				
				if (chunk >= DistributedResource.this.size &&
						DistributedResource.this.size > 0) {
					return this.eof();
				}
				
				if (index >= DistributedResource.this.chunkSize) {
					if (DistributedResource.ioVerbose)
						System.out.println("DistributedResource (" +
											DistributedResource.this.uri +
											"): Reached end of chunk " + this.chunk);
					
					this.b = null;
					this.chunk++;
				}
				
				if (this.b == null) {
					this.b = DistributedResource.this.getData(chunk, true);
					if (this.b == null) this.eof();
					
					if (DistributedResource.ioVerbose) {
						System.out.println("DistributedResource (" +
										DistributedResource.this.uri +
										"): Input stream buffered " +
										(this.b.length / 1000.0) + " kilobytes.");
					}
					
					this.index = 0;
				}
				
				if (this.index >= this.b.length) return this.eof();
				
				int i = this.b[this.index++];
				if (i < 0) i = 256 + i;
				
				this.total++;
				
				return i;
			}
			
			public int eof() {
				if (DistributedResource.ioVerbose) {
					System.out.println("DistributedResource (" +
									DistributedResource.this.uri +
									"): EOF after " +
									(this.total / 1000.0) + " kilobytes.");
				}
				
				return -1;
			}
			
			public void finalize() {
				if (DistributedResource.ioVerbose) {
					System.out.println("DistributedResource (" +
									DistributedResource.this.uri +
									"): Finalizing IO stream after " +
									(this.total / 1000.0) + " kilobytes.");
				}
			}
		};
		
		return in;
	}
	
	public void setURI(String uri) { this.uri = uri; }
	public String getURI() { return this.uri; }
	
	public synchronized void load(IOStreams io) throws IOException {
		this.clearCache();
		
		int s = io.in.readInt();
		
		if (this.verbose)
			System.out.println("DistributedResource.load: " + s + " chunks to load.");
		
		byte b[][] = new byte[0][0];
		
		if (this.data.length < s) {
			b = this.data;
			this.data = null;
			
			if (this.verbose)
				System.out.println("DistributedResource.load: Existing data was incomplete.");
		}
		
		if (this.data == null) {
			this.data = new byte[s][this.chunkSize];
			this.toa = new long[s];
			this.loaded = new boolean[s];
		}
		
		this.tot = 0;
		
		for (int i = 0; i < b.length; i++) {
			this.setData(i, b[i]);
			if (b[i] != null) this.tot += b[i].length;
		}
		
		i: for (int i = 0; i < this.data.length; i++) {
			if (this.loaded[i]) continue i;
			
			io.out.writeInt(i);
			int si = io.in.readInt();
			
			if (si < 0) {
				System.out.println("DistributedResource: Unable to load chunk " + i);
				continue i;
			}
			
			this.data[i] = new byte[this.chunkSize];
			this.toa[i] = System.currentTimeMillis();
			
			for (int j = 0; j < si; j++)
				this.data[i][j] = io.in.readByte();
			
			this.loaded[i] = true;
			this.tot += si;
			
			if (!this.commitAtEnd) this.commitToLocalDB(i);
			
			if (this.verbose)
				System.out.println("DistributedResource.load: Loaded chunk " + i);
		}
		
		this.size = this.data.length;
		
		io.out.writeInt(-1);
		
		if (this.verbose)
			System.out.println("DistributedResource.load: Sent end.");
		
		if (commitAtEnd) this.commitToLocalDB();
		
		this.checkCache();
	}
	
	public synchronized void send(IOStreams io) throws IOException {
		if (this.data == null) this.getData(0, true);
		
		if (this.verbose)
			System.out.println("DistributedResource.send: " + this.data.length + " chunks.");
		
		io.out.writeInt(this.data.length);
		
		i: while (true) {
			int i = io.in.readInt();
			if (i < 0) break i;
			
			if (this.loaded[i]) {
				io.out.writeInt(this.data[i].length);
			} else {
				byte b[] = this.getData(i, true);
				
				if (b == null) {
					io.out.writeInt(-1);
					continue i;
				} else {
					io.out.writeInt(this.data[i].length);
				}
			}
			
			for (int j = 0; j < this.data[i].length; j++)
				io.out.writeByte(this.data[i][j]);
			
			if (this.verbose)
				System.out.println("DistributedResource.send: Sent chunk " + i);
		}
		
		if (this.verbose)
			System.out.println("DistributedResource.send: Recieved end.");
	}
	
	public void loadFromURI() throws IOException {
		throw new IOException("Tried to load DistributedResource from URI -- " + this.uri);
	}

	public void saveLocal(String file) throws IOException {
		throw new IOException("Tried to store DistributedResource to file -- " + this.uri);
	}
}
