// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License

// created: 2018may04 Markus W. Scherer

package com.ibm.icu.util;

import java.util.Arrays;

/**
 * Mutable Unicode code point trie.
 * Fast map from Unicode code points (U+0000..U+10FFFF) to 32-bit integer values.
 * For details see http://site.icu-project.org/design/struct/utrie
 *
 * <p>Setting values (especially ranges) and lookup is fast.
 * The mutable trie is only somewhat space-efficient.
 * It builds a compacted, immutable {@link CodePointTrie}.
 *
 * <p>This trie can be modified while iterating over its contents.
 * For example, it is possible to merge its values with those from another
 * set of ranges (e.g., another @{link CodePointMap}):
 * Iterate over those source ranges; for each of them iterate over this trie;
 * add the source value into the value of each trie range.
 *
 * @stable ICU 63
 */
public final class MutableCodePointTrie extends CodePointMap implements Cloneable {
    /**
     * Constructs a mutable trie that initially maps each Unicode code point to the same value.
     * It uses 32-bit data values until
     * {@link #buildImmutable(com.ibm.icu.util.CodePointTrie.Type, com.ibm.icu.util.CodePointTrie.ValueWidth)}
     * is called.
     * buildImmutable() takes a valueWidth parameter which
     * determines the number of bits in the data value in the resulting {@link CodePointTrie}.
     *
     * @param initialValue the initial value that is set for all code points
     * @param errorValue the value for out-of-range code points and ill-formed UTF-8/16
     * @stable ICU 63
     */
    public MutableCodePointTrie(int initialValue, int errorValue) {
        index = new int[BMP_I_LIMIT];
        index3NullOffset = -1;
        data = new int[INITIAL_DATA_LENGTH];
        dataNullOffset = -1;
        origInitialValue = initialValue;
        this.initialValue = initialValue;
        this.errorValue = errorValue;
        highValue = initialValue;
    }

    /**
     * Clones this mutable trie.
     *
     * @return the clone
     * @stable ICU 63
     */
    @Override
    public MutableCodePointTrie clone() {
        try {
            MutableCodePointTrie builder = (MutableCodePointTrie) super.clone();
            int iCapacity = highStart <= BMP_LIMIT ? BMP_I_LIMIT : I_LIMIT;
            builder.index = new int[iCapacity];
            builder.flags = new byte[UNICODE_LIMIT >> CodePointTrie.SHIFT_3];
            for (int i = 0, iLimit = highStart >> CodePointTrie.SHIFT_3; i < iLimit; ++i) {
                builder.index[i] = index[i];
                builder.flags[i] = flags[i];
            }
            builder.index3NullOffset = index3NullOffset;
            builder.data = data.clone();
            builder.dataLength = dataLength;
            builder.dataNullOffset = dataNullOffset;
            builder.origInitialValue = origInitialValue;
            builder.initialValue = initialValue;
            builder.errorValue = errorValue;
            builder.highStart = highStart;
            builder.highValue = highValue;
            assert index16 == null;
            return builder;
        } catch (CloneNotSupportedException ignored) {
            // Unreachable: Cloning *is* supported.
            return null;
        }
    }

    /**
     * Creates a mutable trie with the same contents as the {@link CodePointMap}.
     *
     * @param map the source map or trie
     * @return the mutable trie
     * @stable ICU 63
     */
    public static MutableCodePointTrie fromCodePointMap(CodePointMap map) {
        // TODO: Consider special code branch for map instanceof CodePointTrie?
        // Use the highValue as the initialValue to reduce the highStart.
        int errorValue = map.get(-1);
        int initialValue = map.get(MAX_UNICODE);
        MutableCodePointTrie mutableTrie = new MutableCodePointTrie(initialValue, errorValue);
        CodePointMap.Range range = new CodePointMap.Range();
        int start = 0;
        while (map.getRange(start, null, range)) {
            int end = range.getEnd();
            int value = range.getValue();
            if (value != initialValue) {
                if (start == end) {
                    mutableTrie.set(start, value);
                } else {
                    mutableTrie.setRange(start, end, value);
                }
            }
            start = end + 1;
        }
        return mutableTrie;
    }

    private void clear() {
        index3NullOffset = dataNullOffset = -1;
        dataLength = 0;
        highValue = initialValue = origInitialValue;
        highStart = 0;
        index16 = null;
    }

    /**
     * {@inheritDoc}
     * @stable ICU 63
     */
    @Override
    public int get(int c) {
        if (c < 0 || MAX_UNICODE < c) {
            return errorValue;
        }
        if (c >= highStart) {
            return highValue;
        }
        int i = c >> CodePointTrie.SHIFT_3;
        if (flags[i] == ALL_SAME) {
            return index[i];
        } else {
            return data[index[i] + (c & CodePointTrie.SMALL_DATA_MASK)];
        }
    }

    private static final int maybeFilterValue(int value, int initialValue, int nullValue,
            ValueFilter filter) {
        if (value == initialValue) {
            value = nullValue;
        } else if (filter != null) {
            value = filter.apply(value);
        }
        return value;
    }

    /**
     * {@inheritDoc}
     *
     * <p>The trie can be modified between calls to this function.
     *
     * @stable ICU 63
     */
    @Override
    public boolean getRange(int start, CodePointTrie.ValueFilter filter,
            CodePointTrie.Range range) {
        if (start < 0 || MAX_UNICODE < start) {
            return false;
        }
        if (start >= highStart) {
            int value = highValue;
            if (filter != null) { value = filter.apply(value); }
            range.set(start, MAX_UNICODE, value);
            return true;
        }
        int nullValue = initialValue;
        if (filter != null) { nullValue = filter.apply(nullValue); }
        int c = start;
        // Initialize to make compiler happy. Real value when haveValue is true.
        int trieValue = 0, value = 0;
        boolean haveValue = false;
        int i = c >> CodePointTrie.SHIFT_3;
        do {
            if (flags[i] == ALL_SAME) {
                int trieValue2 = index[i];
                if (haveValue) {
                    if (trieValue2 != trieValue) {
                        if (filter == null ||
                                maybeFilterValue(trieValue2, initialValue, nullValue,
                                        filter) != value) {
                            range.set(start, c - 1, value);
                            return true;
                        }
                        trieValue = trieValue2;  // may or may not help
                    }
                } else {
                    trieValue = trieValue2;
                    value = maybeFilterValue(trieValue2, initialValue, nullValue, filter);
                    haveValue = true;
                }
                c = (c + CodePointTrie.SMALL_DATA_BLOCK_LENGTH) & ~CodePointTrie.SMALL_DATA_MASK;
            } else /* MIXED */ {
                int di = index[i] + (c & CodePointTrie.SMALL_DATA_MASK);
                int trieValue2 = data[di];
                if (haveValue) {
                    if (trieValue2 != trieValue) {
                        if (filter == null ||
                                maybeFilterValue(trieValue2, initialValue, nullValue,
                                        filter) != value) {
                            range.set(start, c - 1, value);
                            return true;
                        }
                        trieValue = trieValue2;  // may or may not help
                    }
                } else {
                    trieValue = trieValue2;
                    value = maybeFilterValue(trieValue2, initialValue, nullValue, filter);
                    haveValue = true;
                }
                while ((++c & CodePointTrie.SMALL_DATA_MASK) != 0) {
                    trieValue2 = data[++di];
                    if (trieValue2 != trieValue) {
                        if (filter == null ||
                                maybeFilterValue(trieValue2, initialValue, nullValue,
                                        filter) != value) {
                            range.set(start, c - 1, value);
                            return true;
                        }
                        trieValue = trieValue2;  // may or may not help
                    }
                }
            }
            ++i;
        } while (c < highStart);
        assert(haveValue);
        if (maybeFilterValue(highValue, initialValue, nullValue, filter) != value) {
            range.set(start, c - 1, value);
        } else {
            range.set(start, MAX_UNICODE, value);
        }
        return true;
    }

