/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.
 *
 */
package com.netflix.hollow.core.index;

import static java.util.stream.Collectors.joining;

import com.netflix.hollow.core.HollowDataset;
import com.netflix.hollow.core.index.key.PrimaryKey;
import com.netflix.hollow.core.schema.HollowCollectionSchema;
import com.netflix.hollow.core.schema.HollowMapSchema;
import com.netflix.hollow.core.schema.HollowObjectSchema;
import com.netflix.hollow.core.schema.HollowSchema;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * Functionality for processing field paths.
 */
public final class FieldPaths {

    /**
     * Creates an object-based field path given a data set and the field path in symbolic form conforming to paths
     * associated with a primary key.
     *
     * @param dataset the data set
     * @param type the type name from which to bind the field path
     * @param path the symbolic field path
     * @return the field path
     * @throws IllegalArgumentException if the symbolic field path is ill-formed and cannot be bound
     */
    public static FieldPath<ObjectFieldSegment> createFieldPathForPrimaryKey(
            HollowDataset dataset, String type, String path) {
        boolean autoExpand = !path.endsWith("!");
        path = autoExpand ? path : path.substring(0, path.length() - 1);

        FieldPath<FieldSegment> fp = createFieldPath(dataset, type, path, autoExpand, false, false);

        // Erasure trick to avoid copying when it is known the list only contains
        // instances of ObjectFieldSegment
        assert fp.segments.stream().allMatch(o -> o instanceof ObjectFieldSegment);
        @SuppressWarnings( {"unchecked", "raw"})
        FieldPath<ObjectFieldSegment> result = (FieldPath<ObjectFieldSegment>) (FieldPath) fp;
        return result;
    }

    /**
     * Creates a field path given a data set and the field path in symbolic form conforming to paths
     * associated with a hash index.
     *
     * @param dataset the data set
     * @param type the type name from which to bind the field path
     * @param path the symbolic field path
     * @return the field path
     * @throws IllegalArgumentException if the symbolic field path is ill-formed and cannot be bound
     */
    public static FieldPath<FieldSegment> createFieldPathForHashIndex(HollowDataset dataset, String type, String path) {
        return createFieldPath(dataset, type, path, false, false, true);
    }

    /**
     * Creates a field path given a data set and the field path in symbolic form conforming to paths
     * associated with a prefix index.
     *
     * @param dataset the data set
     * @param type the type name from which to bind the field path
     * @param path the symbolic field path
     * @param autoExpand {@code true} if the field path should be expanded (if needed) to a full path referencing
     * an object field whose type is a non-reference type, otherwise the field path is not expanded and must be
     * a full path
     * @return the field path
     * @throws IllegalArgumentException if the symbolic field path is ill-formed and cannot be bound
     */
    public static FieldPath<FieldSegment> createFieldPathForPrefixIndex(
            HollowDataset dataset, String type, String path, boolean autoExpand) {
        // If autoExpand is false then requireFullPath must be true
        boolean requireFullPath = !autoExpand;
        return createFieldPath(dataset, type, path, autoExpand, requireFullPath, true);
    }

    /**
     * Creates a field path given a data set and the field path in symbolic form.
     *
     * @param dataset the data set
     * @param type the type name from which to bind the field path
     * @param path the symbolic field path
     * @param autoExpand {@code true} if the field path should be expanded (if needed) to a full path referencing
     * an object field whose type is a non-reference type, otherwise the field path is not expanded
     * @param requireFullPath {@code true} if a full path is required when {@code autoExpand} is {@code false}.
     * Ignored if {@code autoExpand} is {@code true}.
     * @param traverseSequences {@code true} if lists, sets and maps are traversed, otherwise an
     * {@code IllegalArgumentException} will be thrown
     * @return the field path
     * @throws IllegalArgumentException if the symbolic field path is ill-formed and cannot be bound
     */
    static FieldPath<FieldSegment> createFieldPath(
            HollowDataset dataset, String type, String path,
            boolean autoExpand, boolean requireFullPath, boolean traverseSequences) {
        Objects.requireNonNull(dataset);
        Objects.requireNonNull(type);
        Objects.requireNonNull(path);

        String[] segments = path.isEmpty() ? new String[0] : path.split("\\.");
        List<FieldSegment> fieldSegments = new ArrayList<>();

        String segmentType = type;
        for (int i = 0; i < segments.length; i++) {
            HollowSchema schema = dataset.getSchema(segmentType);
            // @@@ Can this only occur for anything other than the root `type`?
            if (schema == null) {
                throw new FieldPathException(FieldPathException.ErrorKind.NOT_BINDABLE, dataset, type, segments,
                        fieldSegments, null, i);
            }

            String segment = segments[i];
            HollowSchema.SchemaType schemaType = schema.getSchemaType();
            if (schemaType == HollowSchema.SchemaType.OBJECT) {
                HollowObjectSchema objectSchema = (HollowObjectSchema) schema;

                int index = objectSchema.getPosition(segment);
                if (index == -1) {
                    throw new FieldPathException(FieldPathException.ErrorKind.NOT_FOUND, dataset, type, segments,
                            fieldSegments, schema, i);
                }

                segmentType = objectSchema.getReferencedType(index);
                fieldSegments.add(new ObjectFieldSegment(objectSchema, segment, segmentType, index));
            } else if (traverseSequences && (schemaType == HollowSchema.SchemaType.SET
                    || schemaType == HollowSchema.SchemaType.LIST)) {
                HollowCollectionSchema collectionSchema = (HollowCollectionSchema) schema;

                if (!segment.equals("element")) {
                    throw new FieldPathException(FieldPathException.ErrorKind.NOT_FOUND, dataset, type, segments,
                            fieldSegments, schema, i);
                }

                segmentType = collectionSchema.getElementType();
                fieldSegments.add(new FieldSegment(collectionSchema, segment, segmentType));
            } else if (traverseSequences && schemaType == HollowSchema.SchemaType.MAP) {
                HollowMapSchema mapSchema = (HollowMapSchema) schema;

                if (segment.equals("key")) {
                    segmentType = mapSchema.getKeyType();
                } else if (segment.equals("value")) {
                    segmentType = mapSchema.getValueType();
                } else {
                    throw new FieldPathException(FieldPathException.ErrorKind.NOT_FOUND, dataset, type, segments,
                            fieldSegments, schema, i);
                }

                fieldSegments.add(new FieldSegment(mapSchema, segment, segmentType));
            } else if (!traverseSequences) {
                throw new FieldPathException(FieldPathException.ErrorKind.NOT_TRAVERSABLE, dataset, type, segments,
                        fieldSegments, schema, i);
            }

            if (i < segments.length - 1 && segmentType == null) {
                throw new FieldPathException(FieldPathException.ErrorKind.NOT_TRAVERSABLE, dataset, type, segments,
                        fieldSegments, schema, i);
            }
        }

        if (autoExpand) {
            while (segmentType != null) {
                HollowSchema schema = dataset.getSchema(segmentType);

                if (schema.getSchemaType() == HollowSchema.SchemaType.OBJECT) {
                    HollowObjectSchema objectSchema = (HollowObjectSchema) schema;

                    if (objectSchema.numFields() == 1) {
                        segmentType = objectSchema.getReferencedType(0);

                        fieldSegments.add(
                                new ObjectFieldSegment(objectSchema, objectSchema.getFieldName(0), segmentType,
                                        0));
                    } else if (objectSchema.getPrimaryKey() != null && objectSchema.getPrimaryKey().numFields() == 1) {
                        PrimaryKey key = objectSchema.getPrimaryKey();

                        FieldPath<ObjectFieldSegment> expandedFieldSegments;
                        try {
                            expandedFieldSegments =
                                    createFieldPathForPrimaryKey(dataset, key.getType(), key.getFieldPaths()[0]);
                        } catch (FieldPathException cause) {
                            FieldPathException e = new FieldPathException(FieldPathException.ErrorKind.NOT_EXPANDABLE,
                                    dataset, type, segments,
                                    fieldSegments, objectSchema);
                            e.initCause(cause);
                            throw e;
                        }

                        fieldSegments.addAll(expandedFieldSegments.segments);
                        break;
                    } else {
                        throw new FieldPathException(FieldPathException.ErrorKind.NOT_EXPANDABLE, dataset, type,
                                segments,
                                fieldSegments, objectSchema);
                    }
                } else {
                    throw new FieldPathException(FieldPathException.ErrorKind.NOT_EXPANDABLE, dataset, type, segments,
                            fieldSegments, schema);
                }
            }
        } else if (requireFullPath && segmentType != null) {
            throw new FieldPathException(FieldPathException.ErrorKind.NOT_FULL, dataset, type, segments,
                    fieldSegments);
        }

        return new FieldPath<>(type, fieldSegments, !autoExpand);
    }

    /**
     * An exception contain structured information when a field path cannot be bound.
     */
    static final class FieldPathException extends IllegalArgumentException {
        enum ErrorKind {
            NOT_BINDABLE,
            NOT_FOUND,
            NOT_FULL,
            NOT_TRAVERSABLE,
            NOT_EXPANDABLE,
            ;
        }

        final ErrorKind error;
        final String rootType;
        final String[] segments;

        // Prior paths
        final List<FieldSegment> fieldSegments;

        // Partial state
        final HollowSchema enclosingSchema;
        final int segmentIndex;

        FieldPathException(
                ErrorKind error, HollowDataset dataset, String rootType, String[] segments,
                List<FieldSegment> fieldSegments) {
            this(error, dataset, rootType, segments, fieldSegments, null, segments.length);
        }

