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

import java.util.HashMap;

import freenet.client.async.TooManyFilesInsertException;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.node.LowLevelPutException;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;

/**
 * Thrown when a high-level insert fails. For most failures, there will not be a stack trace, or it 
 * will be inaccurate.
 */
public class InsertException extends Exception implements Cloneable {
	private static final long serialVersionUID = -1106716067841151962L;
	
	/** Failure mode, see the constants below. */
	public final InsertExceptionMode mode;
	/** Collect errors when there are multiple failures. The error mode will be FATAL_ERRORS_IN_BLOCKS or
	 * TOO_MANY_RETRIES_IN_BLOCKS i.e. a splitfile failed. */
	public FailureCodeTracker errorCodes;
	/** If a non-serious error, the URI we expect the insert to go to if it had succeeded. */
	public FreenetURI uri;

	/** Extra detail message */
	public final String extra;
	
	/** Get the failure mode. */
	public InsertExceptionMode getMode() {
		return mode;
	}
	
	private static volatile boolean logMINOR;
	
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback() {
			
			@Override
			public void shouldUpdate() {
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	/**
	 * zero arg c'tor for db4o on jamvm
	 */
	@SuppressWarnings("unused")
	private InsertException() {
		mode = null;
		extra = null;
	}

	public InsertException(InsertExceptionMode m, String msg, FreenetURI expectedURI) {
		super(getMessage(m)+": "+msg);
		extra = msg;
		mode = m;
		errorCodes = null;
		this.uri = expectedURI;
		if(mode == InsertExceptionMode.INTERNAL_ERROR)
			Logger.error(this, "Internal error: "+this);
		else if(logMINOR) 
			Logger.minor(this, "Creating InsertException: "+getMessage(mode)+": "+msg, this);
	}
	
	public InsertException(InsertExceptionMode m, FreenetURI expectedURI) {
		super(getMessage(m));
		extra = null;
		mode = m;
		errorCodes = null;
		this.uri = expectedURI;
		if(mode == InsertExceptionMode.INTERNAL_ERROR)
			Logger.error(this, "Internal error: "+this);
		else if(logMINOR) 
			Logger.minor(this, "Creating InsertException: "+getMessage(mode), this);
	}

	public InsertException(InsertExceptionMode mode, Throwable e, FreenetURI expectedURI) {
		super(getMessage(mode)+": "+e.getMessage());
		extra = e.getMessage();
		this.mode = mode;
		errorCodes = null;
		initCause(e);
		this.uri = expectedURI;
		if(mode == InsertExceptionMode.INTERNAL_ERROR)
			Logger.error(this, "Internal error: "+this);
		else if(logMINOR) 
			Logger.minor(this, "Creating InsertException: "+getMessage(mode)+": "+e, this);
	}

	public InsertException(InsertExceptionMode mode, String message, Throwable e, FreenetURI expectedURI) {
		super(getMessage(mode)+": "+message+": "+e.getMessage());
		extra = e.getMessage();
		this.mode = mode;
		errorCodes = null;
		initCause(e);
		this.uri = expectedURI;
		if(mode == InsertExceptionMode.INTERNAL_ERROR)
			Logger.error(this, "Internal error: "+this);
		else if(logMINOR) 
			Logger.minor(this, "Creating InsertException: "+getMessage(mode)+": "+e, this);
	}

	public InsertException(InsertExceptionMode mode, FailureCodeTracker errorCodes, FreenetURI expectedURI) {
		super(getMessage(mode));
		extra = null;
		this.mode = mode;
		this.errorCodes = errorCodes;
		this.uri = expectedURI;
		if(mode == InsertExceptionMode.INTERNAL_ERROR)
			Logger.error(this, "Internal error: "+this);
		else if(logMINOR) 
			Logger.minor(this, "Creating InsertException: "+getMessage(mode), this);
	}

    public InsertException(InsertExceptionMode mode, String message, FailureCodeTracker errorCodes, FreenetURI expectedURI) {
        super(message == null ? getMessage(mode) : (getMessage(mode)+": "+message));
        extra = message;
        this.mode = mode;
        this.errorCodes = errorCodes;
        this.uri = expectedURI;
        if(mode == InsertExceptionMode.INTERNAL_ERROR)
            Logger.error(this, "Internal error: "+this);
        else if(logMINOR) 
            Logger.minor(this, "Creating InsertException: "+getMessage(mode), this);
    }

	public InsertException(InsertExceptionMode mode) {
		super(getMessage(mode));
		extra = null;
		this.mode = mode;
		this.errorCodes = null;
		this.uri = null;
		if(mode == InsertExceptionMode.INTERNAL_ERROR)
			Logger.error(this, "Internal error: "+this);
		else if(logMINOR) 
			Logger.minor(this, "Creating InsertException: "+getMessage(mode), this);
	}

	public InsertException(InsertException e) {
		super(e.getMessage());
		extra = e.extra;
		mode = e.mode;
		errorCodes = e.errorCodes == null ? null : e.errorCodes.clone();
		uri = e.uri;
	}

	public InsertException(TooManyFilesInsertException e) {
		this(InsertExceptionMode.TOO_MANY_FILES, (String)null, null);
	}

	public static InsertException constructFrom(LowLevelPutException e) {
	    switch(e.code) {
	    case LowLevelPutException.COLLISION:
	        return new InsertException(InsertExceptionMode.COLLISION);
	    case LowLevelPutException.INTERNAL_ERROR:
	        return new InsertException(InsertExceptionMode.INTERNAL_ERROR);
	    case LowLevelPutException.REJECTED_OVERLOAD:
	        return new InsertException(InsertExceptionMode.REJECTED_OVERLOAD);
	    case LowLevelPutException.ROUTE_NOT_FOUND:
	        return new InsertException(InsertExceptionMode.ROUTE_NOT_FOUND);
        case LowLevelPutException.ROUTE_REALLY_NOT_FOUND:
            return new InsertException(InsertExceptionMode.ROUTE_REALLY_NOT_FOUND);
	    default:
	        Logger.error(InsertException.class, "Unknown LowLevelPutException: "+e+" code "+e.code, new Exception("error"));
	        return new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Unknown error "+e.code, null);
	    }
    }
	
    private static final HashMap<Integer, InsertExceptionMode> modes = 
        new HashMap<Integer, InsertExceptionMode>();

    public static enum InsertExceptionMode {
        
        /** Caller supplied a URI we cannot use */
        INVALID_URI(1),
        /** Failed to read from or write to a bucket; a kind of internal error */
        BUCKET_ERROR(2),
        /** Internal error of some sort */
        INTERNAL_ERROR(3),
        /** Downstream node was overloaded */
        REJECTED_OVERLOAD(4),
        /** Couldn't find enough nodes to send the data to */
        ROUTE_NOT_FOUND(5),
        /** There were fatal errors in a splitfile insert. */
        FATAL_ERRORS_IN_BLOCKS(6),
        /** Could not insert a splitfile because a block failed too many times */
        TOO_MANY_RETRIES_IN_BLOCKS(7),
        /** Not able to leave the node at all */
        ROUTE_REALLY_NOT_FOUND(8),
        /** Collided with pre-existing content */
        COLLISION(9),
        /** Cancelled by user */
        CANCELLED(10),
        /** Meta string used in the key (most probably '/') */
        META_STRINGS_NOT_SUPPORTED(11),
        /** Invalid binary blob data supplied so cannot insert it */
        BINARY_BLOB_FORMAT_ERROR(12),
        /** Too many files in a directory in a site insert */
        TOO_MANY_FILES(13),
        /** File being uploaded is bigger than maximum supported size */
        TOO_BIG(14);
        
        
        public final int code;
        InsertExceptionMode(int code) {
            this.code = code;
            if(code < 0 || code >= UPPER_LIMIT_ERROR_CODE)
                throw new IllegalArgumentException();
            if(modes.containsKey(code))
                throw new IllegalArgumentException();
            modes.put(code, this);
        }
        public static InsertExceptionMode getByCode(int code) {
            if(modes.get(code) == null) throw new IllegalArgumentException();
            return modes.get(code);
        }

    }

	/** There will never be more error codes than this constant. Must not change, used for some
	 * data structures. */
	public static final int UPPER_LIMIT_ERROR_CODE = 1024;

	/** Get the (localised) short name of this failure mode. */
	public static String getMessage(InsertExceptionMode mode) {
        // FIXME change the l10n to use the keyword not the code
		String ret = NodeL10n.getBase().getString("InsertException.longError."+mode.code);
		if(ret == null)
			return "Unknown error "+mode;
		else return ret;
	}

	/** Get the (localised) long explanation for this failure mode. */
	public static String getShortMessage(InsertExceptionMode mode) {
	    // FIXME change the l10n to use the keyword not the code
		String ret = NodeL10n.getBase().getString("InsertException.shortError."+mode.code);
		if(ret == null)
			return "Unknown error "+mode;
		else return ret;
	}
	
	/** Is this error fatal? Non-fatal errors are errors which are likely to go away with
	 * more retries, or at least for which there is some point retrying.
	 */
	public boolean isFatal() {
		return isFatal(mode);
	}
	
	public static boolean isFatal(InsertExceptionMode mode) {
		switch(mode) {
		case INVALID_URI:
		case FATAL_ERRORS_IN_BLOCKS:
		case COLLISION:
		case CANCELLED:
		case META_STRINGS_NOT_SUPPORTED:
		case BINARY_BLOB_FORMAT_ERROR:
		case TOO_BIG:
        case BUCKET_ERROR: // maybe. No point retrying.
        case INTERNAL_ERROR: // maybe. No point retrying.
			return true;
		case REJECTED_OVERLOAD:
		case TOO_MANY_RETRIES_IN_BLOCKS:
		case ROUTE_NOT_FOUND:
		case ROUTE_REALLY_NOT_FOUND:
			return false;
		default:
			Logger.error(InsertException.class, "Error unknown to isFatal(): "+getMessage(mode));
			return false;
		}
	}

	/**
	 * Construct an InsertException from a bunch of error codes, typically from a splitfile insert.
	 */
	public static InsertException construct(FailureCodeTracker errors) {
		if(errors == null) return null;
		if(errors.isEmpty()) return null;
		if(errors.isOneCodeOnly()) {
			return new InsertException(errors.getFirstCodeInsert());
		}
		InsertExceptionMode mode;
		if(errors.isFatal(true))
			mode = InsertExceptionMode.FATAL_ERRORS_IN_BLOCKS;
		else
			mode = InsertExceptionMode.TOO_MANY_RETRIES_IN_BLOCKS;
		return new InsertException(mode, errors, null);
	}
	
	@Override
	public InsertException clone() {
		// Cloneable shuts up findbugs, but we need to deep copy errorCodes.
		return new InsertException(this);
	}

}