    private void writeBlock(int block, int value) {
        int limit = block + CodePointTrie.SMALL_DATA_BLOCK_LENGTH;
        Arrays.fill(data, block, limit, value);
    }

    /**
     * Sets a value for a code point.
     *
     * @param c the code point
     * @param value the value
     * @stable ICU 63
     */
    public void set(int c, int value) {
        if (c < 0 || MAX_UNICODE < c) {
            throw new IllegalArgumentException("invalid code point");
        }

        ensureHighStart(c);
        int block = getDataBlock(c >> CodePointTrie.SHIFT_3);
        data[block + (c & CodePointTrie.SMALL_DATA_MASK)] = value;
    }

    private void fillBlock(int block, int start, int limit, int value) {
        Arrays.fill(data, block + start, block + limit, value);
    }

    /**
     * Sets a value for each code point [start..end].
     * Faster and more space-efficient than setting the value for each code point separately.
     *
     * @param start the first code point to get the value
     * @param end the last code point to get the value (inclusive)
     * @param value the value
     * @stable ICU 63
     */
    public void setRange(int start, int end, int value) {
        if (start < 0 || MAX_UNICODE < start || end < 0 || MAX_UNICODE < end || start > end) {
            throw new IllegalArgumentException("invalid code point range");
        }
        ensureHighStart(end);

        int limit = end + 1;
        if ((start & CodePointTrie.SMALL_DATA_MASK) != 0) {
            // Set partial block at [start..following block boundary[.
            int block = getDataBlock(start >> CodePointTrie.SHIFT_3);
            int nextStart = (start + CodePointTrie.SMALL_DATA_MASK) & ~CodePointTrie.SMALL_DATA_MASK;
            if (nextStart <= limit) {
                fillBlock(block, start & CodePointTrie.SMALL_DATA_MASK,
                          CodePointTrie.SMALL_DATA_BLOCK_LENGTH, value);
                start = nextStart;
            } else {
                fillBlock(block, start & CodePointTrie.SMALL_DATA_MASK,
                          limit & CodePointTrie.SMALL_DATA_MASK, value);
                return;
            }
        }

        // Number of positions in the last, partial block.
        int rest = limit & CodePointTrie.SMALL_DATA_MASK;

        // Round down limit to a block boundary.
        limit &= ~CodePointTrie.SMALL_DATA_MASK;

        // Iterate over all-value blocks.
        while (start < limit) {
            int i = start >> CodePointTrie.SHIFT_3;
            if (flags[i] == ALL_SAME) {
                index[i] = value;
            } else /* MIXED */ {
                fillBlock(index[i], 0, CodePointTrie.SMALL_DATA_BLOCK_LENGTH, value);
            }
            start += CodePointTrie.SMALL_DATA_BLOCK_LENGTH;
        }

        if (rest > 0) {
            // Set partial block at [last block boundary..limit[.
            int block = getDataBlock(start >> CodePointTrie.SHIFT_3);
            fillBlock(block, 0, rest, value);
        }
    }

    /**
     * Compacts the data and builds an immutable {@link CodePointTrie} according to the parameters.
     * After this, the mutable trie will be empty.
     *
     * <p>The mutable trie stores 32-bit values until buildImmutable() is called.
     * If values shorter than 32 bits are to be stored in the immutable trie,
     * then the upper bits are discarded.
     * For example, when the mutable trie contains values 0x81, -0x7f, and 0xa581,
     * and the value width is 8 bits, then each of these is stored as 0x81
     * and the immutable trie will return that as an unsigned value.
     * (Some implementations may want to make productive temporary use of the upper bits
     * until buildImmutable() discards them.)
     *
     * <p>Not every possible set of mappings can be built into a CodePointTrie,
     * because of limitations resulting from speed and space optimizations.
     * Every Unicode assigned character can be mapped to a unique value.
     * Typical data yields data structures far smaller than the limitations.
     *
     * <p>It is possible to construct extremely unusual mappings that exceed the
     * data structure limits.
     * In such a case this function will throw an exception.
     *
     * @param type selects the trie type
     * @param valueWidth selects the number of bits in a trie data value; if smaller than 32 bits,
     *                   then the values stored in the trie will be truncated first
     *
     * @see #fromCodePointMap(CodePointMap)
     * @stable ICU 63
     */
    public CodePointTrie buildImmutable(CodePointTrie.Type type, CodePointTrie.ValueWidth valueWidth) {
        if (type == null || valueWidth == null) {
            throw new IllegalArgumentException("The type and valueWidth must be specified.");
        }

        try {
            return build(type, valueWidth);
        } finally {
            clear();
        }
    }

    private static final int MAX_UNICODE = 0x10ffff;

    private static final int UNICODE_LIMIT = 0x110000;
    private static final int BMP_LIMIT = 0x10000;
    private static final int ASCII_LIMIT = 0x80;

    private static final int I_LIMIT = UNICODE_LIMIT >> CodePointTrie.SHIFT_3;
    private static final int BMP_I_LIMIT = BMP_LIMIT >> CodePointTrie.SHIFT_3;
    private static final int ASCII_I_LIMIT = ASCII_LIMIT >> CodePointTrie.SHIFT_3;

    private static final int SMALL_DATA_BLOCKS_PER_BMP_BLOCK = (1 << (CodePointTrie.FAST_SHIFT - CodePointTrie.SHIFT_3));

    // Flag values for data blocks.
    private static final byte ALL_SAME = 0;
    private static final byte MIXED = 1;
    private static final byte SAME_AS = 2;

    /** Start with allocation of 16k data entries. */
    private static final int INITIAL_DATA_LENGTH = (1 << 14);

    /** Grow about 8x each time. */
    private static final int MEDIUM_DATA_LENGTH = (1 << 17);

    /**
     * Maximum length of the build-time data array.
     * One entry per 0x110000 code points.
     */
    private static final int MAX_DATA_LENGTH = UNICODE_LIMIT;

    // Flag values for index-3 blocks while compacting/building.
    private static final byte I3_NULL = 0;
    private static final byte I3_BMP = 1;
    private static final byte I3_16 = 2;
    private static final byte I3_18 = 3;

