/* 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 freenet.crypt.CryptFormatException;
import freenet.crypt.DSAPublicKey;
import freenet.io.comm.ByteCounter;
import freenet.io.comm.DMT;
import freenet.io.comm.DisconnectedException;
import freenet.io.comm.Message;
import freenet.io.comm.MessageFilter;
import freenet.io.comm.NotConnectedException;
import freenet.io.comm.PeerRestartedException;
import freenet.io.xfer.WaitedTooLongException;
import freenet.keys.NodeSSK;
import freenet.keys.SSKBlock;
import freenet.keys.SSKVerifyException;
import freenet.store.KeyCollisionException;
import freenet.support.Logger;
import freenet.support.ShortBuffer;
import freenet.support.Logger.LogLevel;
import freenet.support.io.NativeThread;

/**
 * Handles an incoming SSK insert.
 * SSKs need their own insert/request classes, see comments in SSKInsertSender.
 */
public class SSKInsertHandler implements PrioRunnable, ByteCounter {

	private static boolean logMINOR;
	
    static final int DATA_INSERT_TIMEOUT = 30000;
    
    final Node node;
    final long uid;
    final PeerNode source;
    final NodeSSK key;
    final long startTime;
    private SSKBlock block;
    private DSAPublicKey pubKey;
    private short htl;
    private SSKInsertSender sender;
    private byte[] data;
    private byte[] headers;
    private boolean canCommit;
    final InsertTag tag;
    private final boolean canWriteDatastore;
	private final boolean forkOnCacheable;
	private final boolean preferInsert;
	private final boolean ignoreLowBackoff;
	private final boolean realTimeFlag;

	private boolean collided = false;
    
    SSKInsertHandler(NodeSSK key, byte[] data, byte[] headers, short htl, PeerNode source, long id, Node node, long startTime, InsertTag tag, boolean canWriteDatastore, boolean forkOnCacheable, boolean preferInsert, boolean ignoreLowBackoff, boolean realTimeFlag) {
        this.node = node;
        this.uid = id;
        this.source = source;
        this.startTime = startTime;
        this.key = key;
        this.htl = htl;
        this.data = data;
        this.headers = headers;
        this.tag = tag;
        this.canWriteDatastore = canWriteDatastore;
        byte[] pubKeyHash = key.getPubKeyHash();
        pubKey = node.getGetPubKey().getKey(pubKeyHash, false, false, null);
        canCommit = false;
        logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
        this.forkOnCacheable = forkOnCacheable;
        this.preferInsert = preferInsert;
        this.ignoreLowBackoff = ignoreLowBackoff;
        this.realTimeFlag = realTimeFlag;
    }
    
    @Override
	public String toString() {
        return super.toString()+" for "+uid;
    }
    
    @Override
    public void run() {
        try {
            realRun();
        } catch (Throwable t) {
            Logger.error(this, "Caught "+t, t);
        } finally {
            if(logMINOR) Logger.minor(this, "Exiting InsertHandler.run() for "+uid);
            tag.unlockHandler();
        }
    }

