package org.bimserver.ifc.step.serializer;

/******************************************************************************
 * Copyright (C) 2009-2019  BIMserver.org
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see {@literal<http://www.gnu.org/licenses/>}.
 *****************************************************************************/

import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.bimserver.emf.IdEObject;
import org.bimserver.ifc.IfcSerializer;
import org.bimserver.ifc.step.deserializer.IfcParserWriterUtils;
import org.bimserver.models.store.IfcHeader;
import org.bimserver.plugins.HeaderTakingSerializer;
import org.bimserver.plugins.PluginConfiguration;
import org.bimserver.plugins.serializers.ProgressReporter;
import org.bimserver.plugins.serializers.SerializerException;
import org.bimserver.utils.StringUtils;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.EEnum;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EcorePackage;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;

import nl.tue.buildingsmart.schema.EntityDefinition;

public class IfcStepSerializer extends IfcSerializer implements HeaderTakingSerializer {
	private static final byte[] NEW_LINE = "\n".getBytes(Charsets.UTF_8);
	private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(IfcStepSerializer.class);
	private static final EcorePackage ECORE_PACKAGE_INSTANCE = EcorePackage.eINSTANCE;
	private static final String NULL = "NULL";
	private static final String OPEN_CLOSE_PAREN = "()";
	private static final String ASTERISK = "*";
	private static final String PAREN_CLOSE_SEMICOLON = ");";
	private static final String DASH = "#";
	private static final String IFC_LOGICAL = "IfcLogical";
	private static final String IFC_BOOLEAN = "IfcBoolean";
	private static final String DOT = ".";
	private static final String COMMA = ",";
	private static final String OPEN_PAREN = "(";
	private static final String CLOSE_PAREN = ")";
	private static final String BOOLEAN_UNDEFINED = ".U.";
	private static final String DOLLAR = "$";
	private static final String WRAPPED_VALUE = "wrappedValue";
	
	private Iterator<IdEObject> iterator;
	private String headerSchema;
	private long writeCounter;
	private OutputStream outputStream;

	public IfcStepSerializer(PluginConfiguration pluginConfiguration) {
	}

	public void setHeaderSchema(String headerSchema) {
		this.headerSchema = headerSchema;
	}
	
	public boolean write(OutputStream outputStream, ProgressReporter progressReporter) throws SerializerException {
		try {
			this.outputStream = outputStream;
			if (getMode() == Mode.HEADER) {
				writeHeader();
				setMode(Mode.BODY);
				iterator = model.getValues().iterator();
				return true;
			} else if (getMode() == Mode.BODY) {
				if (iterator.hasNext()) {
					IdEObject next = iterator.next();
					// TODO this is ugly, there should be a filter in IfcModel
					// TODO also will cause errors when the last object is not of the right schema...
					while (next.eClass().getEPackage() != getPackageMetaData().getEPackage() && iterator.hasNext()) {
						next = iterator.next();
					}
					write(next);
					writeCounter++;
					if (progressReporter != null) {
						progressReporter.update(writeCounter, model.size());
					}
				} else {
					iterator = null;
					setMode(Mode.FOOTER);
					return write(outputStream, progressReporter);
				}
				return true;
			} else if (getMode() == Mode.FOOTER) {
				writeFooter();
				if (progressReporter != null) {
					progressReporter.update(model.size(), model.size());
				}
				setMode(Mode.FINISHED);
				return true;
			} else if (getMode() == Mode.FINISHED) {
				return false;
			}			
			return false;
		} catch (IOException e) {
			throw new SerializerException(e);
		}
	}

	private void writeFooter() throws IOException {
		println("ENDSEC;");
		println("END-ISO-10303-21;");
	}

	private void writeHeader() throws IOException {
		SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
		println("ISO-10303-21;");
		println("HEADER;");
		IfcHeader ifcHeader = getModel().getModelMetaData().getIfcHeader();
		if (ifcHeader == null) {
			Date date = new Date();
			println("FILE_DESCRIPTION ((''), '2;1');");
			println("FILE_NAME ('', '" + dateFormatter.format(date) + "', (''), (''), '', 'BIMserver', '');");
			println("FILE_SCHEMA (('" + headerSchema + "'));");
		} else {
			print("FILE_DESCRIPTION ((");
			print(StringUtils.concat(ifcHeader.getDescription(), "'", ", "));
			println("), '" + ifcHeader.getImplementationLevel() + "');");
			println("FILE_NAME ('" + ifcHeader.getFilename().replace("\\", "\\\\") + "', '" + dateFormatter.format(ifcHeader.getTimeStamp()) + "', (" + StringUtils.concat(ifcHeader.getAuthor(), "'", ", ") + "), (" + StringUtils.concat(ifcHeader.getOrganization(), "'", ", ") + "), '" + ifcHeader.getPreProcessorVersion() + "', '" + ifcHeader.getOriginatingSystem() + "', '"	+ ifcHeader.getAuthorization() + "');");

			//	println("FILE_SCHEMA (('" + ifcHeader.getIfcSchemaVersion() + "'));");
			println("FILE_SCHEMA (('" + headerSchema + "'));");
		}
		println("ENDSEC;");
		println("DATA;");
		// println("//This program comes with ABSOLUTELY NO WARRANTY.");
		// println("//This is free software, and you are welcome to redistribute it under certain conditions. See www.bimserver.org <http://www.bimserver.org>");
	}

