/*
 * 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 org.apache.servicecomb.foundation.protobuf.internal.schema.serializer;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.servicecomb.foundation.common.concurrent.ConcurrentHashMapEx;
import org.apache.servicecomb.foundation.protobuf.ProtoMapper;
import org.apache.servicecomb.foundation.protobuf.internal.ProtoUtils;
import org.apache.servicecomb.foundation.protobuf.internal.bean.BeanDescriptor;
import org.apache.servicecomb.foundation.protobuf.internal.bean.PropertyDescriptor;

import com.fasterxml.jackson.databind.JavaType;

import io.protostuff.InputEx;
import io.protostuff.OutputEx;
import io.protostuff.SchemaEx;
import io.protostuff.compiler.model.Field;
import io.protostuff.compiler.model.Message;
import io.protostuff.runtime.FieldMapEx;
import io.protostuff.runtime.FieldSchema;

/**
 * <pre>
 * map.put("user", new User())
 * root write from map, but user should write from pojo
 * so one schema should support dynamic and concrete logic at the same time
 * </pre>
 */
public class MessageWriteSchema<T> implements SchemaEx<T> {
  protected final ProtoMapper protoMapper;

  protected final Message message;

  private final JavaType javaType;

  // mostly, one message only relate to one pojo
  private final Class<T> mainPojoCls;

  private FieldMapEx<T> mainPojoFieldMaps;

  private FieldMapEx<Map<Object, Object>> mapFieldMaps;

  // if not equals to mainPojoCls, then will find from pojoFieldMaps
  private final Map<Class<?>, FieldMapEx<?>> pojoFieldMaps = new ConcurrentHashMapEx<>();

  @SuppressWarnings("unchecked")
  public MessageWriteSchema(ProtoMapper protoMapper, Message message, JavaType javaType) {
    this.protoMapper = protoMapper;
    this.message = message;
    this.javaType = javaType;
    this.mainPojoCls = (Class<T>) javaType.getRawClass();
  }

  public Message getMessage() {
    return message;
  }

  @Override
  public T newMessage() {
    throw new UnsupportedOperationException();
  }

  @Override
  public String messageName() {
    return message.getName();
  }

  public JavaType getJavaType() {
    return javaType;
  }

  public Class<T> getMainPojoCls() {
    return mainPojoCls;
  }

  public FieldMapEx<T> getMainPojoFieldMaps() {
    return mainPojoFieldMaps;
  }

  @Override
  public void init() {
    this.mapFieldMaps = protoMapper.getSerializerSchemaManager().createMapFields(message);

    if (ProtoUtils.isWrapProperty(message)) {
      this.mainPojoFieldMaps = createPropertyWrapperFields(javaType);
      return;
    }

    this.mainPojoFieldMaps = createPojoFields(javaType);
  }

  private FieldMapEx<T> createPropertyWrapperFields(JavaType javaType) {
    Field protoField = message.getField(1);

    PropertyDescriptor propertyDescriptor = new PropertyDescriptor();
    propertyDescriptor.setName(protoField.getName());
    propertyDescriptor.setJavaType(javaType);

    FieldSchema<T> fieldSchema = protoMapper.getSerializerSchemaManager()
        .createSchemaField(protoField, propertyDescriptor);
    return FieldMapEx.createFieldMap(Arrays.asList(fieldSchema));
  }

  private FieldMapEx<T> createPojoFields(Type type) {
    SerializerSchemaManager serializerSchemaManager = protoMapper.getSerializerSchemaManager();
    BeanDescriptor beanDescriptor = protoMapper.getBeanDescriptorManager().getOrCreateBeanDescriptor(type);

    List<FieldSchema<T>> fieldSchemas = new ArrayList<>();
    for (Field protoField : message.getFields()) {
      PropertyDescriptor propertyDescriptor = beanDescriptor.getPropertyDescriptors().get(protoField.getName());
      if (propertyDescriptor == null) {
        continue;
      }

      Object getter = propertyDescriptor.getGetter();
      if (getter == null) {
        continue;
      }

      FieldSchema<T> fieldSchema = serializerSchemaManager.createSchemaField(protoField, propertyDescriptor);
      fieldSchemas.add(fieldSchema);
    }

    return FieldMapEx.createFieldMap(fieldSchemas);
  }

  @SuppressWarnings("unchecked")
  @Override
  public void writeTo(OutputEx output, Object value) throws IOException {
    if (value instanceof Map) {
      writeFromMap(output, (Map<String, Object>) value);
      return;
    }

    if (mainPojoCls == value.getClass()) {
      writeFromMainPojo(output, (T) value);
      return;
    }

    writeDynamicPojo(output, value);
  }

  private void writeFromMainPojo(OutputEx output, T value) throws IOException {
    for (FieldSchema<T> fieldSchema : mainPojoFieldMaps.getFields()) {
      fieldSchema.getAndWriteTo(output, value);
    }
  }

  @SuppressWarnings("unchecked")
  private <T> void writeDynamicPojo(OutputEx output, Object dynamicValue) throws IOException {
    FieldMapEx<T> fieldMapEx = (FieldMapEx<T>) this.pojoFieldMaps
        .computeIfAbsent(dynamicValue.getClass(), this::createPojoFields);

    T value = (T) dynamicValue;
    for (FieldSchema<T> fieldSchema : fieldMapEx.getFields()) {
      fieldSchema.getAndWriteTo(output, value);
    }
  }

  protected final void writeFromMap(OutputEx output, Map<String, Object> map) throws IOException {
    for (Entry<String, Object> entry : map.entrySet()) {
      if (entry.getValue() == null) {
        continue;
      }

      FieldSchema<Map<Object, Object>> fieldSchema = mapFieldMaps.getFieldByName(entry.getKey());
      if (fieldSchema != null) {
        fieldSchema.writeTo(output, entry.getValue());
      }
    }
  }

  @Override
  public void mergeFrom(InputEx input, T message) throws IOException {
    throw new UnsupportedOperationException();
  }
}
