package mhtree;

import cz.muni.fi.disa.similarityoperators.cover.AbstractRepresentation.PrecomputedDistances;
import cz.muni.fi.disa.similarityoperators.cover.HullOptimizedRepresentationV3;
import messif.buckets.BucketStorageException;
import messif.objects.LocalAbstractObject;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public abstract class Node implements Serializable {

    /**
     * Serialization ID
     */
    private static final long serialVersionUID = 420L;

    private final InsertType insertType;
    public ObjectToNodeDistance objectToNodeDistance;
    private InternalNode parent;
    private HullOptimizedRepresentationV3 hull;

    protected Node(PrecomputedDistances distances, InsertType insertType, ObjectToNodeDistance objectToNodeDistance) {
        this.hull = new HullOptimizedRepresentationV3(distances);
        this.hull.build();

        this.insertType = insertType;
        this.objectToNodeDistance = objectToNodeDistance;
    }

    protected Node(HullOptimizedRepresentationV3 hull, InsertType insertType, ObjectToNodeDistance objectToNodeDistance) {
        this.hull = hull;
        this.insertType = insertType;
        this.objectToNodeDistance = objectToNodeDistance;
    }

    protected static InternalNode createParent(List<Node> nodes, PrecomputedDistances distances, InsertType insertType, 
                                                ObjectToNodeDistance objectToNodeDistance, MergingMethod mergingMethod) {
        List<LocalAbstractObject> objects = nodes
                .stream()
                .map(mergingMethod::getObjects)
                .flatMap(Collection::stream)
                .collect(Collectors.toList());

        if (nodes.size() == distances.getObjectCount()) {
            return new InternalNode(distances, insertType, objectToNodeDistance, nodes);
        }

        return new InternalNode(distances.getSubset(objects), insertType, objectToNodeDistance, nodes);
    }

    /**
     * Returns a list of hull objects.
     *
     * @return a list of hull objects
     */
    public List<LocalAbstractObject> getHullObjects() {
        return hull.getHull();
    }

    /**
     * Returns true if the {@code object} is covered.
     *
     * @param object the object to be checked
     * @return true if the {@code object} is covered.
     */
    public boolean isCovered(LocalAbstractObject object) {
        return hull.isExternalCovered(object);
    }

    public HullOptimizedRepresentationV3 getHull() {
        return hull;
    }

    public InternalNode getParent() {
        return parent;
    }

    public void setParent(InternalNode parent) {
        this.parent = parent;
    }

    @Override
    public String toString() {
        return "Node{hull=" + hull + '}';
    }

    protected double getDistance(LocalAbstractObject object) {
        return objectToNodeDistance.getDistance(object, this);
    }

    protected double getDistance(LocalAbstractObject object, PrecomputedDistances distances) {
        return objectToNodeDistance.getDistance(object, this, distances);
    }

    protected boolean isLeaf() {
        return (this instanceof LeafNode);
    }

    protected boolean isInternal() {
        return (this instanceof InternalNode);
    }

    protected int getHullObjectCount() {
        return hull.getRepresentativesCount();
    }

    protected void addObjectIntoHull(LocalAbstractObject object) {
        if (isCovered(object)) 
            return;

        switch (insertType) {
            case GREEDY:
                insertGreedy(object);
                break;
            case INCREMENTAL:
                insertHullRebuild(object);
                break;
            case ADD_HULL_OBJECT:
                insertAddNewHullObject(object);
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + insertType);
        }
    }

    /**
     * Adds {@code object} into this node, possibly adding {@code object} to the bucket.
     *
     * @param object object to be added
     * @throws BucketStorageException bucket exception
     */
    protected abstract void addObject(LocalAbstractObject object) throws BucketStorageException;

    /**
     * Returns objects from buckets, such that the leaf node is descendants of the current node.
     *
     * @return objects from buckets, such that the leaf node is descendants of the current node.
     */
    protected abstract List<LocalAbstractObject> getObjects();

    /**
     * Returns the height of this node.
     *
     * @return the height of this node
     */
    protected abstract int getHeight();

    /**
     * Adds this node and descendants into {@code nodes}.
     *
     * @param nodes list of nodes
     */
    protected abstract void gatherNodes(List<Node> nodes);

    /**
     * Adds every descendant leaf node into {@code leafNodes}.
     *
     * @param leafNodes list of leaf nodes
     */
    protected abstract void gatherLeafNodes(List<LeafNode> leafNodes);

    private void insertGreedy(LocalAbstractObject object) {
        List<LocalAbstractObject> objectsFromLeafNodes = getObjects();
        objectsFromLeafNodes.add(object);

        hull = new HullOptimizedRepresentationV3(objectsFromLeafNodes);
        hull.build();
    }

    private void insertAddNewHullObject(LocalAbstractObject object) {
        hull.setHullObjects((List<LocalAbstractObject>) Collections.singleton(object)); // TODO: VLASTA: This does not update the hull by optimizing the hull objects but rather simply adds the new object as next hull object!!!!!
    }

    private void insertHullRebuild(LocalAbstractObject object) {
        List<LocalAbstractObject> hull = this.hull.getHull();
        hull.add(object);

        this.hull = new HullOptimizedRepresentationV3(hull);
        this.hull.build();
    }
}
