package hh.dejsem.net;

import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.net.Uri;

import java.io.File;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

import hh.dejsem.K;
import hh.dejsem.Prefs;
import hh.dejsem.clipboard.HistList;
import hh.dejsem.fm.EntitySize;
import hh.dejsem.fm.NetTaskRef;
import hh.lib.D;

/**
 * operations on network SSL connection to server
 * <p>
 * ● třída je instanciována z instancí LongAsyncTask, které dědí AsyncTask
 *      t.j. kromě konstruktorů jsou všechny metody prováděny ve zvláštních vláknech, takže mohou provádět síťové operace
 * </br>
 * ● instance LongAsyncTask a potažmo NetConnSrv se tvoří pro každou přenosovou dávku,
 *      t.j. jako akce kontextového menu download/upload to server
 * </br>
 * ● přenos dávky lze násilně ukončit v dialogu ProgressMonitor spouštěném z notifikace na Notification Bar
 */
public class NetConnSrv extends NetConn {

	NetTaskRef netTaskRef;

	public NetConnSrv(D d) throws Exception { 									// cmd connection to server
		super(d);
		d.logPrefix += "[cmds]";
		port = K.netNode.portBase;
	}

	public NetConnSrv(D d, String logPrefix, NetTaskRef netTaskRef) throws Exception { 		// data or cmd connection to server
		super(d);
		d.logPrefix += logPrefix;
		this.netTaskRef = netTaskRef;
		port = netTaskRef.getArgs().getInt(K.PORT_KEY, 0);
		if(d.ll(3)) d.l(String.format("netConnSrv=%s, port=%d", this, port));
	}

	void connectServer() throws Exception {
		// connect to the server on either CMD or DATA connection
		if(netIO.isConnected()) return;
		try { connectSock(new InetSocketAddress(Prefs.host, port)); }
		catch (Exception e) { abend(false, "connecting to server", e); }
	}
	
	String pullClip(String fn) throws Exception {
		if(d.ll(3)) d.l("pulling clipboard, fn=" + fn);
		connectServer();
		putCmd(SrvCmd.PULLCLIP.cmd);
		putStr(fn);	
		String data = getStr();
		if(d.ll(3)) d.l("PULLCLIP finished");
		connClose();
		return data;
	}
	
	void pushClip(NetData payload) throws Exception {
		if(d.ll(3)) d.l("pushing clipboard...");
		connectServer();
		putCmd(SrvCmd.PUSHCLIP.cmd);
		if(payload.buf.remaining() == 0) {
			if(d.ll(4)) d.l("empty buffer");
			connClose(); return; }
		netIO.put(payload.buf);
		if(d.ll(3)) d.l("PUSHCLIP finished");
		connClose();
	}

	HistList pullHistList() throws Exception {
		d.l("+++ pullHistList()");
		HistList history = new HistList();
		history.entries = HistList.allocEntries();
		connectServer();
		if(d.ll(3)) d.l("pulling clipboard history");
		putCmd(SrvCmd.PULLHIST.cmd);
		while(true) {
			HistList.HistEntry he = new HistList.HistEntry();
			he.name = getStr();
			if(he.name != null && he.name.length() > 0) {
				he.text = getStr();
				he.text = he.text.replaceAll("\n", "|");
				he.size = getNum();
				he.date = getNum();
				if(d.ll(5)) d.l(
					"name=" + he.name + ", size=" + he.size + ", timestamp=" + he.date +
					", text=" + (he.text == null ? null : (he.text.length() < 20 ? he.text : he.text.substring(0, 20))));
				history.entries.put(he.name, he);
			}
			else break;
		}		
		connClose();
		if(d.ll(3)) d.l("PULLHIST finished");
		return history;
	}