        FieldPathException(
                ErrorKind error, HollowDataset dataset, String rootType, String[] segments,
                List<FieldSegment> fieldSegments, HollowSchema enclosingSchema) {
            this(error, dataset, rootType, segments, fieldSegments, enclosingSchema, segments.length);
        }

        FieldPathException(
                ErrorKind error, HollowDataset dataset, String rootType, String[] segments,
                List<FieldSegment> fieldSegments, HollowSchema enclosingSchema, int segmentIndex) {
            super(message(error, dataset, rootType, segments, fieldSegments, enclosingSchema, segmentIndex));

            this.error = error;
            this.rootType = rootType;
            this.segments = segments;
            this.fieldSegments = Collections.unmodifiableList(fieldSegments);
            this.enclosingSchema = enclosingSchema;
            this.segmentIndex = segmentIndex;
        }

        static String message(
                ErrorKind error, HollowDataset dataset, String rootType, String[] segments,
                List<FieldSegment> fieldSegments, HollowSchema enclosingSchema, int segmentIndex) {
            switch (error) {
                case NOT_BINDABLE:
                    return String.format("Field path \"%s\" cannot be bound to data set %s. " +
                                    "A schema of type named \"%s\" cannot be found for the last segment of the path prefix \"%s\".",
                            toPathString(segments), dataset,
                            getLastTypeName(rootType, fieldSegments), toPathString(segments, segmentIndex + 1));
                case NOT_FOUND:
                    return String.format("Field path \"%s\" not found in data set %s. " +
                                    "A schema of type named \"%s\" does not contain a field for the last segment of the path prefix \"%s\".",
                            toPathString(segments), dataset,
                            enclosingSchema.getName(), toPathString(segments, segmentIndex + 1));
                case NOT_TRAVERSABLE: {
                    if (enclosingSchema.getSchemaType() != HollowSchema.SchemaType.OBJECT) {
                        return String.format("Field path \"%s\" is not traversable in data set %s. " +
                                        "A non-object schema of type named \"%s\" and of schema type %s cannot be traversed for the last segment of the path prefix \"%s\".",
                                toPathString(segments), dataset,
                                enclosingSchema.getName(), enclosingSchema.getSchemaType(),
                                toPathString(segments, segmentIndex + 1));
                    } else {
                        return String.format("Field path \"%s\" is not traversable in data set %s. " +
                                        "An object schema of type named \"%s\" cannot be traversed for the last segment of the path prefix \"%s\". "
                                        +
                                        "The last segment of the path prefix refers to a value (non-reference) field.",
                                toPathString(segments), dataset,
                                enclosingSchema.getName(),
                                toPathString(segments, segmentIndex + 1));
                    }
                }
                case NOT_FULL:
                    return String.format("Field path \"%s\" is not a full path in data set %s. " +
                                    "The last segment of the path is not a value (non-reference) field and refers to a reference field whose schema is of type named \"%s\"",
                            toPathString(segments), dataset,
                            fieldSegments.get(fieldSegments.size() - 1).getTypeName());
                case NOT_EXPANDABLE: {
                    if (enclosingSchema.getSchemaType() == HollowSchema.SchemaType.OBJECT) {
                        HollowObjectSchema objectSchema = (HollowObjectSchema) enclosingSchema;
                        if (objectSchema.numFields() != 1 || objectSchema.getPrimaryKey() == null
                                || objectSchema.getPrimaryKey().numFields() != 1) {
                            return String.format("Field path \"%s\" is not expandable in data set %s. " +
                                            "An object schema of type named \"%s\" cannot be traversed for the last segment of the partially expanded path \"%s\". "
                                            +
                                            "The schema contains more than one field, or has no primary key, or has a primary key with more than one field path.",
                                    toPathString(segments), dataset,
                                    enclosingSchema.getName(),
                                    toPathString(fieldSegments));
                        }
                    }
                    return String.format("Field path \"%s\" is not expandable in data set %s. " +
                                    "A non-object schema of type named \"%s\" and of schema type %s cannot be traversed for the last segment of the partially expanded path \"%s\".",
                            toPathString(segments), dataset,
                            enclosingSchema.getName(), enclosingSchema.getSchemaType(),
                            toPathString(fieldSegments));
                }
                default:
                    throw new InternalError("Cannot reach here");
            }
        }

        static String getLastTypeName(String rootType, List<FieldSegment> fieldSegments) {
            return fieldSegments.isEmpty()
                    ? rootType
                    : fieldSegments.get(fieldSegments.size() - 1).typeName;
        }

        static String toPathString(List<FieldSegment> segments) {
            return segments.stream().map(FieldSegment::getName).collect(joining("."));
        }

        static String toPathString(String[] segments) {
            return toPathString(segments, segments.length);
        }

        static String toPathString(String[] segments, int l) {
            return Arrays.stream(segments).limit(l).collect(joining("."));
        }
    }

    /**
     * A structured representation field path containing field segments.
     *
     * @param <T> the field segment type
     */
    public final static class FieldPath<T extends FieldSegment> {
        final String rootType;
        final List<T> segments;
        final boolean noAutoExpand;

        FieldPath(String rootType, List<T> segments, boolean noAutoExpand) {
            this.rootType = rootType;
            this.segments = Collections.unmodifiableList(segments);
            this.noAutoExpand = noAutoExpand;
        }

        /**
         * Returns the root type from which this field path is bound.
         *
         * @return the root type
         */
        public String getRootType() {
            return rootType;
        }

        /**
         * Returns the field segments.
         *
         * @return the field segments.  The returned list is unmodifiable.
         */
        public List<T> getSegments() {
            return segments;
        }

        /**
         * Returns the field path in nominal form.
         *
         * @return the field path in nominal form
         */
        public String toString() {
            String path = segments.stream().map(FieldPaths.FieldSegment::getName)
                    .collect(joining("."));
            return noAutoExpand ? path + "!" : path;
        }

        @Override public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            FieldPath<?> fieldPath = (FieldPath<?>) o;
            return noAutoExpand == fieldPath.noAutoExpand &&
                    rootType.equals(fieldPath.rootType) &&
                    segments.equals(fieldPath.segments);
        }

        @Override public int hashCode() {
            return Objects.hash(rootType, segments, noAutoExpand);
        }
    }

    /**
     * A structured representation of field segment.
     */
    public static class FieldSegment {
        final HollowSchema enclosingSchema;
        final String name;
        final String typeName;

        FieldSegment(HollowSchema enclosingSchema, String name, String typeName) {
            this.name = name;
            this.typeName = typeName;
            this.enclosingSchema = enclosingSchema;
        }

        /**
         * Returns the enclosing schema that declares a field corresponding to this segment.
         *
         * @return the enclosing schema
         */
        public HollowSchema getEnclosingSchema() {
            return enclosingSchema;
        }

        /**
         * Returns the segment name.
         *
         * @return the segment name
         */
        public String getName() {
            return name;
        }

        /**
         * Returns the schema type name associated with this segment.
         *
         * @return the schema type name.
         */
        public String getTypeName() {
            return typeName;
        }

        @Override public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            FieldSegment that = (FieldSegment) o;
            return Objects.equals(enclosingSchema, that.enclosingSchema) &&
                    Objects.equals(name, that.name) &&
                    Objects.equals(typeName, that.typeName);
        }

        @Override public int hashCode() {
            return Objects.hash(enclosingSchema, name, typeName);
        }
    }

    /**
     * A structured representation of field segment corresponding to a field in an object schema.
     */
    public final static class ObjectFieldSegment extends FieldSegment {
        final int index;
        final HollowObjectSchema.FieldType type;

        ObjectFieldSegment(
                HollowObjectSchema enclosingSchema, String name, String typeName,
                int index) {
            super(enclosingSchema, name, typeName);
            this.index = index;
            this.type = enclosingSchema.getFieldType(index);
        }

        /**
         * {@inheritDoc}
         */
        public HollowObjectSchema getEnclosingSchema() {
            return (HollowObjectSchema) super.getEnclosingSchema();
        }

        /**
         * The field index in the enclosing object schema.
         *
         * @return the field index
         */
        public int getIndex() {
            return index;
        }

        /**
         * The field type of the field in the enclosing object schema.
         *
         * @return the field type
         */
        public HollowObjectSchema.FieldType getType() {
            return type;
        }

        @Override public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            if (!super.equals(o)) {
                return false;
            }
            ObjectFieldSegment that = (ObjectFieldSegment) o;
            return index == that.index &&
                    type == that.type;
        }

        @Override public int hashCode() {
            return Objects.hash(super.hashCode(), index, type);
        }
    }
}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.
 *
 */
package com.netflix.hollow.zenoadapter.util;

import java.util.Arrays;

/**
 * Hash lookup map associate object references to already seen ordinals.
 * The fundamental assumption made here is that objects are immutable, so that
 * once the ordinal is assigned to an object, the ordinal stays the same
 * throughout the life of the object.
 *
 * @author timurua
 *
 */
public class ObjectIdentityOrdinalMap {

    public static final class Entry {
        private final Object key;
        // ordinal
        private final int ordinal;
        // linked list pointer
        private Entry next;

        /**
         * Creates new entry.
         */
        Entry(Object key, int hash, int ordinal, Entry next) {
            this.key = key;
            this.ordinal = ordinal;
            this.next  = next;
        }

        public Object getKey() {
            return key;
        }

        public int getOrdinal() {
            return ordinal;
        }

        @Override
        public String toString() {
            Object v = key;
            return v == null ? "null" : v.toString();
        }

        public int hash() {
            return System.identityHashCode(key);
        }
    }

