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

#include <coral/core/Element.h>
#include "xmi_reader_2.h"
#include <coral/lang/XMI/XMI.h>
#include <coral/core/config.h>
#include "xmi_writer_2.h"

#include <string>
#include <sstream>
#include <fstream>
#include <set>

#include <coral/core/Project.h>
#include "c_time.h"

NAMESPACE_BEGIN(coral);
NAMESPACE_BEGIN(io);


static const std::string eclipse_ns =
  "http://www.eclipse.org/emf/2002/Ecore";
		
XMIStreamer2::XMIStreamer2() {
	;
}

XMIStreamer2::~XMIStreamer2() throw () { ; }

void XMIStreamer2::writeField_xmi2(std::ostream& out,
				   const coral::Property* p,
				   const coral::ElementPrimitive* e,
				   int margin) {
	std::string spaces(margin, ' '); // could be faster...
	const char* pname = p->getName();
	out << spaces
	    << "<" << pname << ">"
	    << escape(e->__str__().toUTF8())
	    << "</" << pname << ">\n";
}
namespace {
	bool ok_to_use(const std::set<std::string>& xmins,
		       const std::string& candidate) {
		return xmins.find(candidate) == xmins.end();
	}
}

/*
 * Invents unique XML Namespace Prefix, given the set of
 * namespaces already used.
 * First, the Package name is tried. Then, pseudorandom characters
 * are added to it until a valid unique prefix has been generated.
 */
std::string XMIStreamer2::invent_unique_xmi_ns_prefix(
	const packages_used_t& pu,
	const Package* p) {
	coral_assert(p);
	std::set<std::string> xmins;
	for (packages_used_t::const_iterator i = pu.begin();
	     i != pu.end();
	     ++i) {
		coral_assert((*i).pkg != p);
		xmins.insert((*i).xmiNsPrefix);
	}

	std::string cand = p->getName();
	// doesn't need to be too random...
	unsigned int rand_seed = reinterpret_cast<unsigned long>(p);
	do {
		if (ok_to_use(xmins, cand)) break;
		cand += "_";
		do {
			cand += 'A' + rand_r(&rand_seed) % 26;
		} while (!ok_to_use(xmins, cand));
	} while (0);

	return cand;
}

/*
 * XM*I* namespace prefixes must be unique in an XM*L*
 * document. They are specific to packages, not to languages,
 * according to XM*I*.
 * Returns .first == true if we had to create the xmlns.
 */
std::pair<bool, const std::string>
XMIStreamer2::get_ns_prefix(const Package* P) {
	for (packages_used_t::reverse_iterator i
		     = packages_used.rbegin();
	     i != packages_used.rend();
	     ++i) {
		if ((*i).pkg == P)
			return std::make_pair(false, (*i).xmiNsPrefix);
	}

	lu_contents_t n;
	n.depth = cur_depth;
	n.pkg = P;
	n.xmiNsPrefix = invent_unique_xmi_ns_prefix(packages_used,
						    P);
	packages_used.push_back(n);
	return std::make_pair(true, packages_used.back().xmiNsPrefix);
}

const std::string XMIStreamer2::writeXMLNS(const Package* P,
					   std::ostream& out) {
	std::pair<bool, const std::string>
		prefix(get_ns_prefix(P));
	if (prefix.first) {
		std::string url(P->getXMLNS());
		out << " xmlns:" << prefix.second << "=";
		if (url.size() == 0) {
			/* BUG xmlns not correct, but we set it just so
			 * loaders are happy... */
			url = "unknown.xml";
		}
		out << qescape(url);
	}
	return prefix.second;
}

bool all_roots_are_eclipse_elements(const Collection* roots) {
	if (roots->size() == 0) return false;
	for (Iterator i(roots); const ref_counted* r = i.next(); ) {
		const Element* e = dynamic_cast<const Element*>(r);
		if (!e) continue;
		bool iseclipse =
			(e->__type__()->__package__()->getXMLNS() == eclipse_ns);
		if (!iseclipse) return false;
		
	}
	return true;
}

