/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * Subscription License ("License"). You may not use this file except in
 * compliance with the License. You can obtain a copy of the License by
 * contacting Liferay, Inc. See the License for the specific language governing
 * permissions and limitations under the License, including but not limited to
 * distribution rights of the Software.
 *
 *
 *
 */

package com.liferay.dynamic.data.mapping.internal.util;

import com.liferay.dynamic.data.mapping.configuration.DDMIndexerConfiguration;
import com.liferay.dynamic.data.mapping.model.DDMFormField;
import com.liferay.dynamic.data.mapping.model.DDMFormFieldType;
import com.liferay.dynamic.data.mapping.model.DDMStructure;
import com.liferay.dynamic.data.mapping.service.DDMStructureLocalService;
import com.liferay.dynamic.data.mapping.storage.DDMFormValues;
import com.liferay.dynamic.data.mapping.storage.Field;
import com.liferay.dynamic.data.mapping.storage.Fields;
import com.liferay.dynamic.data.mapping.util.DDM;
import com.liferay.dynamic.data.mapping.util.DDMFormValuesToFieldsConverter;
import com.liferay.dynamic.data.mapping.util.DDMIndexer;
import com.liferay.petra.string.StringBundler;
import com.liferay.petra.string.StringPool;
import com.liferay.portal.configuration.metatype.bnd.util.ConfigurableUtil;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.json.JSONArray;
import com.liferay.portal.kernel.json.JSONFactoryUtil;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.search.BooleanQuery;
import com.liferay.portal.kernel.search.Document;
import com.liferay.portal.kernel.search.DocumentImpl;
import com.liferay.portal.kernel.search.FieldArray;
import com.liferay.portal.kernel.search.filter.QueryFilter;
import com.liferay.portal.kernel.search.generic.BooleanQueryImpl;
import com.liferay.portal.kernel.search.generic.NestedQuery;
import com.liferay.portal.kernel.util.ArrayUtil;
import com.liferay.portal.kernel.util.FastDateFormatFactoryUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.HtmlUtil;
import com.liferay.portal.kernel.util.LocaleUtil;
import com.liferay.portal.kernel.util.PropsKeys;
import com.liferay.portal.kernel.util.PropsUtil;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.search.engine.SearchEngineInformation;
import com.liferay.portal.search.query.Queries;
import com.liferay.portal.search.sort.FieldSort;
import com.liferay.portal.search.sort.NestedSort;
import com.liferay.portal.search.sort.Sort;
import com.liferay.portal.search.sort.SortBuilderFactory;
import com.liferay.portal.search.sort.SortOrder;
import com.liferay.portal.search.sort.Sorts;

import java.io.Serializable;

import java.math.BigDecimal;

import java.text.Format;

import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;

/**
 * @author Alexander Chow
 */
@Component(
	configurationPid = "com.liferay.dynamic.data.mapping.configuration.DDMIndexerConfiguration",
	immediate = true, service = DDMIndexer.class
)
public class DDMIndexerImpl implements DDMIndexer {

	@Override
	public void addAttributes(
		Document document, DDMStructure ddmStructure,
		DDMFormValues ddmFormValues) {

		boolean legacyDDMIndexFieldsEnabled = isLegacyDDMIndexFieldsEnabled();

		FieldArray fieldArray = (FieldArray)document.getField(DDM_FIELD_ARRAY);

		if ((fieldArray == null) && !legacyDDMIndexFieldsEnabled) {
			fieldArray = new FieldArray(DDM_FIELD_ARRAY);

			document.add(fieldArray);
		}

		Set<Locale> locales = ddmFormValues.getAvailableLocales();

		Fields fields = toFields(ddmStructure, ddmFormValues);

		for (Field field : fields) {
			try {
				String indexType = ddmStructure.getFieldProperty(
					field.getName(), "indexType");

				if (Validator.isNull(indexType) || indexType.equals("none")) {
					continue;
				}

				String name = null;
				Serializable value = null;

				if (GetterUtil.getBoolean(
						ddmStructure.getFieldProperty(
							field.getName(), "localizable"))) {

					for (Locale locale : locales) {
						name = encodeName(
							ddmStructure.getStructureId(), field.getName(),
							locale, indexType);
						value = field.getValue(locale);

						if (legacyDDMIndexFieldsEnabled) {
							addToDocument(
								document, field, name, value, indexType);
						}
						else {
							fieldArray.addField(
								createField(
									field, name, value, indexType, locale));
						}
					}
				}
				else {
					name = encodeName(
						ddmStructure.getStructureId(), field.getName(), null,
						indexType);
					value = field.getValue(ddmFormValues.getDefaultLocale());

					if (legacyDDMIndexFieldsEnabled) {
						addToDocument(document, field, name, value, indexType);
					}
					else {
						fieldArray.addField(
							createField(field, name, value, indexType, null));
					}
				}
			}
			catch (Exception exception) {
				if (_log.isWarnEnabled()) {
					_log.warn(exception, exception);
				}
			}
		}
	}

