/*
 * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. licenses this file to you 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.
 */

package io.siddhi.core.table.holder;

import io.siddhi.core.config.SiddhiAppContext;
import io.siddhi.core.event.ComplexEvent;
import io.siddhi.core.event.ComplexEventChunk;
import io.siddhi.core.event.stream.Operation;
import io.siddhi.core.event.stream.StreamEvent;
import io.siddhi.core.event.stream.StreamEventFactory;
import io.siddhi.core.event.stream.converter.StreamEventConverter;
import io.siddhi.core.exception.OperationNotSupportedException;
import io.siddhi.core.exception.SiddhiAppRuntimeException;
import io.siddhi.core.util.SiddhiConstants;
import io.siddhi.core.util.snapshot.SnapshotRequest;
import io.siddhi.core.util.snapshot.state.Snapshot;
import io.siddhi.core.util.snapshot.state.SnapshotStateList;
import io.siddhi.query.api.definition.AbstractDefinition;
import io.siddhi.query.api.expression.condition.Compare;
import org.apache.log4j.Logger;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import static io.siddhi.core.event.stream.Operation.Operator.ADD;
import static io.siddhi.core.event.stream.Operation.Operator.CLEAR;
import static io.siddhi.core.event.stream.Operation.Operator.DELETE_BY_OPERATOR;
import static io.siddhi.core.event.stream.Operation.Operator.OVERWRITE;
import static io.siddhi.core.event.stream.Operation.Operator.REMOVE;


/**
 * EventHolder implementation where events will be indexed and stored. This will offer faster access compared to
 * other EventHolder implementations. User can only add unique events based on a given primary key.
 */
public class IndexEventHolder implements IndexedEventHolder, Serializable {

    private static final Logger log = Logger.getLogger(IndexEventHolder.class);
    private static final long serialVersionUID = 1272291743721603253L;
    private static final float FULL_SNAPSHOT_THRESHOLD = 2.1f;
    protected final Map<Object, StreamEvent> primaryKeyData;
    protected final Map<String, TreeMap<Object, Set<StreamEvent>>> indexData;
    private final PrimaryKeyReferenceHolder[] primaryKeyReferenceHolders;
    private final String tableName;
    private final String siddhiAppName;
    private final transient SiddhiAppContext siddhiAppContext;
    protected String primaryKeyAttributes = null;
    private StreamEventFactory tableStreamEventFactory;
    private StreamEventConverter eventConverter;
    private Map<String, Integer> indexMetaData;
    private Map<String, Integer> multiPrimaryKeyMetaData = new LinkedHashMap<>();
    private Map<String, Integer> allIndexMetaData = new HashMap<>();
    private ArrayList<Operation> operationChangeLog = new ArrayList<>();
    private long eventsCount;
    private boolean forceFullSnapshot = true;
    private boolean isOperationLogEnabled = true;

