/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.pieShare.pieShareApp.service.shareService;

import org.pieShare.pieShareApp.model.pieFile.FileMeta;
import com.turn.ttorrent.common.Torrent;
import com.turn.ttorrent.tracker.TrackedTorrent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import org.pieShare.pieShareApp.model.PieShareAppBeanNames;
import org.pieShare.pieShareApp.model.pieFile.PieFile;
import org.pieShare.pieShareApp.service.networkService.INetworkService;
import org.pieShare.pieShareApp.task.localTasks.TorrentTask;
import org.pieShare.pieTools.pieUtilities.service.base64Service.api.IBase64Service;
import org.pieShare.pieTools.pieUtilities.service.beanService.IBeanService;
import org.pieShare.pieTools.pieUtilities.service.pieExecutorService.api.IExecutorService;
import org.pieShare.pieTools.pieUtilities.service.pieLogger.PieLogger;

/**
 *
 * @author Svetoslav
 */
public class BitTorrentService implements IBitTorrentService {

	private INetworkService networkService;
	private IBeanService beanService;
	private IExecutorService executorService;
	private IBase64Service base64Service;

	private Semaphore readPorts;
	private Semaphore writePorts;
	//for the time being it is neccessary to work with the FileMeta and not the PieFile due to the fact
	//that there can be multiple unrelated trackers present
	private ConcurrentHashMap<FileMeta, Integer> sharedFiles;
	private ConcurrentHashMap<File, byte[]> cachedMetaInformation;

	public BitTorrentService() {
		this.sharedFiles = new ConcurrentHashMap<>();
	}

	public void setNetworkService(INetworkService networkService) {
		this.networkService = networkService;
	}

	public void setBeanService(IBeanService beanService) {
		this.beanService = beanService;
	}

	public void setExecutorService(IExecutorService executorService) {
		this.executorService = executorService;
	}

	public void setBase64Service(IBase64Service base64Service) {
		this.base64Service = base64Service;
	}

	/**
	 * If this FileMeta allready exists the value will be changed by the given
	 * value. If not it will be created with the given value. It returns true if
	 * the FileMeta was new and false otherwise.
	 *
	 * @param file
	 * @param value
	 * @return
	 */
	private boolean manipulateShareState(FileMeta file, Integer value) {
		synchronized (this.sharedFiles) {
			boolean isNew = true;

			PieLogger.trace(this.getClass(), "Manipulating share state for {} with HashCode {}.", file.getFile().getFileName(), file.hashCode());

			if (this.sharedFiles.containsKey(file)) {
				PieLogger.trace(this.getClass(), "Share state for {} exists.", file.getFile().getFileName());
				value = this.sharedFiles.get(file) + value;

				if (value <= 0) {
					this.sharedFiles.remove(file);
					return false;
				}

				isNew = false;
			}

			if (value >= 0) {
				this.sharedFiles.put(file, value);
			}

			return isNew;
		}
	}

	@Override
	public void initTorrentService() {
		//this section inits the semaphores
		int availablePorts = this.networkService.getNumberOfAvailablePorts(6881, 6889);
		if (availablePorts == 0) {
			//todo: handle this
			PieLogger.error(this.getClass(), "NO PORTS AVAILABLE ON THIS MACHINE!!!");
		}
		this.writePorts = new Semaphore(availablePorts);
		this.readPorts = new Semaphore((availablePorts / 2) - 1);
	}

	@Override
	public byte[] createMetaInformation(File localFile) throws CouldNotCreateMetaDataException {
		try {

			//todo: in future we need to synchronize on the file context so requests for different files can work in parallel
			synchronized (this.cachedMetaInformation) {
				if (this.cachedMetaInformation.containsKey(localFile)) {
					return this.cachedMetaInformation.get(localFile);
				}

				int port = -1;
				synchronized (this) {
					port = this.networkService.reserveAvailablePortStartingFrom(6969);
				}
				URI trackerUri = new URI("http://" + networkService.getLocalHost().getHostAddress() + ":" + String.valueOf(port) + "/announce");
			//todo: error handling when torrent null
				//todo: replace name by nodeName
				Torrent torrent = Torrent.create(localFile, trackerUri, "replaceThisByNodeName");

				ByteArrayOutputStream baos = new ByteArrayOutputStream();
				torrent.save(baos);
				byte[] meta = this.base64Service.encode(baos.toByteArray());
				this.cachedMetaInformation.put(localFile, meta);
				return meta;
			}
		} catch (InterruptedException | IOException | URISyntaxException ex) {
			throw new CouldNotCreateMetaDataException(ex);
		}
	}

	@Override
	public void shareFile(FileMeta meta) {

		//this is important in case we are already sharing that file!
		if (!this.manipulateShareState(meta, 1)) {
			PieLogger.trace(this.getClass(), "Allready sharing file {}!", meta.getFile().getFileName());
			return;
		}

		try {
			this.writePorts.acquire();
			this.handleSharedTorrent(meta, true);
		} catch (InterruptedException ex) {
			PieLogger.error(this.getClass(), "Acquire write failed!", ex);
		} catch (IOException ex) {
			PieLogger.error(this.getClass(), "Init torrent failed!", ex);
			this.torrentClientDone(true, meta);
		}
	}

	@Override
	public void clientDone(FileMeta meta) {
		this.manipulateShareState(meta, -1);
	}

	@Override
	public void handleFile(FileMeta meta) {
		//this is important in case we are already sharing that file!
		if (!this.manipulateShareState(meta, 1)) {
			PieLogger.trace(this.getClass(), "Allready handling file {}!", meta.getFile().getFileName());
			return;
		}

		try {
			this.readPorts.acquire();
			this.writePorts.acquire();
			this.handleSharedTorrent(meta, false);
		} catch (InterruptedException ex) {
			PieLogger.error(this.getClass(), "Acquire read failed!", ex);
		} catch (IOException ex) {
			PieLogger.error(this.getClass(), "Init torrent failed!", ex);
			this.torrentClientDone(false, meta);
		}
	}

	private void handleSharedTorrent(FileMeta meta, boolean seeder) throws IOException {
		Torrent torrent = new Torrent(base64Service.decode(meta.getData()), seeder);
		TorrentTask task = this.beanService.getBean(PieShareAppBeanNames.getTorrentTask());
		task.setFile(meta);
		task.setTorrent(torrent);
		this.executorService.execute(task);
	}

	@Override
	public void torrentClientDone(boolean seeder, FileMeta file) {
		this.writePorts.release();
		if (!seeder) {
			this.readPorts.release();
		}

		synchronized (this) {
			this.sharedFiles.remove(file);
		}
	}

	@Override
	public boolean isShareActive(FileMeta file) {
		return (this.sharedFiles.getOrDefault(file, 0) > 0);
	}

	public boolean activeTorrents() {
		return !this.sharedFiles.isEmpty();
	}
}

@@ -1857,16 +1857,74 @@
 ap%3C%3E();%0A
+%09%09this.cachedMetaInformation = new ConcurrentHashMap%3C%3E();%0A
 %09%7D%0A%0A%09pub

fix fox