MessageUtil.java

/*
 * Copyright 2015-2016 Providence Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package net.morimekta.providence.util;

import net.morimekta.providence.PEnumValue;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageBuilder;
import net.morimekta.providence.PMessageOrBuilder;
import net.morimekta.providence.PType;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PEnumDescriptor;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PList;
import net.morimekta.providence.descriptor.PMap;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PSet;
import net.morimekta.util.Binary;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toList;
import static net.morimekta.util.Binary.fromBase64;
import static net.morimekta.util.Strings.isNullOrEmpty;

/**
 * Convenience methods for handling providence messages.
 */
public class MessageUtil {
    /**
     * Build all items of the collection containing message-or-builders. The list must not
     * contain any null items.
     *
     * @param builders List of message-or-builders.
     * @param <M> The message type.
     * @param <V> The actual value type.
     * @return List of messages or null if null input.
     */
    public static <M extends PMessage<M>, V extends PMessageOrBuilder<M>>
    List<M> toMessageAll(Collection<V> builders) {
        if (builders == null) {
            return null;
        }
        return builders.stream()
                       .map(PMessageOrBuilder::toMessage)
                       .collect(toList());
    }

    /**
     * Mutate all items of the collection containing messages. The list must not
     * contain any null items.
     *
     * @param messages List of messages
     * @param <M> The message type.
     * @param <V> The actual value type.
     * @param <B> The builder type.
     * @return List of builders or null if null input.
     */
    @SuppressWarnings("unchecked")
    public static <M extends PMessage<M>, V extends PMessageOrBuilder<M>, B extends PMessageBuilder<M>>
    List<B> toBuilderAll(Collection<V> messages) {
        if (messages == null) {
            return null;
        }
        return messages.stream()
                       .map(mob -> (B) mob.toBuilder())
                       .collect(toList());
    }

    /**
     * Mutate all values of the map containing message-or-builder values. The map must not
     * contain any null items.
     *
     * @param messages Map with message-or-builder values.
     * @param <K> The map key type.
     * @param <M> The message type.
     * @param <V> The actual value type.
     * @param <B> The builder type.
     * @return Map with builder values or null on null input.
     */
    @SuppressWarnings("unchecked")
    public static <K, M extends PMessage<M>, V extends PMessageOrBuilder<M>, B extends PMessageBuilder<M>>
    Map<K, B> toBuilderValues(Map<K, V> messages) {
        if (messages == null) {
            return null;
        }
        return messages.entrySet()
                       .stream()
                       .collect(Collectors.toMap(Map.Entry::getKey,
                                      e -> (B) e.getValue().toBuilder()));
    }

    /**
     * Mutate all items of the collection containing messages. The list must not
     * contain any null items.
     *
     * @param messages List of messages
     * @param <K> The map key type.
     * @param <M> The message type.
     * @param <V> The actual value type.
     * @return Map with message values, or null on null input.
     */
    public static <K, M extends PMessage<M>, V extends PMessageOrBuilder<M>>
    Map<K, M> toMessageValues(Map<K, V> messages) {
        if (messages == null) {
            return null;
        }
        return messages.entrySet()
                       .stream()
                       .collect(Collectors.toMap(Map.Entry::getKey,
                                      e -> e.getValue().toMessage()));
    }

    /**
     * Build the message from builder if it is not null.
     *
     * @param mob The builder to build.
     * @param <M> The message type.
     * @return The message or null if null input.
     */
    public static <M extends PMessage<M>>
    M toMessageIfNotNull(PMessageOrBuilder<M> mob) {
        if (mob == null) {
            return null;
        }
        return mob.toMessage();
    }

    /**
     * Mutate the message if it is not null.
     *
     * @param mob Message or builder to mutate.
     * @param <M> The message type.
     * @param <B> The builder type.
     * @return The builder or null if null input.
     */
    @SuppressWarnings("unchecked")
    public static <M extends PMessage<M>, B extends PMessageBuilder<M>>
    B toBuilderIfNonNull(PMessageOrBuilder<M> mob) {
        if (mob == null) {
            return null;
        }
        return (B) mob.toBuilder();
    }