	@Override
	public Sort createDDMStructureFieldSort(
			DDMStructure ddmStructure, String fieldName, Locale locale,
			SortOrder sortOrder)
		throws PortalException {

		DDMFormField ddmFormField = ddmStructure.getDDMFormField(fieldName);

		if (GetterUtil.getBoolean(ddmFormField.getProperty("localizable"))) {
			if (locale == null) {
				throw new IllegalArgumentException(
					"Locale cannot be null if the dynamic data mapping form " +
						"field is localizable");
			}
		}
		else {
			locale = null;
		}

		StringBundler sb = new StringBundler(5);

		if (isLegacyDDMIndexFieldsEnabled()) {
			sb.append(
				encodeName(ddmStructure.getStructureId(), fieldName, locale));
		}
		else {
			sb.append(DDMIndexer.DDM_FIELD_ARRAY);
			sb.append(StringPool.PERIOD);

			String indexType = ddmStructure.getFieldProperty(
				fieldName, "indexType");

			sb.append(getValueFieldName(indexType, locale));
		}

		sb.append(StringPool.UNDERLINE);

		String ddmFormFieldType = ddmFormField.getType();

		if (Objects.equals(ddmFormFieldType, DDMFormFieldType.DECIMAL) ||
			Objects.equals(ddmFormFieldType, DDMFormFieldType.INTEGER) ||
			Objects.equals(ddmFormFieldType, DDMFormFieldType.NUMBER) ||
			Objects.equals(ddmFormFieldType, DDMFormFieldType.NUMERIC)) {

			sb.append("Number");
		}
		else {
			sb.append("String");
		}

		FieldSort fieldSort = sorts.field(
			com.liferay.portal.kernel.search.Field.getSortableFieldName(
				sb.toString()),
			sortOrder);

		if (isLegacyDDMIndexFieldsEnabled()) {
			return fieldSort;
		}

		NestedSort nestedSort = sorts.nested(DDMIndexer.DDM_FIELD_ARRAY);

		nestedSort.setFilterQuery(
			queries.term(
				StringBundler.concat(
					DDMIndexer.DDM_FIELD_ARRAY, StringPool.PERIOD,
					DDMIndexer.DDM_FIELD_NAME),
				encodeName(ddmStructure.getStructureId(), fieldName, locale)));

		fieldSort.setNestedSort(nestedSort);

		return fieldSort;
	}

	@Override
	public Sort createDDMStructureFieldSort(
			String ddmStructureFieldName, Locale locale, SortOrder sortOrder)
		throws PortalException {

		String[] ddmStructureFieldNameParts = StringUtil.split(
			ddmStructureFieldName, DDM_FIELD_SEPARATOR);

		long ddmStructureId = GetterUtil.getLong(ddmStructureFieldNameParts[2]);

		String fieldName = StringUtil.replaceLast(
			ddmStructureFieldNameParts[3],
			StringPool.UNDERLINE.concat(LocaleUtil.toLanguageId(locale)),
			StringPool.BLANK);

		DDMStructure ddmStructure = _ddmStructureLocalService.getStructure(
			ddmStructureId);

		return createDDMStructureFieldSort(
			ddmStructure, fieldName, locale, sortOrder);
	}

	public QueryFilter createFieldValueQueryFilter(
			DDMStructure ddmStructure, String fieldName, Locale locale,
			Serializable value)
		throws Exception {

		String indexType = ddmStructure.getFieldProperty(
			fieldName, "indexType");

		return createFieldValueQueryFilter(
			ddmStructure,
			encodeName(ddmStructure.getStructureId(), fieldName, locale), value,
			fieldName, indexType, locale);
	}

	@Override
	public QueryFilter createFieldValueQueryFilter(
			String ddmStructureFieldName, Serializable ddmStructureFieldValue,
			Locale locale)
		throws Exception {

		String[] ddmStructureFieldNameParts = StringUtil.split(
			ddmStructureFieldName, DDM_FIELD_SEPARATOR);

		DDMStructure ddmStructure = _ddmStructureLocalService.getStructure(
			GetterUtil.getLong(ddmStructureFieldNameParts[2]));

		String fieldName = StringUtil.replaceLast(
			ddmStructureFieldNameParts[3],
			StringPool.UNDERLINE.concat(LocaleUtil.toLanguageId(locale)),
			StringPool.BLANK);

		return createFieldValueQueryFilter(
			ddmStructure, ddmStructureFieldName, ddmStructureFieldValue,
			fieldName, ddmStructureFieldNameParts[1], locale);
	}

