package net.minidev.asm;

/*
 *    Copyright 2011-2023 JSON-SMART authors
 *
 * 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.
 */
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Allow access reflect field using runtime generated accessor. BeansAccessor is
 * faster than java.lang.reflect.Method.invoke()
 * 
 * @param <T> the type of the bean being accessed
 * @author uriel Chemouni
 */
public abstract class BeansAccess<T> {
	/**
	 * default constuctor
	 */
	public BeansAccess() {
		super();
	}

	private HashMap<String, Accessor> map;
	private Accessor[] accs;

	/**
	 * set Accessor
	 * @param accs Accessor list
	 */
	protected void setAccessor(Accessor[] accs) {
		int i = 0;
		this.accs = accs;
		map = new HashMap<String, Accessor>();
		for (Accessor acc : accs) {
			acc.index = i++;
			map.put(acc.getName(), acc);
		}
	}

	/**
	 * get internal map
	 * @return a map
	 */
	public HashMap<String, Accessor> getMap() {
		return map;
	}

	/**
	 * get internal accessor
	 * @return Accessor list
	 */
	public Accessor[] getAccessors() {
		return accs;
	}

	/**
	 * cache used to store built BeansAccess
	 */
	private static ConcurrentHashMap<Class<?>, BeansAccess<?>> cache = new ConcurrentHashMap<Class<?>, BeansAccess<?>>();

	// private final static ConcurrentHashMap<Type, AMapper<?>> cache;

	/**
	 * return the BeansAccess corresponding to a type
	 * 
	 * @param type to be access
	 * @param <P> working type
	 * @return the BeansAccess
	 */
	static public <P> BeansAccess<P> get(Class<P> type) {
		return get(type, null);
	}

	/**
	 * return the BeansAccess corresponding to a type
	 * 
	 * @param filter FieldFilter
	 * @param type to be access
	 * @param <P> working type
	 * @return the BeansAccess
	 */
	static public <P> BeansAccess<P> get(Class<P> type, FieldFilter filter) {
		{
			@SuppressWarnings("unchecked")
			BeansAccess<P> access = (BeansAccess<P>) cache.get(type);
			if (access != null)
				return access;
		}
		// extract all access methods
		Accessor[] accs = ASMUtil.getAccessors(type, filter);

		// create new class name
		String className = type.getName();
		String accessClassName;
		if (className.startsWith("java.util."))
			accessClassName = "net.minidev.asm." + className + "AccAccess";
		else
			accessClassName = className.concat("AccAccess");		
		
		// extend class base loader
		DynamicClassLoader loader = new DynamicClassLoader(type.getClassLoader());
		// try to load existing class
		Class<?> accessClass = null;
		try {
			accessClass = loader.loadClass(accessClassName);
		} catch (ClassNotFoundException ignored) {
		}

		LinkedList<Class<?>> parentClasses = getParents(type);

		// if the class do not exists build it
		if (accessClass == null) {
			BeansAccessBuilder builder = new BeansAccessBuilder(type, accs, loader);
			for (Class<?> c : parentClasses)
				builder.addConversion(BeansAccessConfig.classMapper.get(c));
			accessClass = builder.bulid();
		}
		try {
			@SuppressWarnings("unchecked")
			BeansAccess<P> access = (BeansAccess<P>) accessClass.newInstance();
			access.setAccessor(accs);
			cache.putIfAbsent(type, access);
			// add fieldname alias
			for (Class<?> c : parentClasses)
				addAlias(access, BeansAccessConfig.classFiledNameMapper.get(c));
			return access;
		} catch (Exception ex) {
			throw new RuntimeException("Error constructing accessor class: " + accessClassName, ex);
		}
	}

	/**
	 * @param type current type
	 * @return parents hierarchy
	 */
	private static LinkedList<Class<?>> getParents(Class<?> type) {
		LinkedList<Class<?>> m = new LinkedList<Class<?>>();
		while (type != null && !type.equals(Object.class)) {
			m.addLast(type);
			for (Class<?> c : type.getInterfaces())
				m.addLast(c);
			type = type.getSuperclass();
		}
		m.addLast(Object.class);
		return m;
	}

	/**
	 * @param access accessor to use
	 * @param m mapping
	 */
	private static void addAlias(BeansAccess<?> access, HashMap<String, String> m) {
		// HashMap<String, String> m =
		// BeansAccessConfig.classFiledNameMapper.get(type);
		if (m == null)
			return;
		HashMap<String, Accessor> changes = new HashMap<String, Accessor>();
		for (Entry<String, String> e : m.entrySet()) {
			Accessor a1 = access.map.get(e.getValue());
			if (a1 != null)
				changes.put(e.getValue(), a1);
		}
		access.map.putAll(changes);
	}

	/**
	 * set field value by field index
	 * 
	 * @param object object to alter
	 * @param methodIndex field id to update
	 * @param value new value
	 */
	abstract public void set(T object, int methodIndex, Object value);

	/**
	 * get field value by field index
	 * @param object object to operate
	 * @param methodIndex field number to operate
	 * @return value of the field
	 */
	abstract public Object get(T object, int methodIndex);

	/**
	 * create a new targeted object
	 * @return new instance
	 */
	abstract public T newInstance();

	/**
	 * set field value by field name
	 * @param object target object
	 * @param methodName methodName
	 * @param value new field value
	 */
	public void set(T object, String methodName, Object value) {
		int i = getIndex(methodName);
		if (i == -1)
			throw new net.minidev.asm.ex.NoSuchFieldException(methodName + " in " + object.getClass() + " to put value : " + value);
		set(object, i, value);
	}

	/**
	 * get field value by field name
	 * @param object object to operate
	 * @param methodName getter to call
	 * @return field value returned by the getter
	 */
	public Object get(T object, String methodName) {
		return get(object, getIndex(methodName));
	}

	/**
	 * Returns the index of the field accessor.
	 * @param name field name 
	 * @return id of the field
	 */
	public int getIndex(String name) {
		Accessor ac = map.get(name);
		if (ac == null)
			return -1;
		return ac.index;
	}
}