    private static final int INDEX_3_18BIT_BLOCK_LENGTH = CodePointTrie.INDEX_3_BLOCK_LENGTH + CodePointTrie.INDEX_3_BLOCK_LENGTH / 8;

    private int[] index;
    private int index3NullOffset;
    private int[] data;
    private int dataLength;
    private int dataNullOffset;

    private int origInitialValue;
    private int initialValue;
    private int errorValue;
    private int highStart;
    private int highValue;

    /** Temporary array while building the final data. */
    private char[] index16;
    private byte[] flags = new byte[UNICODE_LIMIT >> CodePointTrie.SHIFT_3];

    private void ensureHighStart(int c) {
        if (c >= highStart) {
            // Round up to a CodePointTrie.CP_PER_INDEX_2_ENTRY boundary to simplify compaction.
            c = (c + CodePointTrie.CP_PER_INDEX_2_ENTRY) & ~(CodePointTrie.CP_PER_INDEX_2_ENTRY - 1);
            int i = highStart >> CodePointTrie.SHIFT_3;
            int iLimit = c >> CodePointTrie.SHIFT_3;
            if (iLimit > index.length) {
                int[] newIndex = new int[I_LIMIT];
                for (int j = 0; j < i; ++j) { newIndex[j] = index[j]; }
                index = newIndex;
            }
            do {
                flags[i] = ALL_SAME;
                index[i] = initialValue;
            } while(++i < iLimit);
            highStart = c;
        }
    }

    private int allocDataBlock(int blockLength) {
        int newBlock = dataLength;
        int newTop = newBlock + blockLength;
        if (newTop > data.length) {
            int capacity;
            if (data.length < MEDIUM_DATA_LENGTH) {
                capacity = MEDIUM_DATA_LENGTH;
            } else if (data.length < MAX_DATA_LENGTH) {
                capacity = MAX_DATA_LENGTH;
            } else {
                // Should never occur.
                // Either MAX_DATA_LENGTH is incorrect,
                // or the code writes more values than should be possible.
                throw new AssertionError();
            }
            int[] newData = new int[capacity];
            for (int j = 0; j < dataLength; ++j) { newData[j] = data[j]; }
            data = newData;
        }
        dataLength = newTop;
        return newBlock;
    }

    /**
     * No error checking for illegal arguments.
     * The Java version always returns non-negative values.
     */
    private int getDataBlock(int i) {
        if (flags[i] == MIXED) {
            return index[i];
        }
        if (i < BMP_I_LIMIT) {
            int newBlock = allocDataBlock(CodePointTrie.FAST_DATA_BLOCK_LENGTH);
            int iStart = i & ~(SMALL_DATA_BLOCKS_PER_BMP_BLOCK -1);
            int iLimit = iStart + SMALL_DATA_BLOCKS_PER_BMP_BLOCK;
            do {
                assert(flags[iStart] == ALL_SAME);
                writeBlock(newBlock, index[iStart]);
                flags[iStart] = MIXED;
                index[iStart++] = newBlock;
                newBlock += CodePointTrie.SMALL_DATA_BLOCK_LENGTH;
            } while (iStart < iLimit);
            return index[i];
        } else {
            int newBlock = allocDataBlock(CodePointTrie.SMALL_DATA_BLOCK_LENGTH);
            if (newBlock < 0) { return newBlock; }
            writeBlock(newBlock, index[i]);
            flags[i] = MIXED;
            index[i] = newBlock;
            return newBlock;
        }
    }

    // compaction --------------------------------------------------------------

    private void maskValues(int mask) {
        initialValue &= mask;
        errorValue &= mask;
        highValue &= mask;
        int iLimit = highStart >> CodePointTrie.SHIFT_3;
        for (int i = 0; i < iLimit; ++i) {
            if (flags[i] == ALL_SAME) {
                index[i] &= mask;
            }
        }
        for (int i = 0; i < dataLength; ++i) {
            data[i] &= mask;
        }
    }

    private static boolean equalBlocks(int[] s, int si, int[] t, int ti, int length) {
        while (length > 0 && s[si] == t[ti]) {
            ++si;
            ++ti;
            --length;
        }
        return length == 0;
    }

    private static boolean equalBlocks(char[] s, int si, int[] t, int ti, int length) {
        while (length > 0 && s[si] == t[ti]) {
            ++si;
            ++ti;
            --length;
        }
        return length == 0;
    }

    private static boolean equalBlocks(char[] s, int si, char[] t, int ti, int length) {
        while (length > 0 && s[si] == t[ti]) {
            ++si;
            ++ti;
            --length;
        }
        return length == 0;
    }

    private static boolean allValuesSameAs(int[] p, int pi, int length, int value) {
        int pLimit = pi + length;
        while (pi < pLimit && p[pi] == value) { ++pi; }
        return pi == pLimit;
    }

    /** Search for an identical block. */
    private static int findSameBlock(char[] p, int pStart, int length,
            char[] q, int qStart, int blockLength) {
        // Ensure that we do not even partially get past length.
        length -= blockLength;

        while (pStart <= length) {
            if (equalBlocks(p, pStart, q, qStart, blockLength)) {
                return pStart;
            }
            ++pStart;
        }
        return -1;
    }

    private static int findAllSameBlock(int[] p, int start, int limit,
            int value, int blockLength) {
        // Ensure that we do not even partially get past limit.
        limit -= blockLength;

        for (int block = start; block <= limit; ++block) {
            if (p[block] == value) {
                for (int i = 1;; ++i) {
                    if (i == blockLength) {
                        return block;
                    }
                    if (p[block + i] != value) {
                        block += i;
                        break;
                    }
                }
            }
        }
        return -1;
    }

    /**
     * Look for maximum overlap of the beginning of the other block
     * with the previous, adjacent block.
     */
    private static int getOverlap(int[] p, int length, int[] q, int qStart, int blockLength) {
        int overlap = blockLength - 1;
        assert(overlap <= length);
        while (overlap > 0 && !equalBlocks(p, length - overlap, q, qStart, overlap)) {
            --overlap;
        }
        return overlap;
    }

    private static int getOverlap(char[] p, int length, int[] q, int qStart, int blockLength) {
        int overlap = blockLength - 1;
        assert(overlap <= length);
        while (overlap > 0 && !equalBlocks(p, length - overlap, q, qStart, overlap)) {
            --overlap;
        }
        return overlap;
    }

    private static int getOverlap(char[] p, int length, char[] q, int qStart, int blockLength) {
        int overlap = blockLength - 1;
        assert(overlap <= length);
        while (overlap > 0 && !equalBlocks(p, length - overlap, q, qStart, overlap)) {
            --overlap;
        }
        return overlap;
    }

    private static int getAllSameOverlap(int[] p, int length, int value, int blockLength) {
        int min = length - (blockLength - 1);
        int i = length;
        while (min < i && p[i - 1] == value) { --i; }
        return length - i;
    }

    private static boolean isStartOfSomeFastBlock(int dataOffset, int[] index, int fastILimit) {
        for (int i = 0; i < fastILimit; i += SMALL_DATA_BLOCKS_PER_BMP_BLOCK) {
            if (index[i] == dataOffset) {
                return true;
            }
        }
        return false;
    }