	private void println(String line) throws IOException {
		byte[] bytes = line.getBytes(Charsets.UTF_8);
		outputStream.write(bytes, 0, bytes.length);
		outputStream.write(NEW_LINE, 0, NEW_LINE.length);
	}

	private void print(String text) throws IOException {
		byte[] bytes = text.getBytes(Charsets.UTF_8);
		outputStream.write(bytes, 0, bytes.length);
	}
	
	private void write(IdEObject object) throws SerializerException, IOException {
		EClass eClass = object.eClass();
		if (eClass.getEAnnotation("hidden") != null) {
			return;
		}
		if (eClass.getEPackage() != getPackageMetaData().getEPackage()) {
			return;
		}
		print(DASH);
		long convertedKey = getExpressId(object);
		if (convertedKey == -1) {
			throw new SerializerException("Going to serialize an object with id -1 (" + object.eClass().getName() + ")");
		}
		print(String.valueOf(convertedKey));
		print("= ");
		String upperCase = getPackageMetaData().getUpperCase(eClass);
		if (upperCase == null) {
			throw new SerializerException("Type not found: " + eClass.getName());
		}
		print(upperCase);
		print(OPEN_PAREN);
		boolean isFirst = true;
		EntityDefinition entityBN = getPackageMetaData().getSchemaDefinition().getEntityBN(object.eClass().getName());
		for (EStructuralFeature feature : eClass.getEAllStructuralFeatures()) {
			if (feature.getEAnnotation("hidden") == null && (entityBN != null && (!entityBN.isDerived(feature.getName()) || entityBN.isDerivedOverride(feature.getName())))) {
				EClassifier type = feature.getEType();
				if (type instanceof EEnum) {
					if (!isFirst) {
						print(COMMA);
					}
					writeEnum(object, feature);
					isFirst = false;
				} else if (type instanceof EClass) {
					EReference eReference = (EReference)feature;
					if (!getPackageMetaData().isInverse(eReference)) {
						if (!isFirst) {
							print(COMMA);
						}
						writeEClass(object, feature);
						isFirst = false;
					}
				} else if (type instanceof EDataType) {
					if (!isFirst) {
						print(COMMA);
					}
					writeEDataType(object, entityBN, feature);
					isFirst = false;
				}
			}
		}
		println(PAREN_CLOSE_SEMICOLON);
	}

	private void writeEDataType(IdEObject object, EntityDefinition entityBN, EStructuralFeature feature) throws SerializerException, IOException {
		if (entityBN != null && entityBN.isDerived(feature.getName())) {
			print(ASTERISK);
		} else if (feature.isMany()) {
			writeList(object, feature);
		} else {
			writeObject(object, feature);
		}
	}

	private void writeEClass(IdEObject object, EStructuralFeature feature) throws SerializerException, IOException {
		Object referencedObject = object.eGet(feature);
		if (referencedObject instanceof IdEObject && ((IdEObject)referencedObject).eClass().getEAnnotation("wrapped") != null) {
			writeWrappedValue(object, feature, ((EObject)referencedObject).eClass());
		} else {
			if (referencedObject instanceof EObject && model.contains((IdEObject) referencedObject)) {
				print(DASH);
				print(String.valueOf(getExpressId((IdEObject) referencedObject)));
			} else {
				EntityDefinition entityBN = getPackageMetaData().getSchemaDefinition().getEntityBN(object.eClass().getName());
				if (entityBN != null && entityBN.isDerived(feature.getName())) {
					print(ASTERISK);
				} else if (feature.isMany()) {
					writeList(object, feature);
				} else {
					writeObject(object, feature);
				}
			}
		}
	}

