/* 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.clients.fcp;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;

import freenet.client.HighLevelSimpleClientImpl;
import freenet.client.InsertContext;
import freenet.client.async.PersistenceDisabledException;
import freenet.clients.fcp.ClientPutBase.UploadFrom;
import freenet.clients.fcp.ClientRequest.Persistence;
import freenet.keys.FreenetURI;
import freenet.node.Node;
import freenet.node.RequestStarter;
import freenet.support.HexUtil;
import freenet.support.Logger;
import freenet.support.SimpleFieldSet;
import freenet.support.api.BucketFactory;
import freenet.support.api.RandomAccessBucket;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;
import freenet.support.compress.InvalidCompressionCodecException;
import freenet.support.io.FileBucket;

/**
 * 
 * ClientPut
 * URI=CHK@ // could as easily be an insertable SSK URI
 * Metadata.ContentType=text/html
 * Identifier=Insert-1 // identifier, as always
 * Verbosity=0 // just report when complete
 * MaxRetries=999999 // lots of retries
 * PriorityClass=1 // FProxy priority level
 * 
 * UploadFrom=direct // attached directly to this message
 * DataLength=100 // 100kB
 * or
 * UploadFrom=disk // upload a file from disk
 * Filename=/home/toad/something.html
 * FileHash=021349568329403123
 * Data
 * 
 * Neither IgnoreDS nor DSOnly make sense for inserts.
 */
public class ClientPutMessage extends DataCarryingMessage {

	public final static String NAME = "ClientPut";
	
	final FreenetURI uri;
	final String contentType;
	final long dataLength;
	final String identifier;
	final int verbosity;
	final int maxRetries;
	final boolean getCHKOnly;
	final short priorityClass;
	final Persistence persistence;
	final UploadFrom uploadFromType;
	/** The hash of the file you want the node to deal with.
	 *  it is MANDATORY to do DDA operations and should be computed like that:
	 *  
	 *  Base64Encode(SHA256( Handler.connectionIdentifer + ClientPutMessage.identifier + content)) 
	 */
	final String fileHash;
	final boolean dontCompress;
	final String clientToken;
	final File origFilename;
	final boolean global;
	final FreenetURI redirectTarget;
	/** Filename (hint for the final filename) */
	final String targetFilename;
	final boolean earlyEncode;
	final boolean binaryBlob;
	final boolean canWriteClientCache;
	final String compressorDescriptor;
	final boolean forkOnCacheable;
	final int extraInsertsSingleBlock;
	final int extraInsertsSplitfileHeaderBlock;
	final InsertContext.CompatibilityMode compatibilityMode;
	final byte[] overrideSplitfileCryptoKey;
	final boolean localRequestOnly;
	final boolean realTimeFlag;
	final long metadataThreshold;
	final boolean ignoreUSKDatehints;
	