    private void realRun() {
        // Send Accepted
        Message accepted = DMT.createFNPSSKAccepted(uid, pubKey == null);
        
        try {
			source.sendAsync(accepted, null, this);
		} catch (NotConnectedException e1) {
			if(logMINOR) Logger.minor(this, "Lost connection to source");
			return;
		}
		
		if(tag.shouldSlowDown()) {
			try {
				source.sendAsync(DMT.createFNPRejectedOverload(uid, false, false, realTimeFlag), null, this);
			} catch (NotConnectedException e) {
				// Ignore.
			}
		}
		
		while(headers == null || data == null || pubKey == null) {
			MessageFilter mfDataInsertRejected = MessageFilter.create().setType(DMT.FNPDataInsertRejected).setField(DMT.UID, uid).setSource(source).setTimeout(DATA_INSERT_TIMEOUT);
			MessageFilter mf = mfDataInsertRejected;
			if(headers == null) {
				MessageFilter m = MessageFilter.create().setType(DMT.FNPSSKInsertRequestHeaders).setField(DMT.UID, uid).setSource(source).setTimeout(DATA_INSERT_TIMEOUT);
				mf = m.or(mf);
			}
			if(data == null) {
				MessageFilter m = MessageFilter.create().setType(DMT.FNPSSKInsertRequestData).setField(DMT.UID, uid).setSource(source).setTimeout(DATA_INSERT_TIMEOUT);
				mf = m.or(mf);
			}
			if(pubKey == null) {
				MessageFilter m = MessageFilter.create().setType(DMT.FNPSSKPubKey).setField(DMT.UID, uid).setSource(source).setTimeout(DATA_INSERT_TIMEOUT);
				mf = m.or(mf);
			}
			Message msg;
			try {
				msg = node.getUSM().waitFor(mf, this);
			} catch (DisconnectedException e) {
				if(logMINOR) Logger.minor(this, "Lost connection to source on "+uid);
				return;
			}
			if(msg == null) {
				Logger.normal(this, "Failed to receive all parts (data="+(data==null?"null":"ok")+" headers="+(headers==null?"null":"ok")+" pk="+pubKey+") for "+uid);
				Message failed = DMT.createFNPDataInsertRejected(uid, DMT.DATA_INSERT_REJECTED_RECEIVE_FAILED);
				try {
					source.sendSync(failed, this, realTimeFlag);
				} catch (NotConnectedException e) {
					// Ignore
				} catch (SyncSendWaitedTooLongException e) {
					// Ignore
				}
				return;
			} else if(msg.getSpec() == DMT.FNPSSKInsertRequestHeaders) {
				headers = ((ShortBuffer)msg.getObject(DMT.BLOCK_HEADERS)).getData();
			} else if(msg.getSpec() == DMT.FNPSSKInsertRequestData) {
				data = ((ShortBuffer)msg.getObject(DMT.DATA)).getData();
			} else if(msg.getSpec() == DMT.FNPSSKPubKey) {
				byte[] pubkeyAsBytes = ((ShortBuffer)msg.getObject(DMT.PUBKEY_AS_BYTES)).getData();
				try {
					pubKey = DSAPublicKey.create(pubkeyAsBytes);
					if(logMINOR) Logger.minor(this, "Got pubkey on "+uid+" : "+pubKey);
					Message confirm = DMT.createFNPSSKPubKeyAccepted(uid);
					try {
						source.sendAsync(confirm, null, this);
					} catch (NotConnectedException e) {
						if(logMINOR) Logger.minor(this, "Lost connection to source on "+uid);
						return;
					}
				} catch (CryptFormatException e) {
					Logger.error(this, "Invalid pubkey from "+source+" on "+uid);
					msg = DMT.createFNPDataInsertRejected(uid, DMT.DATA_INSERT_REJECTED_SSK_ERROR);
					try {
						source.sendSync(msg, this, realTimeFlag);
					} catch (NotConnectedException ee) {
						// Ignore
					} catch (SyncSendWaitedTooLongException ee) {
						// Ignore
					}
					return;
				}
			} else if(msg.getSpec() == DMT.FNPDataInsertRejected) {
	        	try {
					source.sendAsync(DMT.createFNPDataInsertRejected(uid, msg.getShort(DMT.DATA_INSERT_REJECTED_REASON)), null, this);
				} catch (NotConnectedException e) {
					// Ignore.
				}
				return;
			} else {
				Logger.error(this, "Unexpected message? "+msg+" on "+this);
			}
		}
		
		try {
			key.setPubKey(pubKey);
			block = new SSKBlock(data, headers, key, false);
		} catch (SSKVerifyException e1) {
			Logger.error(this, "Invalid SSK from "+source, e1);
			Message msg = DMT.createFNPDataInsertRejected(uid, DMT.DATA_INSERT_REJECTED_SSK_ERROR);
			try {
				source.sendSync(msg, this, realTimeFlag);
			} catch (NotConnectedException e) {
				// Ignore
			} catch (SyncSendWaitedTooLongException e) {
				// Ignore
			}
			return;
		}
		
		SSKBlock storedBlock = node.fetch(key, false, false, false, canWriteDatastore, false, null);
		
		if((storedBlock != null) && !storedBlock.equals(block)) {
			try {
				RequestHandler.sendSSK(storedBlock.getRawHeaders(), storedBlock.getRawData(), false, pubKey, source, uid, this, realTimeFlag);
			} catch (NotConnectedException e1) {
				if(logMINOR) Logger.minor(this, "Lost connection to source on "+uid);
				return;
			} catch (WaitedTooLongException e1) {
				Logger.error(this, "Took too long to send ssk datareply to "+uid+" (because of throttling)");
				return;
			} catch (PeerRestartedException e) {
				if(logMINOR) Logger.minor(this, "Source restarted on "+uid);
				return;
			} catch (SyncSendWaitedTooLongException e) {
				Logger.error(this, "Took too long to send ssk datareply to "+uid);
				return;
			}
			block = storedBlock;
		}
		
		if(logMINOR) Logger.minor(this, "Got block for "+key+" for "+uid);
		
        if(htl > 0)
            sender = node.makeInsertSender(block, htl, uid, tag, source, false, false, canWriteDatastore, forkOnCacheable, preferInsert, ignoreLowBackoff, realTimeFlag);
        
        boolean receivedRejectedOverload = false;
        
        while(true) {
            synchronized(sender) {
                try {
                	if(sender.getStatus() == SSKInsertSender.NOT_FINISHED)
                		sender.wait(5000);
                } catch (InterruptedException e) {
                	// Ignore
                }
            }

            if((!receivedRejectedOverload) && sender.receivedRejectedOverload()) {
            	receivedRejectedOverload = true;
            	// Forward it
            	// Does not need to be sent synchronously since is non-terminal.
            	Message m = DMT.createFNPRejectedOverload(uid, false, true, realTimeFlag);
            	try {
					source.sendAsync(m, null, this);
				} catch (NotConnectedException e) {
					if(logMINOR) Logger.minor(this, "Lost connection to source");
					return;
				}
            }
            
            if(sender.hasRecentlyCollided()) {
            	// Forward collision
            	data = sender.getData();
            	headers = sender.getHeaders();
            	collided = true;
        		try {
					block = new SSKBlock(data, headers, key, true);
				} catch (SSKVerifyException e1) {
					// Is verified elsewhere...
					throw new Error("Impossible: " + e1, e1);
				}
				try {
					RequestHandler.sendSSK(headers, data, false, pubKey, source, uid, this, realTimeFlag);
				} catch (NotConnectedException e1) {
					if(logMINOR) Logger.minor(this, "Lost connection to source on "+uid);
					return;
				} catch (WaitedTooLongException e1) {
					Logger.error(this, "Took too long to send ssk datareply to "+uid+" because of bwlimiting");
					return;
				} catch (PeerRestartedException e) {
					Logger.error(this, "Peer restarted on "+uid);
					return;
				} catch (SyncSendWaitedTooLongException e) {
					Logger.error(this, "Took too long to send ssk datareply to "+uid);
					return;
				}
            }
            
            int status = sender.getStatus();
            
            if(status == SSKInsertSender.NOT_FINISHED) {
           		continue;
            }
            
            // Local RejectedOverload's (fatal).
            // Internal error counts as overload. It'd only create a timeout otherwise, which is the same thing anyway.
            // We *really* need a good way to deal with nodes that constantly R_O!
            if((status == SSKInsertSender.TIMED_OUT) ||
            		(status == SSKInsertSender.GENERATED_REJECTED_OVERLOAD) ||
            		(status == SSKInsertSender.INTERNAL_ERROR)) {
            	// Unlock early for originator, late for target; see UIDTag comments.
            	tag.unlockHandler();
                Message msg = DMT.createFNPRejectedOverload(uid, true, true, realTimeFlag);
                try {
					source.sendSync(msg, this, realTimeFlag);
				} catch (NotConnectedException e) {
					if(logMINOR) Logger.minor(this, "Lost connection to source");
					return;
				} catch (SyncSendWaitedTooLongException e) {
					Logger.error(this, "Took too long to send "+msg+" to "+source);
					return;
				}
                // Might as well store it anyway.
                if((status == SSKInsertSender.TIMED_OUT) ||
                		(status == SSKInsertSender.GENERATED_REJECTED_OVERLOAD))
                	canCommit = true;
                finish(status);
                return;
            }
            
            if((status == SSKInsertSender.ROUTE_NOT_FOUND) || (status == SSKInsertSender.ROUTE_REALLY_NOT_FOUND)) {
            	// Unlock early for originator, late for target; see UIDTag comments.
            	tag.unlockHandler();
                Message msg = DMT.createFNPRouteNotFound(uid, sender.getHTL());
                try {
					source.sendSync(msg, this, realTimeFlag);
				} catch (NotConnectedException e) {
					if(logMINOR) Logger.minor(this, "Lost connection to source");
					return;
				} catch (SyncSendWaitedTooLongException e) {
					Logger.error(this, "Took too long to send "+msg+" to source");
				}
                canCommit = true;
                finish(status);
                return;
            }
            
            if(status == SSKInsertSender.SUCCESS) {
            	// Unlock early for originator, late for target; see UIDTag comments.
            	tag.unlockHandler();
            	Message msg = DMT.createFNPInsertReply(uid);
            	try {
					source.sendSync(msg, this, realTimeFlag);
				} catch (NotConnectedException e) {
					if(logMINOR) Logger.minor(this, "Lost connection to source");
					return;
				} catch (SyncSendWaitedTooLongException e) {
					Logger.error(this, "Took too long to send "+msg+" to "+source);
				}
                canCommit = true;
                finish(status);
                return;
            }
            
            // Otherwise...?
            Logger.error(this, "Unknown status code: "+sender.getStatusString());
        	// Unlock early for originator, late for target; see UIDTag comments.
        	tag.unlockHandler();
            Message msg = DMT.createFNPRejectedOverload(uid, true, true, realTimeFlag);
            try {
				source.sendSync(msg, this, realTimeFlag);
			} catch (NotConnectedException e) {
				// Ignore
			} catch (SyncSendWaitedTooLongException e) {
				Logger.error(this, "Took too long to send "+msg+" to "+source);
			}
            finish(status);
            return;
        }
    }

