/*-
 * ~~~~~~licensing~~~~~~
 * test-producers
 * ==========
 * Copyright (C) 2020 - 2022 EmDev LLC
 * ==========
 * 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
 * 
 *      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.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.base.objects.factory.impl;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

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

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import ru.entaxy.esb.platform.runtime.base.connecting.generator.Generated;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory;
import ru.entaxy.platform.base.objects.factory.EntaxyFactoryException;
import ru.entaxy.platform.base.objects.factory.configuration.AbstractElement;
import ru.entaxy.platform.base.objects.factory.configuration.FactoryElement;
import ru.entaxy.platform.base.objects.factory.configuration.FieldsElement;
import ru.entaxy.platform.base.objects.factory.configuration.OutputElement;
import ru.entaxy.platform.base.objects.factory.configuration.OutputsElement;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.base.support.osgi.OSGIUtils;

public class DefaultFactory implements EntaxyFactory {

	private static final Logger log = LoggerFactory.getLogger(DefaultFactory.class);
	
	protected Map<String, Class<? extends AbstractElement<?>>> elementClasses = new HashMap<>();

	protected String factoryId = "";
	
	protected String factoryType = "";
	
	protected String description;
	
	protected String label;
	
	protected String category;
	
	protected FieldsElement fields = null;
	
	protected OutputsElement outputs = new OutputsElement();
	
	protected Map<String, Object> factoryData = new HashMap<>();
	
	protected GenerationHelper helper = null;
	
	public DefaultFactory() {
		super();
		elementClasses.put(FactoryElement.ELEMENT_NAME, FactoryElement.class);
		elementClasses.put(OutputsElement.ELEMENT_NAME, OutputsElement.class);
		elementClasses.put(FieldsElement.ELEMENT_NAME, FieldsElement.class);
	}
	
	protected GenerationHelper getHelper() {
		if (this.helper == null) {
			try {
				helper = OSGIUtils.services().ofClass(GenerationHelper.class).waitService(5000).get();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				log.error("Error getting GenerationHelper", e);
			}
		}
		return this.helper;
	}
	
	public void configure(String configuration) {

		log.debug("CONFIGURE WITH: " + configuration);
		
		GsonBuilder builder = new GsonBuilder();
		Gson gson = builder.create();

		JsonElement je = (new JsonParser()).parse(configuration);
		JsonObject root = je.getAsJsonObject();		
		
		Set<Entry<String, JsonElement>> elementSet = root.entrySet();
		for (Entry<String, JsonElement> entry: elementSet) {
			String name = entry.getKey();
			Object result = null;
			if (elementClasses.containsKey(name)) {
				log.debug("FOUND element class  for [" + name + "] :: " + elementClasses.get(name));
				try {
					result = elementClasses.get(name).getConstructor().newInstance().fromJson(entry.getValue());
				} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
						| InvocationTargetException | NoSuchMethodException | SecurityException e) {
					log.error("Error deserializing [" + name + "]", e);
					continue;
				} 
						// gson.fromJson(entry.getValue(), elementClasses.get(name));
			} else
				result = JSONUtils.element2object(entry.getValue());
			factoryData.put(name, result);
			postprocess(name, result);
		}
		
	}
	
	protected void postprocess(String name, Object object) {
		if (object instanceof FactoryElement) {
			FactoryElement fe = (FactoryElement)object;
			log.debug("POSTPROCESS :: FactoryElement");
			if (CommonUtils.isValid(fe.getId()))
				this.factoryId = fe.getId();
			this.factoryType = fe.getType();
			this.category = fe.getCategory();
			this.label = fe.getLabel();
			this.description = fe.getDescription();
		} else if (object instanceof OutputsElement) {
			log.debug("POSTPROCESS :: OutputsElement");
			this.outputs = (OutputsElement)object;
			log.debug("POSTPROCESS :: Default output config size -> " + 
					this.outputs.getOutput(this.outputs.getDefaultOutputType()).getConfig().size()
			);
			if (this.fields != null) {
				this.outputs.setCommonFields(this.fields.getFieldsMap());
				log.debug("COMMON FIELDS :: " + this.fields.getFieldsMap().size());
			} else {
				log.debug("fields :: null");
			}
		} else if (object instanceof FieldsElement) {
			log.debug("POSTPROCESS :: FieldsElement");
			this.fields = (FieldsElement)object;
			if (this.outputs != null) {
				this.outputs.setCommonFields(this.fields.getFieldsMap());
				log.debug("COMMON FIELDS :: " + this.fields.getFieldsMap().size());
			} else {
				log.debug("outputs :: null");
			}
		}
	}
	
	public boolean isValid() {
		return CommonUtils.isValid(this.factoryId) && CommonUtils.isValid(this.factoryType);
	}
	
	@Override
	public String getId() {
		return this.factoryId;
	}

	public void setFactoryId(String factoryId) {
		this.factoryId = factoryId;
	}

	@Override
	public String getType() {
		return this.factoryType;
	}

	@Override
	public String getDescription() {
		return description;
	}

	@Override
	public String getLabel() {
		return label;
	}

	@Override
	public String getCategory() {
		return category;
	}

	@Override
	public Generated generate(Map<String, Object> parameters) throws EntaxyFactoryException {
		return generate(this.outputs.getDefaultOutputType(), parameters);
	}

	@Override
	public Generated generate(String outputType, Map<String, Object> parameters) throws EntaxyFactoryException {
		if (!this.outputs.hasOutput(outputType))
			throw new EntaxyFactoryException();
		return this.generate(outputType, EntaxyFactory.SCOPE.PUBLIC.label, parameters);
	}
	
	@Override
	public Generated generate(String outputType, String scope, Map<String, Object> parameters) throws EntaxyFactoryException {
		log.debug("Generating for: output type [{}], scope [{}]", outputType, scope);
		
		if (!this.outputs.hasOutput(outputType)) {
			log.debug("Unknown output: {}", outputType);
			throw new EntaxyFactoryException();
		}
		
		OutputElement oe = this.outputs.getOutput(outputType);
		if (!oe.isScopeSupported(scope)) {
			log.error("Scope not supported: {}; supported scopes: [{}]"
					, scope
					, oe.getSupportedScopes().stream().map(s -> s.label).collect(Collectors.joining(",")));
			throw new EntaxyFactoryException();
		}
		
		if (this.getHelper() != null)
			try {
				return this.getHelper().generateForFactory(this, outputType, SCOPE.valueOfLabel(scope), parameters);
			} catch (Exception e) {
				log.error("Generate failed", e);
				// TODO fill the exception
				throw new EntaxyFactoryException();
			}
		return null;
	}
	
	@Override
	public List<OutputInfo> getOutputs() {
		return this.outputs.getOutputs().stream().map(el->(OutputInfo)el).collect(Collectors.toList());
	}
	
	@Override
	public OutputInfo getOutputByType(String outputType) {
		return this.outputs.getOutput(outputType);
	}

	@Override
	public List<FieldInfo> getFields() {
		return this.fields.getFields().stream().map(f -> (FieldInfo)f).collect(Collectors.toList());
	}
	
	@Override
	public List<FieldInfo> getFields(String outputType) {
		return this.outputs.getOutput(outputType).getFields();
	}
	
	@Override
	public OutputInfo getDefaultOutput() {
		return (OutputInfo)outputs.getOutput(outputs.getDefaultOutputType());
	}
}
