package aQute.bnd.make.metatype;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import aQute.bnd.annotation.metatype.Configurable;
import aQute.bnd.annotation.metatype.Meta;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Annotation;
import aQute.bnd.osgi.ClassDataCollector;
import aQute.bnd.osgi.Clazz;
import aQute.bnd.osgi.Clazz.MethodDef;
import aQute.bnd.osgi.Descriptors.TypeRef;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.WriteResource;
import aQute.lib.io.IO;
import aQute.lib.tag.Tag;
import aQute.libg.generics.Create;
public class MetaTypeReader extends WriteResource {
	final Analyzer			reporter;
	Clazz					clazz;
	String					interfaces[];
	Tag						metadata	= new Tag("metatype:MetaData", new String[] {
												"xmlns:metatype", "http://www.osgi.org/xmlns/metatype/v1.1.0"
											});
	Tag						ocd			= new Tag(metadata, "OCD");
	Tag						designate	= new Tag(metadata, "Designate");
	Tag						object		= new Tag(designate, "Object");

	// Resource
	String					extra;

	// Should we process super interfaces
	boolean					inherit;

	// One time init
	boolean					finished;

	// Designate
	boolean					override;
	String					designatePid;
	boolean					factory;

	// AD
	Map<MethodDef,Meta.AD>	methods		= new LinkedHashMap<MethodDef,Meta.AD>();

	// OCD
	Annotation				ocdAnnotation;

	MethodDef				method;

	public MetaTypeReader(Clazz clazz, Analyzer reporter) {
		this.clazz = clazz;
		this.reporter = reporter;
		this.inherit = Processor.isTrue(reporter.getProperty("-metatype-inherit"));
	}

	static Pattern COLLECTION = Pattern.compile("(.*(Collection|Set|List|Queue|Stack|Deque))<(L.+;)>");

	private void addMethod(MethodDef method, Meta.AD ad) throws Exception {

		if (method.isStatic())
			return;

		// Set all the defaults.

		String rtype = method.getGenericReturnType();
		String id = Configurable.mangleMethodName(method.getName());
		String name = Clazz.unCamel(id);

		int cardinality = 0;

		if (rtype.endsWith("[]")) {
			cardinality = Integer.MAX_VALUE;
			rtype = rtype.substring(0, rtype.length() - 2);
		}
		if (rtype.indexOf('<') > 0) {
			if (cardinality != 0)
				reporter.error(
						"AD for %s.%s uses an array of collections in return type (%s), Metatype allows either Vector or array",
						clazz.getClassName().getFQN(), method.getName(), method.getType().getFQN());
			Matcher m = COLLECTION.matcher(rtype);
			if (m.matches()) {
				rtype = Clazz.objectDescriptorToFQN(m.group(3));
				cardinality = Integer.MIN_VALUE;
			}
		}

		Meta.Type type = getType(rtype);

		boolean required = ad == null || ad.required();
		String deflt = null;
		String max = null;
		String min = null;
		String[] optionLabels = null;
		String[] optionValues = null;
		String description = null;

		TypeRef typeRef = reporter.getTypeRefFromFQN(rtype);
		Clazz c = reporter.findClass(typeRef);
		if (c != null && c.isEnum()) {
			optionValues = parseOptionValues(c);
		}

		// Now parse the annotation for any overrides

		if (ad != null) {
			if (ad.id() != null)
				id = ad.id();
			if (ad.name() != null)
				name = ad.name();
			if (ad.cardinality() != 0)
				cardinality = ad.cardinality();
			if (ad.type() != null)
				type = ad.type();
			// if (ad.required() || ad.deflt() == null)
			// required = true;

			if (ad.description() != null)
				description = ad.description();

			if (ad.optionLabels() != null)
				optionLabels = ad.optionLabels();
			if (ad.optionValues() != null)
				optionValues = ad.optionValues();

			if (ad.min() != null)
				min = ad.min();
			if (ad.max() != null)
				max = ad.max();

			if (ad.deflt() != null)
				deflt = ad.deflt();
		}

		if (optionValues != null) {
			if (optionLabels == null || optionLabels.length == 0) {
				optionLabels = new String[optionValues.length];
				for (int i = 0; i < optionValues.length; i++)
					optionLabels[i] = Clazz.unCamel(optionValues[i]);
			}

			if (optionLabels.length != optionValues.length) {
				reporter.error("Option labels and option values not the same length for %s", id);
				optionLabels = optionValues;
			}
		}

		Tag adt = new Tag(this.ocd, "AD");
		adt.addAttribute("name", name);
		adt.addAttribute("id", id);
		adt.addAttribute("cardinality", cardinality);
		adt.addAttribute("required", required);
		adt.addAttribute("default", deflt);
		adt.addAttribute("type", type);
		adt.addAttribute("max", max);
		adt.addAttribute("min", min);
		adt.addAttribute("description", description);

		if (optionLabels != null && optionValues != null) {
			for (int i = 0; i < optionLabels.length; i++) {
				Tag option = new Tag(adt, "Option");
				option.addAttribute("label", optionLabels[i]);
				option.addAttribute("value", optionValues[i]);
			}
		}
	}

	private String[] parseOptionValues(Clazz c) throws Exception {
		final List<String> values = Create.list();

		c.parseClassFileWithCollector(new ClassDataCollector() {
			@Override
			public void field(Clazz.FieldDef def) {
				if (def.isEnum()) {
					values.add(def.getName());
				}
			}
		});
		return values.toArray(new String[0]);
	}

