/**
 * Copyright © 2013-2020 The OpenNTF Domino API Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openntf.domino.graph;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javolution.util.FastMap;
import javolution.util.FastSet;
import javolution.util.function.Equalities;

import org.openntf.domino.Database;
import org.openntf.domino.Document;
import org.openntf.domino.types.BigString;
import org.openntf.domino.types.Null;
import org.openntf.domino.utils.BeanUtils;
import org.openntf.domino.utils.DominoUtils;
import org.openntf.domino.utils.TypeUtils;

import com.tinkerpop.blueprints.Element;

@Deprecated
public abstract class DominoElement implements IDominoElement, Serializable {

	public static boolean setReflectiveProperty(final IDominoElement element, final String prop, final Object value) {
		if (prop == null || prop.isEmpty())
			throw new IllegalArgumentException("Cannot set a null or empty property on a DominoElement");
		boolean result = false;
		Object localValues = value;
		Class<? extends Element> elemClass = element.getClass();
		IDominoProperties domProp = IDominoProperties.Reflect.findMappedProperty(elemClass, prop);
		if (domProp != null) {
			localValues = TypeUtils.objectToClass(value, domProp.getType(), null);
		}
		//TODO NTF - first see if it's a mapped property and if so, find out the type and coerce the value to the proper type
		Method setter = BeanUtils.findSetter(elemClass, prop, BeanUtils.toParameterType(localValues));
		if (setter != null) {
			try {
				setter.invoke(element, localValues);
				result = true;
			} catch (Exception e) {
				DominoUtils.handleException(e);
				System.out.println("Unable to invoke " + setter.getName() + " on a " + element.getClass() + " for property: " + prop
						+ " with a value of " + String.valueOf(value));
			}
		} else {
			if (domProp != null) {
				element.setProperty(domProp, localValues);
				result = true;
			} else {
				element.setProperty(prop, localValues);
				result = true;
			}
		}
		return result;
	}

	public static Object getReflectiveProperty(final IDominoElement element, final String prop) {
		if (prop == null || prop.isEmpty())
			throw new IllegalArgumentException("Cannot set a null or empty property on a DominoElement");
		Object result = false;
		Class<? extends Element> elemClass = element.getClass();
		Method getter = BeanUtils.findGetter(elemClass, prop);
		if (getter != null) {
			try {
				result = getter.invoke(element, (Object[]) null);
			} catch (Exception e) {
				DominoUtils.handleException(e);
				System.out.println("Unable to invoke " + getter.getName() + " on a " + element.getClass() + " for property: " + prop);
			}
		} else {
			IDominoProperties domProp = IDominoProperties.Reflect.findMappedProperty(elemClass, prop);
			if (domProp != null) {
				result = element.getProperty(domProp);
			} else {
				result = element.getProperty(prop);
			}
		}
		return result;
	}

	public static Object getReflectiveProperty(final IDominoElement element, final IDominoProperties prop) {
		return prop.getType().cast(getReflectiveProperty(element, prop.getName()));
	}

	private static final Logger log_ = Logger.getLogger(DominoElement.class.getName());
	private static final long serialVersionUID = 1L;
	public static final String TYPE_FIELD = "_OPEN_GRAPHTYPE";
	private String key_;
	protected transient DominoGraph parent_;
	private String unid_;
	private Map<String, Serializable> props_;
	public final String[] DEFAULT_STR_ARRAY = { "" };

	public static Document toDocument(final DominoElement element) {
		return element.getRawDocument();
	}

	public static enum Properties implements IDominoProperties {
		TITLE(String.class), KEY(String.class), FORM(String.class);

		private Class<?> type_;

		Properties(final Class<?> type) {
			type_ = type;
		}

		@Override
		public Class<?> getType() {
			return type_;
		}

		@Override
		public String getName() {
			return super.name();
		}
	}

	public DominoElement(final DominoGraph parent, final Document doc) {
		parent_ = parent;

		unid_ = doc.getUniversalID().toUpperCase();
	}

	private transient java.lang.Object lockHolder_;

	public synchronized boolean hasLock() {
		return lockHolder_ != null;
	}

	public synchronized boolean lock(final java.lang.Object lockHolder) {
		if (lockHolder_ == null) {
			lockHolder_ = lockHolder;
			return true;
		}
		return false;
	}

	public synchronized boolean unlock(final java.lang.Object lockHolder) {
		if (lockHolder.equals(lockHolder_)) {
			lockHolder_ = null;
			return true;
		}
		return false;
	}

	synchronized void unlock() {
		lockHolder_ = null;
	}

	public String getTitle() {
		return getProperty(Properties.TITLE, false);
	}

	public void setTitle(final String value) {
		setProperty(Properties.TITLE, value);
	}

	public String getKey() {
		return getProperty(Properties.KEY, false);
	}

	public void setKey(final String value) {
		setProperty(Properties.KEY, value);
	}

	public String getForm() {
		return getProperty(Properties.FORM, false);
	}

	public void setForm(final String value) {
		String current = getForm();
		if (current == null || !current.equalsIgnoreCase(value)) {
			setProperty(Properties.FORM, value);
		}
	}

	private Boolean isNew_;

	void setNew(final boolean isnew) {
		isNew_ = isnew;
	}

	public boolean isNew() {
		if (isNew_ == null) {
			isNew_ = false;
		}
		return isNew_;
	}

	private Map<String, Serializable> getProps() {
		if (props_ == null) {
			props_ = new FastMap<String, Serializable>(Equalities.LEXICAL_CASE_INSENSITIVE).atomic();
		}
		return props_;
	}

	public void addProperty(final String propertyName, final Object value) {
		setProperty(propertyName, value);
	}

	private Database getDatabase() {
		return getParent().getRawDatabase();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(final Object o) {
		if (o instanceof DominoElement) {
			return ((DominoElement) o).getId().equals(getId());
		} else {
			return false;
		}
	}

	@Override
	public int hashCode() {
		return getId().hashCode();
	}

	@Override
	public Document getRawDocument() {
		return getDocument();
	}

	public String getRawId() {
		String prefix = getDatabase().getServer() + "!!" + getDatabase().getFilePath();
		return prefix + ": " + getRawDocument().getNoteID();
	}

	@Override
	public int incrementProperty(final String propertyName) {
		Integer result = getProperty(propertyName, Integer.class);
		if (result == null)
			result = 0;
		setProperty(propertyName, ++result);
		return result;
	}

	@Override
	public int decrementProperty(final String propertyName) {
		Integer result = getProperty(propertyName, Integer.class);
		if (result == null)
			result = 0;
		setProperty(propertyName, --result);
		return result;
	}

	private Document getDocument() {
		return getParent().getDocument(unid_, true);
		// Map<String, Document> map = documentCache.get();
		// Document doc = map.get(unid_);
		// if (doc == null) {
		// synchronized (map) {
		// doc = getDatabase().getDocumentByKey(unid_, true);
		// String localUnid = doc.getUniversalID().toUpperCase();
		// if (!unid_.equals(localUnid)) {
		// log_.log(Level.SEVERE, "UNIDs do not match! Expected: " + unid_ + ", Result: " + localUnid);
		// }
		// map.put(unid_, doc);
		// }
		// }
		// return doc;
	}

	@Override
	public String getId() {
		if (key_ == null) {
			key_ = unid_;
		}
		return key_;
	}

	@Override
	public DominoGraph getParent() {
		return parent_;
	}

	@Override
	public boolean hasProperty(final String key) {
		return getPropertyKeys().contains(key);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getProperty(final String key) {
		return (T) getProperty(key, java.lang.Object.class);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getProperty(final String propertyName, final Class<T> type) {
		Object result = null;
		String key = propertyName;
		Map<String, Serializable> props = getProps();
		// synchronized (props) {
		result = props.get(key);
		if (result == null) {
			//			if ("PROGNAME".equalsIgnoreCase(propertyName)) {
			//				System.out.println("DEBUG: " + propertyName + " getting from Document");
			//			}
			try {
				Document doc = getRawDocument();
				result = doc.getItemValue(propertyName, type);
				if (result == null) {
					//					synchronized (props) {
					props.put(key, Null.INSTANCE);
					//					}
				} else if (result instanceof Serializable) {
					//					synchronized (props) {
					props.put(key, (Serializable) result);
					//					}
				} else {
					log_.log(Level.WARNING, "Got a value from the document but it's not Serializable. It's a "
							+ result.getClass().getName());
				}
			} catch (Exception e) {
				log_.log(Level.WARNING, "Exception occured attempting to get value from document for " + propertyName
						+ " so we cannot return a value", e);
				e.printStackTrace();
			}
		} else if (result == Null.INSTANCE) {

		} else {
			if (result != null && !type.isAssignableFrom(result.getClass())) {
				//				if ("PROGNAME".equalsIgnoreCase(propertyName)) {
				//					System.out.println("DEBUG: " + propertyName + " result is a " + result.getClass().getSimpleName());
				//				}
				// System.out.println("AH! We have the wrong type in the property cache! How did this happen?");
				try {
					Document doc = getRawDocument();
					result = doc.getItemValue(propertyName, type);
					if (result == null) {
						//						synchronized (props) {
						props.put(key, Null.INSTANCE);
						//						}
					} else if (result instanceof Serializable) {
						//						synchronized (props) {
						props.put(key, (Serializable) result);
						//						}
					}
				} catch (Exception e) {
					log_.log(Level.WARNING, "Exception occured attempting to get value from document for " + propertyName
							+ " but we have a value in the cache.", e);
				}
			} else {
				//				if ("PROGNAME".equalsIgnoreCase(propertyName)) {
				//					System.out.println("DEBUG: " + propertyName + " result is a " + result.getClass().getSimpleName());
				//				}
			}
		}
		// }
		//		if (result != null && !T.isAssignableFrom(result.getClass())) {
		//			log_.log(Level.WARNING, "Returning a " + result.getClass().getName() + " when we asked for a " + T.getName());
		//		}
		if (result == Null.INSTANCE) {
			result = null;
		}
		//		if ("PROGNAME".equalsIgnoreCase(propertyName)) {
		//			System.out.println("DEBUG: " + propertyName + " result is a " + (result == null ? "null" : result.getClass().getSimpleName()));
		//		}
		return (T) result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getProperty(final String propertyName, final Class<T> type, final boolean allowNull) {
		T result = getProperty(propertyName, type);
		if (allowNull) {
			return result;
		} else {
			if (result == null || Null.INSTANCE == result) {
				if (type.isArray())
					if (type.getComponentType() == String.class) {
						return (T) DEFAULT_STR_ARRAY;
					} else {
						return (T) Array.newInstance(type.getComponentType(), 0);
					}
				if (Boolean.class.equals(type) || Boolean.TYPE.equals(type))
					return (T) Boolean.FALSE;
				if (Integer.class.equals(type) || Integer.TYPE.equals(type))
					return (T) Integer.valueOf(0);
				if (Long.class.equals(type) || Long.TYPE.equals(type))
					return (T) Long.valueOf(0l);
				if (Short.class.equals(type) || Short.TYPE.equals(type))
					return (T) Short.valueOf("0");
				if (Double.class.equals(type) || Double.TYPE.equals(type))
					return (T) Double.valueOf(0d);
				if (Float.class.equals(type) || Float.TYPE.equals(type))
					return (T) Float.valueOf(0f);
				if (String.class.equals(type))
					return (T) "";
				try {
					return type.newInstance();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			} else {
				return result;
			}
		}
	}

	@Override
	public Set<String> getPropertyKeys() {

		return getPropertyKeys(true);
	}

	private FastSet<String> propKeys_;
	private boolean checkedDocProps_ = false;

	private FastSet<String> getPropKeysInt() {
		if (propKeys_ == null) {
			propKeys_ = new FastSet<String>(Equalities.LEXICAL_CASE_INSENSITIVE).atomic();
		}
		return propKeys_;
	}

	@Override
	public Set<String> getPropertyKeys(final boolean includeEdgeFields) {
		if (!checkedDocProps_) {
			Set<String> raws = getRawDocument().keySet();
			getPropKeysInt().addAll(raws);
			checkedDocProps_ = true;
		}
		if (includeEdgeFields) {
			return getPropKeysInt().unmodifiable();
		} else {
			FastSet<String> result = new FastSet<String>(Equalities.LEXICAL_CASE_INSENSITIVE);
			for (String name : getPropKeysInt()) {
				if (!(name.startsWith(DominoVertex.IN_PREFIX) || name.startsWith(DominoVertex.OUT_PREFIX))) {
					result.add(name);
				}
			}
			return result.unmodifiable();
		}
	}

	@Override
	public abstract void remove();

	//	{
	//		getParent().startTransaction(this);
	//		getRawDocument().removePermanently(true);
	//	}

	void _remove() {
		getParent().startTransaction(this);
		getRawDocument().removePermanently(true);
	}

	private FastSet<String> removedProperties_;

	private FastSet<String> getRemovedPropertiesInt() {
		if (removedProperties_ == null) {
			removedProperties_ = new FastSet<String>(Equalities.LEXICAL_CASE_INSENSITIVE).atomic();
		}
		return removedProperties_;
	}

	@Override
	public <T> T removeProperty(final String key) {
		getParent().startTransaction(this);
		T result = getProperty(key);
		Map<String, Serializable> props = getProps();
		//		synchronized (props) {
		props.remove(key);
		//		}
		Document doc = getRawDocument();
		synchronized (doc) {
			doc.removeItem(key);
		}
		//		synchronized (removedProperties_) {
		getRemovedPropertiesInt().add(key);
		//		}
		//		synchronized (propKeys_) {
		getPropKeysInt().remove(key);
		//		}
		return result;
	}

	// public void save() {
	// getRawDocument().save();
	// }

	@Override
	public void setRawDocument(final org.openntf.domino.Document doc) {
		unid_ = doc.getUniversalID().toUpperCase();
	}

	private FastSet<String> changedProperties_;

	private FastSet<String> getChangedPropertiesInt() {
		if (changedProperties_ == null) {
			changedProperties_ = new FastSet<String>(Equalities.LEXICAL_CASE_INSENSITIVE).atomic();
		}
		return changedProperties_;
	}

	//	void setProperty(final String propertyName, final java.lang.Object value, final boolean force) {
	//
	//	}

	@SuppressWarnings("unused")
	@Override
	public void setProperty(final String propertyName, final java.lang.Object value) {
		//		if ("PROGNAME".equalsIgnoreCase(propertyName)) {
		//			System.out.println("DEBUG Setting " + propertyName);
		//		}
		boolean isEdgeCollection = false;
		boolean isEqual = false;
		String key = propertyName;
		Map<String, Serializable> props = getProps();
		Object old = null;
		if (props != null) {
			if (propertyName != null) {

				//				synchronized (propKeys_) {
				getPropKeysInt().add(propertyName);
				//				}
				Object current = getProperty(propertyName);
				if (propertyName.startsWith(DominoVertex.IN_PREFIX) && value instanceof java.util.Collection) {
					isEdgeCollection = true;
				}
				if (current == null && value == null) {
					return;
				}
				if (value != null && current != null) {
					if (!(value instanceof java.util.Collection) && !(value instanceof java.util.Map) && !value.getClass().isArray()) {
						isEqual = value.equals(current);
					}
				}
				if (isEqual) {
					log_.log(Level.FINE, "Not setting property " + propertyName + " because the new value is equal to the existing value");
				}
				boolean changeMade = false;
				//				synchronized (props) {

				if (value instanceof Serializable) {
					if (current == null || Null.INSTANCE.equals(current)) {
						//							if ("PROGNAME".equalsIgnoreCase(propertyName)) {
						//								System.out.println("DEBUG: " + propertyName + " checking FROM NULL values from " + String.valueOf(current)
						//										+ " to " + String.valueOf(value));
						//							}
						getParent().startTransaction(this);
						old = props.put(key, (Serializable) value);
						//							synchronized (changedProperties_) {
						getChangedPropertiesInt().add(propertyName);
						//							}
					} else if (!isEqual) {
						getParent().startTransaction(this);
						old = props.put(key, (Serializable) value);
						//							synchronized (changedProperties_) {
						getChangedPropertiesInt().add(propertyName);
						//							}
					} else {
						//							if ("PROGNAME".equalsIgnoreCase(propertyName)) {
						//								System.out.println("DEBUG: " + propertyName + " equal?? values match from " + String.valueOf(current)
						//										+ " to " + String.valueOf(value));
						//							}
					}
				} else if (value == null) {
					if (current != null && !current.equals(Null.INSTANCE)) {
						getParent().startTransaction(this);
						old = props.put(key, Null.INSTANCE);
						//							synchronized (changedProperties_) {
						getChangedPropertiesInt().add(propertyName);
						//							}
					}
				} else {
					//						if ("PROGNAME".equalsIgnoreCase(propertyName)) {
					//							System.out.println("DEBUG: " + propertyName + " values from " + String.valueOf(current) + " to "
					//									+ String.valueOf(value));
					//						}
					log_.log(Level.WARNING, "Attempted to set property " + propertyName + " to a non-serializable value: "
							+ value.getClass().getName());
				}
				//				}

			} else {
				log_.log(Level.WARNING, "propertyName is null on a setProperty request?");
			}
		} else {
			log_.log(Level.WARNING, "Properties are null for element!");
		}
	}

	protected void reapplyChanges() {
		Map<String, Serializable> props = getProps();
		Document doc = getDocument();
		//		synchronized (props) {
		if (props.isEmpty()) {
			// System.out.println("Cached properties is empty!");
		} else {
			//				synchronized (changedProperties_) {
			// System.out.println("Re-applying cached properties: " + changedProperties_.size());
			for (String s : getChangedPropertiesInt()) {
				//				CharSequence key = new CaseInsensitiveString(s);
				String key = s;
				Object v = props.get(key);
				if (v == null) {
					// System.out.println("Writing a null value for property: " + key
					// + " to an Element document. Probably not good...");
				}
				if (s.startsWith(DominoVertex.IN_PREFIX) || s.startsWith(DominoVertex.OUT_PREFIX)) {
					doc.replaceItemValue(s, v, false);
				} else {
					doc.replaceItemValue(s, v);
				}
			}
			getChangedPropertiesInt().clear();
			//				}

		}
		//		}
		//		synchronized (removedProperties_) {
		for (String key : getRemovedPropertiesInt()) {
			doc.removeItem(key);
		}
		//		}
	}

	@Override
	public int incrementProperty(final IDominoProperties prop) {
		return incrementProperty(prop.getName());
	}

	@Override
	public int decrementProperty(final IDominoProperties prop) {
		return decrementProperty(prop.getName());
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getProperty(final IDominoProperties prop) {
		if (prop == null) {
			log_.log(Level.WARNING, "getProperty was called with a null argument, therefore it's impossible to return a property.");
			return null;
		}
		Class<?> type = prop.getType();
		Object result = getProperty(prop.getName(), type);
		if (result != null && type.isAssignableFrom(result.getClass())) {
			return (T) type.cast(result);
		} else {
			// System.out.println("Property returned a " + (result == null ? "null" : result.getClass().getName())
			// + " even though we requested a " + type.getName());
		}
		return (T) result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getProperty(final IDominoProperties prop, final boolean allowNull) {
		Class<?> type = prop.getType();
		Object result = getProperty(prop.getName(), type, allowNull);
		if (result != null && type.isAssignableFrom(result.getClass())) {
			return (T) type.cast(result);
		} else {
			// System.out.println("Property returned a " + (result == null ? "null" : result.getClass().getName())
			// + " even though we requested a " + type.getName());
		}
		return (T) result;
	}

	@Override
	public void setProperty(final IDominoProperties prop, final java.lang.Object value) {
		Object current = getProperty(prop, true);
		if (current == null || !current.equals(value)) {
			setProperty(prop.getName(), value);
		}
	}

	public static Object fromMapValue(final String key, final Object value) {
		Object result = value;

		return result;
	}

	public static Object toMapValue(final Object value) {
		Object result = value;
		if (EnumSet.class.isAssignableFrom(value.getClass())) {
			System.out.println("DEBUG: Mapping an EnumSet");
			if (!((EnumSet<?>) value).isEmpty()) {
				StringBuilder eListing = new StringBuilder();
				eListing.append('[');
				for (Object rawEnum : (EnumSet<?>) value) {
					if (Enum.class.isAssignableFrom(rawEnum.getClass())) {
						eListing.append(((Enum<?>) rawEnum).name());
					} else {
						eListing.append("ERROR: expected Enum was a " + rawEnum.getClass().getName());
					}
					eListing.append(',');
				}
				eListing.deleteCharAt(eListing.length() - 1);
				eListing.append(']');
				result = eListing.toString();
			} else {
				result = "";
			}
		} else if (Enum.class.isAssignableFrom(value.getClass())) {
			result = ((Enum<?>) value).name();
		} else if (CharSequence.class.isAssignableFrom(value.getClass())) {
			result = ((CharSequence) value).toString();
		} else if (BigString.class.isAssignableFrom(value.getClass())) {
			result = ((BigString) value).toString();
		} else {
			result = value;
		}
		return result;
	}

	public Map<String, Object> toMap(final IDominoProperties[] props, final byte keyStyle) {
		Map<String, Object> result = new LinkedHashMap<String, Object>();
		for (IDominoProperties prop : props) {
			String mapKey = prop.getName();
			if (keyStyle == Character.LOWERCASE_LETTER) {
				mapKey = mapKey.toLowerCase();
			} else if (keyStyle == Character.UPPERCASE_LETTER) {
				mapKey = mapKey.toUpperCase();
			}
			Object value = getProperty(prop, true);
			if (value != null) {
				result.put(mapKey, toMapValue(value));
			}
		}
		return result;
	}

	@Override
	public Map<String, Object> toMap(final IDominoProperties[] props) {
		return toMap(props, (byte) 0);
	}

	public Map<String, Object> toMap(final Set<IDominoProperties> props, final byte keyStyle) {
		return toMap(props.toArray(new IDominoProperties[props.size()]), keyStyle);
	}

	@Override
	public Map<String, Object> toMap(final Set<IDominoProperties> props) {
		return toMap(props, (byte) 0);
	}

	public boolean fromMap(final Map<String, Object> map) {
		boolean result = true;
		for (String key : map.keySet()) {
			boolean success = DominoElement.setReflectiveProperty(this, key, map.get(key));
			if (!success)
				result = false;
		}
		return result;
	}

}