	@Override
	public String encodeName(long ddmStructureId, String fieldName) {
		return encodeName(ddmStructureId, fieldName, null);
	}

	@Override
	public String encodeName(
		long ddmStructureId, String fieldName, Locale locale) {

		String indexType = StringPool.BLANK;
		boolean localizable = true;

		if (ddmStructureId > 0) {
			DDMStructure ddmStructure =
				_ddmStructureLocalService.fetchDDMStructure(ddmStructureId);

			if (ddmStructure != null) {
				try {
					indexType = ddmStructure.getFieldProperty(
						fieldName, "indexType");
					localizable = GetterUtil.getBoolean(
						ddmStructure.getFieldProperty(
							fieldName, "localizable"));
				}
				catch (PortalException portalException) {
					throw new IllegalArgumentException(
						StringBundler.concat(
							"Unable to obtain index tpe for field ", fieldName,
							" and DDM structure ID ", ddmStructureId),
						portalException);
				}
			}
		}

		if (localizable) {
			return encodeName(ddmStructureId, fieldName, locale, indexType);
		}

		return encodeName(ddmStructureId, fieldName, null, indexType);
	}

	@Override
	public String extractIndexableAttributes(
		DDMStructure ddmStructure, DDMFormValues ddmFormValues, Locale locale) {

		Format dateFormat = FastDateFormatFactoryUtil.getSimpleDateFormat(
			PropsUtil.get(PropsKeys.INDEX_DATE_FORMAT_PATTERN));

		StringBundler sb = new StringBundler();

		Fields fields = toFields(ddmStructure, ddmFormValues);

		for (Field field : fields) {
			try {
				String indexType = ddmStructure.getFieldProperty(
					field.getName(), "indexType");

				if (Validator.isNull(indexType) || indexType.equals("none")) {
					continue;
				}

				Serializable value = field.getValue(locale);

				if (value instanceof Boolean || value instanceof Number) {
					sb.append(value);
				}
				else if (value instanceof Date) {
					sb.append(dateFormat.format(value));
				}
				else if (value instanceof Date[]) {
					Date[] dates = (Date[])value;

					for (int i = 0; i < dates.length; i++) {
						sb.append(dateFormat.format(dates[i]));

						if (i < (dates.length - 1)) {
							sb.append(StringPool.SPACE);
						}
					}
				}
				else if (value instanceof Object[]) {
					Object[] values = (Object[])value;

					String type = field.getType();

					boolean richText = type.equals(
						DDMImpl.TYPE_DDM_TEXT_HTML);

					for (int i = 0; i < values.length; i++) {
						if (richText) {
							String valueString = String.valueOf(values[i]);

							sb.append(HtmlUtil.extractText(valueString));
						}
						else {
							sb.append(values[i]);
						}

						if (i < (values.length - 1)) {
							sb.append(StringPool.SPACE);
						}
					}
				}
				else {
					String valueString = String.valueOf(value);

					String type = field.getType();

					if (type.equals(DDMImpl.TYPE_SELECT)) {
						JSONArray jsonArray = JSONFactoryUtil.createJSONArray(
							valueString);

						String[] stringArray = ArrayUtil.toStringArray(
							jsonArray);

						sb.append(stringArray);
					}
					else {
						if (type.equals(DDMImpl.TYPE_DDM_TEXT_HTML)) {
							valueString = HtmlUtil.extractText(valueString);
						}

						sb.append(valueString);
					}
				}

				sb.append(StringPool.SPACE);
			}
			catch (Exception exception) {
				if (_log.isWarnEnabled()) {
					_log.warn(exception, exception);
				}
			}
		}

		if (sb.index() > 0) {
			sb.setIndex(sb.index() - 1);
		}

		return sb.toString();
	}

	@Override
	public String getValueFieldName(String indexType) {
		return getValueFieldName(indexType, null);
	}

	@Override
	public String getValueFieldName(String indexType, Locale locale) {
		String valueFieldName = DDM_VALUE_FIELD_NAME_PREFIX;

		if (indexType != null) {
			valueFieldName = valueFieldName.concat(
				StringUtil.upperCaseFirstLetter(indexType));
		}

		if (locale != null) {
			valueFieldName = StringBundler.concat(
				valueFieldName, StringPool.UNDERLINE,
				LocaleUtil.toLanguageId(locale));
		}

		return valueFieldName;
	}

