// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.madvoc.component;

import jodd.introspector.PropertyDescriptor;
import jodd.madvoc.ScopeData;
import jodd.madvoc.ScopeType;
import jodd.madvoc.MadvocException;
import jodd.madvoc.meta.In;
import jodd.madvoc.meta.InOut;
import jodd.madvoc.meta.Out;
import jodd.introspector.ClassDescriptor;
import jodd.introspector.ClassIntrospector;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.ArrayList;

/**
 * Resolver for {@link jodd.madvoc.ScopeData scope data} for certain types.
 * It does not cache anything as Scope data is cached in {@link jodd.madvoc.ActionConfig}.
 * Resolving only happens during the initialization, and it might be repeated for
 * certain types (as there is no cache), but that is acceptable to reduce memory
 * usage (no cache) and several lookups (for each interceptor) during every request.
 */
public class ScopeDataResolver {

	/**
	 * Resolve scope data in given type for all scope types.
	 * Returns <code>null</code> if no scope data exist.
	 */
	public ScopeData[] resolveScopeData(Class type) {
		final ScopeType[] allScopeTypes = ScopeType.values();

		ScopeData[] scopeData = new ScopeData[allScopeTypes.length];

		int count = 0;

		for (ScopeType scopeType : allScopeTypes) {
			ScopeData sd = inspectClassScopeData(type, scopeType);
			if (sd != null) {
				count++;
			}
			scopeData[scopeType.value()] = sd;
		}

		if (count == 0) {
			return null;
		}

		return scopeData;
	}

	/**
	 * Resolves scope data in given annotations for all scope types.
	 * Returns <code>null</code> if no scope data exist.
	 */
	public ScopeData[] resolveScopeData(String name, Class type, Annotation[] annotations) {
		final ScopeType[] allScopeTypes = ScopeType.values();

		ScopeData[] scopeData = new ScopeData[allScopeTypes.length];

		int count = 0;

		for (ScopeType scopeType : allScopeTypes) {
			ScopeData sd = inspectMethodParameterScopeData(name, type, annotations, scopeType);
			if (sd != null) {
				count++;
			}
			scopeData[scopeType.value()] = sd;
		}

		if (count == 0) {
			return null;
		}

		return scopeData;
	}

	/**
	 * Scans annotation and returns type of Madvoc annotations.
	 */
	public Class<? extends Annotation> detectAnnotationType(Annotation[] annotations) {
		for (Annotation annotation : annotations) {
			if (annotation instanceof In) {
				return annotation.annotationType();
			}
			else if (annotation instanceof Out) {
				return annotation.annotationType();
			}
			else if (annotation instanceof InOut) {
				return annotation.annotationType();
			}
		}
		return null;
	}


	// ---------------------------------------------------------------- inspect method

	/**
	 * Inspects all method parameters for scope type.
	 */
	protected ScopeData inspectMethodParameterScopeData(String name, Class type, Annotation[] annotations, ScopeType scopeType) {
		ScopeData sd = new ScopeData();
		int count = 0;

		for (Annotation annotation : annotations) {

			if (annotation instanceof In) {
				ScopeData.In scopeDataIn = inspectIn((In) annotation, scopeType, name, type);
				if (scopeDataIn != null) {
					count++;
					sd.in = new ScopeData.In[] {scopeDataIn};
				}
			}
			else if (annotation instanceof Out) {
				ScopeData.Out scopeDataOut = inspectOut((Out) annotation, scopeType, name, type);
				if (scopeDataOut != null) {
					count++;
					sd.out = new ScopeData.Out[] {scopeDataOut};
				}
			}
			else if (annotation instanceof InOut) {
				ScopeData.In scopeDataIn = inspectIn((InOut) annotation, scopeType, name, type);
				if (scopeDataIn != null) {
					count++;
					sd.in = new ScopeData.In[] {scopeDataIn};
				}

				ScopeData.Out scopeDataOut = inspectOut((InOut) annotation, scopeType, name, type);
				if (scopeDataOut != null) {
					count++;
					sd.out = new ScopeData.Out[] {scopeDataOut};
				}
			}
		}

		if (count == 0) {
			return null;
		}

		return sd;
	}

	// ---------------------------------------------------------------- inspect class

	/**
	 * Saves value and property name.
	 */
	protected void saveNameTarget(ScopeData.In ii, String value, String propertyName) {
		value = value.trim();
		if (value.length() > 0) {
			ii.name = value;
			ii.target = propertyName;
		}
		else {
			ii.name = propertyName;
			ii.target = null;
		}
	}

	/**
	 * Saves value and property name.
	 */
	protected void saveNameTarget(ScopeData.Out oi, String value, String propertyName) {
		value = value.trim();
		if (value.length() > 0) {
			oi.name = value;
			oi.target = propertyName;
		}
		else {
			oi.name = propertyName;
			oi.target = null;
		}
	}

	/**
	 * Inspects single IN annotation for a property.
	 */
	protected ScopeData.In inspectIn(In in, ScopeType scopeType, String propertyName, Class propertyType) {
		if (in == null) {
			return null;
		}
		ScopeType scope = in.scope();
		if (scope != scopeType) {
			return null;
		}
		ScopeData.In ii = new ScopeData.In();
		saveNameTarget(ii, in.value(), propertyName);
		ii.type = propertyType;
		return ii;
	}

