// 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.ClassIntrospector;
import jodd.introspector.MethodDescriptor;
import jodd.madvoc.ActionConfig;
import jodd.madvoc.ActionConfigSet;
import jodd.madvoc.ActionDef;
import jodd.madvoc.MadvocException;
import jodd.madvoc.macro.PathMacros;
import jodd.petite.meta.PetiteInject;
import jodd.util.ClassLoaderUtil;
import jodd.util.StringUtil;
import jodd.util.collection.SortedArrayList;
import jodd.log.Logger;
import jodd.log.LoggerFactory;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Manages all Madvoc action and aliases registrations.
 */
public class ActionsManager {

	private static final Logger log = LoggerFactory.getLogger(ActionsManager.class);

	@PetiteInject
	protected ActionMethodParser actionMethodParser;

	@PetiteInject
	protected ActionPathMacroManager actionPathMacroManager;

	@PetiteInject
	protected MadvocConfig madvocConfig;

	protected int actionsCount;
	protected boolean asyncMode;
	protected final HashMap<String, ActionConfigSet> map;		// map of all action paths w/o macros
	protected final SortedArrayList<ActionConfigSet> list;		// list of all action paths with macros
	protected final HashMap<String, ActionConfig> configs;		// another map of all action configs
	protected Map<String, String> pathAliases;					// path aliases

	public ActionsManager() {
		this.map = new HashMap<>();
		this.list = new SortedArrayList<>(new ActionConfigSetComparator());
		this.pathAliases = new HashMap<>();
		this.configs = new HashMap<>();
		this.asyncMode = false;
	}

	/**
	 * Comparator that considers first chunks number then action path.
	 */
	public static class ActionConfigSetComparator implements Comparator<ActionConfigSet>, Serializable {
		public int compare(ActionConfigSet set1, ActionConfigSet set2) {
			int deep1 = set1.deep;
			int deep2 = set2.deep;

			if (deep1 == deep2) {
				return set1.actionPath.compareTo(set2.actionPath);
			}
			return deep1 - deep2;
		}
	}

	/**
	 * Returns all registered action configurations.
	 * Returned list is a join of action paths
	 * with and without the macro.
	 */
	public List<ActionConfig> getAllActionConfigurations() {
		List<ActionConfig> all = new ArrayList<>(actionsCount);

		for (ActionConfigSet set : map.values()) {
			all.addAll(set.getActionConfigs());
		}
		for (ActionConfigSet set : list) {
			all.addAll(set.getActionConfigs());
		}
		return all;
	}

	/**
	 * Returns total number of registered actions.
	 */
	public int getActionsCount() {
		return actionsCount;
	}

	/**
	 * Returns <code>true</code> if at least one action has
	 * async mode turned on.
	 */
	public boolean isAsyncModeOn() {
		return asyncMode;
	}

	// ---------------------------------------------------------------- register variations

	/**
	 * Resolves action method for given action class ane method name.
	 */
	public Method resolveActionMethod(Class<?> actionClass, String methodName) {
		MethodDescriptor methodDescriptor = ClassIntrospector.lookup(actionClass).getMethodDescriptor(methodName, false);
		if (methodDescriptor == null) {
			throw new MadvocException("Public method not found: " + actionClass.getSimpleName() + "#" + methodName);
		}
		return methodDescriptor.getMethod();
	}

	/**
	 * Registers action with provided action signature.
	 */
	public ActionConfig register(String actionSignature) {
		return register(actionSignature, null);
	}

	/**
	 * Registers action with provided action signature.
	 */
	public ActionConfig register(String actionSignature, ActionDef actionDef) {
		int ndx = actionSignature.indexOf('#');
		if (ndx == -1) {
			throw new MadvocException("Madvoc action signature syntax error: " + actionSignature);
		}
		String actionClassName = actionSignature.substring(0, ndx);
		String actionMethodName = actionSignature.substring(ndx + 1);
		Class actionClass;
		try {
			actionClass = ClassLoaderUtil.loadClass(actionClassName);
		} catch (ClassNotFoundException cnfex) {
			throw new MadvocException("Madvoc action class not found: " + actionClassName, cnfex);
		}
		return register(actionClass, actionMethodName, actionDef);
	}

	public ActionConfig register(Class actionClass, Method actionMethod) {
		return registerAction(actionClass, actionMethod, null);
	}

	public ActionConfig register(Class actionClass, Method actionMethod, ActionDef actionDef) {
		return registerAction(actionClass, actionMethod, actionDef);
	}

	/**
	 * Registers action with provided action class and method name.
	 */
	public ActionConfig register(Class actionClass, String actionMethodName) {
		Method actionMethod = resolveActionMethod(actionClass, actionMethodName);
		return registerAction(actionClass, actionMethod, null);
	}

	public ActionConfig register(Class actionClass, String actionMethodName, ActionDef actionDef) {
		Method actionMethod = resolveActionMethod(actionClass, actionMethodName);
		return registerAction(actionClass, actionMethod, actionDef);
	}