    public IndexEventHolder(StreamEventFactory tableStreamEventFactory, StreamEventConverter eventConverter,
                            PrimaryKeyReferenceHolder[] primaryKeyReferenceHolders,
                            boolean isPrimaryNumeric, Map<String, Integer> indexMetaData,
                            AbstractDefinition tableDefinition, SiddhiAppContext siddhiAppContext) {
        this.tableStreamEventFactory = tableStreamEventFactory;
        this.eventConverter = eventConverter;
        this.primaryKeyReferenceHolders = primaryKeyReferenceHolders;
        this.indexMetaData = indexMetaData;
        this.tableName = tableDefinition.getId();
        this.siddhiAppName = siddhiAppContext.getName();
        this.siddhiAppContext = siddhiAppContext;

        if (primaryKeyReferenceHolders != null) {
            if (isPrimaryNumeric) {
                primaryKeyData = new TreeMap<Object, StreamEvent>();
            } else {
                primaryKeyData = new HashMap<Object, StreamEvent>();
            }
            if (primaryKeyReferenceHolders.length == 1) {
                allIndexMetaData.put(primaryKeyReferenceHolders[0].getPrimaryKeyAttribute(),
                        primaryKeyReferenceHolders[0].getPrimaryKeyPosition());
                primaryKeyAttributes = primaryKeyReferenceHolders[0].getPrimaryKeyAttribute();
            } else {
                StringBuilder primaryKeyAttributesBuilder = new StringBuilder();
                for (PrimaryKeyReferenceHolder primaryKeyReferenceHolder : primaryKeyReferenceHolders) {
                    multiPrimaryKeyMetaData.put(primaryKeyReferenceHolder.getPrimaryKeyAttribute(),
                            primaryKeyReferenceHolder.getPrimaryKeyPosition());
                    primaryKeyAttributesBuilder.append(primaryKeyReferenceHolder.getPrimaryKeyAttribute())
                            .append(SiddhiConstants.KEY_DELIMITER);
                }
                primaryKeyAttributes = primaryKeyAttributesBuilder.toString();
            }
        } else {
            primaryKeyData = null;
        }
        if (indexMetaData.size() > 0) {
            indexData = new HashMap<String, TreeMap<Object, Set<StreamEvent>>>();
            for (String indexAttributeName : indexMetaData.keySet()) {
                indexData.put(indexAttributeName, new TreeMap<Object, Set<StreamEvent>>());
            }
            allIndexMetaData.putAll(indexMetaData);
        } else {
            indexData = null;
        }

    }

    public void replace(Object key, StreamEvent streamEvent) {
        primaryKeyData.replace(key, streamEvent);
    }

    @Override
    public Set<Object> getAllPrimaryKeyValues() {
        if (primaryKeyData != null) {
            return primaryKeyData.keySet();
        } else {
            return null;
        }
    }

    @Override
    public PrimaryKeyReferenceHolder[] getPrimaryKeyReferenceHolders() {
        return primaryKeyReferenceHolders;
    }

    @Override
    public boolean isMultiPrimaryKeyAttribute(String attributeName) {
        return multiPrimaryKeyMetaData.containsKey(attributeName);
    }

    @Override
    public boolean isAttributeIndexed(String attribute) {
        return allIndexMetaData.containsKey(attribute);
    }

    @Override
    public boolean isAttributeIndexed(int position) {
        return allIndexMetaData.containsValue(position);
    }

    @Override
    public void add(ComplexEventChunk<StreamEvent> addingEventChunk) {
        addingEventChunk.reset();
        while (addingEventChunk.hasNext()) {
            ComplexEvent complexEvent = addingEventChunk.next();
            StreamEvent streamEvent = tableStreamEventFactory.newInstance();
            eventConverter.convertComplexEvent(complexEvent, streamEvent);
            eventsCount++;
            if (isOperationLogEnabled) {
                if (!isFullSnapshot()) {
                    StreamEvent streamEvent2 = tableStreamEventFactory.newInstance();
                    eventConverter.convertComplexEvent(complexEvent, streamEvent2);
                    operationChangeLog.add(new Operation(ADD, streamEvent2));
                } else {
                    operationChangeLog.clear();
                    forceFullSnapshot = true;
                }
            }
            add(streamEvent);
        }
    }

    private void add(StreamEvent streamEvent) {
        StreamEvent existingValue = null;
        if (primaryKeyData != null) {
            Object primaryKey = constructPrimaryKey(streamEvent, primaryKeyReferenceHolders);
            existingValue = primaryKeyData.putIfAbsent(primaryKey, streamEvent);
            if (existingValue != null) {
                Exception e = new SiddhiAppRuntimeException("Siddhi App '" + siddhiAppName + "' table '" +
                        tableName + "' dropping event : " + streamEvent + ", as there is already an event stored " +
                        "with primary key '" + primaryKey + "'");
                if (siddhiAppContext.getRuntimeExceptionListener() != null) {
                    siddhiAppContext.getRuntimeExceptionListener().exceptionThrown(e);
                }
                log.error(e.getMessage(), e);
            }
        }

        if (indexData != null) {
            for (Map.Entry<String, Integer> indexEntry : indexMetaData.entrySet()) {
                TreeMap<Object, Set<StreamEvent>> indexMap = indexData.get(indexEntry.getKey());
                Object key = streamEvent.getOutputData()[indexEntry.getValue()];
                Set<StreamEvent> values = indexMap.get(key);
                if (values == null) {
                    values = new HashSet<StreamEvent>();
                    values.add(streamEvent);
                    indexMap.put(streamEvent.getOutputData()[indexEntry.getValue()], values);
                } else {
                    values.add(streamEvent);
                }
            }
        }

    }

