/* valaconstant.vala
 *
 * Copyright (C) 2006-2011  Jürg Billeter
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.

 * This library 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
 * Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 *
 * Author:
 * 	Jürg Billeter <j@bitron.ch>
 */

using GLib;

/**
 * Represents a type member with a constant value.
 */
public class Vala.Constant : Symbol {
	/**
	 * The data type of this constant.
	 */
	public DataType type_reference {
		get { return _data_type; }
		private set {
			_data_type = value;
			_data_type.parent_node = this;
		}
	}

	/**
	 * The value of this constant.
	 */
	public Expression? value {
		get { return _value; }
		private set {
			_value = value;
			if (_value != null) {
				_value.parent_node = this;
			}
		}
	}

	private DataType _data_type;

	private Expression _value;

	/**
	 * Creates a new constant.
	 *
	 * @param name             constant name
	 * @param type_reference   constant type
	 * @param value            constant value
	 * @param source_reference reference to source code
	 * @return                 newly created constant
	 */
	public Constant (string name, DataType? type_reference, Expression? value, SourceReference? source_reference = null, Comment? comment = null) {
		base (name, source_reference, comment);
		if (type_reference != null) {
			this.type_reference = type_reference;
		}
		this.value = value;
	}

	public override void accept (CodeVisitor visitor) {
		visitor.visit_constant (this);
	}

	public override void accept_children (CodeVisitor visitor) {
		type_reference.accept (visitor);

		if (value != null) {
			value.accept (visitor);
		}
	}

	public override void replace_expression (Expression old_node, Expression new_node) {
		if (value == old_node) {
			value = new_node;
		}
	}

	public override void replace_type (DataType old_type, DataType new_type) {
		if (type_reference == old_type) {
			type_reference = new_type;
		}
	}

	public override bool check (CodeContext context) {
		if (checked) {
			return !error;
		}

		checked = true;

		var old_source_file = context.analyzer.current_source_file;
		var old_symbol = context.analyzer.current_symbol;

		if (source_reference != null) {
			context.analyzer.current_source_file = source_reference.file;
		}
		if (!(parent_symbol is Block)) {
			// non-local constant
			context.analyzer.current_symbol = this;
		}

		type_reference.check (context);

		if (!check_const_type (type_reference, context)) {
			error = true;
			Report.error (source_reference, "`%s' not supported as type for constants", type_reference.to_string ());
			return false;
		}

		// check whether constant type is at least as accessible as the constant
		if (!type_reference.is_accessible (this)) {
			error = true;
			Report.error (source_reference, "constant type `%s' is less accessible than constant `%s'", type_reference.to_string (), get_full_name ());
		}

		if (!external) {
			if (value == null) {
				// constants from fast-vapi files are special
				if (source_type != SourceFileType.FAST) {
					error = true;
					Report.error (source_reference, "A const field requires a value to be provided");
				}
			} else {
				value.target_type = type_reference;

				if (!value.check (context) || type_reference.error) {
					error = true;
					return false;
				}

				if (!value.value_type.compatible (type_reference)) {
					error = true;
					Report.error (source_reference, "Cannot convert from `%s' to `%s'", value.value_type.to_string (), type_reference.to_string ());
					return false;
				}

				// support translated string constants for efficiency / convenience
				// even though the expression is not a compile-time constant
				unowned MethodCall? call = value as MethodCall;
				if (call != null) {
					unowned MethodType? method_type = call.call.value_type as MethodType;
					if (method_type != null && method_type.method_symbol.get_full_name () == "GLib._") {
						// first argument is string
						var literal = call.get_argument_list ().get (0) as StringLiteral;
						if (literal != null) {
							value = literal;
							literal.translate = true;
						}
					}
				}

				if (!value.is_constant ()) {
					error = true;
					Report.error (value.source_reference, "Value must be constant");
					return false;
				}

				// check whether initializer is at least as accessible as the constant
				if (!value.is_accessible (this)) {
					error = true;
					Report.error (value.source_reference, "value is less accessible than constant `%s'", get_full_name ());
				}
			}
		} else {
			if (value != null) {
				error = true;
				Report.error (source_reference, "External constants cannot use values");
			}
		}

		if (!external_package && !hides && get_hidden_member () != null) {
			Report.warning (source_reference, "%s hides inherited constant `%s'. Use the `new' keyword if hiding was intentional", get_full_name (), get_hidden_member ().get_full_name ());
		}

		context.analyzer.current_source_file = old_source_file;
		context.analyzer.current_symbol = old_symbol;

		active = true;

		return !error;
	}

	bool check_const_type (DataType type, CodeContext context) {
		if (type is ValueType) {
			return true;
		} else if (type is VoidType || type is PointerType) {
			return false;
		} else if (type is ArrayType) {
			unowned ArrayType array_type = (ArrayType) type;
			return check_const_type (array_type.element_type, context);
		} else if (type is DelegateType) {
			unowned DelegateType delegate_type = (DelegateType) type;
			return !delegate_type.type_symbol.get_attribute_bool ("CCode", "has_target", true);
		} else if (type.type_symbol != null) {
			return type.type_symbol.is_subtype_of (context.analyzer.string_type.type_symbol);
		} else {
			return false;
		}
	}
}
