// 
// Decompiled by Procyon v0.6.0
// 

package com.google.common.flogger.context;

import java.util.NoSuchElementException;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.AbstractMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import java.util.Set;
import java.util.Map;
import com.google.common.flogger.util.Checks;
import java.util.Comparator;

public final class Tags
{
    private static final Comparator<Object> VALUE_COMPARATOR;
    private static final Comparator<KeyValuePair> KEY_VALUE_COMPARATOR;
    private static final Tags EMPTY_TAGS;
    private final LightweightTagMap map;
    
    public static Builder builder() {
        return new Builder();
    }
    
    public static Tags empty() {
        return Tags.EMPTY_TAGS;
    }
    
    public static Tags of(final String name, final String value) {
        return new Tags(name, value);
    }
    
    public static Tags of(final String name, final boolean value) {
        return new Tags(name, value);
    }
    
    public static Tags of(final String name, final long value) {
        return new Tags(name, value);
    }
    
    public static Tags of(final String name, final double value) {
        return new Tags(name, value);
    }
    
    private Tags(final String name, final Object value) {
        this(new LightweightTagMap(Checks.checkMetadataIdentifier(name), Checks.checkNotNull(value, "value")));
    }
    
    private Tags(final LightweightTagMap map) {
        this.map = map;
    }
    
    public Map<String, Set<Object>> asMap() {
        return this.map;
    }
    
    public boolean isEmpty() {
        return this.map.isEmpty();
    }
    
    public Tags merge(final Tags other) {
        if (other.isEmpty()) {
            return this;
        }
        if (this.isEmpty()) {
            return other;
        }
        return new Tags(new LightweightTagMap(this.map, other.map));
    }
    
    @Override
    public boolean equals(@NullableDecl final Object obj) {
        return obj instanceof Tags && ((Tags)obj).map.equals(this.map);
    }
    
    @Override
    public int hashCode() {
        return ~this.map.hashCode();
    }
    
    @Override
    public String toString() {
        return this.map.toString();
    }
    
    static {
        VALUE_COMPARATOR = new Comparator<Object>() {
            @Override
            public int compare(final Object lhs, final Object rhs) {
                final Type ltype = of(lhs);
                final Type rtype = of(rhs);
                return (ltype == rtype) ? ltype.compare(lhs, rhs) : ltype.compareTo(rtype);
            }
        };
        KEY_VALUE_COMPARATOR = new Comparator<KeyValuePair>() {
            @Override
            public int compare(final KeyValuePair lhs, final KeyValuePair rhs) {
                int signum = lhs.key.compareTo(rhs.key);
                if (signum == 0) {
                    if (lhs.value != null) {
                        signum = ((rhs.value != null) ? Tags.VALUE_COMPARATOR.compare(lhs.value, rhs.value) : 1);
                    }
                    else {
                        signum = ((rhs.value != null) ? -1 : 0);
                    }
                }
                return signum;
            }
        };
        EMPTY_TAGS = new Tags(new LightweightTagMap(Collections.emptyList()));
    }
    
    private enum Type
    {
        BOOLEAN {
            @Override
            int compare(final Object lhs, final Object rhs) {
                return ((Boolean)lhs).compareTo((Boolean)rhs);
            }
        }, 
        STRING {
            @Override
            int compare(final Object lhs, final Object rhs) {
                return ((String)lhs).compareTo((String)rhs);
            }
        }, 
        LONG {
            @Override
            int compare(final Object lhs, final Object rhs) {
                return ((Long)lhs).compareTo((Long)rhs);
            }
        }, 
        DOUBLE {
            @Override
            int compare(final Object lhs, final Object rhs) {
                return ((Double)lhs).compareTo((Double)rhs);
            }
        };
        
        abstract int compare(final Object p0, final Object p1);
        
        private static Type of(final Object tag) {
            if (tag instanceof String) {
                return Type.STRING;
            }
            if (tag instanceof Boolean) {
                return Type.BOOLEAN;
            }
            if (tag instanceof Long) {
                return Type.LONG;
            }
            if (tag instanceof Double) {
                return Type.DOUBLE;
            }
            throw new AssertionError((Object)("invalid tag type: " + tag.getClass()));
        }
    }
    
    private static final class KeyValuePair
    {
        private final String key;
        @NullableDecl
        private final Object value;
        