    /**
     * Casting utility to make into a collection of message-or-builders.
     * This is basically a pure cast, but looks better than doing the cast
     * all over the place.
     *
     * @param items Collection of items to be generic cast.
     * @param <M> Message type.
     * @param <MB> Message-or-builder type.
     * @return The collection of message-or-builder type.
     */
    @SuppressWarnings("unchecked")
    public static <M extends PMessage<M>, MB extends PMessageOrBuilder<M>>
    Collection<PMessageOrBuilder<M>> toMessageOrBuilders(@Nonnull Collection<MB> items) {
        return (Collection<PMessageOrBuilder<M>>) items;
    }

    /**
     * Casting utility to make into a map of message-or-builders.
     * This is basically a pure cast, but looks better than doing the cast
     * all over the place.
     *
     * @param items Map of items to be generic cast.
     * @param <K> Map key type.
     * @param <M> Message type.
     * @param <MB> Message or builder type.
     * @return The map of message-or-builder type.
     */
    @SuppressWarnings("unchecked")
    public static  <K, M extends PMessage<M>, MB extends PMessageOrBuilder<M>>
    Map<K, PMessageOrBuilder<M>> toMessageOrBuilderValues(@Nonnull Map<K, MB> items) {
        return (Map<K, PMessageOrBuilder<M>>) items;
    }

    /**
     * Make a builder of the target message with all differences between
     * source and target marked as modifications.
     *
     * @param source The source message for changes.
     * @param target The message to apply said changes to.
     * @param <M> The message type.
     * @param <B> The builder result type.
     * @return Builder of target with marked modifications.
     */
    @SuppressWarnings("unchecked,rawtypes")
    public static <M extends PMessage<M>, B extends PMessageBuilder<M>>
    B getTargetModifications(PMessageOrBuilder<M> source, PMessageOrBuilder<M> target) {
        B builder;
        if (target instanceof PMessageBuilder) {
            builder = (B) ((PMessageBuilder<M>) target).build().mutate();
        } else {
            builder = (B) ((PMessage<M>) target).mutate();
        }
        for (PField field : source.descriptor().getFields()) {
            if (source.has(field) != target.has(field) ||
                !Objects.equals(source.get(field), target.get(field))) {
                builder.set(field, target.get(field));
            }
        }
        return builder;
    }

    /**
     * Convert a key path to a list of consecutive fields for recursive lookup.
     *
     * @param rootDescriptor The root message descriptor.
     * @param key            The '.' joined field name key.
     * @return Array of fields.
     */
    @Nonnull
    public static PField<?>[] keyPathToFields(@Nonnull PMessageDescriptor<?> rootDescriptor, @Nonnull String key) {
        ArrayList<PField<?>> fields = new ArrayList<>();
        String[]          parts  = key.split("\\.", Byte.MAX_VALUE);
        for (int i = 0; i < (parts.length - 1); ++i) {
            String name = parts[i];
            if (name.isEmpty()) {
                throw new IllegalArgumentException("Empty field name in '" + key + "'");
            }
            PField<?> field = rootDescriptor.findFieldByName(name);
            if (field == null) {
                throw new IllegalArgumentException(
                        "Message " + rootDescriptor.getQualifiedName() + " has no field named " + name);
            }
            if (field.getType() != PType.MESSAGE) {
                throw new IllegalArgumentException(
                        "Field '" + name + "' is not of message type in " + rootDescriptor.getQualifiedName());
            }
            fields.add(field);
            rootDescriptor = (PMessageDescriptor<?>) field.getDescriptor();
        }

        String name = parts[parts.length - 1];
        if (name.isEmpty()) {
            throw new IllegalArgumentException("Empty field name in '" + key + "'");
        }
        PField<?> field = rootDescriptor.findFieldByName(name);
        if (field == null) {
            throw new IllegalArgumentException(
                    "Message " + rootDescriptor.getQualifiedName() + " has no field named " + name);
        }
        fields.add(field);
        return fields.toArray(new PField[0]);
    }

