/*
 * Copyright 2016 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.cloud.logging;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.api.client.util.Types;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.protobuf.ListValue;
import com.google.protobuf.NullValue;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * This class contains static utility methods that operate on or return protobuf's {@code Struct}
 * objects.
 */
final class Structs {

  private static final Function<Value, Object> VALUE_TO_OBJECT = new Function<Value, Object>() {
    @Override
    public Object apply(Value value) {
      return valueToObject(value);
    }
  };
  private static final Function<Object, Value> OBJECT_TO_VALUE = new Function<Object, Value>() {
    @Override
    public Value apply(Object obj) {
      return objectToValue(obj);
    }
  };

  private Structs() {}

  /**
   * This class wraps a protobuf's {@code Struct} object and offers a map interface to it, hiding
   * protobuf types.
   */
  private static final class StructMap extends AbstractMap<String, Object> {

    private final Set<Entry<String, Object>> entrySet;

    private StructMap(Struct struct) {
      this.entrySet = new StructSet(struct);
    }

    private static final class StructSet extends AbstractSet<Entry<String, Object>> {

      private static final Function<Map.Entry<String, Value>, Map.Entry<String, Object>>
          VALUE_TO_OBJECT = new Function<Map.Entry<String, Value>, Map.Entry<String, Object>>() {
            @Override
            public Map.Entry<String, Object> apply(Map.Entry<String, Value> entry) {
              return new AbstractMap.SimpleEntry<>(entry.getKey(), valueToObject(entry.getValue()));
            }
          };

      private final Struct struct;

      private StructSet(Struct struct) {
        this.struct = struct;
      }

      @Override
      public Iterator<Entry<String, Object>> iterator() {
        return Iterators.transform(struct.getFieldsMap().entrySet().iterator(), VALUE_TO_OBJECT);
      }

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

    @Override
    public Set<Entry<String, Object>> entrySet() {
      return entrySet;
    }
  }

  /**
   * Returns an unmodifiable map view of the {@link Struct} parameter.
   */
  static Map<String, Object> asMap(Struct struct) {
    return new StructMap(checkNotNull(struct));
  }

  /**
   * Creates a new {@link Struct} object given the content of the provided {@code map} parameter.
   *
   * <p>Notice that all numbers (int, long, float and double) are serialized as double values.
   * Enums are serialized as strings.
   */
  static Struct newStruct(Map<String, ?> map) {
    Map<String, Value> valueMap = Maps.transformValues(checkNotNull(map), OBJECT_TO_VALUE);
    return Struct.newBuilder().putAllFields(valueMap).build();
  }

  private static Object valueToObject(Value value) {
    switch (value.getKindCase()) {
      case NULL_VALUE:
        return null;
      case NUMBER_VALUE:
        return value.getNumberValue();
      case STRING_VALUE:
        return value.getStringValue();
      case BOOL_VALUE:
        return value.getBoolValue();
      case STRUCT_VALUE:
        return new StructMap(value.getStructValue());
      case LIST_VALUE:
        return Lists.transform(value.getListValue().getValuesList(), VALUE_TO_OBJECT);
      default:
        throw new IllegalArgumentException(String.format("Unsupported protobuf value %s", value));
    }
  }

  @SuppressWarnings("unchecked")
  private static Value objectToValue(final Object obj) {
    Value.Builder builder = Value.newBuilder();
    if (obj == null) {
      builder.setNullValue(NullValue.NULL_VALUE);
      return builder.build();
    }
    Class<?> objClass = obj.getClass();
    if (obj instanceof String) {
      builder.setStringValue((String) obj);
    } else if (obj instanceof Number) {
      builder.setNumberValue(((Number) obj).doubleValue());
    } else if (obj instanceof Boolean) {
      builder.setBoolValue((Boolean) obj);
    } else if (obj instanceof Iterable<?> || objClass.isArray()) {
      builder.setListValue(ListValue.newBuilder()
          .addAllValues(Iterables.transform(Types.iterableOf(obj), OBJECT_TO_VALUE)));
    } else if (objClass.isEnum()) {
      builder.setStringValue(((Enum<?>) obj).name());
    } else if (obj instanceof Map) {
      Map<String, Object> map = (Map<String, Object>) obj;
      builder.setStructValue(newStruct(map));
    } else {
      throw new IllegalArgumentException(String.format("Unsupported protobuf value %s", obj));
    }
    return builder.build();
  }
}