        private KeyValuePair(final String key, @NullableDecl final Object value) {
            this.key = key;
            this.value = value;
        }
    }
    
    public static final class Builder
    {
        private final List<KeyValuePair> keyValuePairs;
        
        public Builder() {
            this.keyValuePairs = new ArrayList<KeyValuePair>();
        }
        
        @CanIgnoreReturnValue
        public Builder addTag(final String name) {
            return this.addImpl(name, null);
        }
        
        @CanIgnoreReturnValue
        public Builder addTag(final String name, final String value) {
            Checks.checkArgument(value != null, "tag value");
            return this.addImpl(name, value);
        }
        
        @CanIgnoreReturnValue
        public Builder addTag(final String name, final boolean value) {
            return this.addImpl(name, value);
        }
        
        @CanIgnoreReturnValue
        public Builder addTag(final String name, final long value) {
            return this.addImpl(name, value);
        }
        
        @CanIgnoreReturnValue
        public Builder addTag(final String name, final double value) {
            return this.addImpl(name, value);
        }
        
        private Builder addImpl(final String name, @NullableDecl final Object value) {
            this.keyValuePairs.add(new KeyValuePair(Checks.checkMetadataIdentifier(name), value));
            return this;
        }
        
        public Tags build() {
            if (this.keyValuePairs.isEmpty()) {
                return Tags.EMPTY_TAGS;
            }
            Collections.sort(this.keyValuePairs, Tags.KEY_VALUE_COMPARATOR);
            return new Tags(new LightweightTagMap(this.keyValuePairs), null);
        }
        
        @Override
        public String toString() {
            return this.build().toString();
        }
    }
    
    private static class LightweightTagMap extends AbstractMap<String, Set<Object>>
    {
        private static final Comparator<Object> ENTRY_COMPARATOR;
        private static final int SMALL_ARRAY_LENGTH = 16;
        private static final int[] singletonOffsets;
        private final Object[] array;
        private final int[] offsets;
        private final Set<Map.Entry<String, Set<Object>>> entrySet;
        private Integer hashCode;
        private String toString;
        
        LightweightTagMap(final String name, final Object value) {
            this.entrySet = new SortedArraySet<Map.Entry<String, Set<Object>>>(-1);
            this.hashCode = null;
            this.toString = null;
            this.offsets = LightweightTagMap.singletonOffsets;
            this.array = new Object[] { this.newEntry(name, 0), value };
        }
        
        LightweightTagMap(final List<KeyValuePair> sortedPairs) {
            this.entrySet = new SortedArraySet<Map.Entry<String, Set<Object>>>(-1);
            this.hashCode = null;
            this.toString = null;
            final int entryCount = countMapEntries(sortedPairs);
            final Object[] array = new Object[entryCount + sortedPairs.size()];
            final int[] offsets = new int[entryCount + 1];
            final int totalElementCount = this.makeTagMap(sortedPairs, entryCount, array, offsets);
            this.array = maybeResizeElementArray(array, totalElementCount);
            this.offsets = offsets;
        }
        
        LightweightTagMap(final LightweightTagMap lhs, final LightweightTagMap rhs) {
            this.entrySet = new SortedArraySet<Map.Entry<String, Set<Object>>>(-1);
            this.hashCode = null;
            this.toString = null;
            final int maxEntryCount = lhs.size() + rhs.size();
            final Object[] array = new Object[lhs.getTotalElementCount() + rhs.getTotalElementCount()];
            final int[] offsets = new int[maxEntryCount + 1];
            final int totalElementCount = this.mergeTagMaps(lhs, rhs, maxEntryCount, array, offsets);
            this.array = adjustOffsetsAndMaybeResize(array, offsets, totalElementCount);
            this.offsets = maybeResizeOffsetsArray(offsets);
        }
        
        private static int countMapEntries(final List<KeyValuePair> sortedPairs) {
            String key = null;
            int count = 0;
            for (final KeyValuePair pair : sortedPairs) {
                if (!pair.key.equals(key)) {
                    key = pair.key;
                    ++count;
                }
            }
            return count;
        }
        
