// This file is part of the Coral project.
// Please read the copyright notice in the LICENSE file.

#include <coral/core/ElementDefinition.h>
#include <coral/core/Element.h>
#include <coral/core/ElementIterator.h>
#include <coral/lib/assert.h>
#include <coral/lang/SMD01/SMD01.h>
#include <coral/lib/unicodestring.h>
#include <coral/core/PyOperation.h>

#include <coral/lib/autovector.h>
#include <sstream>

#include <Python.h>

#include <functional>

static const coral::unicodestring& estr(coral::Element* obj, const char* slot) {
	return dynamic_cast<coral::ElementString*>(obj->__getSlotRO__(slot)[0])->__getString__();
}
static int eenum(coral::Element* obj, const char* slot) {
	return dynamic_cast<coral::ElementEnumeration*>(obj->__getSlotRO__(slot)[0])->__getEValue__();
}
static int eint(coral::Element* obj, const char* slot) {
	return dynamic_cast<coral::ElementInteger*>(obj->__getSlotRO__(slot)[0])->__getIValue__();
}

static coral::Element* eelem(coral::Element* obj, const char* slot) {
	return dynamic_cast<coral::Element*>(obj->__getSlotRO__(slot)[0]);
}

NAMESPACE_BEGIN(coral);

void setCorrespondingElement(coral::Meta2ModelBridge* mm,
			     coral::Element* e) {
	coral_assert(mm);
	coral_assert(e);
	if (mm->m_correspondingElement.get() != NULL) return;
	mm->m_correspondingElement = e;
	e->ref();

}


namespace SMD01 { 

	typedef auto2unrefmap<coral::Element*,
			      coral::ref_counted*,
			      std::less<coral::Element*> > smd2coral_t;

	static void cr_python_function(coral::AbstractElementDefinition* ed,
				       coral::Element* obj) {
		coral_assert(obj);
		coral_assert(obj->oclIsKindOf(SMD01::Operation));
		std::stringstream source;
		{
			coral::Iterator m(obj->__getSlotRO__("code"));
			coral::ref_counted* _child;
			bool begin = true;
			while ((_child = m.next()) != NULL) {
				coral::ElementString* e =
					dynamic_cast<coral::ElementString*>(_child);
				coral_assert(e);
				const coral::unicodestring& value(e->__getString__());
				if (!begin) {
					source << std::endl;
				}
				begin = false;
				source << value.toUTF8();
			}
		}

		bool iswfr = eenum(obj, "isWFR");
		const coral::unicodestring& lang(estr(obj, "language"));
		const coral::unicodestring& name(estr(obj, "name"));
		const std::string name_utf8(name.toUTF8());

		const std::string ssource(source.str());

		PyObject* pyobj = NULL;
		{
			PyObject* locals = PyDict_New();
			PyObject* key = PyString_FromString(name_utf8.c_str());
			if (locals && key) {
				(void) PyRun_String(ssource.c_str(),
						    Py_file_input,
						    PyEval_GetGlobals(),
						    locals);
				if (PyErr_Occurred()) {
					dlog << DEBUGLEVEL_WARNING
					     << "WARNING"
					     << " Failed to add"
					     << " operation " << name_utf8
					     << " to metaclass " << ed->getName()
					     << "!"
					     << std::endl
					     << "The operation was:" << std::endl
					     << ssource
					     << std::endl
					     << std::endl;
					PyErr_Print();
				} else {
					pyobj = PyDict_GetItem(locals, key);
					Py_XINCREF(pyobj);
				}
			}
			Py_XDECREF(locals);
			Py_XDECREF(key);
		}

		if (pyobj) {
			try {
				coral::autounref<coral::PyOperation> p(new coral::PyOperation(pyobj, lang.toUTF8(), ssource, iswfr));
				ed->__addOperation__(p.get());
			} catch (...) {
				Py_DECREF(pyobj);
				throw;
			}
			Py_DECREF(pyobj);
		}
	}