	private void writeObject(IdEObject object, EStructuralFeature feature) throws SerializerException, IOException {
		Object ref = object.eGet(feature);
		if (ref == null || (feature.isUnsettable() && !object.eIsSet(feature))) {
			EClassifier type = feature.getEType();
			if (type instanceof EClass) {
				EStructuralFeature structuralFeature = ((EClass) type).getEStructuralFeature(WRAPPED_VALUE);
				if (structuralFeature != null) {
					String name = structuralFeature.getEType().getName();
					if (name.equals(IFC_BOOLEAN) || name.equals(IFC_LOGICAL) || structuralFeature.getEType() == EcorePackage.eINSTANCE.getEBoolean()) {
						print(BOOLEAN_UNDEFINED);
					} else {
						print(DOLLAR);
					}
				} else {
					print(DOLLAR);
				}
			} else {
				if (type == EcorePackage.eINSTANCE.getEBoolean()) {
					print(BOOLEAN_UNDEFINED);
				} else if (feature.isMany()) {
					print("()");
				} else {
					print(DOLLAR);
				}
			}
		} else {
			if (ref instanceof EObject) {
				writeEmbedded((EObject) ref);
			} else if (feature.getEType() == ECORE_PACKAGE_INSTANCE.getEDouble()) {
				writeDoubleValue((Double)ref, object, feature);
			} else {
				IfcParserWriterUtils.writePrimitive(ref, outputStream);
			}
		}
	}

	private void writeDoubleValue(double value, EObject object, EStructuralFeature feature) throws SerializerException, IOException {
		if (model.isUseDoubleStrings()) {
			Object stringValue = object.eGet(object.eClass().getEStructuralFeature(feature.getName() + "AsString"));
			if (stringValue != null) {
				print((String)stringValue);
				return;
			}
		}
		IfcParserWriterUtils.writePrimitive(value, outputStream);
	}

	private void writeEmbedded(EObject eObject) throws SerializerException, IOException {
		EClass class1 = eObject.eClass();
		print(getPackageMetaData().getUpperCase(class1));
		print(OPEN_PAREN);
		EStructuralFeature structuralFeature = class1.getEStructuralFeature(WRAPPED_VALUE);
		if (structuralFeature != null) {
			Object realVal = eObject.eGet(structuralFeature);
			if (structuralFeature.getEType() == ECORE_PACKAGE_INSTANCE.getEDouble()) {
				writeDoubleValue((Double)realVal, eObject, structuralFeature);
			} else {
				IfcParserWriterUtils.writePrimitive(realVal, outputStream);
			}
		}
		print(CLOSE_PAREN);
	}

	private void writeList(IdEObject object, EStructuralFeature feature) throws SerializerException, IOException {
		List<?> list = (List<?>) object.eGet(feature);
		List<?> doubleStingList = null;
		if (feature.getEType() == EcorePackage.eINSTANCE.getEDouble() && model.isUseDoubleStrings()) {
			EStructuralFeature doubleStringFeature = feature.getEContainingClass().getEStructuralFeature(feature.getName() + "AsString");
			if (doubleStringFeature == null) {
				throw new SerializerException("Field " + feature.getName() + "AsString" + " not found");
			}
			doubleStingList = (List<?>) object.eGet(doubleStringFeature);
		}
		if (list.isEmpty()) {
			if (!feature.isUnsettable()) {
				print(OPEN_CLOSE_PAREN);
			} else {
				print("$");
			}
		} else {
			print(OPEN_PAREN);
			boolean first = true;
			int index = 0;
			for (Object listObject : list) {
				if (!first) {
					print(COMMA);
				}
				if ((listObject instanceof IdEObject) && model.contains((IdEObject)listObject)) {
					IdEObject eObject = (IdEObject) listObject;
					print(DASH);
					print(String.valueOf(getExpressId(eObject)));
				} else {
					if (listObject == null) {
						print(DOLLAR);
					} else {
						if (listObject instanceof IdEObject && feature.getEType().getEAnnotation("wrapped") != null) {
							IdEObject eObject = (IdEObject) listObject;
							Object realVal = eObject.eGet(eObject.eClass().getEStructuralFeature("wrappedValue"));
							if (realVal instanceof Double) {
								if (model.isUseDoubleStrings()) {
									Object stringVal = eObject.eGet(eObject.eClass().getEStructuralFeature("wrappedValueAsString"));
									if (stringVal != null) {
										print((String) stringVal);
									} else {
										IfcParserWriterUtils.writePrimitive(realVal, outputStream);
									}
								} else {
									IfcParserWriterUtils.writePrimitive(realVal, outputStream);
								}
							} else {
								IfcParserWriterUtils.writePrimitive(realVal, outputStream);
							}
						} else if (listObject instanceof EObject) {
							IdEObject eObject = (IdEObject) listObject;
							EClass class1 = eObject.eClass();
							EStructuralFeature structuralFeature = class1.getEStructuralFeature(WRAPPED_VALUE);
							if (structuralFeature != null) {
								Object realVal = eObject.eGet(structuralFeature);
								print(getPackageMetaData().getUpperCase(class1));
								print(OPEN_PAREN);
								if (realVal instanceof Double) {
									writeDoubleValue((Double)realVal, eObject, structuralFeature);
								} else {
									IfcParserWriterUtils.writePrimitive(realVal, outputStream);
								}
								print(CLOSE_PAREN);
							} else {
								if (feature.getEAnnotation("twodimensionalarray") != null) {
									writeList(eObject, eObject.eClass().getEStructuralFeature("List"));
								} else {
									LOGGER.info("Unfollowable reference found from " + object + "(" + object.getOid() + ")." + feature.getName() + " to " + eObject + "(" + eObject.getOid() + ")");
								}
							}
						} else {
							if (doubleStingList != null) {
								if (index < doubleStingList.size()) {
									String val = (String)doubleStingList.get(index);
									if (val == null) {
										IfcParserWriterUtils.writePrimitive(listObject, outputStream);
									} else {
										print(val);
									}
								} else {
									IfcParserWriterUtils.writePrimitive(listObject, outputStream);
								}
							} else {
								IfcParserWriterUtils.writePrimitive(listObject, outputStream);
							}
						}
					}
				}
				first = false;
				index++;
			}
			print(CLOSE_PAREN);
		}
	}

