/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.node;

import java.net.UnknownHostException;

import freenet.config.InvalidConfigValueException;
import freenet.config.NodeNeedRestartException;
import freenet.config.SubConfig;
import freenet.io.comm.FreenetInetAddress;
import freenet.node.SecurityLevels.NETWORK_THREAT_LEVEL;
import freenet.support.Logger;
import freenet.support.api.BooleanCallback;
import freenet.support.api.IntCallback;
import freenet.support.api.StringCallback;

/**
 * Tracks config parameters related to a NodeCrypto. The NodeCrypto may or may not exist. If it exists,
 * parameter changes are passed on to it, if it doesn't, they can be changed trivially.
 * 
 * Allows users to set the opennet port number while opennet is disabled, enable opennet on the fly etc.
 * @author toad
 */
public class NodeCryptoConfig {
	/** Port number. -1 = choose a random available port number at activation time. */
	private int portNumber;
	
	/** Bind address. 0.0.0.0 = all addresses. */
	private FreenetInetAddress bindTo;
	
	/** If nonzero, 1/dropProbability = probability of UdpSocketHandler dropping a packet (for debugging
	 * purposes; not static as we may need to simulate some nodes with more loss than others). */
	private int dropProbability;
	
	/** The NodeCrypto, if there is one */
	private NodeCrypto crypto;
	
	/** Whether we should prevent multiple connections to the same IP (taking into account other
	 * NodeCrypto's - this will usually be set for opennet but not for darknet). */
	private boolean oneConnectionPerAddress;
	
	/** If true, we will allow to connect to nodes via local (LAN etc) IP addresses,
	 * regardless of any per-peer setting. */
	private boolean alwaysAllowLocalAddresses;
	
	/** If true, assume we are NATed regardless of the evidence, and therefore always send
	 * aggressive handshakes (every 10-30 seconds). */
	private boolean assumeNATed;
	
	/** If true, include local addresses on noderefs */
	public boolean includeLocalAddressesInNoderefs;
	
	/** If false we won't make any effort do disguise the length of packets */
	private boolean paddDataPackets;
	