	static coral::ref_counted* cr_obj(smd2coral_t& e_o,
				  coral::Set& properties,
				  coral::Element* obj,
				  bool* old_one) {
		coral_assert(obj);
		coral::autounref<coral::ref_counted> ret(NULL);

		const coral::unicodestring& name = estr(obj, "name");
		const std::string name_utf8(name.toUTF8());
		
		coral::Package* parent = NULL;
		coral::Element* op = obj->__parent__();
		if (op) {
			coral_assert(e_o.find(op) != e_o.end());
			coral::ref_counted* _parent = e_o[op];
			coral_assert(_parent);
			parent = dynamic_cast<coral::Package*>(_parent);
		}

		if (obj->oclIsKindOf(SMD01::Package)) {
			if (obj->oclIsKindOf(SMD01::Language)) {
				const int version = eint(obj, "version");
				const int revision = eint(obj, "revision");
				
				const coral::unicodestring& _xmlns = estr(obj, "URInamespace");
				const std::string xmlns = _xmlns.toUTF8();
				coral_assert(!xmlns.empty());

				autounref<coral::Package> L;
				try {
					L = coral::Package::__getLanguageByXMLNS__(xmlns, false);
				} catch (coral::language_not_found_error& e) {
					;
				}
				if (L.get()) {
					coral::Language* Ltmp = dynamic_cast<coral::Language*>(L.get());
					coral_assert(Ltmp);
					coral_assert(Ltmp->getVersion() == version);
					coral_assert(Ltmp->getRevision() == revision);
					ret = L.get();
					if (old_one) *old_one = true;
					setCorrespondingElement(Ltmp, obj);
				} else {
					coral::Language* lang
						= new coral::Language(name_utf8,
								      version,
								      revision);
					ret = lang;
					setCorrespondingElement(lang, obj);
					lang->setXMLNS(xmlns);
				}
				/* BUG BOGUS REF!
				 * This is needed if we create multiple
				 * Language objects. We only return one
				 * of them, and it has the ref. The rest
				 * are auto-unreffed at the end of smd2lang
				 * and thus havoc starts...
				 */
				ret.get()->ref();
			} else {
				if (parent) {
					try {
						ret = parent->getElement(name_utf8.c_str());
					} catch (...) {
						ret = NULL;
					}
					
					if (ret.get()) {
						if (old_one) *old_one = true;
						ret.get()->ref();
						setCorrespondingElement(dynamic_cast<coral::Package*>(ret.get()), obj);
						goto out_pkg;
					}
				}
				coral::Package* e_pkg = new coral::Package(parent, name_utf8);
				ret = e_pkg;
				setCorrespondingElement(e_pkg, obj);

				const coral::unicodestring& _xmlns = estr(obj, "URInamespace");
				const std::string xmlns = _xmlns.toUTF8();
				coral_assert(!xmlns.empty());
				e_pkg->setXMLNS(xmlns);
			}
		out_pkg:
			;

		} else {
			if (parent && obj->oclIsKindOf(SMD01::AbstractElementDefinition)) {
				try {
					ret = parent->getElement(name_utf8.c_str());
				} catch (...) {
					ret = NULL;
				}
				if (ret.get()) {
					if (old_one) *old_one = true;
					ret.get()->ref();
					setCorrespondingElement(dynamic_cast<coral::Meta2ModelBridge*>(ret.get()), obj);
					goto out;
				}
			}
			if (obj->oclIsKindOf(SMD01::EnumerationDefinition)) {
				coral_assert(parent);
				coral::EnumerationDefinition* e_enum =
					new coral::EnumerationDefinition(parent,
								 name_utf8.c_str());
				ret = e_enum;
				setCorrespondingElement(e_enum, obj);
				coral::Iterator m(obj->__getSlotRO__("values"));
				coral::ref_counted* _child;
				while ((_child = m.next()) != NULL) {
					coral::ElementString* e =
						dynamic_cast<coral::ElementString*>(_child);
					coral_assert(e);
					const coral::unicodestring& value(e->__getString__());
					e_enum->__addEnum__(value.toUTF8().c_str());
				}
			} else if (obj->oclIsKindOf(SMD01::DatatypeDefinition)) {
				coral_assert(parent);
				coral_assert(0);
			} else if (obj->oclIsKindOf(SMD01::ElementDefinition)) {
				coral_assert(parent);
				coral::ElementDefinition* ed = new coral::ElementDefinition(parent, name_utf8.c_str());
				ret = ed;
				setCorrespondingElement(ed, obj);
			} else if (obj->oclIsKindOf(SMD01::Property)) {
				// Delayed creation
				properties.add(obj);
				return NULL;
			} else if (obj->oclIsKindOf(SMD01::Operation)) {
				// Delayed creation
				return NULL;
			} else {
				dlog << DEBUGLEVEL_ERROR
				     << "Converting unknown type "
				     << obj->__type__()->getName()
				     << " in smd2lang.cpp!"
				     << std::endl;
				coral_assert(0);
			}
		}
	out:
		/*
		 * Add to map and return
		 */
		coral::ref_counted* result = ret.get();
		coral_assert(result);
		coral_assert(e_o.find(obj) == e_o.end());
		e_o[obj] = result;
		ret.release();
		return result;
	}