    /**
     * The map is divided into segments to increase concurrency
     */
    private class Segment {

        // The same concept as in HashMap. If the entry array is becoming too
        // dense, it should be increased
        private static final int LOAD_FACTOR_PERCENT = 75;
        private static final int MINIMUM_CAPACITY = 256;
        private static final int MAXIMUM_CAPACITY = (1<<30);

        private int count = 0;
        private int maxThreshold = 0;
        private int minThreshold = 0;
        private Entry[] entries;

        public Segment(){
            resize(MINIMUM_CAPACITY);
        }

        public synchronized void put(Object object, int hashCode, int ordinal) {
            int index = index(hashCode, entries.length);
            Entry current = entries[index];
            Entry prev = null;
            while (current != null) {
                if (current.hash() == hashCode) {
                    Object currentObject = current.getKey();
                    if( currentObject == null){
                        deleteEntry(index, current, prev);
                        current = current.next;
                        continue;
                    } else if (currentObject == object) {
                        return;
                    }
                }
                prev = current;
                current = current.next;
            }
            count++;
            Entry first = entries[index];
            Entry entry = new Entry(object, hashCode, ordinal, first);
            entries[index] = entry;
            entry.next = first;
            checkSize();
            return;
        }

        public synchronized Entry get(Object object, int hashCode) {
            int index = index(hashCode, entries.length);
            Entry current = entries[index];
            Entry prev = null;
            while (current != null) {
                if (current.hash() == hashCode) {
                    Object currentObject = current.getKey();
                    if( currentObject == null){
                        deleteEntry(index, current, prev);
                        current = current.next;
                        continue;
                    } else if (currentObject == object) {
                        return current;
                    }
                }
                prev = current;
                current = current.next;
            }
            return null;
        }

        private void checkSize() {
            if( count >= minThreshold && count <= maxThreshold ){
                return;
            }
            int newCapacity;
            if( count < minThreshold ) {
                newCapacity = Math.max(MINIMUM_CAPACITY, entries.length >> 1);
            } else {
                newCapacity = Math.min(MAXIMUM_CAPACITY, entries.length << 1);
            }

            // nothing should be done, since capacity is not changed
            if (newCapacity == entries.length) {
                return;
            }

            resize(newCapacity);
        }

        private void resize(int newCapacity) {
            Entry[] newEntries = new Entry[newCapacity];
            if( entries != null){
                for(Entry entry : entries){
                    Entry current = entry;
                    while(current != null){
                        Entry newEntry = current;
                        current = current.next;
                        int index = index(newEntry.hash(), newEntries.length);
                        newEntry.next = newEntries[index];
                        newEntries[index] = newEntry;
                    }
                }
            }
            minThreshold = (newEntries.length == MINIMUM_CAPACITY) ? 0 : (newEntries.length * LOAD_FACTOR_PERCENT / 200);
            maxThreshold = (newEntries.length == MAXIMUM_CAPACITY) ? Integer.MAX_VALUE : newEntries.length * LOAD_FACTOR_PERCENT / 100;
            entries = newEntries;
        }

        private void deleteEntry(int index, Entry current, Entry prev) {
            count--;
            if (prev != null) {
                prev.next = current.next;
            } else {
                entries[index] = current.next;
            }
        }

        private final int index(int hashCode, int capacity) {
            return (hashCode >>> ObjectIdentityOrdinalMap.this.logOfSegmentNumber) % capacity;
        }

        public synchronized void clear() {
            Arrays.fill(entries, null);
            count = 0;

            resize(MINIMUM_CAPACITY);
        }

        public synchronized int size() {
            return count;
        }
    }

    private final Segment[] segments;
    private final int mask;
    private final int logOfSegmentNumber;

    public ObjectIdentityOrdinalMap() {
        this(8);
    }

    public ObjectIdentityOrdinalMap(int logOfSegmentNumber) {
        if (logOfSegmentNumber < 1 && logOfSegmentNumber > 32) {
            throw new RuntimeException("Invalid power level");
        }
        segments = new Segment[2 << logOfSegmentNumber];
        for(int i=0; i<segments.length; i++){
            segments[i] = new Segment();
        }
        this.mask = (2 << logOfSegmentNumber) - 1;
        this.logOfSegmentNumber = logOfSegmentNumber;
    }

    /**
     * Associating the obj with an ordinal
     *
     * @param obj
     * @param ordinal
     */
    public void put(Object obj, int ordinal) {
        int hashCode = System.identityHashCode(obj);
        int segment = segment(hashCode);
        segments[segment].put(obj, hashCode, ordinal);
    }

    public Entry getEntry(Object obj) {
        int hashCode = System.identityHashCode(obj);
        int segment = segment(hashCode);
        return segments[segment].get(obj, hashCode);
    }

    private final int segment(int hashCode) {
        return hashCode & mask;
    }

    public void clear(){
        for (Segment segment : segments) {
            segment.clear();
        }
    }

    public int size() {
        int size = 0;
        for (Segment segment : segments) {
            size += segment.size();
        }
        return size;
    }
}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.
 *
 */
package com.netflix.hollow.api.client;

import com.netflix.hollow.api.consumer.HollowConsumer;
import com.netflix.hollow.core.HollowConstants;

/**
 * The HollowUpdatePlanner defines the logic responsible for interacting with a {@link HollowBlobRetriever}
 * to create a {@link HollowUpdatePlan}.
 */
public class HollowUpdatePlanner {

    private final HollowConsumer.BlobRetriever transitionCreator;
    private final HollowConsumer.DoubleSnapshotConfig doubleSnapshotConfig;

    @Deprecated
    public HollowUpdatePlanner(HollowBlobRetriever blobRetriever) {
        this(HollowClientConsumerBridge.consumerBlobRetrieverFor(blobRetriever));
    }

    public HollowUpdatePlanner(HollowConsumer.BlobRetriever blobRetriever) {
        this(blobRetriever, new HollowConsumer.DoubleSnapshotConfig() {
            @Override
            public int maxDeltasBeforeDoubleSnapshot() {
                return 32;
            }

            @Override
            public boolean allowDoubleSnapshot() {
                return true;
            }
        });
    }

    public HollowUpdatePlanner(HollowConsumer.BlobRetriever transitionCreator, HollowConsumer.DoubleSnapshotConfig doubleSnapshotConfig) {
        this.transitionCreator = transitionCreator;
        this.doubleSnapshotConfig = doubleSnapshotConfig;
    }

    /**
     * @return the sequence of steps necessary to initialize a hollow state engine to a given state.
     * @param desiredVersion - The version to which the hollow state engine should be updated once the resultant steps are applied.
     * @throws Exception if the plan cannot be initialized
     */
    public HollowUpdatePlan planInitializingUpdate(long desiredVersion) throws Exception {
        return planUpdate(HollowConstants.VERSION_NONE, desiredVersion, true);
    }

    /**
     * @param currentVersion - The current version of the hollow state engine, or HollowConstants.VERSION_NONE if not yet initialized
     * @param desiredVersion - The version to which the hollow state engine should be updated once the resultant steps are applied.
     * @param allowSnapshot  - Allow a snapshot plan to be created if the destination version is not reachable
     * @return the sequence of steps necessary to bring a hollow state engine up to date.
     * @throws Exception if the plan cannot be updated
     */
    public HollowUpdatePlan planUpdate(long currentVersion, long desiredVersion, boolean allowSnapshot) throws Exception {
        if(desiredVersion == currentVersion)
            return HollowUpdatePlan.DO_NOTHING;

        if (currentVersion == HollowConstants.VERSION_NONE)
            return snapshotPlan(desiredVersion);

        HollowUpdatePlan deltaPlan = deltaPlan(currentVersion, desiredVersion, doubleSnapshotConfig.maxDeltasBeforeDoubleSnapshot());

        long deltaDestinationVersion = deltaPlan.destinationVersion(currentVersion);

        if(deltaDestinationVersion != desiredVersion && allowSnapshot) {
            HollowUpdatePlan snapshotPlan = snapshotPlan(desiredVersion);
            long snapshotDestinationVersion = snapshotPlan.destinationVersion(currentVersion);

            if(snapshotDestinationVersion == desiredVersion
                    || ((deltaDestinationVersion > desiredVersion) && (snapshotDestinationVersion < desiredVersion))
                    || ((snapshotDestinationVersion < desiredVersion) && (snapshotDestinationVersion > deltaDestinationVersion)))

                return snapshotPlan;
        }

        return deltaPlan;
    }

    /**
     * Returns an update plan that if executed will update the client to a version that is either equal to or as close to but
     * less than the desired version as possible. This plan normally contains one snapshot transition and zero or more delta
     * transitions but if no previous versions were found then an empty plan, {@code HollowUpdatePlan.DO_NOTHING}, is returned.
     *
     * @param desiredVersion The desired version to which the client wishes to update to, or update to as close to as possible but lesser than this version
     * @return An update plan containing 1 snapshot transition and 0+ delta transitions if requested versions were found,
     *         or an empty plan, {@code HollowUpdatePlan.DO_NOTHING}, if no previous versions were found
     */
    private HollowUpdatePlan snapshotPlan(long desiredVersion) {
        HollowUpdatePlan plan = new HollowUpdatePlan();
        long nearestPreviousSnapshotVersion = includeNearestSnapshot(plan, desiredVersion);

        // The includeNearestSnapshot function returns a snapshot version that is less than or equal to the desired version
        if(nearestPreviousSnapshotVersion > desiredVersion)
            return HollowUpdatePlan.DO_NOTHING;

        // If the nearest snapshot version is {@code HollowConstants.VERSION_LATEST} then no past snapshots were found, so
        // skip the delta planning and the update plan does nothing
        if(nearestPreviousSnapshotVersion == HollowConstants.VERSION_LATEST)
            return HollowUpdatePlan.DO_NOTHING;

        plan.appendPlan(deltaPlan(nearestPreviousSnapshotVersion, desiredVersion, Integer.MAX_VALUE));

        return plan;
    }