    /**
     * Finds the start of the last range in the trie by enumerating backward.
     * Indexes for code points higher than this will be omitted.
     */
    private int findHighStart() {
        int i = highStart >> CodePointTrie.SHIFT_3;
        while (i > 0) {
            boolean match;
            if (flags[--i] == ALL_SAME) {
                match = index[i] == highValue;
            } else /* MIXED */ {
                int p = index[i];
                for (int j = 0;; ++j) {
                    if (j == CodePointTrie.SMALL_DATA_BLOCK_LENGTH) {
                        match = true;
                        break;
                    }
                    if (data[p + j] != highValue) {
                        match = false;
                        break;
                    }
                }
            }
            if (!match) {
                return (i + 1) << CodePointTrie.SHIFT_3;
            }
        }
        return 0;
    }

    private static final class AllSameBlocks {
        static final int NEW_UNIQUE = -1;
        static final int OVERFLOW = -2;

        AllSameBlocks() {
            mostRecent = -1;
        }

        int findOrAdd(int index, int count, int value) {
            if (mostRecent >= 0 && values[mostRecent] == value) {
                refCounts[mostRecent] += count;
                return indexes[mostRecent];
            }
            for (int i = 0; i < length; ++i) {
                if (values[i] == value) {
                    mostRecent = i;
                    refCounts[i] += count;
                    return indexes[i];
                }
            }
            if (length == CAPACITY) {
                return OVERFLOW;
            }
            mostRecent = length;
            indexes[length] = index;
            values[length] = value;
            refCounts[length++] = count;
            return NEW_UNIQUE;
        }

        /** Replaces the block which has the lowest reference count. */
        void add(int index, int count, int value) {
            assert(length == CAPACITY);
            int least = -1;
            int leastCount = I_LIMIT;
            for (int i = 0; i < length; ++i) {
                assert(values[i] != value);
                if (refCounts[i] < leastCount) {
                    least = i;
                    leastCount = refCounts[i];
                }
            }
            assert(least >= 0);
            mostRecent = least;
            indexes[least] = index;
            values[least] = value;
            refCounts[least] = count;
        }

        int findMostUsed() {
            if (length == 0) { return -1; }
            int max = -1;
            int maxCount = 0;
            for (int i = 0; i < length; ++i) {
                if (refCounts[i] > maxCount) {
                    max = i;
                    maxCount = refCounts[i];
                }
            }
            return indexes[max];
        }

        private static final int CAPACITY = 32;

        private int length;
        private int mostRecent;

        private int[] indexes = new int[CAPACITY];
        private int[] values = new int[CAPACITY];
        private int[] refCounts = new int[CAPACITY];
    }

    // Custom hash table for mixed-value blocks to be found anywhere in the
    // compacted data or index so far.
    private static final class MixedBlocks {
        void init(int maxLength, int newBlockLength) {
            // We store actual data indexes + 1 to reserve 0 for empty entries.
            int maxDataIndex = maxLength - newBlockLength + 1;
            int newLength;
            if (maxDataIndex <= 0xfff) {  // 4k
                newLength = 6007;
                shift = 12;
                mask = 0xfff;
            } else if (maxDataIndex <= 0x7fff) {  // 32k
                newLength = 50021;
                shift = 15;
                mask = 0x7fff;
            } else if (maxDataIndex <= 0x1ffff) {  // 128k
                newLength = 200003;
                shift = 17;
                mask = 0x1ffff;
            } else {
                // maxDataIndex up to around MAX_DATA_LENGTH, ca. 1.1M
                newLength = 1500007;
                shift = 21;
                mask = 0x1fffff;
            }
            if (table == null || newLength > table.length) {
                table = new int[newLength];
            } else {
                Arrays.fill(table, 0, newLength, 0);
            }
            length = newLength;

            blockLength = newBlockLength;
        }

        void extend(int[] data, int minStart, int prevDataLength, int newDataLength) {
            int start = prevDataLength - blockLength;
            if (start >= minStart) {
                ++start;  // Skip the last block that we added last time.
            } else {
                start = minStart;  // Begin with the first full block.
            }
            for (int end = newDataLength - blockLength; start <= end; ++start) {
                int hashCode = makeHashCode(data, start);
                addEntry(data, null, start, hashCode, start);
            }
        }

        void extend(char[] data, int minStart, int prevDataLength, int newDataLength) {
            int start = prevDataLength - blockLength;
            if (start >= minStart) {
                ++start;  // Skip the last block that we added last time.
            } else {
                start = minStart;  // Begin with the first full block.
            }
            for (int end = newDataLength - blockLength; start <= end; ++start) {
                int hashCode = makeHashCode(data, start);
                addEntry(null, data, start, hashCode, start);
            }
        }

        int findBlock(int[] data, int[] blockData, int blockStart) {
            int hashCode = makeHashCode(blockData, blockStart);
            int entryIndex = findEntry(data, null, blockData, null, blockStart, hashCode);
            if (entryIndex >= 0) {
                return (table[entryIndex] & mask) - 1;
            } else {
                return -1;
            }
        }

        int findBlock(char[] data, int[] blockData, int blockStart) {
            int hashCode = makeHashCode(blockData, blockStart);
            int entryIndex = findEntry(null, data, blockData, null, blockStart, hashCode);
            if (entryIndex >= 0) {
                return (table[entryIndex] & mask) - 1;
            } else {
                return -1;
            }
        }

        int findBlock(char[] data, char[] blockData, int blockStart) {
            int hashCode = makeHashCode(blockData, blockStart);
            int entryIndex = findEntry(null, data, null, blockData, blockStart, hashCode);
            if (entryIndex >= 0) {
                return (table[entryIndex] & mask) - 1;
            } else {
                return -1;
            }
        }

        int findAllSameBlock(int[] data, int blockValue) {
            int hashCode = makeHashCode(blockValue);
            int entryIndex = findEntry(data, blockValue, hashCode);
            if (entryIndex >= 0) {
                return (table[entryIndex] & mask) - 1;
            } else {
                return -1;
            }
        }

        private int makeHashCode(int[] blockData, int blockStart) {
            int blockLimit = blockStart + blockLength;
            int hashCode = blockData[blockStart++];
            do {
                hashCode = 37 * hashCode + blockData[blockStart++];
            } while (blockStart < blockLimit);
            return hashCode;
        }

        private int makeHashCode(char[] blockData, int blockStart) {
            int blockLimit = blockStart + blockLength;
            int hashCode = blockData[blockStart++];
            do {
                hashCode = 37 * hashCode + blockData[blockStart++];
            } while (blockStart < blockLimit);
            return hashCode;
        }

        private int makeHashCode(int blockValue) {
            int hashCode = blockValue;
            for (int i = 1; i < blockLength; ++i) {
                hashCode = 37 * hashCode + blockValue;
            }
            return hashCode;
        }