	static coral::ref_counted* mk_obj(smd2coral_t& e_o,
				  coral::Set& properties,
				  coral::Element* obj) {
		coral_assert(obj);
		{
			smd2coral_t::const_iterator i = e_o.find(obj);
			if (i != e_o.end()) return (*i).second;
		}
		/*
		 * For every object, just try to get as high as possible
		 * until we hit a known object, or have to create a
		 * new Language object. Then, depending on the
		 * type of the parent, we unrecursively add new
		 * elements.
		 */
		coral::Element* parent = obj->__parent__();
		if (parent) {
			(void) mk_obj(e_o, properties, parent);
		}
		// Parents are now available, but e_o[obj] maybe isn't
		{
			smd2coral_t::const_iterator i = e_o.find(obj);
			if (i != e_o.end()) return (*i).second;
		}

		// Note that cr_obj returns NULL for delayed creation
		// of some objects
		bool old_one = false;
		coral::ref_counted* elem = cr_obj(e_o, properties, obj, &old_one);

		/*
		 * If this obj's elem has already been created before
		 */
		if (old_one) {
			coral_assert(elem);
			return elem;
		}

		/*
		 * Now, when e_o[obj] and parents are valid,
		 * we can check across associations and children also
		 */

		if (obj->oclIsKindOf(SMD01::Package)) {
			coral_assert(elem);
			if (obj->oclIsKindOf(SMD01::Language)) {
				coral::Language* L = dynamic_cast<coral::Language*>(elem);
				coral_assert(L);
				if (!L->getRoot()) {
					coral::Element* rooted
						= eelem(obj, "root_ed");
					coral_assert(rooted);
					coral::ElementDefinition* ed
						= dynamic_cast<coral::ElementDefinition*>(mk_obj(e_o, properties, rooted));
					coral_assert(ed);
					L->setRoot(ed);
				}
			}
			coral::Iterator p_children(obj->__getSlotRO__("imported_elements"));
			coral::Package* pkg = dynamic_cast<coral::Package*>(elem);
			coral::ref_counted* t1;
			while ((t1 = p_children.next()) != NULL) {
				coral::Element* p
					= dynamic_cast<coral::Element*>(t1);
				coral_assert(p);
				mk_obj(e_o, properties, p);
				coral::ref_counted* r = e_o[p];
				coral_assert(r);
				coral::AbstractElementDefinition* ed =
					dynamic_cast<coral::AbstractElementDefinition*>(r);
				coral_assert(ed);
				// Most likely problems here with cross-importing metamodels?
				pkg->importElement(ed);
			}
			
			coral::Iterator e_children(obj->__getSlotRO__("elements"));
			coral::ref_counted* t2;
			while ((t2 = e_children.next()) != NULL) {
				coral::Element* p
					= dynamic_cast<coral::Element*>(t2);
				coral_assert(p);
				(void) mk_obj(e_o, properties, p);
			}
		} else {
			if (obj->oclIsKindOf(SMD01::AbstractElementDefinition)) {
				coral_assert(elem);
				coral::AbstractElementDefinition* ed
					= dynamic_cast<coral::AbstractElementDefinition*>(elem);
				coral_assert(ed);
				if (eenum(obj, "isAbstract")) {
					ed->setAbstract(true);
				}
				{
					coral::Iterator sc(obj->__getSlotRO__("superclasses"));
					coral::ref_counted* t;
					while ((t = sc.next()) != NULL) {
						coral::Element* e
							= dynamic_cast<coral::Element*>(t);
						coral_assert(e);
						coral::ref_counted* _sc = mk_obj(e_o, properties, e);
						coral::AbstractElementDefinition* sclass
							= dynamic_cast<coral::AbstractElementDefinition*>(_sc);
						coral_assert(sclass);
						ed->addSuperclass(sclass);
					}
				}
				{
					coral::Iterator ops(obj->__getSlotRO__("operations"));
					coral::ref_counted* t;
					while ((t = ops.next()) != NULL) {
						coral::Element* e
							= dynamic_cast<coral::Element*>(t);
						coral_assert(e);
						(void) cr_python_function(ed, e);
					}
				}
 
			}
			if (obj->oclIsKindOf(SMD01::EnumerationDefinition)) {
				;
			} else if (obj->oclIsKindOf(SMD01::DatatypeDefinition)) {
				;
			} else if (obj->oclIsKindOf(SMD01::ElementDefinition)) {
				coral::ElementDefinition* ec = dynamic_cast<coral::ElementDefinition*>(elem);
				coral_assert(ec);
				if (eenum(obj, "MOFDatatype")) {
					ec->setMOFDatatype(true);
				}
				//dlog << DEBUGLEVEL_DEBUG << "Studying " << estr(obj, "name").toUTF8() << std::endl;
				coral::Iterator props(obj->__getSlotRO__("properties"));
				coral::ref_counted* t;
				while ((t = props.next()) != NULL) {
					coral::Element* e
						= dynamic_cast<coral::Element*>(t);
					(void) mk_obj(e_o, properties, e);
				}
			} else if (obj->oclIsKindOf(SMD01::Property)) {
				//dlog << DEBUGLEVEL_DEBUG << "Property " << estr(obj, "name").toUTF8() << std::endl;
				properties.add(obj);
				coral::Element* e1 = eelem(obj, "element_kind");
				if (!e1) {
					Element* parent_elem = obj->__parent__();
					coral::unicodestring name;
					if (parent_elem) name = estr(parent_elem, "name");
					dlog << DEBUGLEVEL_ERROR
					     << "ERROR No element_kind set for " << name << (name.size() > 0 ? ":" : "")
					     << estr(obj, "name")
					     << std::endl;
				}
				coral_assert(e1);
				(void) mk_obj(e_o, properties, e1);
				coral::Element* e2 = eelem(obj, "owner");
				coral_assert(e2);
				(void) mk_obj(e_o, properties, e2);
			} else if (obj->oclIsKindOf(SMD01::Operation)) {
				return NULL;
			} else {
				coral_assert(0);
			}
		}
		return elem;
	}