    private Object constructPrimaryKey(StreamEvent streamEvent,
                                       PrimaryKeyReferenceHolder[] primaryKeyReferenceHolders) {
        if (primaryKeyReferenceHolders.length == 1) {
            return streamEvent.getOutputData()[primaryKeyReferenceHolders[0].getPrimaryKeyPosition()];
        } else {
            StringBuilder stringBuilder = new StringBuilder();
            for (PrimaryKeyReferenceHolder primaryKeyReferenceHolder : primaryKeyReferenceHolders) {
                stringBuilder.append(streamEvent.getOutputData()[primaryKeyReferenceHolder.getPrimaryKeyPosition()])
                        .append(SiddhiConstants.KEY_DELIMITER);
            }
            return stringBuilder.toString();
        }
    }

    protected void handleCachePolicyAttributeUpdate(StreamEvent streamEvent) {

    }

    @Override
    public void overwrite(StreamEvent streamEvent) {
        if (isOperationLogEnabled) {
            if (!isFullSnapshot()) {
                StreamEvent streamEvent2 = tableStreamEventFactory.newInstance();
                eventConverter.convertComplexEvent(streamEvent, streamEvent2);
                operationChangeLog.add(new Operation(OVERWRITE, streamEvent2));
            } else {
                operationChangeLog.clear();
                forceFullSnapshot = true;
            }
        }
        StreamEvent deletedEvent = null;
        if (primaryKeyData != null) {
            Object primaryKey = constructPrimaryKey(streamEvent, primaryKeyReferenceHolders);
            deletedEvent = primaryKeyData.put(primaryKey, streamEvent);
            if (deletedEvent != null) {
                handleCachePolicyAttributeUpdate(streamEvent);
            }
        }

        if (indexData != null) {
            for (Map.Entry<String, Integer> indexEntry : indexMetaData.entrySet()) {
                TreeMap<Object, Set<StreamEvent>> indexMap = indexData.get(indexEntry.getKey());
                Object key = streamEvent.getOutputData()[indexEntry.getValue()];
                if (deletedEvent != null) {
                    Set<StreamEvent> values = indexMap.get(key);
                    values.remove(deletedEvent);
                    if (values.size() == 0) {
                        indexMap.remove(key);
                    }
                }
                Set<StreamEvent> values = indexMap.get(key);
                if (values == null) {
                    values = new HashSet<StreamEvent>();
                    values.add(streamEvent);
                    indexMap.put(streamEvent.getOutputData()[indexEntry.getValue()], values);
                } else {
                    values.add(streamEvent);
                }
            }
        }
    }

    @Override
    public Collection<StreamEvent> getAllEvents() {
        if (primaryKeyData != null) {
            return primaryKeyData.values();
        } else if (indexData != null) {
            HashSet<StreamEvent> resultEventSet = new HashSet<StreamEvent>();
            Iterator<TreeMap<Object, Set<StreamEvent>>> iterator = indexData.values().iterator();
            if (iterator.hasNext()) {
                TreeMap<Object, Set<StreamEvent>> aIndexData = iterator.next();
                for (Set<StreamEvent> streamEvents : aIndexData.values()) {
                    resultEventSet.addAll(streamEvents);
                }
            }
            return resultEventSet;
        } else {
            return new HashSet<StreamEvent>();
        }
    }

    public StreamEvent getEvent(Object key) {
        return primaryKeyData.get(key);
    }