	private void writeWrappedValue(EObject object, EStructuralFeature feature, EClass ec) throws SerializerException, IOException {
		Object get = object.eGet(feature);
		boolean isWrapped = ec.getEAnnotation("wrapped") != null;
		EStructuralFeature structuralFeature = ec.getEStructuralFeature(WRAPPED_VALUE);
		if (get instanceof EObject) {
			boolean isDefinedWrapped = feature.getEType().getEAnnotation("wrapped") != null;
			EObject betweenObject = (EObject) get;
			if (betweenObject != null) {
				if (isWrapped && isDefinedWrapped) {
					Object val = betweenObject.eGet(structuralFeature);
					String name = structuralFeature.getEType().getName();
					if ((name.equals(IFC_BOOLEAN) || name.equals(IFC_LOGICAL)) && val == null) {
						print(BOOLEAN_UNDEFINED);
					} else if (structuralFeature.getEType() == ECORE_PACKAGE_INSTANCE.getEDouble()) {
						writeDoubleValue((Double)val, betweenObject, feature);
					} else {
						IfcParserWriterUtils.writePrimitive(val, outputStream);
					}
				} else {
					writeEmbedded(betweenObject);
				}
			}
		} else if (get instanceof EList<?>) {
			EList<?> list = (EList<?>) get;
			if (list.isEmpty()) {
				if (!feature.isUnsettable()) {
					print(OPEN_CLOSE_PAREN);
				} else {
					print("$");
				}
			} else {
				print(OPEN_PAREN);
				boolean first = true;
				for (Object o : list) {
					if (!first) {
						print(COMMA);
					}
					EObject object2 = (EObject) o;
					Object val = object2.eGet(structuralFeature);
					if (structuralFeature.getEType() == ECORE_PACKAGE_INSTANCE.getEDouble()) {
						writeDoubleValue((Double)val, object2, structuralFeature);
					} else {
						IfcParserWriterUtils.writePrimitive(val, outputStream);
					}
					first = false;
				}
				print(CLOSE_PAREN);
			}
		} else {
			if (get == null) {
				EClassifier type = structuralFeature.getEType();
				if (type.getName().equals("IfcBoolean") || type.getName().equals("IfcLogical") || type == ECORE_PACKAGE_INSTANCE.getEBoolean()) {
					print(BOOLEAN_UNDEFINED);
				} else {
					EntityDefinition entityBN = getPackageMetaData().getSchemaDefinition().getEntityBN(object.eClass().getName());
					if (entityBN != null && entityBN.isDerived(feature.getName())) {
						print(ASTERISK);
					} else {
						print(DOLLAR);
					}
				}
			}
		}
	}

	private void writeEnum(EObject object, EStructuralFeature feature) throws SerializerException, IOException {
		Object val = object.eGet(feature);
		if (feature.getEType().getName().equals("Tristate")) {
			IfcParserWriterUtils.writePrimitive(val, outputStream);
		} else {
			if (val == null) {
				print(DOLLAR);
			} else {
				if (((Enum<?>) val).toString().equals(NULL)) {
					print(DOLLAR);
				} else {
					print(DOT);
					print(val.toString());
					print(DOT);
				}
			}
		}
	}
}