	coral::ref_counted* mk_prop(smd2coral_t& e_o, coral::Element* obj) {
		coral_assert(obj);
		coral_assert(obj->oclIsKindOf(SMD01::Property));
		{
			smd2coral_t::iterator i = e_o.find(obj);
			if (i != e_o.end()) return (*i).second;
		}
		coral::ElementDefinition* owner;
		coral::AbstractElementDefinition* element_kind;
		coral::Element* g_owner = eelem(obj, "owner");
		coral::Element* g_element_kind = eelem(obj, "element_kind");
		coral_assert(e_o.find(g_owner) != e_o.end());
		coral_assert(e_o.find(g_element_kind) != e_o.end());
		owner = dynamic_cast<coral::ElementDefinition*>(e_o[g_owner]);
		//dlog << DEBUGLEVEL_DEBUG << eelem(obj, "element_kind") << std::endl;
		element_kind = dynamic_cast<coral::AbstractElementDefinition*>(e_o[g_element_kind]);
		coral::Element* opposite = eelem(obj, "opposite");
		const std::string name(estr(obj, "name").toUTF8());
		std::string opposite_name;
		if (opposite) {
			opposite_name = estr(opposite, "name").toUTF8();
		}

		//dlog << DEBUGLEVEL_DEBUG << owner->getName() << ":" << name << " --> " << opposite_name << std::endl;

		coral_assert(owner);
		coral_assert(element_kind);

		/* Do we already have the property? */
		coral::Property* p = owner->getPropertyByName(name.c_str());
		if (!p) {
			bool isOrdered = eenum(obj, "isOrdered");
			bool isBag = eenum(obj, "isBag");
			bool isBidirectional = false;
			bool isUnserializable = eenum(obj, "isUnserializable");
			std::string default_value = estr(obj, "defaultValue").toUTF8();
			if (!opposite_name.empty()) {
				isBidirectional = true;
			}
			int lower = eint(obj, "multiplicity_lower");
			int upper = eint(obj, "multiplicity_upper");
			int type = eenum(obj, "type");
			int kind = 0;
			if (isOrdered) kind |= coral::kind_t::KIND_ORDERED;
			if (isBag) kind |= coral::kind_t::KIND_BAG;
			if (isBidirectional) kind |= coral::kind_t::KIND_BIDI;
			if (type == 0) kind |= coral::kind_t::KIND_ASSOCIATION;
			if (type == 1) kind |= coral::kind_t::KIND_COMPOSITION;
			if (isUnserializable) kind |= coral::kind_t::KIND_NOSERIALIZE;
			p = new coral::Property(name.c_str(),
						element_kind,
						opposite_name.c_str(),
						lower,
						upper,
						kind,
						default_value);
			coral::autounref<coral::ref_counted> _p(p);
			setCorrespondingElement(p, obj);
			coral_assert(e_o.find(obj) == e_o.end());
			e_o[obj] = p;
			/*
			 * Must come here.
			 * Opposing properties won't be created
			 * correctly otherwise in
			 * Property.__findOtherProperty(). :(
			 */
			owner->addPropertyRecursive(p);
			_p.release();

		} else {
			coral_assert(e_o.find(obj) == e_o.end());
			e_o[obj] = p;
			p->ref();
			setCorrespondingElement(p, obj);
		}
		
		if (opposite) {
			mk_prop(e_o, opposite);
		}
		return p;
	}