	public ClientPutMessage(SimpleFieldSet fs) throws MessageInvalidException {
		String fnam = null;
		identifier = fs.get("Identifier");
		binaryBlob = fs.getBoolean("BinaryBlob", false);
		global = fs.getBoolean("Global", false);
		localRequestOnly = fs.getBoolean("LocalRequestOnly", false);
		String s = fs.get("CompatibilityMode");
		InsertContext.CompatibilityMode cmode = null;
		if(s == null)
			cmode = InsertContext.CompatibilityMode.COMPAT_DEFAULT;
		else {
			try {
				cmode = InsertContext.CompatibilityMode.valueOf(s);
			} catch (IllegalArgumentException e) {
				try {
					cmode = InsertContext.CompatibilityMode.values()[Integer.parseInt(s)];
				} catch (NumberFormatException e1) {
					throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid CompatibilityMode (not a name and not a number)", identifier, global);
				} catch (ArrayIndexOutOfBoundsException e1) {
					throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid CompatibilityMode (not a valid number)", identifier, global);
				}
			}
		}
		compatibilityMode = cmode.intern();
		s = fs.get("OverrideSplitfileCryptoKey");
		if(s == null)
			overrideSplitfileCryptoKey = null;
		else
			try {
				overrideSplitfileCryptoKey = HexUtil.hexToBytes(s);
			} catch (NumberFormatException e1) {
				throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid splitfile crypto key (not hex)", identifier, global);
			} catch (IndexOutOfBoundsException e1) {
				throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid splitfile crypto key (too short)", identifier, global);
			}
		if(identifier == null)
			throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "No Identifier", null, global);
		try {
			if(binaryBlob)
				uri = new FreenetURI("CHK@");
			else {
				String u = fs.get("URI");
				if(u == null)
					throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "No URI", identifier, global);
				FreenetURI uu = new FreenetURI(u);
				String[] metas = uu.getAllMetaStrings();
				if(metas != null && metas.length == 1) {
					fnam = metas[0];
					uu = uu.setMetaString(null);
				} // if >1, will fail later
				uri = uu;
			}
		} catch (MalformedURLException e) {
			throw new MessageInvalidException(ProtocolErrorMessage.FREENET_URI_PARSE_ERROR, e.getMessage(), identifier, global);
		}
		String verbosityString = fs.get("Verbosity");
		if(verbosityString == null)
			verbosity = 0;
		else {
			try {
				verbosity = Integer.parseInt(verbosityString, 10);
			} catch (NumberFormatException e) {
				throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing Verbosity field: "+e.getMessage(), identifier, global);
			}
		}
		contentType = fs.get("Metadata.ContentType");
		String maxRetriesString = fs.get("MaxRetries");
		if(maxRetriesString == null)
			// default to 0
			maxRetries = 0;
		else {
			try {
				maxRetries = Integer.parseInt(maxRetriesString, 10);
			} catch (NumberFormatException e) {
				throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing MaxSize field: "+e.getMessage(), identifier, global);
			}
		}
		getCHKOnly = fs.getBoolean("GetCHKOnly", false);
		String priorityString = fs.get("PriorityClass");
		if(priorityString == null) {
			// defaults to the one just below FProxy
			priorityClass = RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS;
		} else {
			try {
				priorityClass = Short.parseShort(priorityString);
				if(!RequestStarter.isValidPriorityClass(priorityClass))
					throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid priority class "+priorityClass+" - range is "+RequestStarter.PAUSED_PRIORITY_CLASS+" to "+RequestStarter.MAXIMUM_PRIORITY_CLASS, identifier, global);
			} catch (NumberFormatException e) {
				throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing PriorityClass field: "+e.getMessage(), identifier, global);
			}
		}
		// We do *NOT* check that FileHash is valid here for backward compatibility... and to make the override work
		this.fileHash = fs.get(ClientPutBase.FILE_HASH);
		String uploadFrom = fs.get("UploadFrom");
		if((uploadFrom == null) || uploadFrom.equalsIgnoreCase("direct")) {
			uploadFromType = UploadFrom.DIRECT;
			String dataLengthString = fs.get("DataLength");
			if(dataLengthString == null)
				throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "Need DataLength on a ClientPut", identifier, global);
			try {
				dataLength = Long.parseLong(dataLengthString, 10);
			} catch (NumberFormatException e) {
				throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing DataLength field: "+e.getMessage(), identifier, global);
			}
			this.origFilename = null;
			redirectTarget = null;
		} else if(uploadFrom.equalsIgnoreCase("disk")) {
			uploadFromType = UploadFrom.DISK;
			String filename = fs.get("Filename");
			if(filename == null)
				throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "Missing field Filename", identifier, global);
			File f = new File(filename);
			if(!(f.exists() && f.isFile() && f.canRead()))
				throw new MessageInvalidException(ProtocolErrorMessage.FILE_NOT_FOUND, null, identifier, global);
			dataLength = f.length();
			FileBucket fileBucket = new FileBucket(f, true, false, false, false);
			this.bucket = fileBucket;
			this.origFilename = f;
			redirectTarget = null;
			if(fnam == null)
				fnam = origFilename.getName();
		} else if(uploadFrom.equalsIgnoreCase("redirect")) {
			uploadFromType = UploadFrom.REDIRECT;
			String target = fs.get("TargetURI");
			if(target == null)
				throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "TargetURI missing but UploadFrom=redirect", identifier, global);
			try {
				redirectTarget = new FreenetURI(target);
			} catch (MalformedURLException e) {
				throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid TargetURI: "+e, identifier, global);
			}
			dataLength = 0;
			origFilename = null;
			bucket = null;
		} else
			throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "UploadFrom invalid or unrecognized: "+uploadFrom, identifier, global);
		dontCompress = fs.getBoolean("DontCompress", false);
		String persistenceString = fs.get("Persistence");
		persistence = Persistence.parseOrThrow(persistenceString, identifier, global);
		canWriteClientCache = fs.getBoolean("WriteToClientCache", false);
		clientToken = fs.get("ClientToken");
		String f = fs.get("TargetFilename");
		if(f != null)
			fnam = f;
		if(fnam != null && fnam.indexOf('/') > -1) {
			throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "TargetFilename must not contain slashes", identifier, global);
		}
		if(fnam != null && fnam.length() == 0) {
			fnam = null; // Deliberate override to tell us not to create one.
		}
		if(uri.getRoutingKey() == null && !uri.isKSK())
			targetFilename = fnam;
		else
			targetFilename = null;
		earlyEncode = fs.getBoolean("EarlyEncode", false);
		String codecs = fs.get("Codecs");
		if (codecs != null) {
			COMPRESSOR_TYPE[] ca;
			try {
				ca = COMPRESSOR_TYPE.getCompressorsArrayNoDefault(codecs);
			} catch (InvalidCompressionCodecException e) {
				throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, e.getMessage(), identifier, global);
			}
			if (ca == null) 
				codecs = null;
		}
		compressorDescriptor = codecs;
		if(fs.get("ForkOnCacheable") != null)
			forkOnCacheable = fs.getBoolean("ForkOnCacheable", false);
		else
			forkOnCacheable = Node.FORK_ON_CACHEABLE_DEFAULT;
		extraInsertsSingleBlock = fs.getInt("ExtraInsertsSingleBlock", HighLevelSimpleClientImpl.EXTRA_INSERTS_SINGLE_BLOCK);
		extraInsertsSplitfileHeaderBlock = fs.getInt("ExtraInsertsSplitfileHeaderBlock", HighLevelSimpleClientImpl.EXTRA_INSERTS_SPLITFILE_HEADER);
		realTimeFlag = fs.getBoolean("RealTimeFlag", false);
		metadataThreshold = fs.getLong("MetadataThreshold", -1);
		ignoreUSKDatehints = fs.getBoolean("IgnoreUSKDatehints", false);
	}

	@Override
	public SimpleFieldSet getFieldSet() {
		SimpleFieldSet sfs = new SimpleFieldSet(true);
		sfs.putSingle("URI", uri.toString());
		sfs.putSingle("Identifier", identifier);
		sfs.put("Verbosity", verbosity);
		sfs.put("MaxRetries", maxRetries);
		sfs.putSingle("Metadata.ContentType", contentType);
		sfs.putSingle("ClientToken", clientToken);
		switch(uploadFromType) {
		case DIRECT:
            sfs.putSingle("UploadFrom", "direct");
            sfs.put("DataLength", dataLength);
		    break;
		case DISK:
            sfs.putSingle("UploadFrom", "disk");
            sfs.putSingle("Filename", origFilename.getAbsolutePath());
            sfs.put("DataLength", dataLength);
            break;
		case REDIRECT:
            sfs.putSingle("UploadFrom", "redirect");
            sfs.putSingle("TargetURI", redirectTarget.toString());
            break;
		}
		sfs.put("GetCHKOnly", getCHKOnly);
		sfs.put("PriorityClass", priorityClass);
		sfs.putSingle("Persistence", persistence.toString().toLowerCase());
		sfs.put("DontCompress", dontCompress);
		if (compressorDescriptor != null)
			sfs.putSingle("Codecs", compressorDescriptor);
		sfs.put("Global", global);
		sfs.put("BinaryBlob", binaryBlob);
		return sfs;
	}

	@Override
	public String getName() {
		return NAME;
	}

	@Override
	public void run(FCPConnectionHandler handler, Node node)
			throws MessageInvalidException {
		handler.startClientPut(this);
	}

	/**
	 * Get the length of the trailing field.
	 */
	@Override
	long dataLength() {
		if(uploadFromType == UploadFrom.DIRECT)
			return dataLength;
		else return -1;
	}

	@Override
	String getIdentifier() {
		return identifier;
	}

	@Override
	RandomAccessBucket createBucket(BucketFactory bf, long length, FCPServer server) throws IOException, PersistenceDisabledException {
		if(persistence == Persistence.FOREVER) {
		    if(server.core.killedDatabase()) throw new PersistenceDisabledException();
			return server.core.persistentTempBucketFactory.makeBucket(length);
		} else {
			return super.createBucket(bf, length, server);
		}
	}

	@Override
	boolean isGlobal() {
		return global;
	}

	public void freeData() {
		if(bucket == null) {
			if(dataLength() <= 0)
				return; // Okay.
			Logger.error(this, "bucket is null on "+this+" - freed twice?", new Exception("error"));
			return;
		}
		bucket.free();
	}

}