	NodeCryptoConfig(SubConfig config, int sortOrder, boolean isOpennet, SecurityLevels securityLevels) throws NodeInitException {
		config.register("listenPort", -1 /* means random */, sortOrder++, true, true,
				isOpennet ? "Node.opennetPort" : "Node.port", 
				isOpennet ? "Node.opennetPortLong" : "Node.portLong", 
						new IntCallback() {
			@Override
			public Integer get() {
				synchronized(NodeCryptoConfig.class) {
					if(crypto != null)
						portNumber = crypto.getPortNumber();
					return portNumber;
				}
			}
			@Override
			public void set(Integer val) throws InvalidConfigValueException {
				
				if(portNumber < -1 || portNumber == 0 || portNumber > 65535) {
					throw new InvalidConfigValueException("Invalid port number");
				}
				
				
				synchronized(NodeCryptoConfig.class) {
					if(portNumber == val) return;
					// FIXME implement on the fly listenPort changing
					// Note that this sort of thing should be the exception rather than the rule!!!!
					if(crypto != null)
						throw new InvalidConfigValueException("Switching listenPort on the fly not yet supported");
					portNumber = val;
				}
			}
			@Override
			public boolean isReadOnly() {
				        return true;
			        }		
		}, false);
		
		try{
			portNumber = config.getInt("listenPort");
		}catch (Exception e){
			// FIXME is this really necessary?
			Logger.error(this, "Caught "+e, e);
			System.err.println(e);
			e.printStackTrace();
			portNumber = -1;
		}
		
		config.register("bindTo", "0.0.0.0", sortOrder++, true, true, "Node.bindTo", "Node.bindToLong", new NodeBindtoCallback());
		
		try {
			bindTo = new FreenetInetAddress(config.getString("bindTo"), false);
			
		} catch (UnknownHostException e) {
			throw new NodeInitException(NodeInitException.EXIT_COULD_NOT_BIND_USM, "Invalid bindTo: "+config.getString("bindTo"));
		}
		
		config.register("testingDropPacketsEvery", 0, sortOrder++, true, false, "Node.dropPacketEvery", "Node.dropPacketEveryLong",
				new IntCallback() {

					@Override
					public Integer get() {
						synchronized(NodeCryptoConfig.this) {
							return dropProbability;
						}
					}

					@Override
					public void set(Integer val) throws InvalidConfigValueException {
						if(val < 0) throw new InvalidConfigValueException("testingDropPacketsEvery must not be negative");
						synchronized(NodeCryptoConfig.this) {
							if(val == dropProbability) return;
							dropProbability = val;
							if(crypto == null) return;
						}
						crypto.onSetDropProbability(val);
					}		
			
		}, false);
		dropProbability = config.getInt("testingDropPacketsEvery"); 
		
		config.register("oneConnectionPerIP", isOpennet, sortOrder++, true, false,
				(isOpennet ? "OpennetManager" : "Node") + ".oneConnectionPerIP",
				(isOpennet ? "OpennetManager" : "Node") + ".oneConnectionPerIPLong",
				new BooleanCallback() {

					@Override
					public Boolean get() {
						synchronized(NodeCryptoConfig.this) {
							return oneConnectionPerAddress;
						}
					}

					@Override
					public void set(Boolean val) throws InvalidConfigValueException {
						synchronized(NodeCryptoConfig.this) {
							oneConnectionPerAddress = val;
						}
					}
			
		});
		oneConnectionPerAddress = config.getBoolean("oneConnectionPerIP");
		
		if(isOpennet) {
			securityLevels.addNetworkThreatLevelListener(new SecurityLevelListener<NETWORK_THREAT_LEVEL>() {

				@Override
				public void onChange(NETWORK_THREAT_LEVEL oldLevel, NETWORK_THREAT_LEVEL newLevel) {
					// Might be useful for nodes on the same NAT etc, so turn it off for LOW. Otherwise is sensible.
					// It's always off on darknet, since we can reasonably expect to know our peers, even if we are paranoid
					// about them!
					if(newLevel == NETWORK_THREAT_LEVEL.LOW)
						oneConnectionPerAddress = false;
					if(oldLevel == NETWORK_THREAT_LEVEL.LOW)
						oneConnectionPerAddress = true;
				}
				
			});
		}
		
		config.register("alwaysAllowLocalAddresses", !isOpennet, sortOrder++, true, false, "Node.alwaysAllowLocalAddresses", "Node.alwaysAllowLocalAddressesLong",
				new BooleanCallback() {

					@Override
					public Boolean get() {
						synchronized(NodeCryptoConfig.this) {
							return alwaysAllowLocalAddresses;
						}
					}

					@Override
					public void set(Boolean val) throws InvalidConfigValueException {
						synchronized(NodeCryptoConfig.this) {
							alwaysAllowLocalAddresses = val;
						}
					}			
		});
		alwaysAllowLocalAddresses = config.getBoolean("alwaysAllowLocalAddresses");
		
		config.register("assumeNATed", true, sortOrder++, true, true, "Node.assumeNATed", "Node.assumeNATedLong", new BooleanCallback() {

			@Override
			public Boolean get() {
				return assumeNATed;
			}

			@Override
			public void set(Boolean val) throws InvalidConfigValueException {
				assumeNATed = val;
			}		
		});
		assumeNATed = config.getBoolean("assumeNATed");
		
		// Include local IPs in noderef file
		
		config.register("includeLocalAddressesInNoderefs", !isOpennet, sortOrder++, true, false, "NodeIPDectector.inclLocalAddress", "NodeIPDectector.inclLocalAddressLong", new BooleanCallback() {

			@Override
			public Boolean get() {
				return includeLocalAddressesInNoderefs;
			}

			@Override
			public void set(Boolean val) throws InvalidConfigValueException {
				includeLocalAddressesInNoderefs = val;
			}
		});
		
		includeLocalAddressesInNoderefs = config.getBoolean("includeLocalAddressesInNoderefs");
		
		// enable/disable Padding of outgoing packets (won't affect auth-packets)
		
		config.register("paddDataPackets", true, sortOrder++, true, false, "Node.paddDataPackets", "Node.paddDataPacketsLong", new BooleanCallback() {

			@Override
			public Boolean get() {
				return paddDataPackets;
			}

			@Override
			public void set(Boolean val) throws InvalidConfigValueException, NodeNeedRestartException {
				if (val.equals(get()))
					        return;
				paddDataPackets = val;
			}
		});
		
		paddDataPackets = config.getBoolean("paddDataPackets");
	}

	/** The number of config options i.e. the amount to increment sortOrder by */
	public static final int OPTION_COUNT = 3;
	
	synchronized void starting(NodeCrypto crypto2) {
		if(crypto != null) throw new IllegalStateException("Replacing existing NodeCrypto "+crypto+" with "+crypto2);
		crypto = crypto2;
	}
	
	synchronized void started(NodeCrypto crypto2) {
		if(crypto != null) throw new IllegalStateException("Replacing existing NodeCrypto "+crypto+" with "+crypto2);
	}
	
	synchronized void maybeStarted(NodeCrypto crypto2) {

	}
	
	synchronized void stopping(NodeCrypto crypto2) {
		crypto = null;
	}
	
	public synchronized int getPort() {
		return portNumber;
	}
	
	class NodeBindtoCallback extends StringCallback  {
		
		@Override
		public String get() {
			return bindTo.toString();
		}
		
		@Override
		public void set(String val) throws InvalidConfigValueException {
			if(val.equals(get())) return;
			// FIXME why not? Can't we use freenet.io.NetworkInterface like everywhere else, just adapt it for UDP?
			throw new InvalidConfigValueException("Cannot be updated on the fly");
		}
		@Override
		public boolean isReadOnly() {
			return true;
		}
	}

	public synchronized FreenetInetAddress getBindTo() {
		return bindTo;
	}

	public synchronized void setPort(int port) {
		portNumber = port;
	}

	public synchronized int getDropProbability() {
		return dropProbability;
	}

	public synchronized boolean oneConnectionPerAddress() {
		return oneConnectionPerAddress;
	}

	public synchronized boolean alwaysAllowLocalAddresses() {
		return alwaysAllowLocalAddresses;
	}

	public boolean alwaysHandshakeAggressively() {
		return assumeNATed;
	}
	
	public boolean includeLocalAddressesInNoderefs() {
		return includeLocalAddressesInNoderefs;
	}
	
	public boolean paddDataPackets() {
		return paddDataPackets;
	}
}