	autounref<coral::Language> smd2lang(coral::Element* alanguage) throw (std::exception) {
		if (!alanguage) return autounref<coral::Language>(NULL);
		DEBUG_MAY_FAIL(dlog << DEBUGLEVEL_DEBUG
			       << "DEBUG smd2lang: Converting SMD model "
			       << estr(alanguage, "name")
			       << " version " << eint(alanguage, "version")
			       << "." << eint(alanguage, "revision")
			       << "." << std::endl);
		smd2coral_t e_o;
		coral::Set properties;
		coral::AtomIterator i(alanguage);
		coral::Atom* a;
		while ((a = i.next()) != NULL) {
			coral::Element* obj = dynamic_cast<coral::Element*>(a);
			coral_assert(obj);
			(void) mk_obj(e_o, properties, obj);
		}
		/*
		 * Now we have created
		 * - Languages
		 * - Packages
		 * - AbstractElementDefinitions
		 * - the ED hierarchy
		 * - packages-elements hierarchy
		 * - operations
		 * Missing:
		 * - Properties
		 */
		coral::ref_counted* t;
		coral::Iterator ocl(&properties);
		while ((t = ocl.next()) != NULL) {
			(void) mk_prop(e_o, dynamic_cast<coral::Element*>(t));
		}

		/*
		 * Sigh.. Unfortunately it just might be that the
		 * order of properties is not correct for MOF datatypes
		 */
		for (smd2coral_t::iterator i = e_o.begin();
		     i != e_o.end();
		     ++i) {
			coral::Element* obj =
				dynamic_cast<coral::Element*>(i->first);
			coral_assert(obj);
			if (!obj->oclIsKindOf(SMD01::ElementDefinition)) continue;
			coral::ElementDefinition* ed
				= dynamic_cast<coral::ElementDefinition*>(e_o[obj]);
			coral_assert(ed);
			if (!ed->isMOFDatatype()) continue;
			coral::Iterator props(obj->__getSlotRO__("properties"));
			std::vector<coral::Property*> correct_order;
			coral::ref_counted* _t2;
			while ((_t2 = props.next()) != NULL) {
				coral::Element* t2 = dynamic_cast<coral::Element*>(_t2);
				coral_assert(t2);
				coral::ref_counted* _p = e_o[t2];
				coral_assert(_p);
				coral::Property* p =
					dynamic_cast<coral::Property*>(_p);
				coral_assert(p);
				correct_order.push_back(p);
			}
			/*
			 * Add back anonymous properties
			 */
			const coral::ElementDefinition::properties_type* aproperties
				= ed->getProperties();
			for (coral::ElementDefinition::properties_type::const_iterator j
				     = aproperties->begin();
			     j != aproperties->end();
			     ++j) {
				coral::Property* p = (*j).second;
				if (!p->isAnonymous()) continue;
				correct_order.push_back(p);
			}
			ed->__setCorrectPropertiesOrder(correct_order);
		}
		
		smd2coral_t::iterator findlang = e_o.find(alanguage);
		coral_assert(findlang != e_o.end());
		coral::Language* L
			= dynamic_cast<coral::Language*>((*findlang).second);
		coral_assert(L);

		DEBUG_MAY_FAIL(dlog << DEBUGLEVEL_DEBUG
			       << "DEBUG smd2lang: Converted  SMD model "
			       << estr(alanguage, "name")
			       << " version " << eint(alanguage, "version")
			       << "." << eint(alanguage, "revision")
			       << "." << std::endl);
		L->ref();
		return autounref<coral::Language>(L);
	}
}

NAMESPACE_END();
