/*-
 * ~~~~~~licensing~~~~~~
 * test-producers
 * ==========
 * 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.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.util.tracker.BundleTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.entaxy.platform.base.objects.EntaxyObject;
import ru.entaxy.platform.base.objects.EntaxyObject.OBJECT_RELATION;
import ru.entaxy.platform.base.objects.EntaxyObjectService;
import ru.entaxy.platform.base.objects.EntaxyObjectServiceListener;
import ru.entaxy.platform.base.objects.service.impl.TrackedEntaxyObject.RelationDescriptor;
import ru.entaxy.platform.base.objects.service.tracker.TrackedEntaxyObjectCustomizer;
import ru.entaxy.platform.base.objects.service.tracker.TrackedEntaxyObjectCustomizerListener;
import ru.entaxy.platform.base.support.osgi.tracker.BundleTrackerUtils;
import ru.entaxy.platform.base.support.osgi.tracker.filter.BundleHeaderFilter;

@Component(service = {EntaxyObjectService.class}, immediate = true)
public class EntaxyObjectServiceImpl implements EntaxyObjectService {

	private static final Logger log = LoggerFactory.getLogger(EntaxyObjectServiceImpl.class);
	
	protected BundleContext bundleContext;
	
	protected BundleTracker<List<TrackedEntaxyObject>> objectTracker;
	
	protected Map<String, TrackedEntaxyObject> trackedObjects;
	
	protected Object trackedObjectsLock = new Object();
	
	protected List<EntaxyObjectServiceListener> listeners = new ArrayList<>();
	
	@Activate
	public void activate(ComponentContext componentContext) {
		this.bundleContext = componentContext.getBundleContext();
		if (this.objectTracker != null)
			this.objectTracker.close();
		this.trackedObjects = new ConcurrentHashMap<>();
		openTracker();
	}
	
	@Deactivate
	public void deactivate(ComponentContext componentContext) {
		this.objectTracker.close();
	}
	
	
	@Override
	public List<EntaxyObject> getObjects() {
		return trackedObjects.values().stream().collect(Collectors.toList());
	}

	@Override
	public EntaxyObject findObject(String objectId, String objectType) {
/*		Optional<EntaxyObject> object = getObjects().stream()
				.filter(obj -> (obj.getObjectId().equals(objectId) && obj.getObjectType().equals(objectType)))
				.findFirst();
		return object.orElse(null);
*/
		return this.trackedObjects.get(getFullId(objectId, objectType));
	}

	protected String getFullId(String objectId, String objectType) {
		return objectId + ":" + objectType;
	}

	protected String getFullId(TrackedEntaxyObject entaxyObject) {
		return getFullId(entaxyObject.getId(), entaxyObject.getType());
	}
	
	protected void openTracker() {
		objectTracker = BundleTrackerUtils.<List<TrackedEntaxyObject>>createBuilder()
				.addFilter(
					(new BundleHeaderFilter()).header(EntaxyObject.HEADERS.GENERATED_OBJECTS)	
				)
				.customizer(
					(new TrackedEntaxyObjectCustomizer(this))
					.listener(new TrackedEntaxyObjectCustomizerListener(this))
				)
				.bundleState(Bundle.ACTIVE | Bundle.INSTALLED | Bundle.RESOLVED)
				.get();
		objectTracker.open();		
	}

	public BundleContext getBundleContext() {
		return bundleContext;
	}
	
	@Override
	public void addListener(EntaxyObjectServiceListener listener) {
		this.listeners.add(listener);
	}
	
	@Override
	public void removeListener(EntaxyObjectServiceListener listener) {
		this.listeners.remove(listener);
	}
	
	synchronized protected Map<String, TrackedEntaxyObject> acquireTrackedObjects() {
		return trackedObjects;
	}

	synchronized public void addObjects(List<TrackedEntaxyObject> objects) {
		for (TrackedEntaxyObject object: objects) {

			synchronized (trackedObjectsLock) {
				
				boolean isModified = false;
				TrackedEntaxyObject existing = object;
				
				String fullId = getFullId(object.getId(), object.getType());
				if (acquireTrackedObjects().containsKey(fullId)) {
					// object was created as a placeholder via relations
					updateObject(acquireTrackedObjects().get(fullId), object);
					existing = acquireTrackedObjects().get(fullId);
					isModified = true;
				} else {
					// object is new
					acquireTrackedObjects().put(fullId, object);
				}
				
				List<RelationDescriptor> relations = object.getRelationDescriptors();
				for (RelationDescriptor relationDescriptor: relations) {
					TrackedEntaxyObject targetByDescriptorObject = getOrCreateObject(relationDescriptor.targetId, relationDescriptor.targetType);
					
					TrackedEntaxyObject relationSource = relationDescriptor.relationInverse
							?targetByDescriptorObject
							:existing;
					
					TrackedEntaxyObject relationTarget = relationDescriptor.relationInverse
							?existing
							:targetByDescriptorObject;
					
					relationSource.relation(relationTarget, relationDescriptor);
					
				}
				
				if (isModified)
					notifyModified(trackedObjects.get(fullId));
				else
					notifyAdded(trackedObjects.get(fullId));
			}
		}
	}

	synchronized protected TrackedEntaxyObject getOrCreateObject(String objectId, String objectType) {
		String fullId = getFullId(objectId, objectType);
		if (!this.trackedObjects.containsKey(fullId)) {
			TrackedEntaxyObject teo = TrackedEntaxyObject.create(null, objectId, objectType);
			teo.makeGhost();
			
			this.trackedObjects.put(fullId, teo);
			
			notifyAdded(teo);
		}
		return this.trackedObjects.get(fullId);
	}
	
	protected void updateObject(TrackedEntaxyObject existing, TrackedEntaxyObject newOne) {
		final Map<String, RelationDescriptor> relationDescriptors = new HashMap<>();
		
		existing.getRelationDescriptors().stream()
			.forEach(rd -> relationDescriptors.put(rd.relationName + ":" + rd.relationGroup, rd));
		newOne.getRelationDescriptors().stream()
			.forEach(rd -> relationDescriptors.put(rd.relationName + ":" + rd.relationGroup, rd));
		existing.updateFrom(newOne);
		existing.setRelationDescriptors(relationDescriptors.values().stream().collect(Collectors.toList()));
		
		existing.unmakeGhost();
	}
	
	public void removeObjects(List<TrackedEntaxyObject> objects) {
		for (TrackedEntaxyObject object: objects) {
			String fullId = getFullId(object.getId(), object.getType());
			TrackedEntaxyObject realObject = this.trackedObjects.get(getFullId(object));
			if (realObject == null)
				continue;
			if (!realObject.hasRelations()) {
				this.trackedObjects.remove(fullId);
				notifyRemoved(realObject);
				continue;
			}
			// TODO more accurate processing must be implemented
			// depending on origins and directions of connections
			realObject.clear();
			realObject.makeGhost();
		}
		
		clearGhosts();
	}
	
	protected void clearGhosts() {
		List<TrackedEntaxyObject> ghosts = new ArrayList<>(
				trackedObjects.values().stream().filter(obj -> obj.isGhost)
				.collect(Collectors.toList())
			);
		
		List<TrackedEntaxyObject> ghostsToRemove = new ArrayList<>();
		int currentSize = -1;
		while (currentSize != ghosts.size()) {
			currentSize = ghosts.size();
			ghostsToRemove.clear();
			for (TrackedEntaxyObject ghost: ghosts) {
				
				List<TrackedEntaxyObject> relatedObjects = ghost.getRelations().stream()
					.filter(rel -> !rel.isDefinedInSource())
					.filter(rel -> (OBJECT_RELATION.AGGREGATION.equals(rel.getType()) || OBJECT_RELATION.COMPOSITION.equals(rel.getType())))
					.map(rel -> trackedObjects.get(rel.getTarget().getObjectFullId()))
					.filter(obj -> (obj!=null))
					.filter(obj -> !obj.isGhost())
					.collect(Collectors.toList());
				
				if (!relatedObjects.isEmpty())
					continue;
				
				relatedObjects = ghost.getIncomingRelations().stream()
					.filter(rel -> rel.isDefinedInSource())	
					.filter(rel -> (OBJECT_RELATION.DEPENDENCY.equals(rel.getType()) || OBJECT_RELATION.USE.equals(rel.getType())))
					.map(rel -> trackedObjects.get(rel.getSource().getObjectFullId()))
					.filter(obj -> (obj != null))
					.filter(obj -> !obj.isGhost())
					.collect(Collectors.toList());

				if (!relatedObjects.isEmpty())
					continue;
				
				ghostsToRemove.add(ghost);
			}
			
			for (TrackedEntaxyObject ghost: ghostsToRemove) {
				ghosts.remove(ghost);
				trackedObjects.remove(ghost.getObjectFullId());
				notifyRemoved(ghost);
			}
		}
			
	}
	
	protected void notifyAdded(TrackedEntaxyObject object) {
		for (EntaxyObjectServiceListener listener: listeners)
			listener.objectAdded(object);
	}
	
	protected void notifyModified(TrackedEntaxyObject object) {
		for (EntaxyObjectServiceListener listener: listeners)
			listener.objectModified(object);
	}
	
	protected void notifyRemoved(TrackedEntaxyObject object) {
		for (EntaxyObjectServiceListener listener: listeners)
			listener.objectRemoved(object);
	}
	
}