        private void addEntry(int[] data32, char[] data16, int blockStart, int hashCode, int dataIndex) {
            assert(0 <= dataIndex && dataIndex < mask);
            int entryIndex = findEntry(data32, data16, data32, data16, blockStart, hashCode);
            if (entryIndex < 0) {
                table[~entryIndex] = (hashCode << shift) | (dataIndex + 1);
            }
        }

        private int findEntry(int[] data32, char[] data16,
                int[] blockData32, char[] blockData16, int blockStart, int hashCode) {
            int shiftedHashCode = hashCode << shift;
            int initialEntryIndex = modulo(hashCode, length - 1) + 1;  // 1..length-1
            for (int entryIndex = initialEntryIndex;;) {
                int entry = table[entryIndex];
                if (entry == 0) {
                    return ~entryIndex;
                }
                if ((entry & ~mask) == shiftedHashCode) {
                    int dataIndex = (entry & mask) - 1;
                    if (data32 != null ?
                            equalBlocks(data32, dataIndex, blockData32, blockStart, blockLength) :
                            blockData32 != null ?
                                equalBlocks(data16, dataIndex, blockData32, blockStart, blockLength) :
                                equalBlocks(data16, dataIndex, blockData16, blockStart, blockLength)) {
                        return entryIndex;
                    }
                }
                entryIndex = nextIndex(initialEntryIndex, entryIndex);
            }
        }

        private int findEntry(int[] data, int blockValue, int hashCode) {
            int shiftedHashCode = hashCode << shift;
            int initialEntryIndex = modulo(hashCode, length - 1) + 1;  // 1..length-1
            for (int entryIndex = initialEntryIndex;;) {
                int entry = table[entryIndex];
                if (entry == 0) {
                    return ~entryIndex;
                }
                if ((entry & ~mask) == shiftedHashCode) {
                    int dataIndex = (entry & mask) - 1;
                    if (allValuesSameAs(data, dataIndex, blockLength, blockValue)) {
                        return entryIndex;
                    }
                }
                entryIndex = nextIndex(initialEntryIndex, entryIndex);
            }
        }

        private int nextIndex(int initialEntryIndex, int entryIndex) {
            // U_ASSERT(0 < initialEntryIndex && initialEntryIndex < length);
            return (entryIndex + initialEntryIndex) % length;
        }

        /** Ensures non-negative n % m (that is 0..m-1). */
        private int modulo(int n, int m) {
            int i = n % m;
            if (i < 0) {
                i += m;
            }
            return i;
        }

        // Hash table.
        // The length is a prime number, larger than the maximum data length.
        // The "shift" lower bits store a data index + 1.
        // The remaining upper bits store a partial hashCode of the block data values.
        private int[] table;
        private int length;
        private int shift;
        private int mask;

        private int blockLength;
    }

    private int compactWholeDataBlocks(int fastILimit, AllSameBlocks allSameBlocks) {
        // ASCII data will be stored as a linear table, even if the following code
        // does not yet count it that way.
        int newDataCapacity = ASCII_LIMIT;
        // Add room for a small data null block in case it would match the start of
        // a fast data block where dataNullOffset must not be set in that case.
        newDataCapacity += CodePointTrie.SMALL_DATA_BLOCK_LENGTH;
        // Add room for special values (errorValue, highValue) and padding.
        newDataCapacity += 4;
        int iLimit = highStart >> CodePointTrie.SHIFT_3;
        int blockLength = CodePointTrie.FAST_DATA_BLOCK_LENGTH;
        int inc = SMALL_DATA_BLOCKS_PER_BMP_BLOCK;
        for (int i = 0; i < iLimit; i += inc) {
            if (i == fastILimit) {
                blockLength = CodePointTrie.SMALL_DATA_BLOCK_LENGTH;
                inc = 1;
            }
            int value = index[i];
            if (flags[i] == MIXED) {
                // Really mixed?
                int p = value;
                value = data[p];
                if (allValuesSameAs(data, p + 1, blockLength - 1, value)) {
                    flags[i] = ALL_SAME;
                    index[i] = value;
                    // Fall through to ALL_SAME handling.
                } else {
                    newDataCapacity += blockLength;
                    continue;
                }
            } else {
                assert(flags[i] == ALL_SAME);
                if (inc > 1) {
                    // Do all of the fast-range data block's ALL_SAME parts have the same value?
                    boolean allSame = true;
                    int next_i = i + inc;
                    for (int j = i + 1; j < next_i; ++j) {
                        assert(flags[j] == ALL_SAME);
                        if (index[j] != value) {
                            allSame = false;
                            break;
                        }
                    }
                    if (!allSame) {
                        // Turn it into a MIXED block.
                        if (getDataBlock(i) < 0) {
                            return -1;
                        }
                        newDataCapacity += blockLength;
                        continue;
                    }
                }
            }
            // Is there another ALL_SAME block with the same value?
            int other = allSameBlocks.findOrAdd(i, inc, value);
            if (other == AllSameBlocks.OVERFLOW) {
                // The fixed-size array overflowed. Slow check for a duplicate block.
                int jInc = SMALL_DATA_BLOCKS_PER_BMP_BLOCK;
                for (int j = 0;; j += jInc) {
                    if (j == i) {
                        allSameBlocks.add(i, inc, value);
                        break;
                    }
                    if (j == fastILimit) {
                        jInc = 1;
                    }
                    if (flags[j] == ALL_SAME && index[j] == value) {
                        allSameBlocks.add(j, jInc + inc, value);
                        other = j;
                        break;
                        // We could keep counting blocks with the same value
                        // before we add the first one, which may improve compaction in rare cases,
                        // but it would make it slower.
                    }
                }
            }
            if (other >= 0) {
                flags[i] = SAME_AS;
                index[i] = other;
            } else {
                // New unique same-value block.
                newDataCapacity += blockLength;
            }
        }
        return newDataCapacity;
    }