    /**
     * Append field to the given path.
     *
     * @param fields Fields to make key path of.
     * @return The new appended key path.
     */
    public static String keyPath(@Nonnull PField<?>... fields) {
        if (fields.length == 0) throw new IllegalArgumentException("No field arguments");
        return Arrays.stream(fields).map(PField::getName).collect(Collectors.joining("."));
    }

    /**
     * Append field to the given path.
     *
     * @param path The path to be appended to.
     * @param field The field who's name should be appended.
     * @return The new appended key path.
     */
    public static String keyPathAppend(@Nullable String path, @Nonnull PField<?> field) {
        if (isNullOrEmpty(path)) {
            return field.getName();
        }
        return path + "." + field.getName();
    }

    /**
     * Look up a key in the message structure. If the key is not found, return the
     * default value for that field, and iterate over the fields until the last one.
     * <p>
     * This differs form {@link #optionalInMessage(PMessageOrBuilder, PField...)} by handling
     * the fields' default values.
     * <p>
     * <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
     * constant field enums, in that case you should use optional of the getter
     * and map until you have the last value, which should always return the
     * same, but is compile-time type safe. E.g.:
     *
     * <pre>{@code
     * Optional.ofNullable(message.getFirst())
     *         .map(First::getSecond)
     *         .map(Second::getThird)
     *         .orElse(myDefault);
     * }</pre>
     *
     * @param message The message to look up into.
     * @param fields  Field to get in order.
     * @param <T>     The expected leaf value type.
     * @return The value found or null.
     * @throws IllegalArgumentException When unable to get value from message.
     */
    @Nonnull
    @SuppressWarnings("unchecked,rawtypes")
    public static <T> Optional<T> getInMessage(@Nullable PMessageOrBuilder<?> message, @Nonnull PField<?>... fields) {
        if (fields.length == 0) {
            throw new IllegalArgumentException("No fields arguments");
        }
        PField<?> field = fields[0];
        if (fields.length > 1) {
            if (field.getType() != PType.MESSAGE) {
                throw new IllegalArgumentException("Intermediate field " + field.getName() + " is not a message");
            }
            return getInMessage(message == null ?
                                (PMessage<?>) field.getDefaultValue() :
                                Optional.ofNullable((PMessage<?>) message.get(field.getId()))
                                        .orElse((PMessage) field.getDefaultValue()),
                                Arrays.copyOfRange(fields, 1, fields.length));
        } else {
            return Optional.ofNullable(message == null ?
                                       (T) field.getDefaultValue() :
                                       Optional.ofNullable((T) message.get(field.getId()))
                                               .orElseGet(() -> (T) field.getDefaultValue()));
        }
    }