void XMIStreamer2::saveModel(const Collection* roots,
			     std::ostream& out)
	throw (std::exception) {
	coral_assert(roots);

	packages_used.clear();
	lu_contents_t n;
	n.depth = 0;
	n.pkg = XMI::language;
	n.xmiNsPrefix = "xmi";
	packages_used.push_back(n);
	cur_depth = 1;
		
	autounref<coral::Set> all(new coral::Set());

	std::set<const coral::Element*> saved_elements;

	Iterator i(roots);
	ref_counted* _t;
	while ((_t = i.next()) != NULL) {
		Element* e = dynamic_cast<Element*>(_t);
		coral_assert(e);

		autounref<coral::Set> o(e->__getAllParts__());
		all->unionWith(o.get());
	}

	/*
	 * Get RFC 822 local time string in the C locale
	 */
	std::string curtime(coral::io::getCurrentRFC822Time());

	/*
	 * XMI header
	 */
	out << "<?xml version='1.0' encoding='UTF-8'?>\n"
	    << "<xmi:XMI xmlns:xmi=\"http://schema.omg.org/spec/XMI/2.0\"\n"
	    << "         xmlns:xlink=\"http://www.w3.org/1999/Xlink\"\n"
	    << "         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
	    << "         version='2.0' timestamp='" << curtime << "'>\n";

	/*
	 * Eclipse compat.
	 * Eclipse doesn't react well to
	 * the standard XMI document header, and doesn't seem to
	 * understand the XMI 2.0 namespace, although it can be declared
	 * above successfully
	 * Anyway. Just do not print any documentation if all
	 * roots are eclipse elements. Cute, huh?
	 */
	if (!all_roots_are_eclipse_elements(roots)) {
		out << " <documentation>\n"
		    << "  <exporter>Coral</exporter>\n"
		    << "  <exporterVersion>"
		    << coral::version
		    << "</exporterVersion>\n"
		    << " </documentation>\n"
			;
	}

	/*
	 * Save elements recursively
	 */
	kind_t kind;
	kind.kind = kind_t::KIND_COMPOSITION;

	Iterator i2(roots);
	while ((_t = i2.next()) != NULL) {
		coral::Element* e = dynamic_cast<Element*>(_t);
		coral_assert(e);
		++cur_depth;
		saveElement(saved_elements,
			    all.get(), out, e, 1, "", kind, true,
			    e->__type__()->__language__()->getRoot());
		--cur_depth;
		/* Remove old languages */
		while (!packages_used.empty()
		       && packages_used.back().depth > cur_depth) {
			packages_used.pop_back();
		}
	}

	/*
	 * XMI footer
	 */
	out << "</xmi:XMI>\n";
}

static void writeType(std::ostream& out,
		      const Element* elem,
		      const AbstractElementDefinition* should_be,
		      const std::string& xminsprefix,
		      bool iseclipse) {
	const AbstractElementDefinition* ed = elem->__type__();
	if (should_be && ed != should_be) {
		out << " xmi:type=\"";
		const Package* p = ed->__package__();
		if (p != should_be->__package__()) {
			out << xminsprefix << ":";
		}
		out << elem->__type__()->getName()
		    << "\"";

	}

	/* Eclipse compat */
	if (iseclipse) {
	  out << " xsi:type=\"";
	  out << xminsprefix << ":" << elem->__type__()->getName()
	      << "\"";
	}
}

