/*
 * This file is part of Jstacs.
 *
 * Jstacs is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Jstacs is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Jstacs. If not, see <http://www.gnu.org/licenses/>.
 * 
 * For more information on Jstacs, visit http://www.jstacs.de
 */

package de.jstacs.parameters;

import de.jstacs.DataType;
import de.jstacs.NonParsableException;
import de.jstacs.io.XMLParser;
import de.jstacs.parameters.validation.NumberValidator;
import de.jstacs.parameters.validation.ParameterValidator;

/**
 * Class for a &quot;simple&quot; parameter. Simple parameters are those of the
 * primitive types and single {@link String}s.
 * 
 * @author Jan Grau
 */
public class SimpleParameter extends Parameter implements Rangeable {

	/**
	 * The data type of the parameter value
	 */
	protected DataType datatype;

	/**
	 * The name of the parameter
	 */
	protected String name;
	/**
	 * A comment on the parameter
	 */
	protected String comment;

	/**
	 * Determines if the parameter is required
	 */
	protected boolean required;

	/**
	 * The current value of the parameter
	 */
	protected Object value;

	/**
	 * The default value of the parameter
	 */
	protected Object defaultValue;

	/**
	 * The validator for the parameter values
	 */
	protected ParameterValidator validator;

	/**
	 * <code>true</code> if the parameters are set, <code>false</code> otherwise
	 */
	private boolean isSet;

	/**
	 * The error message or <code>null</code> if no error occurred
	 */
	private String errorMessage;

	/**
	 * Indicates if this {@link SimpleParameter} shall be rangeable
	 */
	private boolean isRangeable;

	/**
	 * The standard constructor for the interface {@link de.jstacs.Storable}.
	 * Constructs a {@link SimpleParameter} out of an XML representation.
	 * 
	 * @param representation
	 *            the XML representation as {@link StringBuffer}
	 * 
	 * @throws NonParsableException
	 *             if the {@link SimpleParameter} could not be restored from the
	 *             {@link StringBuffer} <code>representation</code>
	 */
	public SimpleParameter(StringBuffer representation)
			throws NonParsableException {
		fromXML(representation);
	}

	/**
	 * Constructor for a {@link SimpleParameter} without
	 * {@link ParameterValidator}.
	 * 
	 * @param datatype
	 *            the data type of the parameter value
	 * @param name
	 *            the name of the parameter
	 * @param comment
	 *            a comment on the parameter that tells the user some details
	 *            about it
	 * @param required
	 *            determines if the parameter is required
	 * 
	 * @throws DatatypeNotValidException
	 *             if <code>datatype</code> is not one of the allowed
	 *             {@link DataType}s
	 */
	public SimpleParameter(DataType datatype, String name, String comment,
			boolean required) throws DatatypeNotValidException {
		if (datatype != DataType.BOOLEAN && datatype != DataType.BYTE
				&& datatype != DataType.CHAR && datatype != DataType.DOUBLE
				&& datatype != DataType.FLOAT && datatype != DataType.INT
				&& datatype != DataType.LONG && datatype != DataType.SHORT
				&& datatype != DataType.STRING) {
			throw new DatatypeNotValidException(
					"Only primitive datatypes and Strings are allowed as datatypes of a SimpleParameter!");
		}
		this.datatype = datatype;
		this.name = name;
		this.comment = comment;
		this.required = required;
		this.validator = null;
		this.isSet = false;
		this.isRangeable = datatype != DataType.STRING
				&& datatype != DataType.CHAR;
	}

	/**
	 * Constructor for a {@link SimpleParameter} without
	 * {@link ParameterValidator} but with a default value.
	 * 
	 * @param datatype
	 *            the data type of the parameter value
	 * @param name
	 *            the name of the parameter
	 * @param comment
	 *            a comment on the parameter that tells the user some details
	 *            about it
	 * @param required
	 *            determines if the parameter is required
	 * @param defaultVal
	 *            the default value
	 * 
	 * @throws ParameterException
	 *             if either the default value is not a valid value with respect
	 *             to <code>datatype</code> or <code>datatype</code> is not in
	 *             the values allowed for a {@link SimpleParameter}
	 */
	public SimpleParameter(DataType datatype, String name, String comment,
			boolean required, Object defaultVal) throws ParameterException {
		this(datatype, name, comment, required);
		if (checkValue(defaultVal)) {
			setDefault(defaultVal);
		} else {
			throw new IllegalValueException("Value not valid");
		}
	}