    /**
     * Get a field value from a message with optional chaining. If the field is
     * not set, or any message in the chain leading up to the last message is
     * missing, it will return an empty optional, otherwise the leaf field value.
     * Note that this will only check for MESSAGE type if the message is present
     * and needs to be looked up in.
     * <p>
     * This differs from {@link #getInMessage(PMessageOrBuilder, PField...)} by explicitly
     * <b>NOT</b> handling fields' default values.
     * <p>
     * <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
     * constant field enums, in that case you should use the optional getter
     * and flatMap until you have the last value, which should always return
     * the same, but is compile-time type safe. E.g.:
     *
     * <pre>{@code
     * message.optionalFirst()
     *        .flatMap(First::optionalSecond)
     *        .flatMap(Second::optionalThird)
     *        .orElse(myDefault);
     * }</pre>
     *
     * @param message The message to start looking up field values in.
     * @param fields  Fields to look up in the message.
     * @param <T>     The expected leaf value type.
     * @return Optional field value.
     */
    @Nonnull
    @SuppressWarnings("unchecked")
    public static <T> Optional<T> optionalInMessage(
            @Nullable PMessageOrBuilder<?> message,
            @Nonnull PField<?>... fields) {
        if (fields.length == 0) {
            throw new IllegalArgumentException("No fields arguments");
        }
        if (message == null) {
            return Optional.empty();
        }
        @SuppressWarnings("rawtypes")
        PField field = fields[0];
        if (!message.has(field.getId())) {
            return Optional.empty();
        }

        if (fields.length > 1) {
            if (field.getType() != PType.MESSAGE) {
                throw new IllegalArgumentException("Intermediate field " + field.getName() + " is not a message");
            }
            // Required to preserve generic typing.
            return optionalInMessage((PMessage<?>) message.get(field), Arrays.copyOfRange(fields, 1, fields.length));
        } else {
            return Optional.of(message.get(field.getId()));
        }
    }

    /**
     * Transform a message into a native map structure. This will make messages into {@link java.util.TreeMap}s,
     * maps and collections will be made into it's native mutable counterpart, and this will deeply transform
     * the message, so message fields will also be transformed, and values in maps and collection will too.
     * <p>
     * Note that some special cases will <b>not</b> be transformed, like messages and containers in map keys.
     *
     * @param message The message to be transformed.
     * @return The native map representing the message.
     * @deprecated Use {@link #messageToMap(PMessageOrBuilder)}. Will be removed in future major release.
     *             Function is renamed t
     */
    @Nonnull
    @Deprecated
    public static Map<String, Object> toMap(@Nonnull PMessageOrBuilder<?> message) {
        return messageToMap(message);
    }

    /**
     * Transform a message into a native map structure. This will make messages into {@link java.util.TreeMap}s,
     * maps and collections will be made into it's native mutable counterpart, and this will deeply transform
     * the message, so message fields will also be transformed, and values in maps and collection will too.
     * <p>
     * Note that some special cases will <b>not</b> be transformed, like messages and containers in map keys.
     *
     * @param message The message to be transformed.
     * @return The native map representing the message.
     */
    @Nonnull
    @SuppressWarnings("unchecked,rawtypes")
    public static Map<String, Object> messageToMap(@Nonnull PMessageOrBuilder<?> message) {
        TreeMap<String, Object> out = new TreeMap<>();
        for (PField field : message.descriptor().getFields()) {
            if (message.has(field)) {
                switch (field.getType()) {
                    case MESSAGE: {
                        // Required to preserve generic typing.
                        out.put(field.getName(), messageToMap((PMessage<?>) message.get(field)));
                        break;
                    }
                    case SET:
                    case LIST: {
                        // Required to preserve generic typing.
                        out.put(field.getName(), toCollectionInternal((Collection<Object>) message.get(field)));
                        break;
                    }
                    case MAP: {
                        // Required to preserve generic typing.
                        out.put(field.getName(), toMapInternal((Map<Object, Object>) message.get(field)));
                        break;
                    }
                    default: {
                        // Everything else is already using the best representative
                        // native value.
                        out.put(field.getName(), message.get(field));
                        break;
                    }
                }
            }
        }
        return out;
    }

    /**
     * Coerce value to match the given type descriptor.
     *
     * @param valueType The value type to coerce to.
     * @param value The value to be coerced.
     * @return The coerced value.
     */
    public static Optional<Object> coerce(@Nonnull PDescriptor valueType, Object value) {
        return coerceInternal(valueType, value, false);
    }

    /**
     * Coerce value to match the given type descriptor using struct type
     * checking. This means some loose coercion transitions are not allowed.
     *
     * @param valueType The value type to coerce to.
     * @param value The value to be coerced.
     * @return The coerced value.
     */
    public static Optional<Object> coerceStrict(@Nonnull PDescriptor valueType, Object value) {
        return coerceInternal(valueType, value, true);
    }