    public void deleteEvent(Object key) {
        primaryKeyData.remove(key);
    }

    @Override
    public Collection<StreamEvent> findEvents(String attribute, Compare.Operator operator, Object value) {

        if (primaryKeyData != null && attribute.equals(primaryKeyAttributes)) {
            StreamEvent resultEvent;
            HashSet<StreamEvent> resultEventSet;

            switch (operator) {
                case LESS_THAN:
                    return ((TreeMap<Object, StreamEvent>) primaryKeyData).headMap(value, false).values();
                case GREATER_THAN:
                    return ((TreeMap<Object, StreamEvent>) primaryKeyData).tailMap(value, false).values();
                case LESS_THAN_EQUAL:
                    return ((TreeMap<Object, StreamEvent>) primaryKeyData).headMap(value, true).values();
                case GREATER_THAN_EQUAL:
                    return ((TreeMap<Object, StreamEvent>) primaryKeyData).tailMap(value, true).values();
                case EQUAL:
                    resultEventSet = new HashSet<StreamEvent>();
                    resultEvent = primaryKeyData.get(value);
                    if (resultEvent != null) {
                        resultEventSet.add(resultEvent);
                    }
                    return resultEventSet;
                case NOT_EQUAL:
                    if (primaryKeyData.size() > 0) {
                        resultEventSet = new HashSet<StreamEvent>(primaryKeyData.values());
                    } else {
                        return new HashSet<StreamEvent>();
                    }
                    resultEvent = primaryKeyData.get(value);
                    if (resultEvent != null) {
                        resultEventSet.remove(resultEvent);
                    }
                    return resultEventSet;
            }
        } else {
            HashSet<StreamEvent> resultEventSet = new HashSet<StreamEvent>();
            TreeMap<Object, Set<StreamEvent>> currentIndexedData = indexData.get(attribute);

            Set<StreamEvent> resultEvents;
            switch (operator) {
                case LESS_THAN:
                    for (Set<StreamEvent> eventSet : currentIndexedData.headMap(value, false).values()) {
                        resultEventSet.addAll(eventSet);
                    }
                    return resultEventSet;
                case GREATER_THAN:
                    for (Set<StreamEvent> eventSet : currentIndexedData.tailMap(value, false).values()) {
                        resultEventSet.addAll(eventSet);
                    }
                    return resultEventSet;
                case LESS_THAN_EQUAL:
                    for (Set<StreamEvent> eventSet : currentIndexedData.headMap(value, true).values()) {
                        resultEventSet.addAll(eventSet);
                    }
                    return resultEventSet;
                case GREATER_THAN_EQUAL:
                    for (Set<StreamEvent> eventSet : currentIndexedData.tailMap(value, true).values()) {
                        resultEventSet.addAll(eventSet);
                    }
                    return resultEventSet;
                case EQUAL:
                    resultEvents = currentIndexedData.get(value);
                    if (resultEvents != null) {
                        resultEventSet.addAll(resultEvents);
                    }
                    return resultEventSet;
                case NOT_EQUAL:
                    if (currentIndexedData.size() > 0) {
                        resultEventSet = new HashSet<StreamEvent>();
                        for (Set<StreamEvent> eventSet : currentIndexedData.values()) {
                            resultEventSet.addAll(eventSet);
                        }
                    } else {
                        resultEventSet = new HashSet<StreamEvent>();
                    }

                    resultEvents = currentIndexedData.get(value);
                    if (resultEvents != null) {
                        resultEventSet.removeAll(resultEvents);
                    }
                    return resultEventSet;
            }
        }
        throw new OperationNotSupportedException(operator + " not supported for '" + value + "' by " + getClass()
                .getName());
    }

    @Override
    public void deleteAll() {
        if (isOperationLogEnabled) {
            if (!isFullSnapshot()) {
                operationChangeLog.add(new Operation(CLEAR));
            } else {
                operationChangeLog.clear();
                forceFullSnapshot = true;
            }
        }
        if (primaryKeyData != null) {
            primaryKeyData.clear();
        }
        if (indexData != null) {
            for (TreeMap<Object, Set<StreamEvent>> aIndexedData : indexData.values()) {
                aIndexedData.clear();
            }
        }
    }