	@Override
	public boolean isLegacyDDMIndexFieldsEnabled() {
		if (Objects.equals(searchEngineInformation.getVendorString(), "Solr")) {
			return true;
		}

		return _ddmIndexerConfiguration.enableLegacyDDMIndexFields();
	}

	@Activate
	@Modified
	protected void activate(Map<String, Object> properties) {
		_ddmIndexerConfiguration = ConfigurableUtil.createConfigurable(
			DDMIndexerConfiguration.class, properties);
	}

	protected void addFieldValueRequiredTerm(
		BooleanQuery booleanQuery, String ddmStructureFieldName,
		String ddmStructureFieldValue, String indexType, Locale locale) {

		if (isLegacyDDMIndexFieldsEnabled()) {
			booleanQuery.addRequiredTerm(
				ddmStructureFieldName,
				StringPool.QUOTE + ddmStructureFieldValue + StringPool.QUOTE);

			return;
		}

		booleanQuery.addRequiredTerm(
			StringBundler.concat(
				DDM_FIELD_ARRAY, StringPool.PERIOD, DDM_FIELD_NAME),
			ddmStructureFieldName);
		booleanQuery.addRequiredTerm(
			StringBundler.concat(
				DDM_FIELD_ARRAY, StringPool.PERIOD,
				getValueFieldName(indexType, locale)),
			StringPool.QUOTE + ddmStructureFieldValue + StringPool.QUOTE);
	}

	protected void addToDocument(
			Document document, Field field, String name, Serializable value,
			String indexType)
		throws PortalException {

		if (value instanceof BigDecimal) {
			document.addNumberSortable(name, (BigDecimal)value);
		}
		else if (value instanceof BigDecimal[]) {
			document.addNumberSortable(name, (BigDecimal[])value);
		}
		else if (value instanceof Boolean) {
			document.addKeywordSortable(name, (Boolean)value);
		}
		else if (value instanceof Boolean[]) {
			document.addKeywordSortable(name, (Boolean[])value);
		}
		else if (value instanceof Date) {
			document.addDateSortable(name, (Date)value);
		}
		else if (value instanceof Date[]) {
			document.addDateSortable(name, (Date[])value);
		}
		else if (value instanceof Double) {
			document.addNumberSortable(name, (Double)value);
		}
		else if (value instanceof Double[]) {
			document.addNumberSortable(name, (Double[])value);
		}
		else if (value instanceof Integer) {
			document.addNumberSortable(name, (Integer)value);
		}
		else if (value instanceof Integer[]) {
			document.addNumberSortable(name, (Integer[])value);
		}
		else if (value instanceof Long) {
			document.addNumberSortable(name, (Long)value);
		}
		else if (value instanceof Long[]) {
			document.addNumberSortable(name, (Long[])value);
		}
		else if (value instanceof Float) {
			document.addNumberSortable(name, (Float)value);
		}
		else if (value instanceof Float[]) {
			document.addNumberSortable(name, (Float[])value);
		}
		else if (value instanceof Number[]) {
			Number[] numbers = (Number[])value;

			Double[] doubles = new Double[numbers.length];

			for (int i = 0; i < numbers.length; i++) {
				doubles[i] = numbers[i].doubleValue();
			}

			document.addNumberSortable(name, doubles);
		}
		else if (value instanceof Object[]) {
			String[] valuesString = ArrayUtil.toStringArray((Object[])value);

			if (indexType.equals("keyword")) {
				document.addKeywordSortable(name, valuesString);
			}
			else {
				document.addTextSortable(name, valuesString);
			}
		}
		else {
			String valueString = String.valueOf(value);

			String type = field.getType();

			if (type.equals(DDMFormFieldType.GEOLOCATION)) {
				JSONObject jsonObject = JSONFactoryUtil.createJSONObject(
					valueString);

				double latitude = jsonObject.getDouble("latitude", 0);
				double longitude = jsonObject.getDouble("longitude", 0);

				document.addGeoLocation(
					name.concat("_geolocation"), latitude, longitude);
			}
			else if (type.equals(DDMImpl.TYPE_SELECT)) {
				JSONArray jsonArray = JSONFactoryUtil.createJSONArray(
					valueString);

				String[] stringArray = ArrayUtil.toStringArray(jsonArray);

				document.addKeywordSortable(name, stringArray);
			}
			else {
				if (type.equals(DDMImpl.TYPE_DDM_TEXT_HTML)) {
					valueString = HtmlUtil.extractText(valueString);
				}

				if (indexType.equals("keyword")) {
					document.addKeywordSortable(name, valueString);
				}
				else {
					document.addTextSortable(name, valueString);
				}
			}
		}
	}

