package com.baselet.generator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.baselet.control.basics.geom.Rectangle;
import com.baselet.control.config.ConfigClassGen;
import com.baselet.control.enums.ElementId;
import com.baselet.control.enums.generator.FieldOptions;
import com.baselet.control.enums.generator.MethodOptions;
import com.baselet.control.enums.generator.SignatureOptions;
import com.baselet.diagram.CurrentDiagram;
import com.baselet.diagram.DiagramHandler;
import com.baselet.diagram.FontHandler;
import com.baselet.diagram.Notifier;
import com.baselet.element.ElementFactorySwing;
import com.baselet.element.interfaces.GridElement;
import com.baselet.generator.java.Accessible.AccessFlag;
import com.baselet.generator.java.Field;
import com.baselet.generator.java.JavaClass;
import com.baselet.generator.java.JavaClass.ClassRole;
import com.baselet.generator.java.Method;
import com.baselet.generator.java.bcel.BcelJavaClass;
import com.baselet.generator.java.jp.ClassParserException;
import com.baselet.generator.java.jp.JpJavaClass;
import com.baselet.generator.sorting.AlphabetLayout;
import com.baselet.generator.sorting.HeightLayout;
import com.baselet.generator.sorting.PackageLayout;
import com.baselet.generator.sorting.RelationLayout;
import com.baselet.generator.sorting.SortableElement;
import com.baselet.gui.command.AddElement;

/**
 * Creates a class element from a filename pointing to a .class or .java file according to UML standards,
 * adds the class to the current diagram and resizes this class element to minimum size where all text is visible.
 *
 * @author Lisi Bluemelhuber
 *
 */
public class ClassDiagramConverter {

	private final int GRIDSIZE;

	public ClassDiagramConverter() {
		GRIDSIZE = CurrentDiagram.getInstance().getDiagramHandler().getGridSize();
	}

	public static String convertFailuresToString(List<Exception> failures) {
		StringBuilder sb = new StringBuilder();
		for (Exception failure : failures) {
			if (sb.length() > 0) {
				sb.append('\n');
			}
			sb.append(failure.getMessage());
		}
		return sb.toString();
	}

	public void createClassDiagrams(List<String> filesToOpen) {
		List<Exception> failures = new ArrayList<Exception>();
		List<SortableElement> elements = new ArrayList<SortableElement>();
		for (String filename : filesToOpen) {
			try {
				SortableElement element = createElement(filename);
				if (element != null) {
					elements.add(element);
				}
			} catch (Exception e) {
				failures.add(e);
			}
		}

		if (!failures.isEmpty()) {
			Notifier.getInstance().showError(ClassDiagramConverter.convertFailuresToString(failures));
			return; // if errors are in any of the files don't add any of them
		}

		switch (ConfigClassGen.getInstance().getGenerateClassSortings()) {
			case PACKAGE:
				new PackageLayout().layout(elements);
				break;
			case ALPHABET:
				new AlphabetLayout().layout(elements);
				break;
			case RELATIONS:
				new RelationLayout().layout(elements);
				break;
			default:
				new HeightLayout().layout(elements); // by height
		}

		addElementsToDiagram(elements);
	}

	private SortableElement createElement(String filename) throws Exception {
		JavaClass parsedClass = parseFile(filename);
		if (parsedClass == null) {
			return null;
		}

		String propertiesText = getElementProperties(parsedClass);
		List<String> propList = Arrays.asList(propertiesText.split("\n"));
		Rectangle initialSize = adjustSize(propList);
		GridElement clazz = ElementFactorySwing.create(ElementId.UMLClass, initialSize, propertiesText, null, CurrentDiagram.getInstance().getDiagramHandler());
		return new SortableElement(clazz, parsedClass);
	}

	private void addElementsToDiagram(List<SortableElement> elements) {
		DiagramHandler handler = CurrentDiagram.getInstance().getDiagramHandler();

		for (SortableElement e : elements) {
			new AddElement(e.getElement(),
					handler.realignToGrid(e.getElement().getRectangle().x),
					handler.realignToGrid(e.getElement().getRectangle().y), false).execute(handler);
		}
		handler.setChanged(true);
	}