	public void pushSrv() throws Exception, K.EntryNotFound {
		// from=full_path_entry, to=full_path_entry
		String to = netTaskRef.getArgs().getString(K.FILE_TO_KEY);
		String fn = null;
		try {
			connectServer();
			for(String from: netTaskRef.getArgs().getStringArray(K.FILES_FROM_KEY)) {
				fn = from;
				File f = new File(from);
				File dir = new File(to);
				if(!f.exists()) abend(false, from + " doesn't exist", null);
				if(d.ll(3)) d.l("pushing file...");
				putCmd(K.LongTaskAction.PUSHFILE.action);
				if(netTaskRef.go)
					pushFileToSrv(f, dir);      // recursivelly push file/dir object
				endOfData();                    // end of recurse
				if(d.ll(3))
					d.l(String.format("PUSHFILE finished, entry %s pushed to server %s", f.getPath(), dir.getPath()));
				if(!netTaskRef.go) break;
			}
			batchEnd();
			getNum();                           // wait until server receives all data
		}
		catch(Exception e) { throw new Exception(String.format("COPYing %s to server", fn), e); }
		finally { try { connClose(); } catch(Exception e) {} }
	}

	void pushFileToSrv(File from, File to) throws Exception, K.EntryNotFound {
		File target = new File(to, from.getName());
		EntitySize es = netTaskRef.getProgress().es;
		if(from.isDirectory()) {
			accountDir(es, from.getPath());    // account dir for transfer progress display
			if(from.listFiles() != null)
				for(File f: from.listFiles())
					if(netTaskRef.go) pushFileToSrv(f, target);     // recurse
			new FileInfo(d, from, target).put();	// put directory info once more to setup its orig timestamp
		}
		else {
			new FileInfo(d, from, target).put();
			fio = new FileIO(d, from, "r");
			netData = new NetData(d);
			while(netTaskRef.go && fio.get(netData.buf)) {
				netData.buf.flip();
				accountRec(es, netIO.put(netData.buf), from.getPath());    // account bytes for transfer progress display
				netData.buf.clear(); }
			accountFile(es, from.length());    // account file for transfer progress display
		}
	}

	/**
	 * Expose file located on server to HTTP.
	 *
	 * @param path the path to file on server
	 * @return the path segment of resulting URL
	 * @throws Exception the exception
	 */
	String expose(String path) throws Exception {
		if(d.ll(3)) d.l("exposing " + path);
		connectServer();
		putCmd(SrvCmd.EXPOSE.cmd);
		putStr(path);
		return getStr();    // get from server path segment of URL
	}

	/**
	 * Upload to server and expose on web-server.
	 *
	 * @throws Exception    the exception
	 */
	public String pushFileExpose() throws Exception {
		// from=full_path_entry
		String from = netTaskRef.getArgs().getString(K.FILES_FROM_KEY);
		try {
			File f = new File(from);
			if(!f.exists()) abend(false, from + " doesn't exist", null);
			if(d.ll(3)) d.l("exposing file...");
			connectServer();
			putCmd(K.LongTaskAction.PUSHFIEX.action);
			if(netTaskRef.go) pushFileToSrv(f, new File(""));      // recursivelly push file/dir object
			endOfData();        // end of recurse
			if(d.ll(3)) d.l("PUSHFIEX finished, receiving URI path");
			return getStr();    // return path segment of resulting URL
		}
		catch(Exception e) { throw new Exception(String.format("EXPOSing file %s on http-server", from), e); }
		finally { try { connClose(); } catch(Exception e) {} }
	}