void XMIStreamer2::saveElement(std::set<const Element*>& saved_elements,
			       Set* all,
			       std::ostream& out,
			       const Element* elem,
			       int margin,
			       const char* slotname,
			       kind_t asskind,
			       bool require_ns_prefix,
			       const AbstractElementDefinition* should_be) {
	coral_assert(elem);
	const ElementDefinition* type = elem->__type__();

	bool is_xmi_language =
		(type == XMI::Extension);
	const char separator = ':';

	/*
	 * Eclipse compat
	 */
	bool iseclipse =
	  (elem->__type__()->__package__()->getXMLNS() == eclipse_ns);

	/*
	 * Write element header, <UML:Class...
	 */
	std::string spaces(margin, ' '); // could be faster...
	out << spaces << "<";
	if (require_ns_prefix) {
		if (is_xmi_language) {
			out << "xmi:Extension";
			goto cont;
		} else {
			const Package* pkg = type->__package__();
			std::pair<bool, const std::string>
				prefix(get_ns_prefix(pkg));
			out << prefix.second << separator;
			if (prefix.first) {
				/* NOTE the one we just created
				 * is popped so
				 * writeXMLNS can do its job...
				 * ugly.
				 */
				packages_used.pop_back();
			}
		}
	}
	if (slotname[0] != '\0') {
		out << slotname;
	} else {
		out << type->getName();
	}

	{
		const Package* pkg = type->__package__();
		const std::string& xminsprefix = writeXMLNS(pkg, out);
		if (slotname[0] != '\0') {
		  writeType(out, elem, should_be, xminsprefix,
			    iseclipse);
		}

	}

	if (asskind.isAssociation() && !all->includes(elem)) {
		Project* st = elem->__getRootsProject__();
		std::string filename;
		if (st) filename = st->getHrefFilename();

		std::ostringstream name;
		/*
		 * When copy-pasting, we use either the filename
		 * or a pointer value to determine the Project.
		 * So this part is important.
		 */
		if (filename.c_str()[0] == '\0') {
			std::ostringstream s;
			s << st;
			filename = s.str();
		}

		name << filename
		     << "#xpointer((//*[@xmi:uuid='"
		     << elem->__XMIid__()
		     << "'])[1])";
		out << " xlink:href="<< qescape(name.str())
		    << " xlink:type=\"simple\" />\n";
		return;
	}
	if (asskind.isComposition()) {
		out << " xmi:id=" << qescape(getID(elem))
		    << " xmi:uuid=" << qescape(elem->__XMIid__());
		if (saved_elements.insert(elem).second == false) {
			std::ostringstream s;
			s << "Element has multiple parents, can't save! UUID: "
			  << elem->__XMIid__()
			  << std::endl
			  << " Type: " << type->getName()
			  << " composition property used: " << slotname
			  << std::endl;
			throw save_error(s.str());
		}
	} else {
		out << " xmi:idref=" << qescape(getID(elem))
		    << " />\n";
		return;
	}

 cont:

	std::set<const Property*> ignore_these_properties;
	/*
	 * Attributes and MULT_SINGLE compositions and associations
	 */
	const ElementDefinition::properties_type* mmproperties
		= type->getProperties();
	for (ElementDefinition::properties_type::const_iterator i
		     = mmproperties->begin();
	     i != mmproperties->end();
	     ++i) {
		const Property* mm = (*i).second;
		if (mm->isUnserializable()) continue;
		if (mm->isAnonymous()) continue;
		const AbstractElementDefinition* mdaed
			= mm->getTarget();

		if (!mm->isAtMostOne()) {
		  /*
		   * Eclipse compat
		   */
		  if (!iseclipse) continue;
		  const Property* poppo =
		    mm->getOppositeProperty();
		  if (mm->isComposition()) continue;
		  if (poppo && poppo->isComposition()) continue;
		  if (mdaed->isDatatypeDefinition()) continue;
		  if (mm->isAssociation()) {
		    const Slot& s =
		      elem->__getSlotRO__(mm->getName());
		    bool all_inside_file = true;
		    std::string output_this;
		    bool first = true;
		    for (Iterator it(s); const ref_counted* r = it.next(); ) {
		      if (!all->includes(r)) {
			all_inside_file = false;
			break;
		      }
		      const Element* ee = dynamic_cast<const Element*>(r);
		      if (!ee) continue;
		      if (!first) {
			output_this += " ";
		      }
		      output_this += getID(ee);
		      first = false;
		    }
		    if (all_inside_file) {
		      out << " " << mm->getName() << "=\""
			  << output_this << "\"";
		      ignore_these_properties.insert(mm);
		    }
		  }
		  continue;
		}

		const Slot& s =
			elem->__getSlotRO__(mm->getName());
		const Atom* attrib = s[0];
		if (!attrib) continue;
		const Property* poppo =
			mm->getOppositeProperty();
		std::ostringstream res;
		if (mdaed->isDatatypeDefinition()) {
			/*
			 * Skip broken enumerations
			 */
			if (mdaed->isEnumerationDefinition()
			    && ((static_cast<const coral::EnumerationDefinition*>(mdaed))->size() == 0)) {
				continue;
			}
			res << attrib->__str__().toUTF8();
		} else if (poppo && 
			   (poppo->isComposition())) {
			/* Don't save ref to parent element
			 * in compositions, or the attribute
			 * element either. */
			continue;
		} else {
			/*
			 * We take care of compositions later...
			 */
			if (mm->isComposition())
				continue;
			const Element* a = static_cast<const Element*>(attrib);
			/*
			 * Urk, special case for "attributes" that are outside of the
			 * current model, we can't use XLinks here?
			 */
			if (!all->includes(a)) {
			  /*
			   * Eclipse compat
			   */
			  if (iseclipse) {
			    Project* targetprj =
				    a->__getRootsProject__();
			    const std::string& atypename = a->__type__()->getName();
			    const char* prjname;
			    if (targetprj &&
				targetprj->getHrefFilename() == "coral/lang/ecore10/def/ecore_model.xmi") {
				    prjname = "http://www.eclipse.org/emf/2002/Ecore#//";
			    } else {
				    prjname = targetprj->getHrefFilename().c_str();
			    }
			    res << "ecore:" << atypename << " " << prjname << "#";

			    std::vector<std::string> names;
			    const Element* orig = a;
			    for (;;) {
				    const Element* next = a->__parent__();
				    // Don't write the topmost element's name
				    if (!next) break;
				    try {
					    const unicodestring& aname = a->__getSlotByName__("name")[0]->__getString__();
					    
					    names.push_back(aname.toUTF8());
					    // The '/' is the identifier separator, don't accept it...
					    if (names.back().find('/') != std::string::npos) {
						    throw coral::bad_slot("Found an '/' in Eclipse element name!");
					    }
				    } catch (coral::bad_slot& exc) {
					    /* Uh oh, we can't do this. Revert to just using xmi:id.
					     * NOTE! The "orig" element can be in a different file, which means
					     * that a generated xmi:id won't do! Argh! Just use the UUID instead
					     * and make sure getID creates xmi:id values for Eclipse elements with
					     * the xmi:uuid. */
					    names.clear();
					    res << orig->__XMIid__();
					    break;
				    }
				    a = next;
			    }
			    if (!names.empty()) {
				    res << "//";
				    for (std::vector<std::string>::reverse_iterator ii = names.rbegin(); ; ) {
					    res << *ii;
					    if (++ii == names.rend()) {
						    break;
					    }
					    res << "/";
				    }
			    }

			    ignore_these_properties.insert(mm);
			  } else {
				  continue;
			  }
			} else {
				res << getID(a);
			}
		}
		out << " " << mm->getName() << "="
		    << qescape(res.str());
	}
	out << ">\n";

	/*
	 * Other people's XMI extensions here
	 */

	/*
	 * Write out multivalued associations and single-valued
	 * element compositions
	 * Recurse throught compositions and attribute elements.
	 */

	for (ElementDefinition::properties_type::const_iterator i =
		     mmproperties->begin();
	     i != mmproperties->end();
	     ++i) {
		const Property* mm = (*i).second;
		if (mm->isUnserializable()) continue;
		if (mm->isAnonymous()) continue;
		if (ignore_these_properties.find(mm) != ignore_these_properties.end())
		  continue;
		const AbstractElementDefinition* mmtype
			= mm->getTarget();
		if (mm->isAtMostOne()) {
			if (mmtype->isDatatypeDefinition())
				continue;

			const Slot* mf = elem->__hasSlotByName__(mm->getName());
			if (!mf || mf->__len__() == 0) continue;
			const Atom* a = (*mf)[0];
			if (!a) continue;
			if (!mm->isComposition() && all->includes(a)) continue;
		}

		/*
		 * Faster, if nothing exists, or it has
		 * no length...
		 */
		const Slot* mf = elem->__hasSlotByName__(mm->getName());
		if (!mf || mf->__len__() == 0) continue;

		/*
		 * Also, remember to skip if this is an
		 * association back to the composition
		 */
		const Property* other
			= mm->getOppositeProperty();
		if (other
		    && other->isComposition()) {
			continue;
		}

		/*
		 * Where is the feature defined?
		 * <UML:GeneralizableElement.specialization>...
		 * Feature header.
		 */
		const AbstractElementDefinition* base =mm->getOwner();

		/*
		 * Hack! Avoid "<XMI:extension.our_contents>"
		 */
		bool children_require_ns_prefix = true;
		const char* children_slot_name = "";
		const AbstractElementDefinition* children_should_be = NULL;
		if (base != XMI::Extension
		    && mmtype != XMI::Extension) {
			children_require_ns_prefix = false;
			children_slot_name = mm->getName();
			children_should_be = mm->getTarget();
		}

		Iterator m(mf);
		const ref_counted* _one;
		while ((_one = m.next()) != NULL) {
			const Atom* _atom
				= dynamic_cast<const Atom*>(_one);
			coral_assert(_atom);
			/*
			 * This for multi-valued strings etc..
			 * Production rule 5c of XMI-DI 2.0
			 */
			if (const ElementPrimitive* p
			    = dynamic_cast<const ElementPrimitive*>(_atom)) {
				writeField_xmi2(out, mm, p, margin + 1);
				continue;
			}
			const Element* one
				= dynamic_cast<const Element*>(_one);
			coral_assert(one);
			++cur_depth;
			saveElement(saved_elements,
				    all,
				    out,
				    one,
				    margin + 1,
				    children_slot_name,
				    mm->getKindT(),
				    children_require_ns_prefix,
				    children_should_be);
			--cur_depth;
			/* Remove old languages */
			while (!packages_used.empty()
			       && packages_used.back().depth > cur_depth) {
				packages_used.pop_back();
			}
		}
	}
	/*
	 * Write footer, </UML:Class>...
	 */
	out << spaces << "</";
	if (is_xmi_language) {
		out << "xmi:Extension";
	} else {
		if (require_ns_prefix) {
			const Package* pkg = type->__package__();
			std::pair<bool, const std::string>
				prefix(get_ns_prefix(pkg));
			out << prefix.second << separator;
		}
		if (slotname[0] != '\0') {
			out << slotname;
		} else {
			out << type->getName();
		}
	}
	out << ">\n";
}


NAMESPACE_END();
NAMESPACE_END();
