/*
 * Copyright (c) 2018. Univocity Software Pty Ltd
 * <p/>
 * 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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 com.univocity.parsers.common.record;

import com.univocity.parsers.annotations.helpers.*;
import com.univocity.parsers.common.*;
import com.univocity.parsers.common.fields.*;
import com.univocity.parsers.conversions.*;

import java.lang.annotation.*;
import java.util.*;

class RecordMetaDataImpl<C extends Context> implements RecordMetaData {

	final C context;

	@SuppressWarnings("rawtypes")
	private Map<Class, Conversion> conversionByType = new HashMap<Class, Conversion>();

	@SuppressWarnings("rawtypes")
	private Map<Class, Map<Annotation, Conversion>> conversionsByAnnotation = new HashMap<Class, Map<Annotation, Conversion>>();

	private Map<Integer, Annotation> annotationHashes = new HashMap<Integer, Annotation>();
	private MetaData[] indexMap;

	private FieldConversionMapping conversions = null;

	RecordMetaDataImpl(C context) {
		this.context = context;
	}

	private MetaData getMetaData(String name) {
		int index = context.indexOf(name);
		if (index == -1) {
			getValidatedHeaders();
			throw new IllegalArgumentException("Header name '" + name + "' not found. Available columns are: " + Arrays.asList(selectedHeaders()));
		}
		return getMetaData(index);
	}

	private NormalizedString[] getValidatedHeaders() {
		NormalizedString[] headers = NormalizedString.toIdentifierGroupArray(context.headers());
		if (headers == null || headers.length == 0) {
			throw new IllegalStateException("No headers parsed from input nor provided in the user settings. Only index-based operations are available.");
		}
		return headers;
	}

	private MetaData getMetaData(Enum<?> column) {
		NormalizedString[] headers = NormalizedString.toIdentifierGroupArray(context.headers());
		if (headers == null || headers.length == 0) {
			throw new IllegalStateException("No headers parsed from input nor provided in the user settings. Only index-based operations are available.");
		}
		return getMetaData(context.indexOf(column));
	}

	public MetaData getMetaData(int index) {
		if (indexMap == null || indexMap.length < index + 1 || indexMap[index] == null) {
			synchronized (this) {
				if (indexMap == null || indexMap.length < index + 1 || indexMap[index] == null) {
					int startFrom = 0;
					int lastIndex = index;

					if (indexMap != null) {
						startFrom = indexMap.length;
						indexMap = Arrays.copyOf(indexMap, index + 1);
					} else {
						String[] headers = context.headers();
						if (headers != null && lastIndex < headers.length) {
							lastIndex = headers.length;
						}

						int[] indexes = context.extractedFieldIndexes();
						if (indexes != null) {
							for (int i = 0; i < indexes.length; i++) {
								if (lastIndex < indexes[i]) {
									lastIndex = indexes[i];
								}
							}
						}

						indexMap = new MetaData[lastIndex + 1];
					}

					for (int i = startFrom; i < lastIndex + 1; i++) {
						indexMap[i] = new MetaData(i);
					}
				}
			}
		}
		return indexMap[index];
	}

	@Override
	public int indexOf(Enum<?> column) {
		return getMetaData(column).index;
	}

	MetaData metadataOf(String headerName) {
		return getMetaData(headerName);
	}

	MetaData metadataOf(Enum<?> column) {
		return getMetaData(column);
	}

	MetaData metadataOf(int columnIndex) {
		return getMetaData(columnIndex);
	}

	@Override
	public int indexOf(String headerName) {
		return getMetaData(headerName).index;
	}

	@Override
	public Class<?> typeOf(Enum<?> column) {
		return getMetaData(column).type;
	}

	@Override
	public Class<?> typeOf(String headerName) {
		return getMetaData(headerName).type;
	}

	@Override
	public Class<?> typeOf(int columnIndex) {
		return getMetaData(columnIndex).type;
	}

	@Override
	public <T> void setDefaultValueOfColumns(T defaultValue, Enum<?>... columns) {
		for (Enum<?> column : columns) {
			getMetaData(column).defaultValue = defaultValue;
		}
	}

	@Override
	public <T> void setDefaultValueOfColumns(T defaultValue, String... headerNames) {
		for (String headerName : headerNames) {
			getMetaData(headerName).defaultValue = defaultValue;
		}
	}

	@Override
	public <T> void setDefaultValueOfColumns(T defaultValue, int... columnIndexes) {
		for (int columnIndex : columnIndexes) {
			getMetaData(columnIndex).defaultValue = defaultValue;
		}
	}

	@Override
	public Object defaultValueOf(Enum<?> column) {
		return getMetaData(column).defaultValue;
	}

	@Override
	public Object defaultValueOf(String headerName) {
		return getMetaData(headerName).defaultValue;
	}

	@Override
	public Object defaultValueOf(int columnIndex) {
		return getMetaData(columnIndex).defaultValue;
	}

	private FieldConversionMapping getConversions() {
		if (conversions == null) {
			conversions = new FieldConversionMapping();
		}
		return conversions;
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	@Override
	public <T extends Enum<T>> FieldSet<T> convertFields(Class<T> enumType, Conversion... conversions) {
		return (FieldSet) getConversions().applyConversionsOnFieldEnums(conversions);
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	@Override
	public FieldSet<String> convertFields(Conversion... conversions) {
		return getConversions().applyConversionsOnFieldNames(conversions);
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	@Override
	public FieldSet<Integer> convertIndexes(Conversion... conversions) {
		return getConversions().applyConversionsOnFieldIndexes(conversions);
	}

	@Override
	public String[] headers() {
		return context.headers();
	}

	@Override
	public String[] selectedHeaders() {
		return context.selectedHeaders();
	}

	String getValue(String[] data, String headerName) {
		MetaData md = metadataOf(headerName);
		if (md.index >= data.length) {
			return null;
		}
		return data[md.index];
	}

	String getValue(String[] data, int columnIndex) {
		MetaData md = metadataOf(columnIndex);
		return data[md.index];
	}

	String getValue(String[] data, Enum<?> column) {
		MetaData md = metadataOf(column);
		return data[md.index];
	}

	@SuppressWarnings("rawtypes")
	private <T> T convert(MetaData md, String[] data, Class<T> expectedType, Conversion[] conversions) {
		return expectedType.cast(convert(md, data, conversions));
	}

	@SuppressWarnings("rawtypes")
	private Object convert(MetaData md, String[] data, Object defaultValue, Conversion[] conversions) {
		Object out = convert(md, data, conversions);
		return out == null ? defaultValue : out;
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	private static Object convert(MetaData md, String[] data, Conversion[] conversions) {
		Object out = data[md.index];
		for (int i = 0; i < conversions.length; i++) {
			out = conversions[i].execute(out);
		}
		return out;
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	<T> T getValue(String[] data, String headerName, T defaultValue, Conversion[] conversions) {
		return (T) convert(metadataOf(headerName), data, defaultValue, conversions);
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	<T> T getValue(String[] data, int columnIndex, T defaultValue, Conversion[] conversions) {
		return (T) convert(metadataOf(columnIndex), data, defaultValue, conversions);
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	<T> T getValue(String[] data, Enum<?> column, T defaultValue, Conversion[] conversions) {
		return (T) convert(metadataOf(column), data, defaultValue, conversions);
	}

	@SuppressWarnings("rawtypes")
	<T> T getValue(String[] data, String headerName, Class<T> expectedType, Conversion[] conversions) {
		return convert(metadataOf(headerName), data, expectedType, conversions);
	}

	@SuppressWarnings("rawtypes")
	<T> T getValue(String[] data, int columnIndex, Class<T> expectedType, Conversion[] conversions) {
		return convert(metadataOf(columnIndex), data, expectedType, conversions);
	}

	@SuppressWarnings("rawtypes")
	<T> T getValue(String[] data, Enum<?> column, Class<T> expectedType, Conversion[] conversions) {
		return convert(metadataOf(column), data, expectedType, conversions);
	}

	@SuppressWarnings({"rawtypes", "unchecked"})
	private <T> T convert(MetaData md, String[] data, Class<T> type, T defaultValue, Annotation annotation) {
		Object out = md.index < data.length ? data[md.index] : null;

		if (out == null) {
			out = defaultValue == null ? md.defaultValue : defaultValue;
		}

		if (annotation == null) {
			initializeMetadataConversions(data, md);
			out = md.convert(out);

			if (out == null) {
				out = defaultValue == null ? md.defaultValue : defaultValue;
			}
		}

		if (type != null) {
			if (out != null && type.isAssignableFrom(out.getClass())) {
				return (T) out;
			}
			Conversion conversion;
			if (type != String.class) {
				if (annotation == null) {
					conversion = conversionByType.get(type);
					if (conversion == null) {
						conversion = AnnotationHelper.getDefaultConversion(type, null, null);
						conversionByType.put(type, conversion);
					}
				} else {
					Map<Annotation, Conversion> m = conversionsByAnnotation.get(type);
					if (m == null) {
						m = new HashMap<Annotation, Conversion>();
						conversionsByAnnotation.put(type, m);
					}
					conversion = m.get(annotation);
					if (conversion == null) {
						conversion = AnnotationHelper.getConversion(type, annotation);
						m.put(annotation, conversion);
					}
				}

				if (conversion == null) {
					String message = "";
					if (type == Date.class || type == Calendar.class) {
						message = ". Need to specify format for date";
					}
					DataProcessingException exception = new DataProcessingException("Cannot convert '{value}' to " + type.getName() + message);
					exception.setValue(out);
					exception.setErrorContentLength(context.errorContentLength());
					throw exception;


				}
				out = conversion.execute(out);
			}

		}
		if (type == null) {
			return (T) out;
		}
		try {
			return type.cast(out);
		} catch (ClassCastException e) {
			DataProcessingException exception = new DataProcessingException("Cannot cast value '{value}' of type " + out.getClass().toString() + " to " + type.getName());
			exception.setValue(out);
			exception.setErrorContentLength(context.errorContentLength());
			throw exception;
		}
	}

	private void initializeMetadataConversions(String[] data, MetaData md) {
		if (conversions != null) {
			synchronized (this) {

				String[] headers = headers();
				if (headers == null) {
					headers = data;
				}
				conversions.prepareExecution(false, headers);
				md.setDefaultConversions(conversions.getConversions(md.index, md.type));
			}
		}
	}

	<T> T getObjectValue(String[] data, String headerName, Class<T> type, T defaultValue) {
		return convert(metadataOf(headerName), data, type, defaultValue, null);
	}

	<T> T getObjectValue(String[] data, int columnIndex, Class<T> type, T defaultValue) {
		return convert(metadataOf(columnIndex), data, type, defaultValue, null);
	}

	<T> T getObjectValue(String[] data, Enum<?> column, Class<T> type, T defaultValue) {
		return convert(metadataOf(column), data, type, defaultValue, null);
	}

	<T> T getObjectValue(String[] data, String headerName, Class<T> type, T defaultValue, String format, String... formatOptions) {
		if (format == null) {
			return getObjectValue(data, headerName, type, defaultValue);
		}
		return convert(metadataOf(headerName), data, type, defaultValue, buildAnnotation(type, format, formatOptions));
	}

	<T> T getObjectValue(String[] data, int columnIndex, Class<T> type, T defaultValue, String format, String... formatOptions) {
		if (format == null) {
			return getObjectValue(data, columnIndex, type, defaultValue);
		}
		return convert(metadataOf(columnIndex), data, type, defaultValue, buildAnnotation(type, format, formatOptions));
	}

	<T> T getObjectValue(String[] data, Enum<?> column, Class<T> type, T defaultValue, String format, String... formatOptions) {
		if (format == null) {
			return getObjectValue(data, column, type, defaultValue);
		}
		return convert(metadataOf(column), data, type, defaultValue, buildAnnotation(type, format, formatOptions));
	}

	static Annotation buildBooleanStringAnnotation(final String[] trueStrings, final String[] falseStrings) {
		return new com.univocity.parsers.annotations.BooleanString() {
			@Override
			public String[] trueStrings() {
				return trueStrings == null ? ArgumentUtils.EMPTY_STRING_ARRAY : trueStrings;
			}

			@Override
			public String[] falseStrings() {
				return falseStrings == null ? ArgumentUtils.EMPTY_STRING_ARRAY : falseStrings;
			}

			@Override
			public Class<? extends Annotation> annotationType() {
				return com.univocity.parsers.annotations.BooleanString.class;
			}
		};
	}

	private static Annotation newFormatAnnotation(final String format, final String... formatOptions) {
		return new com.univocity.parsers.annotations.Format() {
			@Override
			public String[] formats() {
				return new String[]{format};
			}

			@Override
			public String[] options() {
				return formatOptions;
			}

			@Override
			public Class<? extends Annotation> annotationType() {
				return com.univocity.parsers.annotations.Format.class;
			}
		};
	}

	<T> Annotation buildAnnotation(Class<T> type, final String args1, final String... args2) {
		Integer hash = (type.hashCode() * 31) + String.valueOf(args1).hashCode() + (31 * Arrays.toString(args2).hashCode());
		Annotation out = annotationHashes.get(hash);
		if (out == null) {
			if (type == Boolean.class || type == boolean.class) {
				out = buildBooleanStringAnnotation(args1 == null ? null : new String[]{args1}, args2);
			} else {
				out = newFormatAnnotation(args1, args2);
			}
			annotationHashes.put(hash, out);
		}
		return out;
	}

	@SuppressWarnings("rawtypes")
	@Override
	public void setTypeOfColumns(Class<?> type, Enum... columns) {
		for (int i = 0; i < columns.length; i++) {
			getMetaData(columns[i]).type = type;
		}
	}

	@Override
	public void setTypeOfColumns(Class<?> type, String... headerNames) {
		for (int i = 0; i < headerNames.length; i++) {
			getMetaData(headerNames[i]).type = type;
		}
	}

	@Override
	public void setTypeOfColumns(Class<?> type, int... columnIndexes) {
		for (int i = 0; i < columnIndexes.length; i++) {
			getMetaData(columnIndexes[i]).type = type;
		}
	}

	@Override
	public boolean containsColumn(String headerName) {
		if (headerName == null) {
			return false;
		}
		return context.indexOf(headerName) != -1;
	}
}