    /**
     * Compacts a build-time trie.
     *
     * The compaction
     * - removes blocks that are identical with earlier ones
     * - overlaps each new non-duplicate block as much as possible with the previously-written one
     * - works with fast-range data blocks whose length is a multiple of that of
     *   higher-code-point data blocks
     *
     * It does not try to find an optimal order of writing, deduplicating, and overlapping blocks.
     */
    private int compactData(
            int fastILimit, int[] newData, int dataNullIndex, MixedBlocks mixedBlocks) {
        // The linear ASCII data has been copied into newData already.
        int newDataLength = 0;
        for (int i = 0; newDataLength < ASCII_LIMIT;
                newDataLength += CodePointTrie.FAST_DATA_BLOCK_LENGTH, i += SMALL_DATA_BLOCKS_PER_BMP_BLOCK) {
            index[i] = newDataLength;
        }

        int blockLength = CodePointTrie.FAST_DATA_BLOCK_LENGTH;
        mixedBlocks.init(newData.length, blockLength);
        mixedBlocks.extend(newData, 0, 0, newDataLength);

        int iLimit = highStart >> CodePointTrie.SHIFT_3;
        int inc = SMALL_DATA_BLOCKS_PER_BMP_BLOCK;
        int fastLength = 0;
        for (int i = ASCII_I_LIMIT; i < iLimit; i += inc) {
            if (i == fastILimit) {
                blockLength = CodePointTrie.SMALL_DATA_BLOCK_LENGTH;
                inc = 1;
                fastLength = newDataLength;
                mixedBlocks.init(newData.length, blockLength);
                mixedBlocks.extend(newData, 0, 0, newDataLength);
            }
            if (flags[i] == ALL_SAME) {
                int value = index[i];
                // Find an earlier part of the data array of length blockLength
                // that is filled with this value.
                int n = mixedBlocks.findAllSameBlock(newData, value);
                // If we find a match, and the current block is the data null block,
                // and it is not a fast block but matches the start of a fast block,
                // then we need to continue looking.
                // This is because this small block is shorter than the fast block,
                // and not all of the rest of the fast block is filled with this value.
                // Otherwise trie.getRange() would detect that the fast block starts at
                // dataNullOffset and assume incorrectly that it is filled with the null value.
                while (n >= 0 && i == dataNullIndex && i >= fastILimit && n < fastLength &&
                        isStartOfSomeFastBlock(n, index, fastILimit)) {
                    n = findAllSameBlock(newData, n + 1, newDataLength, value, blockLength);
                }
                if (n >= 0) {
                    index[i] = n;
                } else {
                    n = getAllSameOverlap(newData, newDataLength, value, blockLength);
                    index[i] = newDataLength - n;
                    int prevDataLength = newDataLength;
                    while (n < blockLength) {
                        newData[newDataLength++] = value;
                        ++n;
                    }
                    mixedBlocks.extend(newData, 0, prevDataLength, newDataLength);
                }
            } else if (flags[i] == MIXED) {
                int block = index[i];
                int n = mixedBlocks.findBlock(newData, data, block);
                if (n >= 0) {
                    index[i] = n;
                } else {
                    n = getOverlap(newData, newDataLength, data, block, blockLength);
                    index[i] = newDataLength - n;
                    int prevDataLength = newDataLength;
                    while (n < blockLength) {
                        newData[newDataLength++] = data[block + n++];
                    }
                    mixedBlocks.extend(newData, 0, prevDataLength, newDataLength);
                }
            } else /* SAME_AS */ {
                int j = index[i];
                index[i] = index[j];
            }
        }

        return newDataLength;
    }