	/**
	 * Constructor for a {@link SimpleParameter} with a
	 * {@link ParameterValidator}.
	 * 
	 * @param datatype
	 *            the data type of the parameter value
	 * @param name
	 *            the name of the parameter
	 * @param comment
	 *            a comment on the parameter that tells the user some details
	 *            about it
	 * @param required
	 *            determines if the parameter is required
	 * @param validator
	 *            the validator for the parameter values
	 * 
	 * @throws DatatypeNotValidException
	 *             if <code>datatype</code> is not in the values allowed for a
	 *             {@link SimpleParameter}
	 */
	public SimpleParameter(DataType datatype, String name, String comment,
			boolean required, ParameterValidator validator)
			throws DatatypeNotValidException {
		this(datatype, name, comment, required);
		this.validator = validator;
	}

	/**
	 * Constructor for a {@link SimpleParameter} with validator and default
	 * value.
	 * 
	 * @param datatype
	 *            the data type of the parameter value
	 * @param name
	 *            the name of the parameter
	 * @param comment
	 *            a comment on the parameter that tells the user some details
	 *            about it
	 * @param required
	 *            determines if the parameter is required
	 * @param validator
	 *            the validator for the parameter values
	 * @param defaultVal
	 *            the default value
	 * 
	 * @throws ParameterException
	 *             if either the default value is not a valid value with respect
	 *             to <code>datatype</code> or <code>datatype</code> is not in
	 *             the values allowed for a {@link SimpleParameter}
	 */
	public SimpleParameter(DataType datatype, String name, String comment,
			boolean required, ParameterValidator validator, Object defaultVal)
			throws ParameterException {
		this(datatype, name, comment, required, validator);
		if (checkValue(defaultVal)) {
			setDefault(defaultVal);
		} else {
			throw new IllegalValueException("Value not valid");
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#clone()
	 */
	@Override
	public SimpleParameter clone() throws CloneNotSupportedException {
		SimpleParameter clone = (SimpleParameter) super.clone();
		if (validator != null) {
			clone.validator = validator.clone();
		}
		return clone;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#hasDefaultOrIsSet()
	 */
	@Override
	public boolean hasDefaultOrIsSet() {
		if (isSet) {
			return true;
		} else {
			return getValue() != null;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#isSet()
	 */
	@Override
	public boolean isSet() {
		return isSet;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Rangeable#isRangeable()
	 */
	public boolean isRangeable() {
		return isRangeable;
	}

	/**
	 * Sets the value returned by {@link #isRangeable()} to
	 * <code>rangeable</code>.
	 * 
	 * @param rangeable
	 *            the new value that determines if this {@link SimpleParameter}
	 *            is rangeable
	 */
	public void setRangeable(boolean rangeable) {
		this.isRangeable = rangeable;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Rangeable#getRangedInstance()
	 */
	public Parameter getRangedInstance() throws Exception {
		if (isRangeable()) {
			return new RangeParameter(this);
		} else {
			throw new Exception("Parameter " + name + " is not rangeable!");
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#isAtomic()
	 */
	@Override
	public boolean isAtomic() {
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#getName()
	 */
	@Override
	public String getName() {
		return name;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#getDatatype()
	 */
	@Override
	public DataType getDatatype() {
		return datatype;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#getComment()
	 */
	@Override
	public String getComment() {
		return comment;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#isRequired()
	 */
	@Override
	public boolean isRequired() {
		return required;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#checkValue(java.lang.Object)
	 */
	@Override
	public boolean checkValue(Object value) {
		if (validator == null) {
			if (datatype == DataType.BOOLEAN) {
				if (value instanceof Boolean || value instanceof String) {
					errorMessage = null;
					return true;
				} else {
					errorMessage = "The specified value is no boolean.";
				}
			} else if (datatype == DataType.CHAR) {
				if (value instanceof Character
						|| (value instanceof String && ((String) value)
								.length() == 1)) {
					errorMessage = null;
					return true;
				} else {
					errorMessage = "The specified value is no character or null.";
				}
			} else if (datatype == DataType.BYTE) {
				if (value instanceof Byte) {
					errorMessage = null;
					return true;
				} else if (value instanceof String) {
					try {
						Byte.parseByte((String) value);
						errorMessage = null;
						return true;
					} catch (NumberFormatException e) {
						errorMessage = "Specified value is not a byte.";
						return false;
					}
				}
			} else if (datatype == DataType.SHORT) {
				if (value instanceof Short) {
					errorMessage = null;
					return true;
				} else if (value instanceof String) {
					try {
						Short.parseShort((String) value);
						errorMessage = null;
						return true;
					} catch (NumberFormatException e) {
						errorMessage = "Specified value is not a short.";
						return false;
					}
				}
			} else if (datatype == DataType.INT) {
				if (value instanceof Integer) {
					errorMessage = null;
					return true;
				} else if (value instanceof String) {
					try {
						Integer.parseInt((String) value);
						errorMessage = null;
						return true;
					} catch (NumberFormatException e) {
						errorMessage = "Specified value is not an integer.";
						return false;
					}
				}
			} else if (datatype == DataType.LONG) {
				if (value instanceof Long) {
					errorMessage = null;
					return true;
				} else if (value instanceof String) {
					try {
						Long.parseLong((String) value);
						errorMessage = null;
						return true;
					} catch (NumberFormatException e) {
						errorMessage = "Specified value is not a long.";
						return false;
					}
				}
			} else if (datatype == DataType.FLOAT) {
				if (value instanceof Float) {
					errorMessage = null;
					return true;
				} else if (value instanceof String) {
					try {
						Float.parseFloat((String) value);
						errorMessage = null;
						return true;
					} catch (NumberFormatException e) {
						errorMessage = "Specified value is not a float.";
						return false;
					}
				}
			} else if (datatype == DataType.DOUBLE) {
				if (value instanceof Double) {
					errorMessage = null;
					return true;
				} else if (value instanceof String) {
					try {
						Double.parseDouble((String) value);
						errorMessage = null;
						return true;
					} catch (NumberFormatException e) {
						errorMessage = "Specified value is not a double.";
						return false;
					}
				}
			} else if (datatype == DataType.STRING && value instanceof String
					&& (!required || ((String) value).length() > 0)) {
				errorMessage = null;
				return true;
			}
			if (value == null || value instanceof String
					&& ((String) value).length() == 0) {
				errorMessage = null;
			} else {
				errorMessage = "Value is not of the expected format or null.";
			}
			return false;

		} else {
			Object value2 = value;
			if (value instanceof String) {
				if (((String) value).length() > 0) {
					try {
						if (datatype == DataType.BYTE) {
							value2 = new Byte((String) value);
						} else if (datatype == DataType.SHORT) {
							value2 = new Short((String) value);
						} else if (datatype == DataType.INT) {
							value2 = new Integer((String) value);
						} else if (datatype == DataType.LONG) {
							value2 = new Long((String) value);
						} else if (datatype == DataType.FLOAT) {
							value2 = new Float((String) value);
						} else if (datatype == DataType.DOUBLE) {
							value2 = new Double((String) value);
						}
					} catch (NumberFormatException e) {
						errorMessage = "Value is not of the expected format or null.";
						return false;
					}
				} else {
					errorMessage = "";
					return false;
				}
			}

			if (validator.checkValue(value2)) {
				errorMessage = validator.getErrorMessage();
				return true;
			} else {
				errorMessage = validator.getErrorMessage();
				return false;
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#getErrorMessage()
	 */
	@Override
	public String getErrorMessage() {
		return errorMessage;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#setDefault(java.lang.Object)
	 */
	@Override
	public void setDefault(Object defaultValue) throws IllegalValueException {
		if (checkValue(defaultValue)) {
			this.defaultValue = defaultValue;
			setValue(defaultValue);
		} else {
			System.out.println("value not valid!");
			throw new IllegalValueException("default value not valid");
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#simplify()
	 */
	@Override
	public void simplify() {

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#reset()
	 */
	@Override
	public void reset() {
		if (defaultValue != null) {
			try {
				setDefault(defaultValue);
			} catch (Exception e) {
				value = null;
			}
		} else {
			value = null;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#setValue(java.lang.Object)
	 */
	@Override
	public void setValue(Object value2) throws IllegalValueException {
		if (checkValue(value2)) {
			if (value2 instanceof String && datatype != DataType.STRING) {
				String s = (String) value2;
				// System.out.println("s is: "+s+", datatype: "+datatype);
				try {
					switch (datatype) {
					case BOOLEAN:
						value = new Boolean(s);
						break;
					case CHAR:
						value = s.toCharArray()[0];
						break;
					case BYTE:
						value = new Byte(s);
						break;
					case SHORT:
						value = new Short(s);
						break;
					case INT:
						value = new Integer(s);
						break;
					case LONG:
						value = new Long(s);
						break;
					case FLOAT:
						value = new Float(s);
						break;
					case DOUBLE:
						value = new Double(s);
						break;
					default:
						errorMessage = "Parameter value not of the expected type!";
						throw new IllegalValueException(errorMessage);
					}
				} catch (Exception e) {
					System.out.println("value not valid2!");
					errorMessage = "Value not valid\n" + e.getMessage();
					throw new IllegalValueException("Value not valid\n"
							+ e.getMessage());
				}
			} else {
				this.value = value2;
				this.isSet = true;
			}
		} else {
			System.out.println("value not valid!");
			throw new IllegalValueException("value of "+getName()+" not valid");
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#getValue()
	 */
	@Override
	public Object getValue() {
		return value;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#toXML()
	 */
	@Override
	public StringBuffer toXML() {
		StringBuffer buf = super.toXML();
		XMLParser.addTags(buf, "superParameter");
		XMLParser.appendObjectWithTags(buf, datatype, "datatype");
		XMLParser.appendObjectWithTags(buf, name, "name");
		XMLParser.appendObjectWithTags(buf, comment, "comment");
		XMLParser.appendObjectWithTags(buf, required, "required");
		XMLParser.appendObjectWithTags(buf, isSet, "isSet");
		XMLParser.appendObjectWithTags(buf, errorMessage, "errorMessage");
		XMLParser.appendObjectWithTags(buf, isRangeable, "isRangeable");
		XMLParser.appendObjectWithTags(buf, validator, "validator");
		/*
		if (validator != null) {
			StringBuffer buf2 = new StringBuffer();
			XMLParser.appendObjectWithTags(buf2, validator.getClass().getName(), "className");
			buf2.append(validator.toXML());
			XMLParser.addTags(buf2, "validator");
			buf.append(buf2);
		} else {
			XMLParser.appendObjectWithTags( buf, null, "validator" );
		}
		*/
		XMLParser.appendObjectWithTags(buf, defaultValue, "defaultValue");
		XMLParser.appendObjectWithTags(buf, value, "value");
		/*
		if (value != null) {
			XMLParser.appendObjectWithTags(buf, value.toString(), "value");
		} else {
			XMLParser.appendObjectWithTags(buf, "null", "value");
		}
		*/
		XMLParser.addTags(buf, "simpleParameter");

		return buf;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.jstacs.parameters.Parameter#fromXML(java.lang.StringBuffer)
	 */
	@Override
	protected void fromXML(StringBuffer representation)
			throws NonParsableException {
		representation = XMLParser.extractForTag(representation, "simpleParameter");
		super.fromXML(XMLParser.extractForTag(representation,"superParameter"));
		datatype = XMLParser.extractObjectForTags(representation, "datatype", DataType.class );// TODO XMLP14CONV This and (possibly) the following lines have been converted automatically
		name = XMLParser.extractObjectForTags(representation, "name", String.class );
		comment = XMLParser.extractObjectForTags(representation, "comment", String.class );
		required = XMLParser.extractObjectForTags(representation, "required", boolean.class );
		isSet = XMLParser.extractObjectForTags(representation, "isSet", boolean.class );
		errorMessage = XMLParser.parseString( XMLParser.extractObjectForTags(representation, "errorMessage", String.class ) );
		StringBuffer help = XMLParser.extractForTag(representation,"isRangeable");
		if (help == null) {
			isRangeable = false;
		} else {
			isRangeable = Boolean.parseBoolean(help.toString());
		}
		validator = XMLParser.extractObjectForTags( representation, "validator", ParameterValidator.class );
		/*
		String valid = XMLParser.extractObjectForTags(representation, "validator", String.class );
		if (valid.equals("null")) {
			validator = null;
		} else {
			StringBuffer buf = new StringBuffer(valid);
			String className = XMLParser.extractObjectAndAttributesForTags(buf, "className", null, null, String.class, false );// TODO XMLP14CONV This and (possibly) the following lines have been converted automatically
			try {
				validator = (ParameterValidator) Class.forName(className)
						.getConstructor(new Class[] { StringBuffer.class })
						.newInstance(buf);
			} catch (Exception e) {
				e.printStackTrace();
				throw new NonParsableException(e.getMessage());
			}
		}/**/
		if( !XMLParser.hasTag(representation, "defaultValue", null, null) ) {
			defaultValue = null;
		} else {
			defaultValue = XMLParser.extractObjectForTags(representation, "defaultValue" );
			if ( defaultValue != null ) {
				try {
					this.setDefault(defaultValue);
				} catch (Exception e) {
					e.printStackTrace();
					throw new NonParsableException(e.getMessage());
				}
			}
		}
		
		String val = XMLParser.extractObjectForTags(representation, "value", String.class );
		if ( val != null ) {
			try {
				this.setValue(val);
			} catch (Exception e) {
				e.printStackTrace();
				throw new NonParsableException(e.getMessage());
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object o2) {
		if (o2 instanceof SimpleParameter) {
			SimpleParameter par2 = (SimpleParameter) o2;
			if (par2.comment.equals(comment) && par2.name.equals(name)
					&& par2.required == required && par2.datatype == datatype) {
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}

	/**
	 * Class for an {@link Exception} that can be thrown if the provided
	 * <code>int</code>-value that represents a data type is not one of the
	 * values defined in {@link DataType}.
	 * 
	 * @author Jan Grau
	 */
	public static class DatatypeNotValidException extends ParameterException {

		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		/**
		 * Creates a new {@link DatatypeNotValidException} with an error
		 * message.
		 * 
		 * @param reason
		 *            the error message
		 */
		public DatatypeNotValidException(String reason) {
			super("The datatype is not valid for this type of parameter: "
					+ reason);
		}

	}

	/**
	 * This exception is thrown if a parameter is not valid.
	 * 
	 * @author Jan Grau
	 */
	public static class IllegalValueException extends ParameterException {

		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		/**
		 * Creates a new {@link IllegalValueException} with the reason of the
		 * exception <code>reason</code> as error message.
		 * 
		 * @param reason
		 *            the reason to give as error message
		 */
		public IllegalValueException(String reason) {
			super("Parameter not permitted: " + reason);
		}

	}

	/**
	 * Returns the {@link ParameterValidator} used in this
	 * {@link SimpleParameter}. This may be <code>null</code>.
	 * 
	 * @return the validator used in this {@link SimpleParameter}
	 */
	public ParameterValidator getValidator() {
		return validator;
	}

	/**
	 * Sets a new {@link ParameterValidator} for this {@link SimpleParameter}.
	 * 
	 * @param validator
	 *            the new parameter validator
	 */
	public void setValidator(ParameterValidator validator) {
		this.validator = validator;
	}
	
	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		return name + " (" + comment
			+ (validator!=null && validator instanceof NumberValidator?", " + validator.toString():"")
			+ (defaultValue!=null?", default = " + defaultValue.toString():"")
			+ (required ? "" : ", OPTIONAL" )
			+ ")\t= " + value;
	}
}