    @Override
    public void deleteAll(Collection<StreamEvent> storeEventSet) {
        for (StreamEvent streamEvent : storeEventSet) {
            if (isOperationLogEnabled) {
                if (!isFullSnapshot()) {
                    StreamEvent streamEvent2 = tableStreamEventFactory.newInstance();
                    eventConverter.convertComplexEvent(streamEvent, streamEvent2);
                    operationChangeLog.add(new Operation(REMOVE, streamEvent));
                } else {
                    operationChangeLog.clear();
                    forceFullSnapshot = true;
                }
            }
            deleteAll(streamEvent);

        }
    }

    private void deleteAll(StreamEvent streamEvent) {
        if (primaryKeyData != null) {
            Object primaryKey = constructPrimaryKey(streamEvent, primaryKeyReferenceHolders);
            StreamEvent deletedEvent = primaryKeyData.remove(primaryKey);
            if (indexData != null) {
                deleteFromIndexes(deletedEvent);
            }
        } else if (indexData != null) {
            deleteFromIndexes(streamEvent);
        }
    }

    @Override
    public void delete(String attribute, Compare.Operator operator, Object value) {

        if (isOperationLogEnabled) {
            if (!isFullSnapshot()) {
                operationChangeLog.add(new Operation(DELETE_BY_OPERATOR, new Object[]{attribute, operator, value}));
            } else {
                operationChangeLog.clear();
                forceFullSnapshot = true;
            }
        }

        if (primaryKeyData != null && attribute.equals(primaryKeyAttributes)) {
            switch (operator) {

                case LESS_THAN:
                    for (Iterator<StreamEvent> iterator = ((TreeMap<Object, StreamEvent>) primaryKeyData).
                            headMap(value, false).values().iterator();
                         iterator.hasNext(); ) {
                        StreamEvent toDeleteEvent = iterator.next();
                        iterator.remove();
                        deleteFromIndexes(toDeleteEvent);
                    }
                    return;
                case GREATER_THAN:
                    for (Iterator<StreamEvent> iterator = ((TreeMap<Object, StreamEvent>) primaryKeyData).
                            tailMap(value, false).values().iterator();
                         iterator.hasNext(); ) {
                        StreamEvent toDeleteEvent = iterator.next();
                        iterator.remove();
                        deleteFromIndexes(toDeleteEvent);
                    }
                    return;
                case LESS_THAN_EQUAL:
                    for (Iterator<StreamEvent> iterator = ((TreeMap<Object, StreamEvent>) primaryKeyData).
                            headMap(value, true).values().iterator();
                         iterator.hasNext(); ) {
                        StreamEvent toDeleteEvent = iterator.next();
                        iterator.remove();
                        deleteFromIndexes(toDeleteEvent);
                    }
                    return;
                case GREATER_THAN_EQUAL:
                    for (Iterator<StreamEvent> iterator = ((TreeMap<Object, StreamEvent>) primaryKeyData).
                            tailMap(value, true).values().iterator();
                         iterator.hasNext(); ) {
                        StreamEvent toDeleteEvent = iterator.next();
                        iterator.remove();
                        deleteFromIndexes(toDeleteEvent);
                    }
                    return;
                case EQUAL:
                    StreamEvent deletedEvent = primaryKeyData.remove(value);
                    if (deletedEvent != null) {
                        deleteFromIndexes(deletedEvent);
                    }
                    return;
                case NOT_EQUAL:
                    StreamEvent streamEvent = primaryKeyData.get(value);
                    deleteAll();
                    if (streamEvent != null) {
                        add(streamEvent);
                    }
                    return;
            }
        } else {
            switch (operator) {

                case LESS_THAN:
                    for (Iterator<Set<StreamEvent>> iterator = indexData.get(attribute).
                            headMap(value, false).values().iterator();
                         iterator.hasNext(); ) {
                        Set<StreamEvent> deletedEventSet = iterator.next();
                        deleteFromIndexesAndPrimaryKey(attribute, deletedEventSet);
                        iterator.remove();
                    }
                    return;
                case GREATER_THAN:
                    for (Iterator<Set<StreamEvent>> iterator = indexData.get(attribute).
                            tailMap(value, false).values().iterator();
                         iterator.hasNext(); ) {
                        Set<StreamEvent> deletedEventSet = iterator.next();
                        deleteFromIndexesAndPrimaryKey(attribute, deletedEventSet);
                        iterator.remove();
                    }
                    return;
                case LESS_THAN_EQUAL:
                    for (Iterator<Set<StreamEvent>> iterator = indexData.get(attribute).
                            headMap(value, true).values().iterator();
                         iterator.hasNext(); ) {
                        Set<StreamEvent> deletedEventSet = iterator.next();
                        deleteFromIndexesAndPrimaryKey(attribute, deletedEventSet);
                        iterator.remove();
                    }
                    return;
                case GREATER_THAN_EQUAL:
                    for (Iterator<Set<StreamEvent>> iterator = indexData.get(attribute).
                            tailMap(value, true).values().iterator();
                         iterator.hasNext(); ) {
                        Set<StreamEvent> deletedEventSet = iterator.next();
                        deleteFromIndexesAndPrimaryKey(attribute, deletedEventSet);
                        iterator.remove();
                    }
                    return;
                case EQUAL:
                    Set<StreamEvent> deletedEventSet = indexData.get(attribute).remove(value);
                    if (deletedEventSet != null && deletedEventSet.size() > 0) {
                        deleteFromIndexesAndPrimaryKey(attribute, deletedEventSet);
                    }
                    return;
                case NOT_EQUAL:
                    Set<StreamEvent> matchingEventSet = indexData.get(attribute).get(value);
                    deleteAll();
                    for (StreamEvent matchingEvent : matchingEventSet) {
                        add(matchingEvent);
                    }
                    return;
            }
        }
        throw new OperationNotSupportedException(operator + " not supported for '" + value + "' by " + getClass()
                .getName());
    }