    // --------------------
    // ---   INTERNAL   ---
    // --------------------

    @SuppressWarnings("unchecked")
    private static Collection<Object> toCollectionInternal(Collection<Object> collection) {
        Collection<Object> out;
        if (collection instanceof SortedSet) {
            out = new TreeSet<>(((SortedSet<Object>) collection).comparator());
        } else if (collection instanceof Set) {
            out = new HashSet<>();
        } else {
            out = new ArrayList<>();
        }
        for (Object item : collection) {
            if (item instanceof PMessageOrBuilder) {
                out.add(messageToMap((PMessageOrBuilder<?>) item));
            } else if (item instanceof Collection) {
                out.add(toCollectionInternal((Collection<Object>) item));
            } else if (item instanceof Map) {
                out.add(toMapInternal((Map<Object, Object>) item));
            } else {
                out.add(item);
            }
        }

        return out;
    }

    // Visible for testing.
    @SuppressWarnings("unchecked")
    static Map<Object, Object> toMapInternal(Map<Object, Object> collection) {
        Map<Object, Object> out;
        if (collection instanceof SortedMap) {
            out = new TreeMap<>(((SortedMap<Object, Object>) collection).comparator());
        } else {
            out = new HashMap<>();
        }

        for (Map.Entry<Object, Object> item : collection.entrySet()) {
            Object value;
            if (item.getValue() instanceof PMessageOrBuilder) {
                value = toMap((PMessageOrBuilder<?>) item.getValue());
            } else if (item.getValue() instanceof Collection) {
                value = toCollectionInternal((Collection<Object>) item.getValue());
            } else if (item.getValue() instanceof Map) {
                value = toMapInternal((Map<Object, Object>) item.getValue());
            } else {
                value = item.getValue();
            }
            // Do not transform the key, as both sorting and equality are easily
            // messed up.
            out.put(item.getKey(), value);
        }

        return out;
    }

