// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you 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.apache.juneau.oapi;

import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.httppart.HttpPartSchema.Type.*;
import static org.apache.juneau.httppart.HttpPartSchema.Format.*;
import static org.apache.juneau.httppart.HttpPartSchema.CollectionFormat.*;

import java.util.*;

import org.apache.juneau.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.uon.*;

/**
 * Session object that lives for the duration of a single use of {@link OpenApiParser}.
 *
 * <p>
 * This class is NOT thread safe.
 * It is typically discarded after one-time use although it can be reused within the same thread.
 */
public class OpenApiParserSession extends UonParserSession {

	// Cache these for faster lookup
	private static final BeanContext BC = BeanContext.DEFAULT;
	private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class);
	private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class);
	private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class);
	private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class);
	private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class);
	private static final ClassMeta<ObjectList> CM_ObjectList = BC.getClassMeta(ObjectList.class);
	private static final ClassMeta<ObjectMap> CM_ObjectMap = BC.getClassMeta(ObjectMap.class);

	private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT;

	//-------------------------------------------------------------------------------------------------------------------
	// Instance
	//-------------------------------------------------------------------------------------------------------------------

	private final OpenApiParser ctx;

	/**
	 * Create a new session using properties specified in the context.
	 *
	 * @param ctx
	 * 	The context creating this session object.
	 * 	The context contains all the configuration settings for this object.
	 * @param args
	 * 	Runtime session arguments.
	 */
	protected OpenApiParserSession(OpenApiParser ctx, ParserSessionArgs args) {
		super(ctx, args);
		this.ctx = ctx;
	}


	@SuppressWarnings("unchecked")
	@Override /* HttpPartParser */
	public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws ParseException, SchemaValidationException {
		boolean isOptional = type.isOptional();

		while (type != null && type.isOptional())
			type = (ClassMeta<T>)type.getElementType();

		if (type == null)
			type = (ClassMeta<T>)object();

		schema = ObjectUtils.firstNonNull(schema, getSchema(), DEFAULT_SCHEMA);

		T t = parseInner(partType, schema, in, type);
		if (t == null && type.isPrimitive())
			t = type.getPrimitiveDefault();
		schema.validateOutput(t, ctx);

		if (isOptional)
			t = (T)Optional.ofNullable(t);

		return t;
	}

	@SuppressWarnings({ "unchecked" })
	private<T> T parseInner(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws SchemaValidationException, ParseException {
		schema.validateInput(in);
		if (in == null) {
			if (schema.getDefault() == null)
				return null;
			in = schema.getDefault();
		} else {
			HttpPartSchema.Type t = schema.getType(type);
			HttpPartSchema.Format f = schema.getFormat(type);

			if (t == STRING) {
				if (type.isObject()) {
					if (f == BYTE)
						return (T)base64Decode(in);
					if (f == DATE || f == DATE_TIME)
						return (T)parseIsoCalendar(in);
					if (f == BINARY)
						return (T)fromHex(in);
					if (f == BINARY_SPACED)
						return (T)fromSpacedHex(in);
					if (f == HttpPartSchema.Format.UON)
						return super.parse(partType, schema, in, type);
					return (T)in;
				}
				if (f == BYTE)
					return toType(base64Decode(in), type);
				if (f == DATE || f == DATE_TIME)
					return toType(parseIsoCalendar(in), type);
				if (f == BINARY)
					return toType(fromHex(in), type);
				if (f == BINARY_SPACED)
					return toType(fromSpacedHex(in), type);
				if (f == HttpPartSchema.Format.UON)
					return super.parse(partType, schema, in, type);
				return toType(in, type);

			} else if (t == ARRAY) {
				if (type.isObject())
					type = (ClassMeta<T>)CM_ObjectList;

				ClassMeta<?> eType = type.isObject() ? string() : type.getElementType();
				if (eType == null)
					eType = schema.getParsedType().getElementType();

				HttpPartSchema.CollectionFormat cf = schema.getCollectionFormat();
				String[] ss = new String[0];

				if (cf == MULTI)
					ss = new String[]{in};
				else if (cf == CSV)
					ss = split(in, ',');
				else if (cf == PIPES)
					ss = split(in, '|');
				else if (cf == SSV)
					ss = splitQuoted(in);
				else if (cf == TSV)
					ss = split(in, '\t');
				else if (cf == HttpPartSchema.CollectionFormat.UON)
					return super.parse(partType, null, in, type);
				else if (cf == NO_COLLECTION_FORMAT) {
					if (firstNonWhitespaceChar(in) == '@' && lastNonWhitespaceChar(in) == ')')
						return super.parse(partType, null, in, type);
					ss = split(in, ',');
				}

				HttpPartSchema items = schema.getItems();
				if (items == null)
					items = HttpPartSchema.DEFAULT;
				Object[] o = new Object[ss.length];
				for (int i = 0; i < ss.length; i++)
					o[i] = parse(partType, items, ss[i], eType);
				if (type.hasMutaterFrom(schema.getParsedType()) || schema.getParsedType().hasMutaterTo(type))
					return toType(toType(o, schema.getParsedType()), type);
				return toType(o, type);

			} else if (t == BOOLEAN) {
				if (type.isObject())
					type = (ClassMeta<T>)CM_Boolean;
				if (type.isBoolean())
					return super.parse(partType, schema, in, type);
				return toType(super.parse(partType, schema, in, CM_Boolean), type);

			} else if (t == INTEGER) {
				if (type.isObject()) {
					if (f == INT64)
						type = (ClassMeta<T>)CM_Long;
					else
						type = (ClassMeta<T>)CM_Integer;
				}
				if (type.isNumber())
					return super.parse(partType, schema, in, type);
				return toType(super.parse(partType, schema, in, CM_Integer), type);

			} else if (t == NUMBER) {
				if (type.isObject()) {
					if (f == DOUBLE)
						type = (ClassMeta<T>)CM_Double;
					else
						type = (ClassMeta<T>)CM_Float;
				}
				if (type.isNumber())
					return super.parse(partType, schema, in, type);
				return toType(super.parse(partType, schema, in, CM_Integer), type);

			} else if (t == OBJECT) {
				if (type.isObject())
					type = (ClassMeta<T>)CM_ObjectMap;
				if (schema.hasProperties() && type.isMapOrBean()) {
					try {
						if (type.isBean()) {
							BeanMap<T> m = BC.createBeanSession().newBeanMap(type.getInnerClass());
							for (Map.Entry<String,Object> e : parse(partType, DEFAULT_SCHEMA, in, CM_ObjectMap).entrySet()) {
								String key = e.getKey();
								BeanPropertyMeta bpm = m.getPropertyMeta(key);
								m.put(key, parse(partType, schema.getProperty(key), stringify(e.getValue()), bpm == null ? object() : bpm.getClassMeta()));
							}
							return m.getBean();
						}
						Map<String,Object> m = (Map<String,Object>)type.newInstance();
						for (Map.Entry<String,Object> e : parse(partType, DEFAULT_SCHEMA, in, CM_ObjectMap).entrySet()) {
							String key = e.getKey();
							m.put(key, parse(partType, schema.getProperty(key), stringify(e.getValue()), object()));
						}
						return (T)m;
					} catch (Exception e1) {
						throw new ParseException(e1, "Could not instantiate type ''{0}''.", type);
					}
				}
				return super.parse(partType, schema, in, type);

			} else if (t == FILE) {
				throw new ParseException("File part not supported.");

			} else if (t == NO_TYPE) {
				// This should never be returned by HttpPartSchema.getType(ClassMeta).
				throw new ParseException("Invalid type.");
			}
		}

		return super.parse(partType, schema, in, type);
	}

	private <T> T toType(Object in, ClassMeta<T> type) throws ParseException {
		try {
			return convertToType(in, type);
		} catch (InvalidDataConversionException e) {
			throw new ParseException(e.getMessage());
		}
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Other methods
	//-----------------------------------------------------------------------------------------------------------------

	@Override /* Session */
	public ObjectMap toMap() {
		return super.toMap()
			.append("OpenApiParserSession", new DefaultFilteringObjectMap()
			);
	}
}