    /**
     * If canCommit, and we have received all the data, and it
     * verifies, then commit it.
     */
    private void finish(int code) {
    	if(logMINOR) Logger.minor(this, "Finishing");
    	
    	if(canCommit) {
    		commit();
    	}
    	
        if(code != SSKInsertSender.TIMED_OUT && code != SSKInsertSender.GENERATED_REJECTED_OVERLOAD &&
        		code != SSKInsertSender.INTERNAL_ERROR && code != SSKInsertSender.ROUTE_REALLY_NOT_FOUND) {
        	int totalSent = getTotalSentBytes();
        	int totalReceived = getTotalReceivedBytes();
        	if(sender != null) {
        		totalSent += sender.getTotalSentBytes();
        		totalReceived += sender.getTotalReceivedBytes();
        	}
        	if(logMINOR) Logger.minor(this, "Remote SSK insert cost "+totalSent+ '/' +totalReceived+" bytes ("+code+ ')');
        	node.getNodeStats().remoteSskInsertBytesSentAverage.report(totalSent);
        	node.getNodeStats().remoteSskInsertBytesReceivedAverage.report(totalReceived);
        	if(code == SSKInsertSender.SUCCESS) {
        		// Can report both sides
        		node.getNodeStats().successfulSskInsertBytesSentAverage.report(totalSent);
        		node.getNodeStats().successfulSskInsertBytesReceivedAverage.report(totalReceived);
        	}
        }

    }

    private void commit() {
		try {
			node.store(block, node.shouldStoreDeep(key, source, sender == null ? new PeerNode[0] : sender.getRoutedTo()), collided, false, canWriteDatastore, false);
		} catch (KeyCollisionException e) {
			Logger.normal(this, "Collision on "+this);
		}
	}

	private final Object totalBytesSync = new Object();
    private int totalBytesSent;
    private int totalBytesReceived;
    
	@Override
	public void sentBytes(int x) {
		synchronized(totalBytesSync) {
			totalBytesSent += x;
		}
		node.getNodeStats().insertSentBytes(true, x);
	}

	@Override
	public void receivedBytes(int x) {
		synchronized(totalBytesSync) {
			totalBytesReceived += x;
		}
		node.getNodeStats().insertReceivedBytes(true, x);
	}
	
	public int getTotalSentBytes() {
		return totalBytesSent;
	}
	
	public int getTotalReceivedBytes() {
		return totalBytesReceived;
	}

	@Override
	public void sentPayload(int x) {
		node.sentPayload(x);
		node.getNodeStats().insertSentBytes(true, -x);
	}

	@Override
	public int getPriority() {
		return NativeThread.HIGH_PRIORITY;
	}
    
}