	// ---------------------------------------------------------------- registration

	/**
	 * Registration main point. Does two things:
	 * <ul>
	 *     <li>{@link jodd.madvoc.component.ActionMethodParser#parse(Class, java.lang.reflect.Method, jodd.madvoc.ActionDef) parse action}
	 *     and creates {@link jodd.madvoc.ActionConfig}</li>
	 *     <li>{@link #registerAction(jodd.madvoc.ActionConfig) registers} created {@link jodd.madvoc.ActionConfig}</li>
	 * </ul>
	 * Returns created {@link ActionConfig}.
	 * @see #registerAction(jodd.madvoc.ActionConfig)
	 */
	protected ActionConfig registerAction(Class actionClass, Method actionMethod, ActionDef actionDef) {
		ActionConfig actionConfig = actionMethodParser.parse(actionClass, actionMethod, actionDef);
		if (actionConfig == null) {
			return null;
		}
		return registerAction(actionConfig);
	}

	/**
	 * Registers manually created {@link ActionConfig action configurations}.
	 * Optionally, if action path with the same name already exist,
	 * exception will be thrown.
	 */
	public ActionConfig registerAction(ActionConfig actionConfig) {
		String actionPath = actionConfig.actionPath;

		if (log.isDebugEnabled()) {
			log.debug("Registering Madvoc action: " + actionConfig.actionPath + " to: " +
					actionConfig.getActionString());
		}

		ActionConfigSet set = createActionConfigSet(actionConfig.actionPath);

		if (set.actionPathMacros != null) {
			// new action patch contain macros
			int ndx = -1;
			for (int i = 0; i < list.size(); i++) {
				if (list.get(i).actionPath.equals(actionPath)) {
					ndx = i;
					break;
				}
			}
			if (ndx < 0) {
				list.add(set);
			} else {
				set = list.get(ndx);
			}
		} else {
			// action path is without macros
			if (!map.containsKey(actionConfig.actionPath)) {
				map.put(actionConfig.actionPath, set);
			} else {
				set = map.get(actionConfig.actionPath);
			}

		}
		boolean isDuplicate = set.add(actionConfig);

		if (madvocConfig.isDetectDuplicatePathsEnabled()) {
			if (isDuplicate) {
				throw new MadvocException("Duplicate action path for " + actionConfig);
			}
		}

		// finally

		configs.put(actionConfig.getActionString(), actionConfig);

		if (!isDuplicate) {
			actionsCount++;
		}

		// async check
		if (actionConfig.isAsync()) {
			asyncMode = true;
		}

		return actionConfig;
	}

	/**
	 * Creates new action config set from the action path.
	 */
	protected ActionConfigSet createActionConfigSet(String actionPath) {
		PathMacros pathMacros = actionPathMacroManager.buildActionPathMacros(actionPath);

		return new ActionConfigSet(actionPath, pathMacros);
	}

	// ---------------------------------------------------------------- look-up

	/**
	 * Returns action configurations for provided action path.
	 * First it lookups for exact <code>actionPath</code>.
	 * If action path is not registered, it is split into chunks
	 * and match against macros.
	 * Returns <code>null</code> if action path is not registered.
	 * <code>method</code> must be in uppercase.
	 */
	public ActionConfig lookup(String actionPath, String method) {

		// 1st try: the map

		ActionConfigSet actionConfigSet = map.get(actionPath);
		if (actionConfigSet != null) {
			ActionConfig actionConfig = actionConfigSet.lookup(method);
			if (actionConfig != null) {
				return actionConfig;
			}
		}

		// 2nd try: the list

		int actionPathDeep = StringUtil.count(actionPath, '/');

		int len = list.size();

		int lastMatched = -1;
		int maxMatchedChars = -1;

		for (int i = 0; i < len; i++) {
			actionConfigSet = list.get(i);

			int deep = actionConfigSet.deep;
			if (deep < actionPathDeep) {
				continue;
			}
			if (deep > actionPathDeep) {
				break;
			}

			// same deep level, try the fully match

			int matchedChars = actionConfigSet.actionPathMacros.match(actionPath);

			if (matchedChars == -1) {
				continue;
			}

			if (matchedChars > maxMatchedChars) {
				maxMatchedChars = matchedChars;
				lastMatched = i;
			}
		}

		if (lastMatched < 0) {
			return null;
		}

		ActionConfigSet set = list.get(lastMatched);

		return set.lookup(method);
	}

	/**
	 * Lookups action config for given action class and method string (aka 'action string').
	 * The action string has the following format: <code>className#methodName</code>.
	 * @see jodd.madvoc.ActionConfig#getActionString()
	 */
	public ActionConfig lookup(String actionString) {
		return configs.get(actionString);
	}

	// ---------------------------------------------------------------- aliases

	/**
	 * Registers new path alias.
	 */
	public void registerPathAlias(String alias, String path) {
		pathAliases.put(alias, path);
	}

	/**
	 * Returns path alias.
	 */
	public String lookupPathAlias(String alias) {
		return pathAliases.get(alias);
	}

}