	Meta.Type getType(String rtype) {
		if (rtype.endsWith("[]")) {
			rtype = rtype.substring(0, rtype.length() - 2);
			if (rtype.endsWith("[]"))
				throw new IllegalArgumentException("Can only handle array of depth one");
		}

		if ("boolean".equals(rtype) || Boolean.class.getName().equals(rtype))
			return Meta.Type.Boolean;
		else if ("byte".equals(rtype) || Byte.class.getName().equals(rtype))
			return Meta.Type.Byte;
		else if ("char".equals(rtype) || Character.class.getName().equals(rtype))
			return Meta.Type.Character;
		else if ("short".equals(rtype) || Short.class.getName().equals(rtype))
			return Meta.Type.Short;
		else if ("int".equals(rtype) || Integer.class.getName().equals(rtype))
			return Meta.Type.Integer;
		else if ("long".equals(rtype) || Long.class.getName().equals(rtype))
			return Meta.Type.Long;
		else if ("float".equals(rtype) || Float.class.getName().equals(rtype))
			return Meta.Type.Float;
		else if ("double".equals(rtype) || Double.class.getName().equals(rtype))
			return Meta.Type.Double;
		else
			return Meta.Type.String;
	}

	class Find extends ClassDataCollector {

		@Override
		public void method(MethodDef mdef) {
			method = mdef;
			methods.put(mdef, null);
		}

		@Override
		public void annotation(Annotation annotation) {
			try {
				Meta.OCD ocd = annotation.getAnnotation(Meta.OCD.class);
				Meta.AD ad = annotation.getAnnotation(Meta.AD.class);
				if (ocd != null) {
					MetaTypeReader.this.ocdAnnotation = annotation;
				}
				if (ad != null) {
					assert method != null;
					// Fixup required since it is default true
					// but we have no access to these defaults.
					// i.e. the defaults are implemented in the code
					// thus here
					try {
						if (annotation.get("required") == null)
							annotation.put("required", true);
					} catch (Exception e) {
						// can fail ... see #514
					}

					methods.put(method, ad);
				}
			} catch (Exception e) {
				reporter.error("Error during annotation parsing %s : %s", clazz, e);
				e.printStackTrace();
			}
		}

	}

	@Override
	public void write(OutputStream out) throws IOException {
		try {
			finish();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		PrintWriter pw = IO.writer(out, UTF_8);
		pw.print("<?xml version='1.0' encoding='UTF-8'?>\n");
		metadata.print(0, pw);
		pw.flush();
	}

	void finish() throws Exception {
		if (!finished) {
			finished = true;
			clazz.parseClassFileWithCollector(new Find());
			Meta.OCD ocd = null;
			if (this.ocdAnnotation != null)
				ocd = this.ocdAnnotation.getAnnotation(Meta.OCD.class);
			else
				ocd = Configurable.createConfigurable(Meta.OCD.class, new HashMap<String,Object>());

			// defaults
			String id = clazz.getClassName().getFQN();
			String name = Clazz.unCamel(clazz.getClassName().getShortName());
			String description = null;
			String localization = id;
			boolean factory = this.factory;

			if (ocd.id() != null)
				id = ocd.id();

			if (ocd.name() != null)
				name = ocd.name();

			if (ocd.localization() != null)
				localization = ocd.localization();

			if (ocd.description() != null)
				description = ocd.description();

			String pid = id;
			if (override) {
				pid = this.designatePid;
				factory = this.factory;
				id = this.designatePid; // for the felix problems
			} else {
				if (ocdAnnotation.get("factory") != null) {
					factory = true;
				}
			}

			this.ocd.addAttribute("name", name);
			this.ocd.addAttribute("id", id);
			this.ocd.addAttribute("description", description);
			this.metadata.addAttribute("localization", localization);

			// do ADs
			for (Map.Entry<MethodDef,Meta.AD> entry : methods.entrySet())
				addMethod(entry.getKey(), entry.getValue());

			this.designate.addAttribute("pid", pid);
			if (factory)
				this.designate.addAttribute("factoryPid", pid);

			this.object.addAttribute("ocdref", id);

			if (inherit) {
				handleInheritedClasses(clazz);
			}
		}
	}

	private void handleInheritedClasses(Clazz child) throws Exception {
		TypeRef[] ifaces = child.getInterfaces();
		if (ifaces != null) {
			for (TypeRef ref : ifaces) {
				parseAndMergeInheritedMetadata(ref, child);
			}
		}
		TypeRef superClazz = child.getSuper();
		if (superClazz != null) {
			parseAndMergeInheritedMetadata(superClazz, child);
		}
	}

	private void parseAndMergeInheritedMetadata(TypeRef ref, Clazz child) throws Exception {
		if (ref.isJava())
			return;
		Clazz ec = reporter.findClass(ref);
		if (ec == null) {
			reporter.error(
					"Missing inherited class for Metatype annotations: %s from %s", ref, child.getClassName());
		} else {
			@SuppressWarnings("resource")
			MetaTypeReader mtr = new MetaTypeReader(ec, reporter);
			mtr.setDesignate(designatePid, factory);
			mtr.finish();
			for (Map.Entry<MethodDef,Meta.AD> entry : mtr.methods.entrySet())
				addMethod(entry.getKey(), entry.getValue());

			handleInheritedClasses(ec);
		}
	}

	public void setDesignate(String pid, boolean factory) {
		this.override = true;
		this.factory = factory;
		this.designatePid = pid;
	}

	@Override
	public long lastModified() {
		return 0;
	}
}