    private HollowUpdatePlan deltaPlan(long currentVersion, long desiredVersion, int maxDeltas) {
        HollowUpdatePlan plan = new HollowUpdatePlan();
        if(currentVersion < desiredVersion) {
            applyForwardDeltasToPlan(currentVersion, desiredVersion, plan, maxDeltas);
        } else if(currentVersion > desiredVersion) {
            applyReverseDeltasToPlan(currentVersion, desiredVersion, plan, maxDeltas);
        }

        return plan;
    }

    private long applyForwardDeltasToPlan(long currentVersion, long desiredVersion, HollowUpdatePlan plan, int maxDeltas) {
        int transitionCounter = 0;

        while(currentVersion < desiredVersion && transitionCounter < maxDeltas) {
            currentVersion = includeNextDelta(plan, currentVersion, desiredVersion);
            transitionCounter++;
        }
        return currentVersion;
    };

    private long applyReverseDeltasToPlan(long currentVersion, long desiredVersion, HollowUpdatePlan plan, int maxDeltas) {
        long achievedVersion = currentVersion;
        int transitionCounter = 0;

        while (currentVersion > desiredVersion && transitionCounter < maxDeltas) {
            currentVersion = includeNextReverseDelta(plan, currentVersion);
            if (currentVersion != HollowConstants.VERSION_NONE)
                achievedVersion = currentVersion;
            transitionCounter++;
        }

        return achievedVersion;
    }

    /**
     * Includes the next delta only if it will not take us *after* the desired version
     */
    private long includeNextDelta(HollowUpdatePlan plan, long currentVersion, long desiredVersion) {
        HollowConsumer.Blob transition = transitionCreator.retrieveDeltaBlob(currentVersion);

        if(transition != null) {
            if(transition.getToVersion() <= desiredVersion) {
                plan.add(transition);
            }

            return transition.getToVersion();
        }

        return HollowConstants.VERSION_LATEST;
    }

    private long includeNextReverseDelta(HollowUpdatePlan plan, long currentVersion) {
        HollowConsumer.Blob transition = transitionCreator.retrieveReverseDeltaBlob(currentVersion);
        if(transition != null) {
            plan.add(transition);
            return transition.getToVersion();
        }

        return HollowConstants.VERSION_NONE;
    }

    private long includeNearestSnapshot(HollowUpdatePlan plan, long desiredVersion) {
        HollowConsumer.Blob transition = transitionCreator.retrieveSnapshotBlob(desiredVersion);
        if(transition != null) {
            plan.add(transition);
            return transition.getToVersion();
        }

        return HollowConstants.VERSION_LATEST;
    }

}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.
 *
 */
package com.netflix.hollow.core.index;

import com.netflix.hollow.core.memory.encoding.FixedLengthElementArray;
import com.netflix.hollow.core.memory.encoding.FixedLengthMultipleOccurrenceElementArray;
import com.netflix.hollow.core.memory.pool.ArraySegmentRecycler;
import com.netflix.hollow.core.memory.pool.WastefulRecycler;
import com.netflix.hollow.core.read.engine.HollowReadStateEngine;
import com.netflix.hollow.core.read.engine.HollowTypeStateListener;
import com.netflix.hollow.core.read.engine.object.HollowObjectTypeReadState;
import com.netflix.hollow.core.read.iterator.HollowOrdinalIterator;
import com.netflix.hollow.core.schema.HollowObjectSchema;
import java.util.ArrayDeque;
import java.util.BitSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Queue;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * This class builds a prefix index. A prefix index can be used to build applications like auto-complete, spell checker.
 */
public class HollowPrefixIndex implements HollowTypeStateListener {

    private final FieldPath fieldPath;
    private final HollowReadStateEngine readStateEngine;
    private final String type;
    private final int estimatedMaxStringDuplicates;

    private volatile TST prefixIndexVolatile;
    private ArraySegmentRecycler memoryRecycle;

    private int totalWords;
    private int averageWordLen;
    private int maxOrdinalOfType;

    private boolean buildIndexOnUpdate;

    /**
     * This constructor defaults the estimatedMaxStringDuplicates to 4. If you expect a large
     * number of duplicate strings across your type, you should provide your own estimate for
     * estimatedMaxStringDuplicates.
     *
     * @param readStateEngine the read state
     * @param type the type name
     * @param fieldPath the field path
     * @see #HollowPrefixIndex(HollowReadStateEngine, String, String)
     */
    @SuppressWarnings("WeakerAccess")
    public HollowPrefixIndex(HollowReadStateEngine readStateEngine, String type, String fieldPath) {
        // pass in a hardcoded estimate of 4 for now - in the future we could calculate this
        this(readStateEngine, type, fieldPath, 4);
    }

    /**
     * Initializes a new prefix index.
     *
     * @param readStateEngine              state engine to read data from
     * @param type                         type in the read state engine. Ordinals for this type
     *                                     will be returned when queried for a prefix.
     * @param fieldPath                    fieldPath should ultimately lead to a string field.
     *                                     The fields in the path could reference another Object,
     *                                     List, Set or a Map. The fields should be separated by ".".
     * @param estimatedMaxStringDuplicates The estimated number of strings that are duplicated
     *                                     across instances of your type. Note that this means an
     *                                     exactly matching string, not a prefix match. This
     *                                     parameter affects the efficiency of the index building.
     */
    @SuppressWarnings("WeakerAccess")
    public HollowPrefixIndex(HollowReadStateEngine readStateEngine, String type, String fieldPath,
            int estimatedMaxStringDuplicates) {

        if (readStateEngine == null) throw new IllegalArgumentException("Read state engine cannot be null");
        if (type == null) throw new IllegalArgumentException("type cannot be null");
        if (fieldPath == null || fieldPath.isEmpty())
            throw new IllegalArgumentException("fieldPath cannot be null or empty");
        if (estimatedMaxStringDuplicates < 1) {
            throw new IllegalArgumentException("estimatedMaxStringDuplicates cannot be < 1");
        }

        this.readStateEngine = readStateEngine;
        this.type = type;
        this.estimatedMaxStringDuplicates = estimatedMaxStringDuplicates;
        this.fieldPath = new FieldPath(readStateEngine, type, fieldPath);
        if (!this.fieldPath.getLastFieldType().equals(HollowObjectSchema.FieldType.STRING))
            throw new IllegalArgumentException("Field path should lead to a string type");

        // create memory recycle for using shared memory pools.
        memoryRecycle = WastefulRecycler.DEFAULT_INSTANCE;
        buildIndexOnUpdate = true;
        initialize();
    }

    // initialize field positions and field paths.
    private void initialize() {

        String lastRefType = this.fieldPath.getLastRefTypeInPath();

        // get all cardinality to estimate size of array bits needed.
        totalWords = readStateEngine.getTypeState(lastRefType).getPopulatedOrdinals().cardinality();
        averageWordLen = 0;
        double avg = 0;
        HollowObjectTypeReadState objectTypeReadState = (HollowObjectTypeReadState) readStateEngine.getTypeState(lastRefType);
        BitSet keyBitSet = objectTypeReadState.getPopulatedOrdinals();
        int ordinal = keyBitSet.nextSetBit(0);
        while (ordinal != -1) {
            avg += ((double) objectTypeReadState.readString(ordinal, 0).length()) / ((double) objectTypeReadState.maxOrdinal());
            ordinal = keyBitSet.nextSetBit(ordinal + 1);
        }
        averageWordLen = (int) Math.ceil(avg);

        HollowObjectTypeReadState valueState = (HollowObjectTypeReadState) readStateEngine.getTypeDataAccess(type);
        maxOrdinalOfType = valueState.maxOrdinal();

        // initialize the prefix index.
        build();
    }

    private void build() {

        if (!buildIndexOnUpdate) return;
        // tell memory recycler to use current tst's long arrays next time when long array is requested.
        // note reuse only happens once swap is called and bits are reset
        TST current = prefixIndexVolatile;
        if (current != null) current.recycleMemory(memoryRecycle);

        long estimatedNumberOfNodes = estimateNumNodes(totalWords, averageWordLen);
        TST tst = new TST(estimatedNumberOfNodes, estimatedMaxStringDuplicates, maxOrdinalOfType,
                memoryRecycle);
        BitSet ordinals = readStateEngine.getTypeState(type).getPopulatedOrdinals();
        int ordinal = ordinals.nextSetBit(0);
        while (ordinal != -1) {
            for (String key : getKeys(ordinal)) {
                tst.insert(key, ordinal);
            }
            ordinal = ordinals.nextSetBit(ordinal + 1);
        }

        prefixIndexVolatile = tst;
        // safe to return previous long arrays on next request for long array.
        memoryRecycle.swap();
        buildIndexOnUpdate = false;
    }

    /**
     * Estimates the total number of nodes that will required to create the index.
     * Override this method if lower/higher estimate is needed compared to the default implementation.
     *
     * @param totalWords the total number of words
     * @param averageWordLen the average word length
     * @return the estimated total number of nodes
     */
    @SuppressWarnings("WeakerAccess")
    protected long estimateNumNodes(long totalWords, long averageWordLen) {
        return totalWords * averageWordLen;
    }