    @SuppressWarnings("unchecked")
    private static Optional<Object> coerceInternal(@Nonnull PDescriptor valueType, Object val, boolean strict) {
        if (val == null) {
            return Optional.empty();
        }
        switch (valueType.getType()) {
            case VOID:
                // Void does'nt really care about the value.
                if (val == Boolean.TRUE) {
                    return Optional.of(Boolean.TRUE);
                } else if (val == Boolean.FALSE) {
                    throw new IllegalArgumentException("Invalid void value " + val.toString());
                }
                break;
            case BOOL:
                if (val instanceof Boolean) {
                    return Optional.of(val);
                } else if (val instanceof Number && !(val instanceof Float) && !(val instanceof Double)) {
                    return Optional.of(((Number) val).longValue() != 0L);
                } else if (val instanceof PEnumValue) {  // e.g. enum
                    return Optional.of(((PEnumValue<?>) val).asInteger() != 0);
                } else if (strict) {
                    break;
                } else if (val instanceof CharSequence) {
                    switch (val.toString().toLowerCase(Locale.US)) {
                        case "true":
                        case "t":
                        case "yes":
                        case "y":
                        case "1":
                            return Optional.of(Boolean.TRUE);
                        case "false":
                        case "f":
                        case "no":
                        case "n":
                        case "0":
                            return Optional.of(Boolean.FALSE);
                    }
                    throw new IllegalArgumentException("Unknown boolean value for string '" + val + "'");
                }
                break;
            case BYTE:
                if (val instanceof Number) {
                    return Optional.of((byte) asInteger(valueType, (Number) val, Byte.MIN_VALUE, Byte.MAX_VALUE));
                } else if (val instanceof Boolean) {
                    return Optional.of((Boolean) val ? (byte) 1 : (byte) 0);
                } else if (val instanceof PEnumValue) {  // e.g. enum
                    return Optional.of((byte) asInteger(valueType,
                                                        ((PEnumValue<?>) val).asInteger(),
                                                        Byte.MIN_VALUE,
                                                        Byte.MAX_VALUE));
                } else if (strict) {
                    break;
                } else if (val instanceof CharSequence) {
                    try {
                        CharSequence cs = (CharSequence) val;
                        if (HEX.matcher(cs).matches()) {
                            return Optional.of(Byte.parseByte(cs.subSequence(2, cs.length()).toString(), 16));
                        } else {
                            return Optional.of(Byte.parseByte(val.toString()));
                        }
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException("Invalid string value '" + val + "' for type byte", e);
                    }
                }
                break;
            case I16:
                if (val instanceof Number) {
                    return Optional.of((short) asInteger(valueType, (Number) val, Short.MIN_VALUE, Short.MAX_VALUE));
                } else if (val instanceof Boolean) {
                    return Optional.of((Boolean) val ? (short) 1 : (short) 0);
                } else if (val instanceof PEnumValue) {  // e.g. enum
                    return Optional.of((short) asInteger(valueType,
                                                         ((PEnumValue<?>) val).asInteger(),
                                                         Short.MIN_VALUE,
                                                         Short.MAX_VALUE));
                } else if (strict) {
                    break;
                } else if (val instanceof CharSequence) {
                    try {
                        CharSequence cs = (CharSequence) val;
                        if (HEX.matcher(cs).matches()) {
                            return Optional.of(Short.parseShort(cs.subSequence(2, cs.length()).toString(), 16));
                        } else {
                            return Optional.of(Short.parseShort(val.toString()));
                        }
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException("Invalid string value '" + val + "' for type i16", e);
                    }
                }
                break;
            case I32:
                if (val instanceof Number) {
                    return Optional.of(asInteger(valueType, (Number) val, Integer.MIN_VALUE, Integer.MAX_VALUE));
                } else if (val instanceof Boolean) {
                    return Optional.of((Boolean) val ? 1 : 0);
                } else if (val instanceof PEnumValue) {  // e.g. enum
                    return Optional.of(((PEnumValue<?>) val).asInteger());
                } else if (strict) {
                    break;
                } else if (val instanceof CharSequence) {
                    try {
                        CharSequence cs = (CharSequence) val;
                        if (HEX.matcher(cs).matches()) {
                            return Optional.of(Integer.parseInt(cs.subSequence(2, cs.length()).toString(), 16));
                        } else {
                            return Optional.of(Integer.parseInt(val.toString()));
                        }
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException("Invalid string value '" + val + "' for type i32", e);
                    }
                }
                break;
            case I64:
                if (val instanceof Float || val instanceof Double) {
                    long l = ((Number) val).longValue();
                    if ((double) l != ((Number) val).doubleValue()) {
                        throw new IllegalArgumentException("Truncating long decimals from " + val.toString());
                    }
                    return Optional.of(l);
                } else if (val instanceof Number) {
                    return Optional.of(((Number) val).longValue());
                } else if (val instanceof Boolean) {
                    return Optional.of((Boolean) val ? 1L : 0L);
                } else if (val instanceof PEnumValue) {  // e.g. enum
                    return Optional.of((long) ((PEnumValue<?>) val).asInteger());
                } else if (strict) {
                    break;
                } else if (val instanceof CharSequence) {
                    try {
                        CharSequence cs = (CharSequence) val;
                        if (HEX.matcher(cs).matches()) {
                            return Optional.of(Long.parseLong(cs.subSequence(2, cs.length()).toString(), 16));
                        } else {
                            return Optional.of(Long.parseLong(val.toString()));
                        }
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException("Invalid string value '" + val + "' for type i64", e);
                    }
                }
                break;
            case DOUBLE:
                if (val instanceof Number) {
                    return Optional.of(((Number) val).doubleValue());
                } else if (strict) {
                    break;
                } else if (val instanceof PEnumValue) {
                    return Optional.of((double) ((PEnumValue<?>) val).asInteger());
                }
                break;
            case STRING:
                if (val instanceof CharSequence) {
                    return Optional.of(val.toString());
                } else if (strict) {
                    break;
                } else if (val instanceof PEnumValue) {
                    return Optional.of(((PEnumValue<?>) val).asString());
                } else {
                    return Optional.of(val.toString());
                }
            case BINARY:
                if (val instanceof Binary) {
                    return Optional.of(val);
                } else if (strict) {
                    break;
                } else if (val instanceof CharSequence) {
                    return Optional.of(fromBase64(val.toString()));
                }
                break;
            case ENUM: {
                PEnumDescriptor<?> ed = (PEnumDescriptor<?>) valueType;
                if (val instanceof PEnumValue) {
                    PEnumValue<?> verified = ((PEnumDescriptor<?>) valueType).findById(((PEnumValue<?>) val).asInteger());
                    if (val.equals(verified)) {
                        return Optional.of(verified);
                    }
                } else if (val instanceof Number && !(val instanceof Double) && !(val instanceof Float)) {
                    int        i  = ((Number) val).intValue();
                    PEnumValue<?> ev = ed.findById(i);
                    if (ev != null) {
                        return Optional.of(ev);
                    }
                    throw new IllegalArgumentException("Unknown " + valueType.getQualifiedName() +
                                                       " value for id " + val.toString());
                } else if (val instanceof CharSequence) {
                    CharSequence cs = (CharSequence) val;
                    if (!strict && UNSIGNED.matcher(cs).matches()) {
                        int        i  = Integer.parseInt(val.toString());
                        PEnumValue<?> ev = ed.findById(i);
                        if (ev != null) {
                            return Optional.of(ev);
                        }
                    } else if (!strict && HEX.matcher(cs).matches()) {
                        int        i  = Integer.parseInt(cs.subSequence(2, cs.length()).toString(), 16);
                        PEnumValue<?> ev = ed.findById(i);
                        if (ev != null) {
                            return Optional.of(ev);
                        }
                    } else {
                        PEnumValue<?> ev = ed.findByName(val.toString());
                        if (ev != null) {
                            return Optional.of(ev);
                        }
                    }
                    throw new IllegalArgumentException("Unknown " + valueType.getQualifiedName() +
                                                       " value for string '" + val.toString() + "'");
                }
                throw new IllegalArgumentException(
                        "Invalid value type " + val.getClass() + " for enum " + valueType.toString());
            }
            case MESSAGE: {
                if (val instanceof PMessage) {
                    if (valueType.equals(((PMessage<?>) val).descriptor())) {
                        return Optional.of(val);
                    } else {
                        throw new IllegalArgumentException(
                                "Unable to cast message type " +
                                ((PMessage<?>) val).descriptor().getQualifiedName() + " to " +
                                valueType.getQualifiedName());

                    }
                } else if (val instanceof PMessageBuilder) {
                    if (valueType.equals(((PMessageBuilder<?>) val).descriptor())) {
                        return Optional.of(((PMessageBuilder<?>) val).build());
                    } else {
                        throw new IllegalArgumentException(
                                "Unable to cast message type " +
                                ((PMessageBuilder<?>) val).descriptor().getQualifiedName() + " to " +
                                valueType.getQualifiedName());
                    }
                } else if (!strict && val instanceof Map) {
                    PMessageDescriptor<?> md      = (PMessageDescriptor<?>) valueType;
                    PMessageBuilder<?>    builder = md.builder();
                    for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) val).entrySet()) {
                        if (!(entry.getKey() instanceof CharSequence)) {
                            throw new IllegalArgumentException("Invalid message map key: " + entry.getKey().toString());
                        }
                        PField<?> field = md.findFieldByName(entry.getKey().toString());
                        if (field == null) {
                            throw new IllegalArgumentException(
                                    "No such field " + entry.getKey() + " in " + md.getQualifiedName());
                        }
                        builder.set(field.getId(),
                                    coerceInternal(field.getDescriptor(), entry.getValue(), false).orElse(null));
                    }
                    return Optional.of(builder.build());
                }
                throw new IllegalArgumentException(
                        "Invalid value type " + val.getClass() + " for message " + valueType.toString());
            }
            case LIST: {
                if (val instanceof Collection) {
                    PList<Object>         pl      = (PList<Object>) valueType;
                    PList.Builder<Object> builder = pl.builder(((Collection<?>) val).size());
                    for (Object o : (Collection<?>) val) {
                        Object value = coerceInternal(pl.itemDescriptor(), o, strict).orElse(null);
                        if (value != null) {
                            builder.add(value);
                        } else if (strict) {
                            throw new IllegalArgumentException("Null value in list");
                        }
                    }
                    return Optional.of(builder.build());
                }
                throw new IllegalArgumentException(
                        "Invalid value type " + val.getClass() + " for " + valueType.toString());
            }
            case SET:
                if (val instanceof Collection) {
                    PSet<Object>         pl      = (PSet<Object>) valueType;
                    PSet.Builder<Object> builder = pl.builder(((Collection<?>) val).size());
                    for (Object o : (Collection<?>) val) {
                        Object value = coerceInternal(pl.itemDescriptor(), o, strict).orElse(null);
                        if (value != null) {
                            builder.add(value);
                        } else if (strict) {
                            throw new IllegalArgumentException("Null value in set");
                        }
                    }
                    return Optional.of(builder.build());
                }
                throw new IllegalArgumentException(
                        "Invalid value type " + val.getClass() + " for " + valueType.toString());
            case MAP:
                if (val instanceof Map) {
                    PMap<Object, Object>         pl      = (PMap<Object, Object>) valueType;
                    PMap.Builder<Object, Object> builder = pl.builder(((Map<?,?>) val).size());
                    for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) val).entrySet()) {
                        Object key   = coerceInternal(pl.keyDescriptor(), entry.getKey(), strict).orElse(null);
                        Object value = coerceInternal(pl.itemDescriptor(), entry.getValue(), strict).orElse(null);
                        if (key != null && value != null) {
                            builder.put(key, value);
                        } else if (strict) {
                            throw new IllegalArgumentException("Null key or value in map");
                        }
                    }
                    return Optional.of(builder.build());
                }
                throw new IllegalArgumentException(
                        "Invalid value type " + val.getClass() + " for " + valueType.toString());
        }
        throw new IllegalArgumentException("Invalid value type " + val.getClass() + " for type " + valueType.getType());
    }

    private static int asInteger(PDescriptor descriptor, Number value, int min, int max) {
        if (value instanceof Float || value instanceof Double) {
            long l = value.longValue();
            if ((double) l != value.doubleValue()) {
                throw new IllegalArgumentException(
                        "Truncating " + descriptor.getName() + " decimals from " + value.toString());
            }
            return validateInRange(descriptor.getName(), l, min, max);
        } else {
            return validateInRange(descriptor.getName(), value.longValue(), min, max);
        }
    }

    private static int validateInRange(String type, long l, int min, int max) throws IllegalArgumentException {
        if (l < min) {
            throw new IllegalArgumentException(type + " value outside of bounds: " + l + " < " + min);
        } else if (l > max) {
            throw new IllegalArgumentException(type + " value outside of bounds: " + l + " > " + max);
        }
        return (int) l;
    }

    private static final Pattern UNSIGNED = Pattern.compile("(0|[1-9][0-9]*)");
    private static final Pattern HEX      = Pattern.compile("0x[0-9a-fA-F]+");
    private MessageUtil() {}
}