package jadx.core.dex.nodes.parser;

import jadx.core.Consts;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.attributes.annotations.Annotation;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.utils.exceptions.JadxRuntimeException;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SignatureParser {

	private static final Logger LOG = LoggerFactory.getLogger(SignatureParser.class);
	private static final char STOP_CHAR = 0;

	private final String sign;
	private final int end;
	private int pos;
	private int mark;

	public SignatureParser(String signature) {
		sign = signature;
		end = sign.length();
		pos = -1;
		mark = 0;
	}

	@SuppressWarnings("unchecked")
	public static SignatureParser fromNode(IAttributeNode node) {
		Annotation a = node.getAnnotation(Consts.DALVIK_SIGNATURE);
		if (a == null) {
			return null;
		}
		return new SignatureParser(mergeSignature((List<String>) a.getDefaultValue()));
	}

	private char next() {
		pos++;
		if (pos >= end) {
			return STOP_CHAR;
		}
		return sign.charAt(pos);
	}

	private boolean lookAhead(char ch) {
		int next = pos + 1;
		return next < end && sign.charAt(next) == ch;
	}

	private void mark() {
		mark = pos;
	}

	/**
	 * Exclusive slice.
	 *
	 * @return string from 'mark' to current position (not including current character)
	 */
	private String slice() {
		if (mark >= pos) {
			return "";
		}
		return sign.substring(mark, pos);
	}

	/**
	 * Inclusive slice (includes current character)
	 */
	private String inclusiveSlice() {
		if (mark >= pos) {
			return "";
		}
		return sign.substring(mark, pos + 1);
	}

	private boolean forwardTo(char lastChar) {
		int startPos = pos;
		char ch;
		while ((ch = next()) != STOP_CHAR) {
			if (ch == lastChar) {
				return true;
			}
		}
		pos = startPos;
		return false;
	}

	private void consume(char exp) {
		char c = next();
		if (exp != c) {
			throw new JadxRuntimeException("Consume wrong char: '" + c + "' != '" + exp
					+ "', sign: " + debugString());
		}
	}

	private boolean tryConsume(char exp) {
		if (lookAhead(exp)) {
			next();
			return true;
		}
		return false;
	}

	private String consumeUntil(char lastChar) {
		mark();
		return forwardTo(lastChar) ? slice() : null;
	}

	public ArgType consumeType() {
		char ch = next();
		mark();
		switch (ch) {
			case 'L':
				ArgType obj = consumeObjectType(false);
				if (obj != null) {
					return obj;
				}
				break;
			case 'T':
				next();
				mark();
				if (forwardTo(';')) {
					return ArgType.genericType(slice());
				}
				break;
			case '[':
				return ArgType.array(consumeType());

			case STOP_CHAR:
				return null;

			default:
				// primitive type (one char)
				ArgType type = ArgType.parse(ch);
				if (type != null) {
					return type;
				}
				break;
		}
		throw new JadxRuntimeException("Can't parse type: " + debugString());
	}

	private ArgType consumeObjectType(boolean incompleteType) {
		mark();
		int ch;
		do {
			ch = next();
			if (ch == STOP_CHAR) {
				return null;
			}
		} while (ch != '<' && ch != ';');

		if (ch == ';') {
			String obj;
			if (incompleteType) {
				obj = slice().replace('/', '.');
			} else {
				obj = inclusiveSlice();
			}
			return ArgType.object(obj);
		} else {
			// generic type start ('<')
			String obj = slice();
			if (!incompleteType) {
				obj += ";";
			}
			ArgType[] genArr = consumeGenericArgs();
			consume('>');

			ArgType genericType = ArgType.generic(obj, genArr);
			if (lookAhead('.')) {
				consume('.');
				next();
				// type parsing not completed, proceed to inner class
				ArgType inner = consumeObjectType(true);
				return ArgType.genericInner(genericType, inner.getObject(), inner.getGenericTypes());
			} else {
				consume(';');
				return genericType;
			}
		}
	}

	private ArgType[] consumeGenericArgs() {
		List<ArgType> list = new LinkedList<ArgType>();
		ArgType type;
		do {
			if (lookAhead('*')) {
				next();
				type = ArgType.wildcard();
			} else if (lookAhead('+')) {
				next();
				type = ArgType.wildcard(consumeType(), 1);
			} else if (lookAhead('-')) {
				next();
				type = ArgType.wildcard(consumeType(), -1);
			} else {
				type = consumeType();
			}
			if (type != null) {
				list.add(type);
			}
		} while (type != null && !lookAhead('>'));
		return list.toArray(new ArgType[list.size()]);
	}

	/**
	 * Map of generic types names to extends classes.
	 * <p/>
	 * Example: "<T:Ljava/lang/Exception;:Ljava/lang/Object;>"
	 */
	public Map<ArgType, List<ArgType>> consumeGenericMap() {
		if (!lookAhead('<')) {
			return Collections.emptyMap();
		}
		Map<ArgType, List<ArgType>> map = new LinkedHashMap<ArgType, List<ArgType>>(2);
		consume('<');
		while (true) {
			if (lookAhead('>') || next() == STOP_CHAR) {
				break;
			}
			String id = consumeUntil(':');
			if (id == null) {
				LOG.error("Can't parse generic map: {}", sign);
				return Collections.emptyMap();
			}
			tryConsume(':');
			List<ArgType> types = consumeExtendsTypesList();
			map.put(ArgType.genericType(id), types);
		}
		consume('>');
		return map;
	}

	/**
	 * List of types separated by ':' last type is 'java.lang.Object'.
	 * <p/>
	 * Example: "Ljava/lang/Exception;:Ljava/lang/Object;"
	 */
	private List<ArgType> consumeExtendsTypesList() {
		List<ArgType> types = Collections.emptyList();
		boolean next;
		do {
			ArgType argType = consumeType();
			if (!argType.equals(ArgType.OBJECT)) {
				if (types.isEmpty()) {
					types = new LinkedList<ArgType>();
				}
				types.add(argType);
			}
			next = lookAhead(':');
			if (next) {
				consume(':');
			}
		} while (next);
		return types;
	}

	public List<ArgType> consumeMethodArgs() {
		consume('(');
		if (lookAhead(')')) {
			consume(')');
			return Collections.emptyList();
		}
		List<ArgType> args = new LinkedList<ArgType>();
		do {
			args.add(consumeType());
		} while (!lookAhead(')'));
		consume(')');
		return args;
	}

	private static String mergeSignature(List<String> list) {
		if (list.size() == 1) {
			return list.get(0);
		}
		StringBuilder sb = new StringBuilder();
		for (String s : list) {
			sb.append(s);
		}
		return sb.toString();
	}

	private String debugString() {
		return sign + " at position " + pos + " ('" + sign.charAt(pos) + "')";
	}

	@Override
	public String toString() {
		if (pos == -1) {
			return sign;
		}
		return sign.substring(0, mark) + '{' + sign.substring(mark, pos) + '}' + sign.substring(pos);
	}
}