	protected com.liferay.portal.kernel.search.Field createField(
			Field ddmStructureField, String name, Serializable value,
			String indexType, Locale locale)
		throws PortalException {

		Document document = new DocumentImpl();

		String valueFieldName = getValueFieldName(indexType, locale);

		addToDocument(
			document, ddmStructureField, valueFieldName, value, indexType);

		Map<String, com.liferay.portal.kernel.search.Field> fields =
			document.getFields();

		com.liferay.portal.kernel.search.Field ddmField =
			new com.liferay.portal.kernel.search.Field("");

		ddmField.addField(
			new com.liferay.portal.kernel.search.Field(DDM_FIELD_NAME, name));

		ddmField.addField(
			new com.liferay.portal.kernel.search.Field(
				DDM_VALUE_FIELD_NAME, valueFieldName));

		for (com.liferay.portal.kernel.search.Field field : fields.values()) {
			ddmField.addField(field);
		}

		return ddmField;
	}

	protected QueryFilter createFieldValueQueryFilter(
			DDMStructure ddmStructure, String ddmStructureFieldName,
			Serializable ddmStructureFieldValue, String fieldName,
			String indexType, Locale locale)
		throws Exception {

		BooleanQuery booleanQuery = new BooleanQueryImpl();

		if (ddmStructure.hasField(fieldName)) {
			ddmStructureFieldValue = _ddm.getIndexedFieldValue(
				ddmStructureFieldValue, ddmStructure.getFieldType(fieldName));
		}

		if (ddmStructureFieldValue instanceof String[]) {
			String[] ddmStructureFieldValueArray =
				(String[])ddmStructureFieldValue;

			for (String ddmStructureFieldValueString :
					ddmStructureFieldValueArray) {

				addFieldValueRequiredTerm(
					booleanQuery, ddmStructureFieldName,
					ddmStructureFieldValueString, indexType, locale);
			}
		}
		else {
			addFieldValueRequiredTerm(
				booleanQuery, ddmStructureFieldName,
				String.valueOf(ddmStructureFieldValue), indexType, locale);
		}

		if (isLegacyDDMIndexFieldsEnabled()) {
			return new QueryFilter(booleanQuery);
		}

		return new QueryFilter(new NestedQuery(DDM_FIELD_ARRAY, booleanQuery));
	}

	protected String encodeName(
		long ddmStructureId, String fieldName, Locale locale,
		String indexType) {

		StringBundler sb = new StringBundler(8);

		sb.append(DDM_FIELD_PREFIX);

		if (Validator.isNotNull(indexType)) {
			sb.append(indexType);
			sb.append(DDM_FIELD_SEPARATOR);
		}

		sb.append(ddmStructureId);
		sb.append(DDM_FIELD_SEPARATOR);
		sb.append(fieldName);

		if (locale != null) {
			sb.append(StringPool.UNDERLINE);
			sb.append(LocaleUtil.toLanguageId(locale));
		}

		return sb.toString();
	}

	@Reference(unbind = "-")
	protected void setDDM(DDM ddm) {
		_ddm = ddm;
	}

	@Reference(unbind = "-")
	protected void setDDMFormValuesToFieldsConverter(
		DDMFormValuesToFieldsConverter ddmFormValuesToFieldsConverter) {

		_ddmFormValuesToFieldsConverter = ddmFormValuesToFieldsConverter;
	}

	@Reference(unbind = "-")
	protected void setDDMStructureLocalService(
		DDMStructureLocalService ddmStructureLocalService) {

		_ddmStructureLocalService = ddmStructureLocalService;
	}

	protected Fields toFields(
		DDMStructure ddmStructure, DDMFormValues ddmFormValues) {

		try {
			return _ddmFormValuesToFieldsConverter.convert(
				ddmStructure, ddmFormValues);
		}
		catch (PortalException portalException) {
			_log.error(
				"Unable to convert DDMFormValues to Fields", portalException);
		}

		return new Fields();
	}

	@Reference
	protected Queries queries;

	@Reference
	protected SearchEngineInformation searchEngineInformation;

	@Reference
	protected SortBuilderFactory sortBuilderFactory;

	@Reference
	protected Sorts sorts;

	private static final Log _log = LogFactoryUtil.getLog(DDMIndexerImpl.class);

	private DDM _ddm;
	private DDMFormValuesToFieldsConverter _ddmFormValuesToFieldsConverter;
	private volatile DDMIndexerConfiguration _ddmIndexerConfiguration;
	private DDMStructureLocalService _ddmStructureLocalService;

}