    /**
     * Return the key to index in prefix index. Override this method to support tokens for the key. By default keys are indexed as lower case characters.
     *
{@code
     *     String[] keys = super.getKey(ordinal);
     *     String[] tokens = keys[0].split(" ")
     *     return tokens;
     * }

     *
     * @param ordinal ordinal of the parent type.
     * @return keys to index.
     */
    protected String[] getKeys(int ordinal) {
        Object[] values = fieldPath.findValues(ordinal);
        String[] stringValues = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            stringValues[i] = ((String) values[i]).toLowerCase();
        }
        return stringValues;
    }

    /**
     * Query the index to find all the ordinals that match the given prefix. Example -
     *
{@code
     *     HollowOrdinalIterator iterator = index.findKeysWithPrefix("a");
     *     int ordinal = iterator.next();
     *     while(ordinal != HollowOrdinalIterator.NO_MORE_ORDINAL) {
     *         // print the result using API
     *     }
     * }

     *

     * For larger data sets, querying smaller prefixes will be longer than querying for prefixes that are longer.
     *
     * @param prefix findKeysWithPrefix prefix.
     * @return An instance of HollowOrdinalIterator to iterate over ordinals that match the given findKeysWithPrefix.
     */
    @SuppressWarnings("WeakerAccess")
    public HollowOrdinalIterator findKeysWithPrefix(String prefix) {
        TST current;
        HollowOrdinalIterator it;
        do {
            current = prefixIndexVolatile;
            it = current.findKeysWithPrefix(prefix);
        } while (current != this.prefixIndexVolatile);
        return it;
    }

    /**
     * Check if the given key exists in the index.
     *
     * @param key the key
     * @return {@code true} if the key exists, otherwise {@code false}
     */
    public boolean contains(String key) {
        if (key == null) throw new IllegalArgumentException("key cannot be null");
        TST current;
        boolean result;
        do {
            current = prefixIndexVolatile;
            result = current.contains(key);
        } while (current != this.prefixIndexVolatile);
        return result;
    }

    /**
     * Use this method to keep the index updated with delta changes on the read state engine.
     * Remember to call detachFromDeltaUpdates to stop the delta changes.
     * NOTE: Each delta updates creates a new prefix index and swaps the new with current.
     */
    @SuppressWarnings("WeakerAccess")
    public void listenForDeltaUpdates() {
        readStateEngine.getTypeState(type).addListener(this);
    }

    /**
     * Stop delta updates for this index.
     */
    @SuppressWarnings("WeakerAccess")
    public void detachFromDeltaUpdates() {
        readStateEngine.getTypeState(type).removeListener(this);
    }

    @Override
    public void beginUpdate() {
        // before delta is applied -> no action to be taken
    }

    @Override
    public void addedOrdinal(int ordinal) {
        buildIndexOnUpdate = true;
    }

    @Override
    public void removedOrdinal(int ordinal) {
        buildIndexOnUpdate = true;
    }

    @Override
    public void endUpdate() {
        // pass 1 for delta support - rebuild the tree and swap the new tree with the one that is serving the queries.
        // next pass -  improve the index build time or add support for remove method.
        initialize();
    }

    private static class TST {

        private enum NodeType {
            Left, Right, Middle
        }

        // each node segment can be thought of as 16 bit key and bits to hold index of its children
        private int bitsPerNode;
        private int bitsPerKey;
        private int bitsForChildPointer;
        private int bitsPerOrdinal;

        // helper offsets
        private long leftChildOffset;
        private long middleChildOffset;
        private long rightChildOffset;
        private long isLeafNodeFlagOffset;

        private long maxNodes;
        private FixedLengthElementArray nodes;
        private FixedLengthMultipleOccurrenceElementArray ordinalSet;
        private long indexTracker;

        /**
         * Create new prefix index. Represents a ternary search tree.
         *
         * @param estimatedNumNodes estimate number of max nodes that will created.
         * @param estimatedMaxStringDuplicates estimated number string duplicates across all nodes
         * @param maxOrdinalValue  max ordinal that can be referenced
         * @param memoryRecycler   to reuse arrays from memory pool
         */
        private TST(long estimatedNumNodes, int estimatedMaxStringDuplicates, int maxOrdinalValue,
                ArraySegmentRecycler memoryRecycler) {

            // best guess
            maxNodes = estimatedNumNodes;

            // bits for pointers in a single node:
            bitsPerKey = 16;// key
            bitsForChildPointer = 64 - Long.numberOfLeadingZeros(maxNodes);// a child pointer
            bitsPerOrdinal = maxOrdinalValue == 0 ? 1 : 32 - Integer.numberOfLeadingZeros(maxOrdinalValue);

            // bits to represent one node
            bitsPerNode = bitsPerKey + (3 * bitsForChildPointer) + 1;

            nodes = new FixedLengthElementArray(memoryRecycler, bitsPerNode * maxNodes);
            ordinalSet = new FixedLengthMultipleOccurrenceElementArray(memoryRecycler,
                    maxNodes, bitsPerOrdinal, estimatedMaxStringDuplicates);
            indexTracker = 0;

            // initialize offsets
            leftChildOffset = bitsPerKey;// after first 16 bits in node is first left child offset.
            middleChildOffset = leftChildOffset + bitsForChildPointer;
            rightChildOffset = middleChildOffset + bitsForChildPointer;
            isLeafNodeFlagOffset = rightChildOffset + bitsForChildPointer;
        }

        // tell memory recycler to use these long array on next long array request from memory ONLY AFTER swap is called on memory recycler
        private void recycleMemory(ArraySegmentRecycler memoryRecycler) {
            nodes.destroy(memoryRecycler);
            ordinalSet.destroy();
        }

        private long getChildOffset(NodeType nodeType) {
            long offset;
            if (nodeType.equals(NodeType.Left)) offset = leftChildOffset;
            else if (nodeType.equals(NodeType.Middle)) offset = middleChildOffset;
            else offset = rightChildOffset;
            return offset;
        }

        private long getChildIndex(long currentNode, NodeType nodeType) {
            long offset = getChildOffset(nodeType);
            return nodes.getElementValue((currentNode * bitsPerNode) + offset, bitsForChildPointer);
        }

        private void setChildIndex(long currentNode, NodeType nodeType, long indexForNode) {
            long offset = getChildOffset(nodeType);
            nodes.setElementValue((currentNode * bitsPerNode) + offset, bitsForChildPointer, indexForNode);
        }

        private void setKey(long index, char ch) {
            nodes.setElementValue(index * bitsPerNode, bitsPerKey, ch);
        }

        private long getKey(long nodeIndex) {
            return nodes.getElementValue(nodeIndex * bitsPerNode, bitsPerKey);
        }

        private boolean isLeafNode(long nodeIndex) {
            return nodes.getElementValue((nodeIndex * bitsPerNode) + isLeafNodeFlagOffset, 1) == 1;
        }

        private void addOrdinal(long nodeIndex, long ordinal) {
            ordinalSet.addElement(nodeIndex, ordinal);
            nodes.setElementValue((nodeIndex * bitsPerNode) + isLeafNodeFlagOffset, 1, 1);
        }

        private Set<Integer> getOrdinals(long nodeIndex) {
            return ordinalSet.getElements(nodeIndex).stream()
                    .map(Long::intValue).collect(Collectors.toSet());
        }

        /**
         * Insert into ternary search tree for the given key and ordinal.
         */
        private void insert(String key, int ordinal) {
            if (key == null) throw new IllegalArgumentException("Null key cannot be indexed");
            long currentNodeIndex = 0;
            int keyIndex = 0;

            while (keyIndex < key.length()) {

                char ch = key.charAt(keyIndex);
                if (getKey(currentNodeIndex) == 0) {
                    setKey(currentNodeIndex, ch);
                    indexTracker++;
                    if (indexTracker >= maxNodes)
                        throw new IllegalStateException("Index Tracker reached max capacity. Try with larger estimate of number of nodes");
                }

                long keyAtCurrentNode = getKey(currentNodeIndex);
                if (ch < keyAtCurrentNode) {
                    long leftIndex = getChildIndex(currentNodeIndex, NodeType.Left);
                    if (leftIndex == 0) leftIndex = indexTracker;
                    setChildIndex(currentNodeIndex, NodeType.Left, leftIndex);
                    currentNodeIndex = leftIndex;
                } else if (ch > keyAtCurrentNode) {
                    long rightIndex = getChildIndex(currentNodeIndex, NodeType.Right);
                    if (rightIndex == 0) rightIndex = indexTracker;
                    setChildIndex(currentNodeIndex, NodeType.Right, rightIndex);
                    currentNodeIndex = rightIndex;
                } else {
                    keyIndex++;
                    if (keyIndex < key.length()) {
                        long midIndex = getChildIndex(currentNodeIndex, NodeType.Middle);
                        if (midIndex == 0) midIndex = indexTracker;
                        setChildIndex(currentNodeIndex, NodeType.Middle, midIndex);
                        currentNodeIndex = midIndex;
                    }
                }
            }
            addOrdinal(currentNodeIndex, ordinal);
        }

        /**
         * This functions checks if the given key exists in the trie.
         *
         * @return index of the node that findNodeWithKey the last character of the key, if not found then returns -1.
         */
        private long findNodeWithKey(String key) {
            long index = -1;

            boolean atRoot = true;
            long currentNodeIndex = 0;
            int keyIndex = 0;

            while (true) {
                if (currentNodeIndex == 0 && !atRoot) break;
                long currentValue = getKey(currentNodeIndex);
                char ch = key.charAt(keyIndex);
                if (ch < currentValue) currentNodeIndex = getChildIndex(currentNodeIndex, NodeType.Left);
                else if (ch > currentValue) currentNodeIndex = getChildIndex(currentNodeIndex, NodeType.Right);
                else {
                    if (keyIndex == (key.length() - 1)) {
                        index = currentNodeIndex;
                        break;
                    }
                    currentNodeIndex = getChildIndex(currentNodeIndex, NodeType.Middle);
                    keyIndex++;
                }
                if (atRoot) atRoot = false;
            }
            return index;
        }

        private boolean contains(String key) {
            long nodeIndex = findNodeWithKey(key);
            return nodeIndex >= 0 && isLeafNode(nodeIndex);
        }

        /**
         * Find all the ordinals that match the given prefix.
         */
        private HollowOrdinalIterator findKeysWithPrefix(String prefix) {
            if (prefix == null) throw new IllegalArgumentException("Cannot findKeysWithPrefix null prefix");
            final Set<Integer> ordinals = new HashSet<>();
            long currentNodeIndex = findNodeWithKey(prefix.toLowerCase());

            if (currentNodeIndex >= 0) {

                if (isLeafNode(currentNodeIndex))
                    ordinals.addAll(getOrdinals(currentNodeIndex));

                // go to all leaf nodes from current node mid pointer
                long subTree = getChildIndex(currentNodeIndex, NodeType.Middle);
                if (subTree != 0) {
                    Queue<Long> queue = new ArrayDeque<>();
                    queue.add(subTree);
                    while (!queue.isEmpty()) {
                        long nodeIndex = queue.remove();
                        long left = getChildIndex(nodeIndex, NodeType.Left);
                        long mid = getChildIndex(nodeIndex, NodeType.Middle);
                        long right = getChildIndex(nodeIndex, NodeType.Right);

                        if (left == 0 && mid == 0 && right == 0) {
                            if (isLeafNode(nodeIndex)) ordinals.addAll(getOrdinals(nodeIndex));
                        }
                        if (left != 0) queue.add(left);
                        if (mid != 0) queue.add(mid);
                        if (right != 0) queue.add(right);
                    }
                }
            }

            return new HollowOrdinalIterator() {
                private Iterator<Integer> it = ordinals.iterator();

                @Override
                public int next() {
                    if (it.hasNext()) return it.next();
                    return NO_MORE_ORDINALS;
                }
            };
        }
    }
}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.
 *
 */