    @Override
    public boolean containsEventSet(String attribute, Compare.Operator operator, Object value) {
        if (primaryKeyData != null && attribute.equals(primaryKeyAttributes)) {
            switch (operator) {
                case LESS_THAN:
                    return ((TreeMap<Object, StreamEvent>) primaryKeyData).lowerKey(value) != null;
                case GREATER_THAN:
                    return ((TreeMap<Object, StreamEvent>) primaryKeyData).higherKey(value) != null;
                case LESS_THAN_EQUAL:
                    return ((TreeMap<Object, StreamEvent>) primaryKeyData).ceilingKey(value) != null;
                case GREATER_THAN_EQUAL:
                    return ((TreeMap<Object, StreamEvent>) primaryKeyData).floorKey(value) != null;
                case EQUAL:
                    return primaryKeyData.get(value) != null;
                case NOT_EQUAL:
                    return primaryKeyData.size() > 1;
            }
        } else {
            TreeMap<Object, Set<StreamEvent>> currentIndexedData = indexData.get(attribute);

            switch (operator) {

                case LESS_THAN:
                    return currentIndexedData.lowerKey(value) != null;
                case GREATER_THAN:
                    return currentIndexedData.higherKey(value) != null;
                case LESS_THAN_EQUAL:
                    return currentIndexedData.ceilingKey(value) != null;
                case GREATER_THAN_EQUAL:
                    return currentIndexedData.floorKey(value) != null;
                case EQUAL:
                    return currentIndexedData.get(value) != null;
                case NOT_EQUAL:
                    return currentIndexedData.size() > 1;
            }
        }
        throw new OperationNotSupportedException(operator + " not supported for '" + value + "' by " + getClass()
                .getName());
    }

