/*-
 * ~~~~~~licensing~~~~~~
 * object-factory
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.base.objects.factory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import ru.entaxy.platform.base.objects.factory.EntaxyFactory.CONFIGURATION.DIRECTIVES.OVERRIDE_MODE;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;

public class Importer {

	protected JsonObject origin;
	
	protected Map<String, JsonObject> importedSources = new HashMap<>();
	
	public Importer(JsonObject origin) {
		this.origin = origin;
	}
	
	public List<ImportInfo> findImports(){
		return findImports(this.origin);
	}
	
	public List<ImportInfo> findImports(JsonObject object) {
		List<ImportInfo> result = new ArrayList<>();
		if (object.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.IMPORT))
			result.add(new ImportInfo(object));
		for (Entry<String, JsonElement> entry: object.entrySet())
			if (entry.getValue().isJsonObject())
				result.addAll(findImports(entry.getValue().getAsJsonObject()));
		return result;
	} 
	
	public void processImports(List<ImportInfo> imports) {
		for (ImportInfo ii: imports) {
			JsonObject newData = new JsonObject();
			for (ImportDescriptor id: ii.importDescriptors) {
				
				JsonObject imported = importedSources.get(id.factoryId);
				if (imported == null)
					continue;
				
				JsonElement je = JSONUtils.findElement(imported, id.location);
				if ((je==null) || !je.isJsonObject())
					continue;
				
				Filter f;
				if (id.origin.has("filter"))
					f = CompositeFilter.create(id.origin.get("filter"));
				else 
					f = new NullFilter();

				List<Entry<String, JsonElement>> filteredList = je.getAsJsonObject().entrySet()
					.stream()
					.filter(e->f.isAccepted(e.getKey(), e.getValue()))
					.collect(Collectors.toList());

				JsonObject idResult = new JsonObject();
				
				for (Entry<String, JsonElement> entry: filteredList)
					idResult.add(id.prefix + entry.getKey(), entry.getValue());
				
				EntaxyFactoryUtils.processObjectOverriding(
						newData
						, idResult
						, OVERRIDE_MODE.UPDATE);
			}
			
			JsonObject current = ii.owner.deepCopy();
			current.remove(EntaxyFactory.CONFIGURATION.DIRECTIVES.IMPORT);
			EntaxyFactoryUtils.processObjectOverriding(newData, current, OVERRIDE_MODE.UPDATE);
			Set<String> keys = new HashSet<>(ii.owner.keySet());
			for (String key: keys)
				ii.owner.remove(key);
			for (Entry<String, JsonElement> entry: newData.entrySet())
				ii.owner.add(entry.getKey(), entry.getValue());
		}
	}
	
	// INTERNAL CLASSES
	
	public static abstract class Filter {
	
		public static final String ATTRIBUTE_KEY = "$key";
		public static final String ATTRIBUTE_VALUE = "$value";
		
		abstract public boolean isAccepted(String key, JsonElement value);
		
	}
	
	public static class CompositeFilter extends Filter {
		
		List<Filter> filters = new ArrayList<>();
		
		@Override
		public boolean isAccepted(String key, JsonElement value) {
			for (Filter f: filters)
				if (!f.isAccepted(key, value))
					return false;
			return true;
		}
		
		public static Filter create(JsonElement object) {
			
			if (!object.isJsonObject())
				return new NullFilter();
			
			final CompositeFilter filter = new CompositeFilter();
		
			if (object.getAsJsonObject().has(ContainsFilter.FILTER_NAME)) {
				JsonElement je = object.getAsJsonObject().get(ContainsFilter.FILTER_NAME);
				if (je.isJsonArray())
					je.getAsJsonArray().forEach(e->filter.filters.add(new ContainsFilter(e)));
			}

			if (object.getAsJsonObject().has(ContainedFilter.FILTER_NAME)) {
				JsonElement je = object.getAsJsonObject().get(ContainedFilter.FILTER_NAME);
				if (je.isJsonArray())
					je.getAsJsonArray().forEach(e->filter.filters.add(new ContainedFilter(e)));
			}
			
			return filter;
			
		}
		
	}
	
	public static class NullFilter extends Filter {
		
		@Override
		public boolean isAccepted(String key, JsonElement value) {
			return true;
		}
		
	}

	public static abstract class CommonFilter extends Filter {

		String attribute = null;
		List<String> values = new ArrayList<>();
		
		boolean isInversed = false;
		
		protected CommonFilter(JsonElement element) {
			if (!element.isJsonObject())
				return;
			JsonObject jo = element.getAsJsonObject();
			if (jo.has("inverse"))
				this.isInversed = jo.get("inverse").getAsBoolean(); 
			
		} 
		
		@Override
		public final boolean isAccepted(String key, JsonElement value) {
			if (isInversed)
				return !calculateAccepted(key, value);
			else
				return calculateAccepted(key, value);
		}		
		
		protected abstract boolean calculateAccepted(String key, JsonElement value);
		
	}
	
	public static class ContainedFilter extends CommonFilter {

		public static final String FILTER_NAME = "contained";

		public ContainedFilter(JsonElement element) {
			super(element);
			if (!element.isJsonObject())
				return;
			JsonObject jo = element.getAsJsonObject();
			if (jo.has("attribute"))
				this.attribute = jo.get("attribute").getAsString(); 
			if (jo.has("values")) {
				if (jo.get("values").isJsonPrimitive())
					values.add(jo.get("values").getAsString());
				if (jo.get("values").isJsonArray())
					jo.get("values").getAsJsonArray().forEach(v->values.add(v.getAsString()));
			}
		}

		@Override
		protected boolean calculateAccepted(String key, JsonElement value) {
			if (values.isEmpty())
				return true;
			String testValue = "";
			if (ATTRIBUTE_KEY.equals(attribute))
				testValue = key;
			else if (ATTRIBUTE_VALUE.equals(attribute))
				testValue = value.getAsString();
			else {
				if (value.isJsonObject()) {
					JsonObject jo = value.getAsJsonObject();
					if (jo.has(attribute))
						testValue = jo.get(attribute).getAsString();
				}
			}
			if (!CommonUtils.isValid(testValue))
				return false;

			return values.contains(testValue);
		}

		
	}
	
	public static class ContainsFilter extends CommonFilter {

		public static final String FILTER_NAME = "contains";
		
		String separator = ",";
		boolean containsAll = false;
		
		public ContainsFilter(JsonElement element) {
			super(element);
			if (!element.isJsonObject())
				return;
			JsonObject jo = element.getAsJsonObject();
			if (jo.has("attribute"))
				this.attribute = jo.get("attribute").getAsString(); 
			if (jo.has("separator"))
				this.separator = jo.get("separator").getAsString(); 
			if (jo.has("containsAll"))
				this.containsAll = jo.get("containsAll").getAsBoolean(); 
			if (jo.has("values")) {
				if (jo.get("values").isJsonPrimitive())
					values.add(jo.get("values").getAsString());
				if (jo.get("values").isJsonArray())
					jo.get("values").getAsJsonArray().forEach(v->values.add(v.getAsString()));
			}
		}
		
		@Override
		protected boolean calculateAccepted(String key, JsonElement value) {
			if (values.isEmpty())
				return true;
			
			String testValue = "";
			List<String> vals = new ArrayList<>();
			
			if (ATTRIBUTE_KEY.equals(attribute))
				testValue = key;
			else if (ATTRIBUTE_VALUE.equals(attribute))
				testValue = value.getAsString();
			else {
				if (value.isJsonObject()) {
					JsonObject jo = value.getAsJsonObject();
					if (jo.has(attribute)) {
						JsonElement je = jo.get(attribute);
						if (je.isJsonPrimitive()) {
							testValue = jo.get(attribute).getAsString();

							if (!CommonUtils.isValid(testValue))
								return false;
							
							String[] arr = testValue.split(separator);
							vals = Arrays.asList(arr).stream().map(s->s.trim()).collect(Collectors.toList());
						} else if (je.isJsonArray()) {
							JsonArray ja = je.getAsJsonArray();
							for (int i=0; i<ja.size(); i++) {
								if (ja.get(i) == null)
									continue;
								if (ja.get(i).isJsonPrimitive())
									vals.add(ja.get(i).getAsString());
								else
									vals.add(ja.get(i).toString());
							}
						}
					}
				}
			}
			
			if (containsAll)
				return vals.containsAll(values);
			
			for (String v: values)
				if (vals.contains(v))
					return true;
			
			return false;
		}
		
	}
	
	public static class ImportInfo {
		public JsonObject owner;
		public JsonElement importElement;
		public List<ImportDescriptor> importDescriptors = new ArrayList<>();
		
		public ImportInfo(JsonObject owner) {
			
			List<JsonObject> foundImports = new ArrayList<>();
			
			this.owner = owner;
			this.importElement = owner.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.IMPORT);
			if (this.importElement.isJsonObject()) {
				foundImports.add(this.importElement.getAsJsonObject());
			}
			if (this.importElement.isJsonArray()) {
				JsonArray ja = this.importElement.getAsJsonArray();
				for (int i=0; i<ja.size(); i++) {
					JsonElement je = ja.get(i);
					if (je.isJsonObject())
						foundImports.add(je.getAsJsonObject());
				}
			}
			for (JsonObject jo: foundImports)
				this.importDescriptors.add(new ImportDescriptor(jo));
		}
		
	}
	
	public static class ImportDescriptor {
		public String factoryId;
		public String location = "";
		public String prefix = "";
		public JsonObject origin;
		
		public ImportDescriptor (JsonObject origin) {
			this.origin = origin;
			if (this.origin.has("sourceFactoryId"))
				this.factoryId = this.origin.get("sourceFactoryId").getAsString();
			if (this.origin.has("location"))
				this.location = this.origin.get("location").getAsString();
			if (this.origin.has("prefix"))
				this.prefix = this.origin.get("prefix").getAsString();
		}
		
	}

	// S & G
	
	public Map<String, JsonObject> getImportedSources() {
		return importedSources;
	}

	public void addImportedSources(Map<String, JsonObject> importedSources) {
		this.importedSources.putAll(importedSources);
	}
	
	public void setImportedSources(Map<String, JsonObject> importedSources) {
		this.importedSources = importedSources;
	}

	public JsonObject getOrigin() {
		return origin;
	}
	
	
}