package com.netflix.hollow.api.producer.validation;

import com.netflix.hollow.api.producer.HollowProducer.ReadState;
import com.netflix.hollow.core.read.engine.HollowTypeReadState;

/**
 * Used to validate if the cardinality change in current cycle is with in the allowed percent for a given typeName.
 * Ex: 0% allowableVariancePercent ensures type cardinality does not vary at all for cycle to cycle.
 * Ex: Number of state in United States.
 * 10% allowableVariancePercent: from previous cycle any addition or removal within 10% cardinality is valid.
 * Anything more results in failure of validation.
 *
 * @author lkanchanapalli {@literal<lavanya65@yahoo.com>}
 */
public class RecordCountVarianceValidator implements ValidatorListener {
    private static final String ZERO_PREVIOUS_COUNT_WARN_MSG_FORMAT =
            "Previous record count is 0. Not running RecordCountVarianceValidator for type %s. "
                    + "This scenario is not expected except when starting a new namespace.";
    private static final String FAILED_RECORD_COUNT_VALIDATION =
            "Record count validation for type %s has failed as actual change percent %s "
                    + "is greater than allowed change percent %s.";

    private static final String DATA_TYPE_NAME = "Typename";
    private static final String ALLOWABLE_VARIANCE_PERCENT_NAME = "AllowableVariancePercent";
    private static final String LATEST_CARDINALITY_NAME = "LatestRecordCount";
    private static final String PREVIOUS_CARDINALITY_NAME = "PreviousRecordCount";
    private static final String ACTUAL_CHANGE_PERCENT_NAME = "ActualChangePercent";

    private static final String NAME = RecordCountVarianceValidator.class.getName();

    private final String typeName;

    private final float allowableVariancePercent;

    /**
     * @param typeName type name
     * @param allowableVariancePercent: Used to validate if the cardinality change in current cycle is with in the
     * allowed percent.
     * Ex: 0% allowableVariancePercent ensures type cardinality does not vary at all for cycle to cycle.
     * Ex: Number of state in United States.
     * 10% allowableVariancePercent: from previous cycle any addition or removal within 10% cardinality is valid.
     * Anything more results in failure of validation.
     */
    public RecordCountVarianceValidator(String typeName, float allowableVariancePercent) {
        this.typeName = typeName;
        if (allowableVariancePercent < 0) {
            throw new IllegalArgumentException("RecordCountVarianceValidator for type " + typeName
                    + ": cannot have allowableVariancePercent less than 0. Value provided: "
                    + allowableVariancePercent);
        }
        this.allowableVariancePercent = allowableVariancePercent;
    }

    @Override
    public String getName() {
        return NAME + "_" + typeName;
    }

    @Override
    public ValidationResult onValidate(ReadState readState) {
        ValidationResult.ValidationResultBuilder vrb = ValidationResult.from(this);
        vrb.detail(ALLOWABLE_VARIANCE_PERCENT_NAME, allowableVariancePercent)
                .detail(DATA_TYPE_NAME, typeName);

        HollowTypeReadState typeState = readState.getStateEngine().getTypeState(typeName);
        int latestCardinality = typeState.getPopulatedOrdinals().cardinality();
        int previousCardinality = typeState.getPreviousOrdinals().cardinality();
        vrb.detail(LATEST_CARDINALITY_NAME, latestCardinality)
                .detail(PREVIOUS_CARDINALITY_NAME, previousCardinality);

        if (previousCardinality == 0) {
            return vrb.detail("skipped", Boolean.TRUE).
                    passed(String.format(ZERO_PREVIOUS_COUNT_WARN_MSG_FORMAT, typeName));
        }

        float actualChangePercent = getChangePercent(latestCardinality, previousCardinality);
        vrb.detail(ACTUAL_CHANGE_PERCENT_NAME, actualChangePercent);

        if (Float.compare(actualChangePercent, allowableVariancePercent) > 0) {
            String message = String.format(FAILED_RECORD_COUNT_VALIDATION, typeName, actualChangePercent,
                    allowableVariancePercent);
            return vrb.failed(message);
        }

        return vrb.passed();
    }

    // protected for tests
    float getChangePercent(int latestCardinality, int previousCardinality) {
        int diff = Math.abs(latestCardinality - previousCardinality);
        return (100.0f * diff) / previousCardinality;
    }
}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.
 *
 */
package com.netflix.hollow.core.read.filter;

