package hh.dejsem.net;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;

import java.io.File;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;

import hh.dejsem.K;
import hh.dejsem.fm.EntitySize;
import hh.dejsem.fm.NetTaskRef;
import hh.lib.D;

/**
 * network SSL connection to peer
 * ● Net opers must not be performed from constructors as they are called from main process
 * ● all other methods are called from subprocess and may perform net opers
 */
public class NetConnPeer extends NetConn {
	String locHost = null;
	boolean peerServerSide = true;
	public InetSocketAddress peerIp;
	SendPeerHostThread sendPeerHostThread;
	NetTaskRef netTaskRef;
	// ----------------------------------------------------------
	
	public NetConnPeer(D d, NetTaskRef netTaskRef, boolean listen) throws Exception {
		super(d);
		this.netTaskRef = netTaskRef;
		peerServerSide = listen;
	}

	/**
	 * Broadcast via UDP addr:port as server side of peer connection.
	 *
	 * @param abendHandler the abend handler
	 * @throws SocketException      the socket exception
	 * @throws UnknownHostException the unknown host exception
	 */
	void sendPeerHost_UDP(Handler abendHandler) throws SocketException, UnknownHostException {
		if(d.ll(3)) d.l("advertising peer server side via UDP broadcast");
		String ipPortAdvert = String.format("%012d%s%012d%s%012d", K.UDP_ADVERT_KEY.length(), K.UDP_ADVERT_KEY, locHost.length(), locHost, port);
		sendPeerHostThread = new SendPeerHostThread(d, abendHandler, ipPortAdvert);
		sendPeerHostThread.start();
	}

	public void pushBatchSize(EntitySize targetSize) throws Exception {
		peerIp = new NetUDP(d).keepReceivingPeerIp();
		connectPeerAsClient();
		d.l(3, String.format("sending batchsize[%d]", targetSize.getTargetSize()));
		putAttrs(targetSize);
	}

	public void pushPeer(String from) throws Exception {
		// from=full_path_file, to=relative_path_entry
		connectPeerAsClient();
		File ffrom = new File(from);
		File fto = new File(ffrom.getName());
		pushFileToPeer(ffrom, fto);
		if(d.ll(3)) d.l("partial PUSHPEER finished");
	}

	void pushFileToPeer(File from, File to) throws Exception {
		if(d.ll(3)) d.l("pushing to peer, from=" + from.getPath() + ", to=" + to.getPath());
		/*final EntitySize es = netTaskRef.progress.transfSize;*/
		final EntitySize es = netTaskRef.getProgress().es;
		if(netTaskRef.go) putAttrs(from, to.getPath());
		if(from.isDirectory()) {
			accountDir(es, from.getPath());    // account dir for transfer progress display
			for(String fn: from.list()) if(netTaskRef.go) pushFileToPeer(new File(from, fn), new File(to, fn));
			if(netTaskRef.go) putAttrs(from, to.getPath());
		}
		else {
			netData = new NetData(d);
			fio = new FileIO(d, from, "r");
			while(netTaskRef.go && fio.get(netData.buf)) {
				netData.buf.flip();
				accountRec(es, netIO.put(netData.buf), from.getPath());    // send buf and account bytes for transfer progress display
				netData.buf.clear(); }
			accountFile(es, from.length());    // account file for transfer progress display
		}
	}

	public long pullBatchSize() throws Exception {
		// accept connection from peer and get size of batch which peer will send
		fileInfo.get();
		return fileInfo.size;
	}

	public void pullDataFromPeer(String to) throws Exception {
		// to=full_path_dir
		final EntitySize es = netTaskRef.getProgress().es;
		netData = new NetData(d);
		while(netTaskRef.go && fileInfo.get()) {
			if(d.ll(3)) d.l("pulling from peer, fn=" + fileInfo.fn + ", to=" + to);
			file = new File(to, fileInfo.fn);

			if(fileInfo.size < 0) {		// 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 {                      // file
				String relParent = file.getParent().replace(to, "");    // parent dir relative to "to"
				if(relParent.startsWith("/")) relParent = relParent.substring(1);
				fio = new FileIO(d, localFS.doCreateDir(to, relParent), 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
			}
			if(!file.setLastModified(fileInfo.timestamp * 1000)) netTaskRef.failedTimeStamp = true;
		}
		if(d.ll(2)) d.l("PULLPEER finished");
	}

	void getLocHost() throws Exception {
		// get local ipv4 addr to bind to
		locHost = null;
		if(isLAN())
			try {
				for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
				    NetworkInterface intf = en.nextElement();
				    for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
				        InetAddress inetAddress = enumIpAddr.nextElement();
				        if(inetAddress.isLoopbackAddress()) continue;
				        if(inetAddress.getClass().getSimpleName().equals("Inet6Address")) continue;
				        else { locHost = inetAddress.getHostAddress().toString(); break; } } }
			} catch (SocketException e) { abend(false, "network interfaces info", e); }
		if(d.ll(3)) d.l("local ip=" + locHost);
	}

	public void bindSockToPeer() throws Exception {
		connClose();
		if(locHost == null) getLocHost();

		for(port = K.netNode.portBase + 1; port < K.netNode.portBase + 3; port++) {
			if(d.ll(3)) d.l("binding to " + locHost + ":" + port);
			try {
				netIO.bind(locHost, port);
				//K.cmdConn.declarePeerHost(locHost, port); // atavismus - přešlo se na UDP
				break;
			} catch (Exception e) {
				if(e.getMessage().contains("Address already in use")) continue;
				else throw new Exception("bind SSL server socket", e);
			}
		}
		if(!netIO.isBound()) throw new Exception("all ports to peers are busy");
		else d.logPrefix += "[" + port + "]";
	}

	void acceptConnFromPeer(Handler abendHandler) throws Exception {
		if(isPeerConnected()) return;
		bindSockToPeer();
		sendPeerHost_UDP(abendHandler);
		if(d.ll(3)) d.l("accepting peer conn on " + locHost + ":" + port);
		try {
			netIO.accept();
			netIO.put(new NetData(d, "ACCEPTED").buf);
			if(d.ll(4)) d.l("connection acceptance conformed");
		}
		catch(Exception e) {
			boolean quiet = false;
			if(!netTaskRef.go) quiet = true;    // task byl explicitně zastaven
			abend(quiet, "peer server accept", e); }
		finally { sendPeerHostThread.stopAdv();  }   // stop server host:port advertising
	}

	public boolean isPeerConnected() throws Exception {
		if(!isLAN()) abend(false, "no LAN connection", null);
		return netIO.isConnected();
	}

	boolean isLAN() {
		// determine LAN connectivity for peer-to-peer operations
		NetworkInfo ni = ((ConnectivityManager)d.getContext().getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
		return ni != null && ni.isConnected();
	}

	void connectPeerAsClient() throws Exception {
		if(isPeerConnected()) return;
		connectSock(peerIp);
	}

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

	void bindUSB() {}
}