	/**
	 * Adjusts a Class GridElement to the minimum size where all text is visible.
	 *
	 * @param clazz
	 * @return
	 */
	private Rectangle adjustSize(List<String> strings) {
		// GridElement clazz not yet fully initialized, cannot call clazz.getHandler();
		FontHandler fontHandler = CurrentDiagram.getInstance().getDiagramHandler().getFontHandler();

		int width = 0;
		int height = strings.size();
		double heightTweaker = 0.1;
		for (String string : strings) {
			if (string.isEmpty()) {
				heightTweaker += 1;
			}
			else if (string.equals("--")) {
				heightTweaker += 0.5;
			}
			if (fontHandler.getTextWidth(string) > width) {
				width = (int) (fontHandler.getTextWidth(string) + fontHandler.getDistanceBetweenTexts()) + 10;
			}
		}
		height = (int) (fontHandler.getFontSize() + fontHandler.getDistanceBetweenTexts()) * (height - (int) heightTweaker);

		return new Rectangle(0, 0, align(width), align(height)); // width&height must be multiples of grid size
	}

	private int align(int n) {
		return n - n % GRIDSIZE + GRIDSIZE;
	}

	private String getElementProperties(JavaClass parsedClass) {
		StringBuilder sb = new StringBuilder("");

		createTopSection(parsedClass, sb);
		sb.append("--\n");

		createFieldSection(parsedClass, sb);
		sb.append("--\n");

		createMethodSection(parsedClass, sb);
		sb.append("--\n");

		return sb.toString();
	}

	private void createMethodSection(JavaClass parsedClass, StringBuilder sb) {
		for (Method method : parsedClass.getMethods()) {
			if (ConfigClassGen.getInstance().getGenerateClassMethods() == MethodOptions.PUBLIC && method.getAccess() == AccessFlag.PUBLIC) {
				sb.append(getMethodString(method));
			}
			else if (ConfigClassGen.getInstance().getGenerateClassMethods() == MethodOptions.ALL) {
				sb.append(getMethodString(method));
			}
		}
	}

	private String getMethodString(Method method) {
		if (ConfigClassGen.getInstance().getGenerateClassSignatures() == SignatureOptions.PARAMS_ONLY) {
			return method.getAccess() + method.getName() + "(" + method.getSignature() + ")\n";
		}
		else if (ConfigClassGen.getInstance().getGenerateClassSignatures() == SignatureOptions.RETURN_ONLY) {
			return method.getAccess() + method.getName() + ": " + method.getReturnType() + "\n";
		}
		else {
			return method.getAccess() + method.getName() + "(" + method.getSignature() + "): " + method.getReturnType() + "\n";
		}
	}

	private void createFieldSection(JavaClass parsedClass, StringBuilder sb) {
		for (Field field : parsedClass.getFields()) {
			if (ConfigClassGen.getInstance().getGenerateClassFields() == FieldOptions.PUBLIC && field.getAccess() == AccessFlag.PUBLIC) {
				sb.append(field.getAccess()).append(field.getName()).append(": ").append(field.getType()).append("\n");
			}
			else if (ConfigClassGen.getInstance().getGenerateClassFields() == FieldOptions.ALL) {
				sb.append(field.getAccess()).append(field.getName()).append(": ").append(field.getType()).append("\n");
			}
		}
	}

	private void createTopSection(JavaClass parsedClass, StringBuilder sb) {
		ClassRole role = parsedClass.getRole();
		if (role == ClassRole.INTERFACE) {
			sb.append("<<").append(role).append(">>\n").append(AlphabetLayout.getClassName(parsedClass));
		}
		else if (role == ClassRole.ABSTRACT) {
			sb.append("/").append(AlphabetLayout.getClassName(parsedClass)).append("/");
		}
		else {
			sb.append(AlphabetLayout.getClassName(parsedClass));
		}
		sb.append("\n");
	}

	private JavaClass parseFile(String filename) throws Exception {
		String extension = getExtension(filename);
		if (extension.equals("java")) {
			return new JpJavaClass(filename);
		}
		else if (extension.equals("class")) {
			return new BcelJavaClass(filename);
		}
		else {
			throw new ClassParserException("Unknown extension " + extension + " of file " + filename);
		}
	}

	private String getExtension(String filename) {
		int dotPosition = filename.lastIndexOf(".");
		return filename.substring(dotPosition + 1, filename.length());
	}
}