        private int makeTagMap(final List<KeyValuePair> sortedPairs, final int entryCount, final Object[] array, final int[] offsets) {
            String key = null;
            Object value = null;
            int newEntryIndex = 0;
            int valueStart = entryCount;
            for (final KeyValuePair pair : sortedPairs) {
                if (!pair.key.equals(key)) {
                    key = pair.key;
                    array[newEntryIndex] = this.newEntry(key, newEntryIndex);
                    offsets[newEntryIndex] = valueStart;
                    ++newEntryIndex;
                    value = null;
                }
                if (pair.value != null && !pair.value.equals(value)) {
                    value = pair.value;
                    array[valueStart++] = value;
                }
            }
            if (newEntryIndex != entryCount) {
                throw new ConcurrentModificationException("corrupted tag map");
            }
            return offsets[entryCount] = valueStart;
        }
        
        private int mergeTagMaps(final LightweightTagMap lhs, final LightweightTagMap rhs, final int maxEntryCount, final Object[] array, final int[] offsets) {
            int valueStart = maxEntryCount;
            offsets[0] = valueStart;
            int lhsEntryIndex = 0;
            Map.Entry<String, SortedArraySet<Object>> lhsEntry = lhs.getEntryOrNull(lhsEntryIndex);
            int rhsEntryIndex = 0;
            Map.Entry<String, SortedArraySet<Object>> rhsEntry = rhs.getEntryOrNull(rhsEntryIndex);
            int newEntryIndex = 0;
            while (lhsEntry != null || rhsEntry != null) {
                int signum = (lhsEntry == null) ? 1 : ((rhsEntry == null) ? -1 : 0);
                if (signum == 0) {
                    signum = lhsEntry.getKey().compareTo((String)rhsEntry.getKey());
                    if (signum == 0) {
                        array[newEntryIndex] = this.newEntry(lhsEntry.getKey(), newEntryIndex);
                        ++newEntryIndex;
                        valueStart = mergeValues(lhsEntry.getValue(), rhsEntry.getValue(), array, valueStart);
                        offsets[newEntryIndex] = valueStart;
                        lhsEntry = lhs.getEntryOrNull(++lhsEntryIndex);
                        rhsEntry = rhs.getEntryOrNull(++rhsEntryIndex);
                        continue;
                    }
                }
                if (signum < 0) {
                    valueStart = this.copyEntryAndValues(lhsEntry, newEntryIndex++, valueStart, array, offsets);
                    lhsEntry = lhs.getEntryOrNull(++lhsEntryIndex);
                }
                else {
                    valueStart = this.copyEntryAndValues(rhsEntry, newEntryIndex++, valueStart, array, offsets);
                    rhsEntry = rhs.getEntryOrNull(++rhsEntryIndex);
                }
            }
            return newEntryIndex;
        }
        
        private static int mergeValues(final SortedArraySet<?> lhs, final SortedArraySet<?> rhs, final Object[] array, int valueStart) {
            int lhsIndex = 0;
            int rhsIndex = 0;
            while (lhsIndex < lhs.size() || rhsIndex < rhs.size()) {
                int signum = (lhsIndex == lhs.size()) ? 1 : ((rhsIndex == rhs.size()) ? -1 : 0);
                if (signum == 0) {
                    signum = Tags.VALUE_COMPARATOR.compare(lhs.getValue(lhsIndex), rhs.getValue(rhsIndex));
                }
                Object value;
                if (signum < 0) {
                    value = lhs.getValue(lhsIndex++);
                }
                else {
                    value = rhs.getValue(rhsIndex++);
                    if (signum == 0) {
                        ++lhsIndex;
                    }
                }
                array[valueStart++] = value;
            }
            return valueStart;
        }
        
        private int copyEntryAndValues(final Map.Entry<String, SortedArraySet<Object>> entry, final int entryIndex, final int valueStart, final Object[] array, final int[] offsets) {
            final SortedArraySet<Object> values = entry.getValue();
            final int valueCount = values.getEnd() - values.getStart();
            System.arraycopy(values.getValuesArray(), values.getStart(), array, valueStart, valueCount);
            array[entryIndex] = this.newEntry(entry.getKey(), entryIndex);
            final int valueEnd = valueStart + valueCount;
            return offsets[entryIndex + 1] = valueEnd;
        }
        