import com.netflix.hollow.api.consumer.HollowConsumer;
import com.netflix.hollow.core.read.engine.HollowBlobReader;
import com.netflix.hollow.core.read.engine.HollowReadStateEngine;
import com.netflix.hollow.core.schema.HollowCollectionSchema;
import com.netflix.hollow.core.schema.HollowMapSchema;
import com.netflix.hollow.core.schema.HollowObjectSchema;
import com.netflix.hollow.core.schema.HollowObjectSchema.FieldType;
import com.netflix.hollow.core.schema.HollowSchema;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * A HollowFilterConfig specifies a subset of the available fields in a data model.  It can be specified
 * as either an exclude filter, or an include filter.
 *

 * An exclude filter specifies fields which are excluded from the subset.  With an exclude filter,
 * all fields are included by default and only the identified field are excluded.
 *

 * An include filter (the default) specifies fields which are included in the subset.  With an include filter,
 * all fields are excluded by default and only the identified fields are included.
 *

 * A HollowFilterConfig can be used to reduce the heap footprint on consumers, either with
 * {@link HollowConsumer.Builder#withFilterConfig(HollowFilterConfig)} if using a {@link HollowConsumer} or with
 * {@link HollowBlobReader#readSnapshot(java.io.InputStream, HollowFilterConfig)} if using a {@link HollowReadStateEngine}
 * directly.
 *

 * Note that when using this to configure a filter for a consumer, only the snapshot must be filtered.  Subsequent
 * deltas will automatically use the same filter.
 *
 */
public class HollowFilterConfig {

    private final ObjectFilterConfig INCLUDE_ALL = new ObjectFilterConfig(Boolean.TRUE);
    private final ObjectFilterConfig INCLUDE_NONE = new ObjectFilterConfig(Boolean.FALSE);

    private final boolean isExcludeFilter;
    private final Set<String> specifiedTypes;
    private final Map<String, ObjectFilterConfig> specifiedFieldConfigs;

    /**
     * Create a new include filter.
     */
    public HollowFilterConfig() {
        this(false);
    }

    /**
     * Create a new filter
     * @param isExcludeFilter true for an exclude filter, false for an include filter.
     */
    public HollowFilterConfig(boolean isExcludeFilter) {
        this.isExcludeFilter = isExcludeFilter;
        this.specifiedTypes = new HashSet<String>();
        this.specifiedFieldConfigs = new HashMap<String, ObjectFilterConfig>();
    }

    /**
     * Add a type.  All fields in the type will be either excluded or included, depending on
     * whether this is an exclude or include filter, respectively.
     *
     * @param type the type name
     */
    public void addType(String type) {
        specifiedTypes.add(type);
    }

    /**
     * Add a type, plus recursively add any directly or transitively referenced types.
     *
     * All fields in these types will be either excluded or included, depending on whether
     * this is an exclude or include filter, respectively.
     *
     * @param type A type from the data model.
     * @param schemas All schemas from the data model.
     */
    public void addTypeRecursive(String type, Collection<HollowSchema> schemas) {
        addTypeRecursive(type, mapSchemas(schemas));
    }

    /**
     * Add a type, plus recursively add any directly or transitively referenced types.
     *
     * All fields in these types will be either excluded or included, depending on whether
     * this is an exclude or include filter, respectively.
     *
     *
     * @param type A type from the data model.
     * @param schemas A map of typeName to schema including all schemas for this data model.
     */
    public void addTypeRecursive(String type, Map<String, HollowSchema> schemas) {
        addType(type);
        HollowSchema schema = schemas.get(type);
        switch(schema.getSchemaType()) {
        case OBJECT:
            HollowObjectSchema objSchema = (HollowObjectSchema)schema;
            for(int i=0;i<objSchema.numFields();i++) {
                if(objSchema.getFieldType(i) == FieldType.REFERENCE)
                    addTypeRecursive(objSchema.getReferencedType(i), schemas);
            }
            break;
        case MAP:
            addTypeRecursive(((HollowMapSchema)schema).getKeyType(), schemas);
            addTypeRecursive(((HollowMapSchema)schema).getValueType(), schemas);
            break;
        case LIST:
        case SET:
            addTypeRecursive(((HollowCollectionSchema)schema).getElementType(), schemas);
            break;
        }
    }

    /**
     * Add an individual field from an OBJECT schema.  This field will be either
     * excluded or included, depending on whether this is an exclude or include filter, respectively.
     *
     * @param type The OBJECT type from the data model
     * @param objectField The field in the specified type to either include or exclude.
     */
    public void addField(String type, String objectField) {
        ObjectFilterConfig typeConfig = specifiedFieldConfigs.get(type);
        if(typeConfig == null) {
            typeConfig = new ObjectFilterConfig();
            specifiedFieldConfigs.put(type, typeConfig);
        }
        typeConfig.addField(objectField);
    }

    /**
     * Add an individual field from an OBJECT schema, plus recursively add any directly or transitively referenced types.
     * This field will be either excluded or included, depending on whether this is an exclude or include filter, respectively.
     *
     * @param type The OBJECT type from the data model
     * @param objectField The field in the specified type to either include or exclude.
     * @param schemas All schemas from the data model.
     */
    public void addFieldRecursive(String type, String objectField, Collection<HollowSchema> schemas) {
        addFieldRecursive(type, objectField, mapSchemas(schemas));
    }

    /**
     * Add an individual field from an OBJECT schema, plus recursively add any directly or transitively referenced types.
     * This field will be either excluded or included, depending on whether this is an exclude or include filter, respectively.
     *
     * @param type The OBJECT type from the data model
     * @param objectField The field in the specified type to either include or exclude.
     * @param schemas A map of typeName to schema including all schemas for this data model.
     */
    public void addFieldRecursive(String type, String objectField, Map<String, HollowSchema> schemas) {
        addField(type, objectField);
        HollowObjectSchema schema = (HollowObjectSchema)schemas.get(type);
        if(schema.getFieldType(objectField) == FieldType.REFERENCE) {
            addTypeRecursive(schema.getReferencedType(objectField), schemas);
        }
    }

    /**
     * @param type A type from the data model
     * @return whether or not this filter includes the specified type.
     */
    public boolean doesIncludeType(String type) {
        if(isExcludeFilter)
            return !specifiedTypes.contains(type);

        return specifiedTypes.contains(type) || specifiedFieldConfigs.containsKey(type);
    }

    /**
     * @return true if this is an exclude filter.  False otherwise.
     */
    public boolean isExcludeFilter() {
        return isExcludeFilter;
    }

    public int numSpecifiedTypes() {
        return specifiedTypes.size();
    }

    public Set<String> getSpecifiedTypes() {
        return specifiedTypes;
    }

    public ObjectFilterConfig getObjectTypeConfig(String type) {
        ObjectFilterConfig typeConfig = specifiedFieldConfigs.get(type);
        if(typeConfig != null)
            return typeConfig;

        if(isExcludeFilter) {
            if(specifiedTypes.contains(type))
                return INCLUDE_NONE;
            return INCLUDE_ALL;
        } else {
            if(specifiedTypes.contains(type))
                return INCLUDE_ALL;
            return INCLUDE_NONE;
        }
    }

    public class ObjectFilterConfig {
        private final Boolean alwaysAnswer;
        private final Set<String> specifiedFields;

        public ObjectFilterConfig() {
            this(null);
        }

        public ObjectFilterConfig(Boolean alwaysAnswer) {
            this.specifiedFields = new HashSet<String>();
            this.alwaysAnswer = alwaysAnswer;
        }

        private void addField(String fieldName) {
            specifiedFields.add(fieldName);
        }

        public boolean includesField(String field) {
            if(alwaysAnswer != null)
                return alwaysAnswer.booleanValue();

            if(isExcludeFilter)
                return !specifiedFields.contains(field);
            return specifiedFields.contains(field);
        }

        public int numIncludedFields() {
            return specifiedFields.size();
        }
    }

    /**
     * Write this HollowFilterConfig to a human-readable, and parseable String.
     *
     * This can be used to serialize a configuration.  The returned String can be used to
     * recreate the {@link HollowFilterConfig} using {@link HollowFilterConfig#fromString(String)}
     */
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append(isExcludeFilter ? "EXCLUDE" : "INCLUDE");

        for(String type : specifiedTypes) {
            builder.append('\n').append(type);
        }

        for(Map.Entry<String, ObjectFilterConfig> entry : specifiedFieldConfigs.entrySet()) {
            String typeName = entry.getKey();
            ObjectFilterConfig typeConfig = entry.getValue();
            if(typeConfig.specifiedFields.isEmpty()) {
                builder.append('\n').append(typeName);
            } else {
                for(String field : typeConfig.specifiedFields) {
                    builder.append('\n').append(typeName).append('.').append(field);
                }
            }
        }

        return builder.toString();
    }

    /**
     * Parse a HollowFilterConfig from the specified String.  The String should contain multiple lines.
     * The first line should be either "EXCLUDE" or "INCLUDE".  Subsequent lines should be one of the following:
     *

     *
<typeName>

     *
<typeName>.<fieldName>

     *

     *
     * @param conf the configuration as a string
     * @return the filter configuration
     */
    public static HollowFilterConfig fromString(String conf) {
        String lines[] = conf.split("\n");

        HollowFilterConfig config = new HollowFilterConfig("EXCLUDE".equals(lines[0]));

        for(int i=1;i<lines.length;i++) {
            int delimiterIdx = lines[i].indexOf('.');
            if(delimiterIdx == -1) {
                config.addType(lines[i]);
            } else {
                String type = lines[i].substring(0, delimiterIdx);
                String field = lines[i].substring(delimiterIdx+1);
                config.addField(type, field);
            }
        }

        return config;
    }

    private Map<String, HollowSchema> mapSchemas(Collection<HollowSchema> schemas) {
        Map<String, HollowSchema> schemaMap = new HashMap<String, HollowSchema>();
        for(HollowSchema schema : schemas) {
            schemaMap.put(schema.getName(), schema);
        }
        return schemaMap;
    }

}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.
 *
 */
package com.netflix.hollow.core.read.engine;

import com.netflix.hollow.api.sampling.HollowSampler;
import com.netflix.hollow.core.memory.encoding.GapEncodedVariableLengthIntegerReader;
import com.netflix.hollow.core.memory.pool.ArraySegmentRecycler;
import com.netflix.hollow.core.read.dataaccess.HollowDataAccess;
import com.netflix.hollow.core.read.dataaccess.HollowTypeDataAccess;
import com.netflix.hollow.core.schema.HollowSchema;
import com.netflix.hollow.tools.checksum.HollowChecksum;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.BitSet;
import java.util.stream.Stream;

/**
 * A HollowTypeReadState contains and is the root handle to all of the records of a specific type in
 * a {@link HollowReadStateEngine}.
 */
public abstract class HollowTypeReadState implements HollowTypeDataAccess {

    protected static final HollowTypeStateListener[] EMPTY_LISTENERS = new HollowTypeStateListener[0];

    protected final HollowReadStateEngine stateEngine;
    protected final HollowSchema schema;
    protected HollowTypeStateListener[] stateListeners;

    public HollowTypeReadState(HollowReadStateEngine stateEngine, HollowSchema schema) {
        this.stateEngine = stateEngine;
        this.schema = schema;
        this.stateListeners = EMPTY_LISTENERS;
    }

    /**
     * Add a {@link HollowTypeStateListener} to this type.
     * @param listener the listener to add
     */
    public void addListener(HollowTypeStateListener listener) {
        HollowTypeStateListener[] newListeners = Arrays.copyOf(stateListeners, stateListeners.length + 1);
        newListeners[newListeners.length - 1] = listener;
        stateListeners = newListeners;
    }

    /**
     * Remove a specific {@link HollowTypeStateListener} from this type.
     * @param listener the listener to remove
     */
    public void removeListener(HollowTypeStateListener listener) {
        if (stateListeners.length == 0)
            return;

        stateListeners = Stream.of(stateListeners)
                .filter(l -> l != listener)
                .toArray(HollowTypeStateListener[]::new);
    }

    /**
     * @return all {@link HollowTypeStateListener}s currently associated with this type.
     */
    public HollowTypeStateListener[] getListeners() {
        return stateListeners;
    }

    /**
     * @param listenerClazz the listener class
     * @return a {@link HollowTypeStateListener} of the specified class currently associated with this type, or
     * null if none is currently attached.
     * @param <T> the type of the listener
     */
    @SuppressWarnings("unchecked")
    public <T extends HollowTypeStateListener> T getListener(Class<T> listenerClazz) {
        for (HollowTypeStateListener listener : stateListeners) {
            if (listenerClazz.isAssignableFrom(listener.getClass())) {
                return (T) listener;
            }
        }
        return null;
    }

    /**
     * Returns the BitSet containing the currently populated ordinals in this type state.
     *

     * WARNING: Do not modify the returned BitSet.
     * @return the bit containing the currently populated ordinals
     */
    public BitSet getPopulatedOrdinals() {
        return getListener(PopulatedOrdinalListener.class).getPopulatedOrdinals();
    }

    /**
     * Returns the BitSet containing the populated ordinals in this type state prior to the previous delta transition.
     *

     * WARNING: Do not modify the returned BitSet.
     * @return the bit containing the previously populated ordinals
     */
    public BitSet getPreviousOrdinals() {
        return getListener(PopulatedOrdinalListener.class).getPreviousOrdinals();
    }

    /**
     * @return The maximum ordinal currently populated in this type state.
     */
    public abstract int maxOrdinal();

    public abstract void readSnapshot(DataInputStream dis, ArraySegmentRecycler recycler) throws IOException;
    public abstract void applyDelta(DataInputStream dis, HollowSchema schema, ArraySegmentRecycler memoryRecycler) throws IOException;

    public HollowSchema getSchema() {
        return schema;
    }

    @Override
    public HollowDataAccess getDataAccess() {
        return stateEngine;
    }

    /**
     * @return the {@link HollowReadStateEngine} which this type state belongs to.
     */
    public HollowReadStateEngine getStateEngine() {
        return stateEngine;
    }

    protected void notifyListenerAboutDeltaChanges(GapEncodedVariableLengthIntegerReader removals, GapEncodedVariableLengthIntegerReader additions, int shardNumber, int numShards) {
        for(HollowTypeStateListener stateListener : stateListeners) {
            removals.reset();
            int removedOrdinal = removals.nextElement();
            while(removedOrdinal < Integer.MAX_VALUE) {
                stateListener.removedOrdinal((removedOrdinal * numShards) + shardNumber);
                removals.advance();
                removedOrdinal = removals.nextElement();
            }

            additions.reset();
            int addedOrdinal = additions.nextElement();
            while(addedOrdinal < Integer.MAX_VALUE) {
                stateListener.addedOrdinal((addedOrdinal * numShards) + shardNumber);
                additions.advance();
                addedOrdinal = additions.nextElement();
            }
        }
    }

    public abstract HollowSampler getSampler();

    protected abstract void invalidate();

    public HollowChecksum getChecksum(HollowSchema withSchema) {
        HollowChecksum cksum = new HollowChecksum();
        applyToChecksum(cksum, withSchema);
        return cksum;
    }

    protected abstract void applyToChecksum(HollowChecksum checksum, HollowSchema withSchema);

    @Override
    public HollowTypeReadState getTypeState() {
        return this;
    }

    /**
     * @return an approximate accounting of the current heap footprint occupied by this type state.
     */
    public abstract long getApproximateHeapFootprintInBytes();

    /**
     * @return an approximate accounting of the current cost of the "ordinal holes" in this type state.
     */
    public abstract long getApproximateHoleCostInBytes();

    /**
     * @return The number of shards into which this type is split.  Sharding is transparent, so this has no effect on normal usage.
     */
    public abstract int numShards();

}


/*
 *  Copyright 2016-2019 Netflix, Inc.
 *
 *     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.
 *
 */
package com.netflix.hollow.core.schema;

import com.netflix.hollow.core.HollowDataset;
import com.netflix.hollow.core.HollowStateEngine;
import com.netflix.hollow.core.schema.HollowObjectSchema.FieldType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class HollowSchemaSorter {

    /**
     * Dependency types come before dependent types
     *
     * @param dataset the data set
     * @return the dependent schema
     */
    public static List<HollowSchema> dependencyOrderedSchemaList(HollowDataset dataset) {
        return dependencyOrderedSchemaList(dataset.getSchemas());
    }

    /**
     * Dependency types come before dependent types
     *
     * @param schemas the schema
     * @return the dependent schema
     */
    public static List<HollowSchema> dependencyOrderedSchemaList(Collection<HollowSchema> schemas) {
        DependencyIndex idx = new DependencyIndex();
        Map<String, HollowSchema> schemaMap = new HashMap<String, HollowSchema>();
        for(HollowSchema schema : schemas) {
            schemaMap.put(schema.getName(), schema);
            idx.indexSchema(schema, schemas);
        }

        List<HollowSchema> orderedSchemas = new ArrayList<HollowSchema>();

        while(idx.hasMoreTypes())
            orderedSchemas.add(schemaMap.get(idx.getNextType()));

        return orderedSchemas;
    }

    private static class DependencyIndex {
        private final Map<String, Set<String>> dependencyIndex;
        private final Map<String, Set<String>> reverseDependencyIndex;

        public DependencyIndex() {
            this.dependencyIndex = new HashMap<String, Set<String>>();
            this.reverseDependencyIndex = new HashMap<String, Set<String>>();
        }

        public boolean hasMoreTypes() {
            for(Map.Entry<String, Set<String>> entry : dependencyIndex.entrySet()) {
                if(entry.getValue().isEmpty()) {
                    return true;
                }
            }

            return false;
        }

        public String getNextType() {
            List<String> availableTypes = new ArrayList<String>();
            for(Map.Entry<String, Set<String>> entry : dependencyIndex.entrySet()) {
                if(entry.getValue().isEmpty()) {
                    availableTypes.add(entry.getKey());
                }
            }

            String firstAvailableType = availableTypes.get(0);

            for(int i=1;i<availableTypes.size();i++) {
                if(availableTypes.get(i).compareTo(firstAvailableType) < 0)
                    firstAvailableType = availableTypes.get(i);
            }

            removeType(firstAvailableType);
            return firstAvailableType;
        }

        private void indexSchema(HollowSchema schema, Collection<HollowSchema> allSchemas) {
            if(schema instanceof HollowCollectionSchema) {
                String elementType = ((HollowCollectionSchema) schema).getElementType();
                addDependency(schema.getName(), elementType, allSchemas);
            } else if(schema instanceof HollowMapSchema) {
                String keyType = ((HollowMapSchema)schema).getKeyType();
                String valueType = ((HollowMapSchema)schema).getValueType();

                addDependency(schema.getName(), keyType, allSchemas);
                addDependency(schema.getName(), valueType, allSchemas);
            } else if(schema instanceof HollowObjectSchema) {
                HollowObjectSchema objectSchema = (HollowObjectSchema) schema;
                for(int i=0;i<objectSchema.numFields();i++) {
                    if(objectSchema.getFieldType(i) == FieldType.REFERENCE) {
                        String refType = objectSchema.getReferencedType(i);
                        addDependency(schema.getName(), refType, allSchemas);
                    }
                }
            }

            getList(schema.getName(), dependencyIndex);
            getList(schema.getName(), reverseDependencyIndex);
        }

        private void removeType(String type) {
            Set<String> dependents = reverseDependencyIndex.remove(type);

            for(String dependent : dependents)
                dependencyIndex.get(dependent).remove(type);

            dependencyIndex.remove(type);
        }

        private void addDependency(String dependent, String dependency, Collection<HollowSchema> allSchemas) {
            if(schemaExists(dependency, allSchemas)) {
                getList(dependent, dependencyIndex).add(dependency);
                getList(dependency, reverseDependencyIndex).add(dependent);
            }
        }

        private boolean schemaExists(String schemaName, Collection<HollowSchema> allSchemas) {
            for(HollowSchema schema : allSchemas) {
                if(schema.getName().equals(schemaName))
                    return true;
            }
            return false;
        }

        private Set<String> getList(String key, Map<String, Set<String>> dependencyIndex2) {
            Set<String> list = dependencyIndex2.get(key);
            if(list == null) {
                list = new HashSet<String>();
                dependencyIndex2.put(key, list);
            }
            return list;
        }
    }

    /**
     * @param stateEngine the state engine
     * @param dependentType the dependent type name
     * @param dependencyType the dependency type name
     * @return Whether or not the dependencyType is equal to, referenced by, or transitively referenced by the dependentType.
     */
    public static boolean typeIsTransitivelyDependent(HollowStateEngine stateEngine, String dependentType, String dependencyType) {
        if(dependentType.equals(dependencyType))
            return true;

        HollowSchema dependentTypeSchema = stateEngine.getSchema(dependentType);

        if(dependentTypeSchema == null)
            return false;

        switch(dependentTypeSchema.getSchemaType()) {
        case OBJECT:
            HollowObjectSchema objectSchema = (HollowObjectSchema)dependentTypeSchema;

            for(int i=0;i<objectSchema.numFields();i++) {
                if(objectSchema.getFieldType(i) == FieldType.REFERENCE) {
                    if(typeIsTransitivelyDependent(stateEngine, objectSchema.getReferencedType(i), dependencyType))
                        return true;
                }
            }

            break;
        case LIST:
        case SET:
            return typeIsTransitivelyDependent(stateEngine, ((HollowCollectionSchema)dependentTypeSchema).getElementType(), dependencyType);
        case MAP:
            return typeIsTransitivelyDependent(stateEngine, ((HollowMapSchema)dependentTypeSchema).getKeyType(), dependencyType)
                    || typeIsTransitivelyDependent(stateEngine, ((HollowMapSchema)dependentTypeSchema).getValueType(), dependencyType);
        }

        return false;
    }

}