    private int compactIndex(int fastILimit, MixedBlocks mixedBlocks) {
        int fastIndexLength = fastILimit >> (CodePointTrie.FAST_SHIFT - CodePointTrie.SHIFT_3);
        if ((highStart >> CodePointTrie.FAST_SHIFT) <= fastIndexLength) {
            // Only the linear fast index, no multi-stage index tables.
            index3NullOffset = CodePointTrie.NO_INDEX3_NULL_OFFSET;
            return fastIndexLength;
        }

        // Condense the fast index table.
        // Also, does it contain an index-3 block with all dataNullOffset?
        char[] fastIndex = new char[fastIndexLength];
        int i3FirstNull = -1;
        for (int i = 0, j = 0; i < fastILimit; ++j) {
            int i3 = index[i];
            fastIndex[j] = (char)i3;
            if (i3 == dataNullOffset) {
                if (i3FirstNull < 0) {
                    i3FirstNull = j;
                } else if (index3NullOffset < 0 &&
                        (j - i3FirstNull + 1) == CodePointTrie.INDEX_3_BLOCK_LENGTH) {
                    index3NullOffset = i3FirstNull;
                }
            } else {
                i3FirstNull = -1;
            }
            // Set the index entries that compactData() skipped.
            // Needed when the multi-stage index covers the fast index range as well.
            int iNext = i + SMALL_DATA_BLOCKS_PER_BMP_BLOCK;
            while (++i < iNext) {
                i3 += CodePointTrie.SMALL_DATA_BLOCK_LENGTH;
                index[i] = i3;
            }
        }

        mixedBlocks.init(fastIndexLength, CodePointTrie.INDEX_3_BLOCK_LENGTH);
        mixedBlocks.extend(fastIndex, 0, 0, fastIndexLength);

        // Examine index-3 blocks. For each determine one of:
        // - same as the index-3 null block
        // - same as a fast-index block
        // - 16-bit indexes
        // - 18-bit indexes
        // We store this in the first flags entry for the index-3 block.
        //
        // Also determine an upper limit for the index-3 table length.
        int index3Capacity = 0;
        i3FirstNull = index3NullOffset;
        boolean hasLongI3Blocks = false;
        // If the fast index covers the whole BMP, then
        // the multi-stage index is only for supplementary code points.
        // Otherwise, the multi-stage index covers all of Unicode.
        int iStart = fastILimit < BMP_I_LIMIT ? 0 : BMP_I_LIMIT;
        int iLimit = highStart >> CodePointTrie.SHIFT_3;
        for (int i = iStart; i < iLimit;) {
            int j = i;
            int jLimit = i + CodePointTrie.INDEX_3_BLOCK_LENGTH;
            int oredI3 = 0;
            boolean isNull = true;
            do {
                int i3 = index[j];
                oredI3 |= i3;
                if (i3 != dataNullOffset) {
                    isNull = false;
                }
            } while (++j < jLimit);
            if (isNull) {
                flags[i] = I3_NULL;
                if (i3FirstNull < 0) {
                    if (oredI3 <= 0xffff) {
                        index3Capacity += CodePointTrie.INDEX_3_BLOCK_LENGTH;
                    } else {
                        index3Capacity += INDEX_3_18BIT_BLOCK_LENGTH;
                        hasLongI3Blocks = true;
                    }
                    i3FirstNull = 0;
                }
            } else {
                if (oredI3 <= 0xffff) {
                    int n = mixedBlocks.findBlock(fastIndex, index, i);
                    if (n >= 0) {
                        flags[i] = I3_BMP;
                        index[i] = n;
                    } else {
                        flags[i] = I3_16;
                        index3Capacity += CodePointTrie.INDEX_3_BLOCK_LENGTH;
                    }
                } else {
                    flags[i] = I3_18;
                    index3Capacity += INDEX_3_18BIT_BLOCK_LENGTH;
                    hasLongI3Blocks = true;
                }
            }
            i = j;
        }

        int index2Capacity = (iLimit - iStart) >> CodePointTrie.SHIFT_2_3;

        // Length of the index-1 table, rounded up.
        int index1Length = (index2Capacity + CodePointTrie.INDEX_2_MASK) >> CodePointTrie.SHIFT_1_2;

        // Index table: Fast index, index-1, index-3, index-2.
        // +1 for possible index table padding.
        int index16Capacity = fastIndexLength + index1Length + index3Capacity + index2Capacity + 1;
        index16 = Arrays.copyOf(fastIndex, index16Capacity);

        mixedBlocks.init(index16Capacity, CodePointTrie.INDEX_3_BLOCK_LENGTH);
        MixedBlocks longI3Blocks = null;
        if (hasLongI3Blocks) {
            longI3Blocks = new MixedBlocks();
            longI3Blocks.init(index16Capacity, INDEX_3_18BIT_BLOCK_LENGTH);
        }

        // Compact the index-3 table and write an uncompacted version of the index-2 table.
        char[] index2 = new char[index2Capacity];
        int i2Length = 0;
        i3FirstNull = index3NullOffset;
        int index3Start = fastIndexLength + index1Length;
        int indexLength = index3Start;
        for (int i = iStart; i < iLimit; i += CodePointTrie.INDEX_3_BLOCK_LENGTH) {
            int i3;
            byte f = flags[i];
            if (f == I3_NULL && i3FirstNull < 0) {
                // First index-3 null block. Write & overlap it like a normal block, then remember it.
                f = dataNullOffset <= 0xffff ? I3_16 : I3_18;
                i3FirstNull = 0;
            }
            if (f == I3_NULL) {
                i3 = index3NullOffset;
            } else if (f == I3_BMP) {
                i3 = index[i];
            } else if (f == I3_16) {
                int n = mixedBlocks.findBlock(index16, index, i);
                if (n >= 0) {
                    i3 = n;
                } else {
                    if (indexLength == index3Start) {
                        // No overlap at the boundary between the index-1 and index-3 tables.
                        n = 0;
                    } else {
                        n = getOverlap(index16, indexLength,
                                       index, i, CodePointTrie.INDEX_3_BLOCK_LENGTH);
                    }
                    i3 = indexLength - n;
                    int prevIndexLength = indexLength;
                    while (n < CodePointTrie.INDEX_3_BLOCK_LENGTH) {
                        index16[indexLength++] = (char)index[i + n++];
                    }
                    mixedBlocks.extend(index16, index3Start, prevIndexLength, indexLength);
                    if (hasLongI3Blocks) {
                        longI3Blocks.extend(index16, index3Start, prevIndexLength, indexLength);
                    }
                }
            } else {
                assert(f == I3_18);
                assert(hasLongI3Blocks);
                // Encode an index-3 block that contains one or more data indexes exceeding 16 bits.
                int j = i;
                int jLimit = i + CodePointTrie.INDEX_3_BLOCK_LENGTH;
                int k = indexLength;
                do {
                    ++k;
                    int v = index[j++];
                    int upperBits = (v & 0x30000) >> 2;
                    index16[k++] = (char)v;
                    v = index[j++];
                    upperBits |= (v & 0x30000) >> 4;
                    index16[k++] = (char)v;
                    v = index[j++];
                    upperBits |= (v & 0x30000) >> 6;
                    index16[k++] = (char)v;
                    v = index[j++];
                    upperBits |= (v & 0x30000) >> 8;
                    index16[k++] = (char)v;
                    v = index[j++];
                    upperBits |= (v & 0x30000) >> 10;
                    index16[k++] = (char)v;
                    v = index[j++];
                    upperBits |= (v & 0x30000) >> 12;
                    index16[k++] = (char)v;
                    v = index[j++];
                    upperBits |= (v & 0x30000) >> 14;
                    index16[k++] = (char)v;
                    v = index[j++];
                    upperBits |= (v & 0x30000) >> 16;
                    index16[k++] = (char)v;
                    index16[k - 9] = (char)upperBits;
                } while (j < jLimit);
                int n = longI3Blocks.findBlock(index16, index16, indexLength);
                if (n >= 0) {
                    i3 = n | 0x8000;
                } else {
                    if (indexLength == index3Start) {
                        // No overlap at the boundary between the index-1 and index-3 tables.
                        n = 0;
                    } else {
                        n = getOverlap(index16, indexLength,
                                       index16, indexLength, INDEX_3_18BIT_BLOCK_LENGTH);
                    }
                    i3 = (indexLength - n) | 0x8000;
                    int prevIndexLength = indexLength;
                    if (n > 0) {
                        int start = indexLength;
                        while (n < INDEX_3_18BIT_BLOCK_LENGTH) {
                            index16[indexLength++] = index16[start + n++];
                        }
                    } else {
                        indexLength += INDEX_3_18BIT_BLOCK_LENGTH;
                    }
                    mixedBlocks.extend(index16, index3Start, prevIndexLength, indexLength);
                    if (hasLongI3Blocks) {
                        longI3Blocks.extend(index16, index3Start, prevIndexLength, indexLength);
                    }
                }
            }
            if (index3NullOffset < 0 && i3FirstNull >= 0) {
                index3NullOffset = i3;
            }
            // Set the index-2 table entry.
            index2[i2Length++] = (char)i3;
        }
        assert(i2Length == index2Capacity);
        assert(indexLength <= index3Start + index3Capacity);

        if (index3NullOffset < 0) {
            index3NullOffset = CodePointTrie.NO_INDEX3_NULL_OFFSET;
        }
        if (indexLength >= (CodePointTrie.NO_INDEX3_NULL_OFFSET + CodePointTrie.INDEX_3_BLOCK_LENGTH)) {
            // The index-3 offsets exceed 15 bits, or
            // the last one cannot be distinguished from the no-null-block value.
            throw new IndexOutOfBoundsException(
                    "The trie data exceeds limitations of the data structure.");
        }

        // Compact the index-2 table and write the index-1 table.
        // assert(CodePointTrie.INDEX_2_BLOCK_LENGTH == CodePointTrie.INDEX_3_BLOCK_LENGTH) :
        //     "must re-init mixedBlocks";
        int blockLength = CodePointTrie.INDEX_2_BLOCK_LENGTH;
        int i1 = fastIndexLength;
        for (int i = 0; i < i2Length; i += blockLength) {
            int n;
            if ((i2Length - i) >= blockLength) {
                // normal block
                assert(blockLength == CodePointTrie.INDEX_2_BLOCK_LENGTH);
                n = mixedBlocks.findBlock(index16, index2, i);
            } else {
                // highStart is inside the last index-2 block. Shorten it.
                blockLength = i2Length - i;
                n = findSameBlock(index16, index3Start, indexLength,
                        index2, i, blockLength);
            }
            int i2;
            if (n >= 0) {
                i2 = n;
            } else {
                if (indexLength == index3Start) {
                    // No overlap at the boundary between the index-1 and index-3/2 tables.
                    n = 0;
                } else {
                    n = getOverlap(index16, indexLength, index2, i, blockLength);
                }
                i2 = indexLength - n;
                int prevIndexLength = indexLength;
                while (n < blockLength) {
                    index16[indexLength++] = index2[i + n++];
                }
                mixedBlocks.extend(index16, index3Start, prevIndexLength, indexLength);
            }
            // Set the index-1 table entry.
            index16[i1++] = (char)i2;
        }
        assert(i1 == index3Start);
        assert(indexLength <= index16Capacity);

        return indexLength;
    }