	/**
	 * Upload to server and expose on web-server.
	 *
	 * @throws Exception     the exception
	 */
	public String pushStreamExpose() throws Exception {
		if(d.ll(3)) d.l("exposing stream...");
		Uri uri = (Uri)(netTaskRef.getArgs().getParcelable(K.STREAM_KEY));
		ContentResolver cr = d.getContext().getContentResolver();
		InputStream is = cr.openInputStream(uri);
		ReadableByteChannel ic = Channels.newChannel(is);
		ContentProviderClient cc = cr.acquireContentProviderClient(uri);
		Long size = cc.openAssetFile(uri, "r").getLength();
		String mimeType = cc.getType(uri);
		if(mimeType == null) mimeType = "";
		connectServer();
		putCmd(K.LongTaskAction.PUSHSTEX.action);
		NetChOps.putStr(d, mimeType, netIO);
		NetChOps.putNum(d, size, netIO);
		netData = new NetData(d);
		InChIO chio = new InChIO(d, ic);
		EntitySize es = netTaskRef.getProgress().es;
		while(netTaskRef.go && chio.get(netData.buf)) {
			netData.buf.flip();
			accountRec(es, netIO.put(netData.buf), uri.getPath());    // account for transfer progress display
			netData.buf.clear(); }
		accountFile(es, size);  // account for transfer progress display
		if(d.ll(3)) d.l("PUSHSTEX finished, receiving URI path");
		return getStr();        // return path segment of exposed URL
	}

	public void pullSrv() throws Exception {
		// from=full_path_entry, to=full_path_target_directory
		String to = netTaskRef.getArgs().getString(K.FILE_TO_KEY);
		if(to == null) return;
		if(d.ll(3)) d.l(String.format("pulling into %s/ ...", to));
		EntitySize es = netTaskRef.getProgress().es;
		connectServer();
		String fn = null;
		try {
			for(String from: netTaskRef.getArgs().getStringArray(K.FILES_FROM_KEY)) {
				fn = from;
				putCmd(K.LongTaskAction.PULLFILE.action);
				putStr(from);
				netData = new NetData(d);
				boolean found = false;
				while(netTaskRef.go && fileInfo.get()) {
					found = true;
					file = new File(to, fileInfo.fn);
					if(d.ll(4)) d.l(String.format(""));

					if(fileInfo.size < 0) {     // object is directory
						localFS.doCreateDir(to, fileInfo.fn);   // to - absolutní, fn - relativní k to
						if(!file.isDirectory())
							abend(false, "unable to create server dir " + to, null);
						accountDir(es, file.getPath());            // account dir for transfer progress display
					}

					else {                      // object is file
						if(file.exists())
							file.delete();      // FileIO bere file jako random access, tak proto delete
						fio = new FileIO(d, localFS.doCreateDir(file.getParent(), null), file, "rw");
						long rest = fileInfo.size;
						while(netTaskRef.go && rest > 0) {
							netData.buf.clear();
							if(rest < netData.buf.limit()) netData.buf.limit((int)rest);
							if(!netIO.get(netData.buf)) break;
							int transferred = netData.buf.position();
							rest -= transferred;
							netData.buf.flip();
							fio.put(netData.buf);
							accountRec(es, transferred, file.getPath());    // account bytes for transfer progress display
						}
						fio.close();
						accountFile(es, file.length());    // account file for transfer progress display
					}
					// there is a bug in setLastModified() - success is device dependent - see https://code.google.com/p/android/issues/detail?id=18624
					if(!file.setLastModified(fileInfo.timestamp * 1000))
						netTaskRef.failedTimeStamp = true;
					if(d.ll(4)) d.l(file.getPath() + " pulled, dir=" + (fileInfo.size == -1));
				}
				if(!found) {
					if(d.ll(3)) d.l("PULLFILE: entry " + from + " not found");
					throw new K.EntryNotFound();
				}
				if(d.ll(3)) d.l("PULLFILE finished");
				if(!netTaskRef.go) break;   // net task cancelled
			}
			batchEnd();
		}
		catch(Exception e) { throw new Exception(String.format("COPY %s from server", fn), e); }
		finally { try { connClose(); } catch(Exception e) {} }
	}

	void pullListSrv(String from, String to) throws Exception { 
		// from=full_path_dir, to=full_path_cache_file
		connectServer();
		if(d.ll(3)) d.l("pulling file list, cwdFl=" + from);
		putCmd(SrvCmd.PULLLIST.cmd);
		NetChOps.putStr(d, from, netIO);
		try {
			FileIO fio = new FileIO(d, new File(to), "rw");
			NetChOps.putStr(d, from, fio);							// cache cwdFl name
			while(fileInfo.get(netIO)) fileInfo.put(fio);	// cache cwdFl list
			fio.close(); }
		catch(Exception e) { abend(false, "put server dir list to cache", e); }
		if(d.ll(3)) d.l("PULLLIST finished");
	}