	/**
	 * Inspects single INOUT annotation as IN.
	 * @see #inspectIn(jodd.madvoc.meta.In, jodd.madvoc.ScopeType, String, Class)
	 */
	protected ScopeData.In inspectIn(InOut inOut, ScopeType scopeType, String propertyName, Class propertyType) {
		if (inOut == null) {
			return null;
		}
		ScopeType scope = inOut.scope();
		if (scope != scopeType) {
			return null;
		}
		ScopeData.In ii = new ScopeData.In();
		saveNameTarget(ii, inOut.value(), propertyName);
		ii.type = propertyType;
		return ii;
	}

	/**
	 * Inspects single OUT annotation for a property.
	 */
	protected ScopeData.Out inspectOut(Out out, ScopeType scopeType, String propertyName, Class propertyType) {
		if (out == null) {
			return null;
		}
		ScopeType scope = out.scope();
		if (scope != scopeType) {
			return null;
		}
		ScopeData.Out oi = new ScopeData.Out();
		saveNameTarget(oi, out.value(), propertyName);
		oi.type = propertyType;
		return oi;
	}

	/**
	 * Inspects single INOUT annotation as OUT.
	 * @see #inspectOut(jodd.madvoc.meta.Out, jodd.madvoc.ScopeType, String, Class)
	 */
	protected ScopeData.Out inspectOut(InOut inOut, ScopeType scopeType, String propertyName, Class propertyType) {
		if (inOut == null) {
			return null;
		}
		ScopeType scope = inOut.scope();
		if (scope != scopeType) {
			return null;
		}
		ScopeData.Out oi = new ScopeData.Out();
		saveNameTarget(oi, inOut.value(), propertyName);
		oi.type = propertyType;
		return oi;
	}

	/**
	 * Inspect action for all In/Out annotations.
	 * Returns <code>null</code> if there are no In and Out data.
	 */
	protected ScopeData inspectClassScopeData(Class actionClass, ScopeType scopeType) {
		ClassDescriptor cd = ClassIntrospector.lookup(actionClass);

		PropertyDescriptor[] allProperties = cd.getAllPropertyDescriptors();

		List<ScopeData.In> listIn = new ArrayList<>(allProperties.length);
		List<ScopeData.Out> listOut = new ArrayList<>(allProperties.length);

		for (PropertyDescriptor pd : allProperties) {
			// collect annotations

			In in = null;

			if (pd.getFieldDescriptor() != null) {
				in = pd.getFieldDescriptor().getField().getAnnotation(In.class);
			}
			if (in == null && pd.getWriteMethodDescriptor() != null) {
				in = pd.getWriteMethodDescriptor().getMethod().getAnnotation(In.class);
			}
			if (in == null && pd.getReadMethodDescriptor() != null) {
				in = pd.getReadMethodDescriptor().getMethod().getAnnotation(In.class);
			}

			InOut inout = null;

			if (pd.getFieldDescriptor() != null) {
				inout = pd.getFieldDescriptor().getField().getAnnotation(InOut.class);
			}
			if (inout == null && pd.getWriteMethodDescriptor() != null) {
				inout = pd.getWriteMethodDescriptor().getMethod().getAnnotation(InOut.class);
			}
			if (inout == null && pd.getReadMethodDescriptor() != null) {
				inout = pd.getReadMethodDescriptor().getMethod().getAnnotation(InOut.class);
			}

			Out out = null;

			if (pd.getFieldDescriptor() != null) {
				out = pd.getFieldDescriptor().getField().getAnnotation(Out.class);
			}
			if (out == null && pd.getWriteMethodDescriptor() != null) {
				out = pd.getWriteMethodDescriptor().getMethod().getAnnotation(Out.class);
			}
			if (out == null && pd.getReadMethodDescriptor() != null) {
				out = pd.getReadMethodDescriptor().getMethod().getAnnotation(Out.class);
			}

			// checks

			if (inout != null) {
				if (in != null || out != null) {
					throw new MadvocException("@InOut can not be used with @In or @Out: " + pd.getClassDescriptor().getClass() + '#' + pd.getName());
				}
			}


			// inspect all

			ScopeData.In ii = inspectIn(in, scopeType, pd.getName(), pd.getType());
			if (ii != null) {
				listIn.add(ii);
			}
			ii = inspectIn(inout, scopeType, pd.getName(), pd.getType());
			if (ii != null) {
				listIn.add(ii);
			}

			ScopeData.Out oi = inspectOut(out, scopeType, pd.getName(), pd.getType());
			if (oi != null) {
				listOut.add(oi);
			}
			oi = inspectOut(inout, scopeType, pd.getName(), pd.getType());
			if (oi != null) {
				listOut.add(oi);
			}
		}

		if ((listIn.isEmpty()) && (listOut.isEmpty())) {
			return null;
		}

		ScopeData scopeData = new ScopeData();
		if (!listIn.isEmpty()) {
			scopeData.in = listIn.toArray(new ScopeData.In[listIn.size()]);
		}
		if (!listOut.isEmpty()) {
			scopeData.out = listOut.toArray(new ScopeData.Out[listOut.size()]);
		}
		return scopeData;
	}

}