    private int compactTrie(int fastILimit) {
        // Find the real highStart and round it up.
        assert((highStart & (CodePointTrie.CP_PER_INDEX_2_ENTRY - 1)) == 0);
        highValue = get(MAX_UNICODE);
        int realHighStart = findHighStart();
        realHighStart = (realHighStart + (CodePointTrie.CP_PER_INDEX_2_ENTRY - 1)) &
            ~(CodePointTrie.CP_PER_INDEX_2_ENTRY - 1);
        if (realHighStart == UNICODE_LIMIT) {
            highValue = initialValue;
        }

        // We always store indexes and data values for the fast range.
        // Pin highStart to the top of that range while building.
        int fastLimit = fastILimit << CodePointTrie.SHIFT_3;
        if (realHighStart < fastLimit) {
            for (int i = (realHighStart >> CodePointTrie.SHIFT_3); i < fastILimit; ++i) {
                flags[i] = ALL_SAME;
                index[i] = highValue;
            }
            highStart = fastLimit;
        } else {
            highStart = realHighStart;
        }

        int[] asciiData = new int[ASCII_LIMIT];
        for (int i = 0; i < ASCII_LIMIT; ++i) {
            asciiData[i] = get(i);
        }

        // First we look for which data blocks have the same value repeated over the whole block,
        // deduplicate such blocks, find a good null data block (for faster enumeration),
        // and get an upper bound for the necessary data array length.
        AllSameBlocks allSameBlocks = new AllSameBlocks();
        int newDataCapacity = compactWholeDataBlocks(fastILimit, allSameBlocks);
        int[] newData = Arrays.copyOf(asciiData, newDataCapacity);

        int dataNullIndex = allSameBlocks.findMostUsed();

        MixedBlocks mixedBlocks = new MixedBlocks();
        int newDataLength = compactData(fastILimit, newData, dataNullIndex, mixedBlocks);
        assert(newDataLength <= newDataCapacity);
        data = newData;
        dataLength = newDataLength;
        if (dataLength > (0x3ffff + CodePointTrie.SMALL_DATA_BLOCK_LENGTH)) {
            // The offset of the last data block is too high to be stored in the index table.
            throw new IndexOutOfBoundsException(
                    "The trie data exceeds limitations of the data structure.");
        }

        if (dataNullIndex >= 0) {
            dataNullOffset = index[dataNullIndex];
            initialValue = data[dataNullOffset];
        } else {
            dataNullOffset = CodePointTrie.NO_DATA_NULL_OFFSET;
        }

        int indexLength = compactIndex(fastILimit, mixedBlocks);
        highStart = realHighStart;
        return indexLength;
    }

    private CodePointTrie build(CodePointTrie.Type type, CodePointTrie.ValueWidth valueWidth) {
        // The mutable trie always stores 32-bit values.
        // When we build a UCPTrie for a smaller value width, we first mask off unused bits
        // before compacting the data.
        switch (valueWidth) {
        case BITS_32:
            break;
        case BITS_16:
            maskValues(0xffff);
            break;
        case BITS_8:
            maskValues(0xff);
            break;
        default:
            // Should be unreachable.
            throw new IllegalArgumentException();
        }

        int fastLimit = type == CodePointTrie.Type.FAST ? BMP_LIMIT : CodePointTrie.SMALL_LIMIT;
        int indexLength = compactTrie(fastLimit >> CodePointTrie.SHIFT_3);

        // Ensure data table alignment: The index length must be even for uint32_t data.
        if (valueWidth == CodePointTrie.ValueWidth.BITS_32 && (indexLength & 1) != 0) {
            index16[indexLength++] = 0xffee;  // arbitrary value
        }

        // Make the total trie structure length a multiple of 4 bytes by padding the data table,
        // and store special values as the last two data values.
        int length = indexLength * 2;
        if (valueWidth == CodePointTrie.ValueWidth.BITS_16) {
            if (((indexLength ^ dataLength) & 1) != 0) {
                // padding
                data[dataLength++] = errorValue;
            }
            if (data[dataLength - 1] != errorValue || data[dataLength - 2] != highValue) {
                data[dataLength++] = highValue;
                data[dataLength++] = errorValue;
            }
            length += dataLength * 2;
        } else if (valueWidth == CodePointTrie.ValueWidth.BITS_32) {
            // 32-bit data words never need padding to a multiple of 4 bytes.
            if (data[dataLength - 1] != errorValue || data[dataLength - 2] != highValue) {
                if (data[dataLength - 1] != highValue) {
                    data[dataLength++] = highValue;
                }
                data[dataLength++] = errorValue;
            }
            length += dataLength * 4;
        } else {
            int and3 = (length + dataLength) & 3;
            if (and3 == 0 && data[dataLength - 1] == errorValue && data[dataLength - 2] == highValue) {
                // all set
            } else if(and3 == 3 && data[dataLength - 1] == highValue) {
                data[dataLength++] = errorValue;
            } else {
                while (and3 != 2) {
                    data[dataLength++] = highValue;
                    and3 = (and3 + 1) & 3;
                }
                data[dataLength++] = highValue;
                data[dataLength++] = errorValue;
            }
            length += dataLength;
        }
        assert((length & 3) == 0);

        // Fill the index and data arrays.
        char[] trieIndex;
        if (highStart <= fastLimit) {
            // Condense only the fast index from the mutable-trie index.
            trieIndex = new char[indexLength];
            for (int i = 0, j = 0; j < indexLength; i += SMALL_DATA_BLOCKS_PER_BMP_BLOCK, ++j) {
                trieIndex[j] = (char)index[i];
            }
        } else {
            if (indexLength == index16.length) {
                trieIndex = index16;
                index16 = null;
            } else {
                trieIndex = Arrays.copyOf(index16, indexLength);
            }
        }

        // Write the data array.
        switch (valueWidth) {
        case BITS_16: {
            // Write 16-bit data values.
            char[] data16 = new char[dataLength];
            for (int i = 0; i < dataLength; ++i) { data16[i] = (char)data[i]; }
            return type == CodePointTrie.Type.FAST ?
                    new CodePointTrie.Fast16(trieIndex, data16, highStart,
                            index3NullOffset, dataNullOffset) :
                    new CodePointTrie.Small16(trieIndex, data16, highStart,
                            index3NullOffset, dataNullOffset);
        }
        case BITS_32: {
            // Write 32-bit data values.
            int[] data32 = Arrays.copyOf(data, dataLength);
            return type == CodePointTrie.Type.FAST ?
                    new CodePointTrie.Fast32(trieIndex, data32, highStart,
                            index3NullOffset, dataNullOffset) :
                    new CodePointTrie.Small32(trieIndex, data32, highStart,
                            index3NullOffset, dataNullOffset);
        }
        case BITS_8: {
            // Write 8-bit data values.
            byte[] data8 = new byte[dataLength];
            for (int i = 0; i < dataLength; ++i) { data8[i] = (byte)data[i]; }
            return type == CodePointTrie.Type.FAST ?
                    new CodePointTrie.Fast8(trieIndex, data8, highStart,
                            index3NullOffset, dataNullOffset) :
                    new CodePointTrie.Small8(trieIndex, data8, highStart,
                            index3NullOffset, dataNullOffset);
        }
        default:
            // Should be unreachable.
            throw new IllegalArgumentException();
        }
    }
}