	int longtask() throws Exception {
		connectServer();
		putCmd(SrvCmd.LONGTASK.cmd);
		int port = (int)getNum();
		K.cmdConnClose(d);
		if(d.ll(3)) d.l("long running task scheduled on " + port + " port");
		return port;
	}

	void move(String from, String to) throws Exception, K.EntryNotFound {
		// from=full_path_entry, to=full_path_entry
		if(d.ll(3)) d.l("moving " + from + " to " + to);
		connectServer();
		putCmd(SrvCmd.MOVE.cmd);
		putStr(from);
		putStr(to);
		if(d.ll(3)) d.l("MOVE finished");
	}

	void hide(String path) throws Exception {
		if(d.ll(3)) d.l("hiding " + path);
		connectServer();
		putCmd(SrvCmd.HIDE.cmd);
		putStr(path);
	}

	/*String exposeUp(String path) throws Exception {     // ???
		if(d.ll(3)) d.l("exposing " + path);
		connectServer();
		putCmd(SrvCmd.EXPOSEUP.cmd);
		putStr(path);
		return getStr();
	}*/

	void createDir(String dirName) throws Exception { 
		// dirName=simple name
		if(d.ll(3)) d.l("creating directory " + dirName);
		connectServer();
		putCmd(SrvCmd.CREATEDIR.cmd);
		putStr(dirName);
		if(d.ll(3)) d.l("CREATEDIR finished");
	}

	void delete(String from) throws Exception { 
		// from=full_path_file
		if(d.ll(3)) d.l("deleting " + from);
		connectServer();
		putCmd(SrvCmd.DELETE.cmd);
		putStr(from);
		if(d.ll(3)) d.l("DELETE finished");
	}

	long getFreeSpace() throws Exception {
		if(d.ll(3)) d.l("asking for free space");
		connectServer();
		putCmd(SrvCmd.FREE.cmd);
		long free = getNum();
		if(d.ll(3)) d.l("FREE finished, got=" + free);
		return free;
	}

	EntitySize reckon(String fp) throws Exception { 
		// from=full_path_file
		if(d.ll(3)) d.l("reckoning " + fp);
		connectServer();
		putCmd(SrvCmd.RECKON.cmd);
		putStr(fp);
		return new EntitySize(getNum(), (int)getNum(), (int)getNum());
		/*EntitySize ds = new EntitySize(getNum(), (int)getNum(), (int)getNum());
		*//*ds.size = getNum();
		ds.fnum = (int)getNum();
		ds.dnum = (int)getNum();*//*
		return ds;*/
	}

	InetSocketAddress getPeerHost() throws Exception {
		// get addr of peer declaring itself as server side of peer connection
		String host = null;
		int port = 0;
		try {
			if(d.ll(3)) d.l("getting peer server");
			connectServer();
			putCmd(SrvCmd.GETPEER.cmd);
			host = getStr();
			port = (int)getNum();
			if(d.ll(3)) d.l("got peer server=" + host + ", port=" + port);
			K.cmdConnClose(d); }
		catch (Exception e) { abend(false, "get peer host", e); }
		if(host == null) abend(new K.NoPeerListening());
		return new InetSocketAddress(host, port);
	}

	void declarePeerHost(String host, int port) throws Exception {
		// declare itself as server side of peer connection
		if(d.ll(3)) d.l("declaring peer server on " + host + ":" + port);
		connectServer();
		putCmd(SrvCmd.SETPEER.cmd);
		putStr(host);
		putNum(port);
		connClose();
	}

	public void connClose() { try { netIO.closeSc(); } catch(Exception e) { d.abendMsg("closing", e); } }
}