    private void deleteFromIndexesAndPrimaryKey(String currentAttribute, Set<StreamEvent> deletedEventSet) {
        for (StreamEvent deletedEvent : deletedEventSet) {
            if (primaryKeyData != null) {
                Object primaryKey = constructPrimaryKey(deletedEvent, primaryKeyReferenceHolders);
                primaryKeyData.remove(primaryKey);
            }
            for (Map.Entry<String, Integer> indexEntry : indexMetaData.entrySet()) {
                if (!currentAttribute.equals(indexEntry.getKey())) {
                    TreeMap<Object, Set<StreamEvent>> indexMap = indexData.get(indexEntry.getKey());
                    Object key = deletedEvent.getOutputData()[indexEntry.getValue()];
                    Set<StreamEvent> values = indexMap.get(key);
                    if (values != null) {
                        values.remove(deletedEvent);
                        if (values.size() == 0) {
                            indexMap.remove(key);
                        }
                    }
                }
            }
        }
    }

    private void deleteFromIndexes(StreamEvent toDeleteEvent) {
        if (indexMetaData != null) {
            for (Map.Entry<String, Integer> indexEntry : indexMetaData.entrySet()) {
                TreeMap<Object, Set<StreamEvent>> indexMap = indexData.get(indexEntry.getKey());
                Object key = toDeleteEvent.getOutputData()[indexEntry.getValue()];
                Set<StreamEvent> values = indexMap.get(key);
                if (values != null) {
                    values.remove(toDeleteEvent);
                    if (values.size() == 0) {
                        indexMap.remove(key);
                    }
                }
            }
        }
    }

    private boolean isFullSnapshot() {
        return operationChangeLog.size() > (eventsCount * FULL_SNAPSHOT_THRESHOLD)
                || forceFullSnapshot
                || SnapshotRequest.isRequestForFullSnapshot();
    }

    public Snapshot getSnapshot() {
        if (isFullSnapshot()) {
            forceFullSnapshot = false;
            return new Snapshot(this, false);
        } else {
            Snapshot snapshot = new Snapshot(operationChangeLog, true);
            operationChangeLog = new ArrayList<>();
            return snapshot;
        }
    }

    public void restore(SnapshotStateList snapshotStatelist) {
        TreeMap<Long, Snapshot> revisions = snapshotStatelist.getSnapshotStates();
        Iterator<Map.Entry<Long, Snapshot>> itr = revisions.entrySet().iterator();
        this.isOperationLogEnabled = false;
        while (itr.hasNext()) {
            Map.Entry<Long, Snapshot> snapshotEntry = itr.next();
            if (!snapshotEntry.getValue().isIncrementalSnapshot()) {
                this.deleteAll();
                IndexEventHolder snapshotEventHolder = (IndexEventHolder) snapshotEntry.getValue().getState();
                if (primaryKeyData != null) {
                    primaryKeyData.clear();
                    primaryKeyData.putAll(snapshotEventHolder.primaryKeyData);
                }
                if (indexData != null) {
                    indexData.clear();
                    indexData.putAll(snapshotEventHolder.indexData);
                }
                forceFullSnapshot = false;
            } else {
                ArrayList<Operation> operations = (ArrayList<Operation>) snapshotEntry.getValue().getState();
                for (Operation op : operations) {
                    switch (op.operation) {
                        case ADD:
                            add((StreamEvent) op.parameters);
                            break;
                        case REMOVE:
                            deleteAll((StreamEvent) op.parameters);
                            break;
                        case CLEAR:
                            deleteAll();
                            break;
                        case OVERWRITE:
                            overwrite((StreamEvent) op.parameters);
                            break;
                        case DELETE_BY_OPERATOR:
                            Object[] args = (Object[]) op.parameters;
                            delete((String) args[0], (Compare.Operator) args[1], args[2]);
                            break;
                        default:
                            continue;
                    }
                }
            }
        }
        this.isOperationLogEnabled = true;
    }

    @Override
    public int size() {
        return primaryKeyData.size();
    }
}