        private static Object[] adjustOffsetsAndMaybeResize(final Object[] array, final int[] offsets, final int entryCount) {
            final int maxEntries = offsets[0];
            final int offsetReduction = maxEntries - entryCount;
            if (offsetReduction == 0) {
                return array;
            }
            for (int i = 0; i <= entryCount; ++i) {
                final int n = i;
                offsets[n] -= offsetReduction;
            }
            Object[] dstArray = array;
            final int totalElementCount = offsets[entryCount];
            final int valueCount = totalElementCount - entryCount;
            if (mustResize(array.length, totalElementCount)) {
                dstArray = new Object[totalElementCount];
                System.arraycopy(array, 0, dstArray, 0, entryCount);
            }
            System.arraycopy(array, maxEntries, dstArray, entryCount, valueCount);
            return dstArray;
        }
        
        private static Object[] maybeResizeElementArray(final Object[] array, final int bestLength) {
            return mustResize(array.length, bestLength) ? Arrays.copyOf(array, bestLength) : array;
        }
        
        private static int[] maybeResizeOffsetsArray(final int[] offsets) {
            final int bestLength = offsets[0] + 1;
            return mustResize(offsets.length, bestLength) ? Arrays.copyOf(offsets, bestLength) : offsets;
        }
        
        private static boolean mustResize(final int actualLength, final int bestLength) {
            return actualLength > 16 && 9 * actualLength > 10 * bestLength;
        }
        
        private Map.Entry<String, SortedArraySet<Object>> newEntry(final String key, final int index) {
            return new SimpleImmutableEntry<String, SortedArraySet<Object>>(key, new SortedArraySet<Object>(index));
        }
        
        private Map.Entry<String, SortedArraySet<Object>> getEntryOrNull(final int index) {
            return (index < this.offsets[0]) ? ((Map.Entry)this.array[index]) : null;
        }
        
        private int getTotalElementCount() {
            return this.offsets[this.size()];
        }
        
        @Override
        public Set<Map.Entry<String, Set<Object>>> entrySet() {
            return this.entrySet;
        }
        
        @Override
        public int hashCode() {
            if (this.hashCode == null) {
                this.hashCode = super.hashCode();
            }
            return this.hashCode;
        }
        
        @Override
        public String toString() {
            if (this.toString == null) {
                this.toString = super.toString();
            }
            return this.toString;
        }
        
        static {
            ENTRY_COMPARATOR = new Comparator<Object>() {
                @Override
                public int compare(final Object s1, final Object s2) {
                    return ((Map.Entry)s1).getKey().compareTo((String)((Map.Entry)s2).getKey());
                }
            };
            singletonOffsets = new int[] { 1, 2 };
        }
        
        class SortedArraySet<T> extends AbstractSet<T>
        {
            final int index;
            
            SortedArraySet(final int index) {
                this.index = index;
            }
            
            Object[] getValuesArray() {
                return LightweightTagMap.this.array;
            }
            
            Object getValue(final int n) {
                return LightweightTagMap.this.array[this.getStart() + n];
            }
            
            int getStart() {
                return (this.index == -1) ? 0 : LightweightTagMap.this.offsets[this.index];
            }
            
            int getEnd() {
                return LightweightTagMap.this.offsets[this.index + 1];
            }
            
            private Comparator<Object> getComparator() {
                return (this.index == -1) ? LightweightTagMap.ENTRY_COMPARATOR : Tags.VALUE_COMPARATOR;
            }
            
            @Override
            public int size() {
                return this.getEnd() - this.getStart();
            }
            
            @Override
            public boolean contains(final Object o) {
                return Arrays.binarySearch(LightweightTagMap.this.array, this.getStart(), this.getEnd(), o, this.getComparator()) >= 0;
            }
            
            @Override
            public Iterator<T> iterator() {
                return new Iterator<T>() {
                    private int n = 0;
                    
                    @Override
                    public boolean hasNext() {
                        return this.n < SortedArraySet.this.size();
                    }
                    
                    @Override
                    public T next() {
                        final int index = this.n;
                        if (index < SortedArraySet.this.size()) {
                            final T value = (T)LightweightTagMap.this.array[SortedArraySet.this.getStart() + index];
                            this.n = index + 1;
                            return value;
                        }
                        throw new NoSuchElementException();
                    }
                };
            }
        }
    }
}
