/*
 * Copyright 2011 JBoss, by Red Hat, Inc
 *
 * 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 org.jboss.errai.marshalling.server.marshallers;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.Charset;

import org.jboss.errai.codegen.meta.MetaField;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.common.client.protocols.SerializationParts;
import org.jboss.errai.marshalling.client.api.Marshaller;
import org.jboss.errai.marshalling.client.api.MarshallingSession;
import org.jboss.errai.marshalling.client.api.exceptions.MarshallingException;
import org.jboss.errai.marshalling.client.api.json.EJObject;
import org.jboss.errai.marshalling.client.api.json.EJValue;
import org.jboss.errai.marshalling.client.util.MarshallUtil;
import org.jboss.errai.marshalling.client.util.NumbersUtils;
import org.jboss.errai.marshalling.rebind.DefinitionsFactory;
import org.jboss.errai.marshalling.rebind.api.model.ConstructorMapping;
import org.jboss.errai.marshalling.rebind.api.model.FactoryMapping;
import org.jboss.errai.marshalling.rebind.api.model.InstantiationMapping;
import org.jboss.errai.marshalling.rebind.api.model.Mapping;
import org.jboss.errai.marshalling.rebind.api.model.MappingDefinition;
import org.jboss.errai.marshalling.rebind.api.model.MemberMapping;
import org.jboss.errai.marshalling.server.EncodingSession;
import org.jboss.errai.marshalling.server.MappingContextSingleton;
import org.jboss.errai.marshalling.server.api.ServerMarshaller;
import org.mvel2.DataConversion;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.Charset;

/**
 * @author Mike Brock
 */
public class DefaultDefinitionMarshaller implements ServerMarshaller<Object> {
  static final Charset UTF_8 = Charset.forName("UTF-8");

  private final MappingDefinition definition;

  public DefaultDefinitionMarshaller(final MappingDefinition definition) {
    this.definition = definition;
  }

  public static void setProperty(final Object i, final Field f, final Object v) {
    try {
      f.setAccessible(true);
      f.set(i, DataConversion.convert(v, f.getType()));
    }
    catch (Exception e) {
      throw new RuntimeException("could not set field (inst=" + i + "; field=" + f + "; val=" + v + ")", e);
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public Class<Object> getTypeHandled() {
    return (Class<Object>) definition.getMappingClass().asClass();
  }
  
  @Override
  public Object[] getEmptyArray() {
    return (Object[]) Array.newInstance(getTypeHandled(), 0);
  }

  @SuppressWarnings({"ConstantConditions", "unchecked"})
  @Override
  public Object demarshall(final EJValue o, final MarshallingSession ctx) {
    try {
      if (o.isObject() != null) {
        final EJObject oMap = o.isObject();
        final Object newInstance;

        if (MarshallUtil.isEncodedObject(oMap)) {
          if (MarshallUtil.isEncodedNumeric(oMap)) {
            return NumbersUtils.getEncodedNumber(oMap);
          }

          final String objID = oMap.get(SerializationParts.OBJECT_ID).isString().stringValue();

          if (ctx.hasObject(objID)) {
            newInstance = ctx.getObject(Object.class, objID);

            /**
             * If this only contains 2 fields, it is only a graph reference.
             */
            if (oMap.size() == 2) {
              return newInstance;
            }
          }
          else {
            /**
             * Check to see if this object is instantiate only... meaning it has no fields to marshall.
             */
            if (oMap.containsKey(SerializationParts.INSTANTIATE_ONLY)) {
              newInstance = getTypeHandled().newInstance();
              ctx.recordObject(objID, newInstance);
              return newInstance;
            }

            final InstantiationMapping cMapping = definition.getInstantiationMapping();
            final Object[] parms = new Object[cMapping.getMappings().length];
            final Class[] targetTypes = cMapping.getSignature();

            int i = 0;
            for (final Mapping mapping : cMapping.getMappings()) {
              final Marshaller<Object> marshaller = ctx.getMarshallerInstance(mapping.getType().getFullyQualifiedName());
              //noinspection unchecked
              parms[i] = DataConversion.convert(
                      marshaller.demarshall(oMap.get(mapping.getKey()), ctx), targetTypes[i++]);
            }

            if (cMapping instanceof ConstructorMapping) {
              final Constructor constructor = ((ConstructorMapping) cMapping).getMember().asConstructor();
              constructor.setAccessible(true);
              newInstance = constructor.newInstance(parms);
            }
            else {
              newInstance = ((FactoryMapping) cMapping).getMember().asMethod().invoke(null, parms);
            }

            ctx.recordObject(objID, newInstance);
          }

          for (final MemberMapping mapping : definition.getWritableMemberMappings()) {
            final EJValue o1 = oMap.get(mapping.getKey());

            if (!o1.isNull()) {
              final Marshaller<Object> marshaller
                      = ctx.getMarshallerInstance(mapping.getType().getFullyQualifiedName());

              if (mapping.getBindingMember() instanceof MetaField) {
                final MetaField f = (MetaField) mapping.getBindingMember();

                setProperty(newInstance, f.asField(),
                        marshaller.demarshall(o1, ctx));
              }
              else {
                final Method m = ((MetaMethod) mapping.getBindingMember()).asMethod();

                m.invoke(newInstance, DataConversion.convert(
                        marshaller.demarshall(o1, ctx),
                        m.getParameterTypes()[0]));
              }
            }
          }

          return newInstance;
        }
        else if (oMap.containsKey(SerializationParts.ENUM_STRING_VALUE)) {
          return Enum.valueOf(getClassReference(oMap),
                  oMap.get(SerializationParts.ENUM_STRING_VALUE).isString().stringValue());
        }
        else {
          throw new RuntimeException("bad payload");
        }
      }
      else {
        return o.getRawValue();
      }
    }
    catch (Exception e) {
      throw new MarshallingException("Failed to demarshall an instance of " + definition.getMappingClass(), e);
    }
  }

  @Override
  public String marshall(final Object o, final MarshallingSession ctx) {
    final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1024);
    try {
      marshall(byteArrayOutputStream, o, ctx);
      return new String(byteArrayOutputStream.toByteArray());
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }


  @Override
  public void marshall(final OutputStream outstream, final Object o, final MarshallingSession mSession) throws IOException {

    if (o == null) {
      outstream.write("null".getBytes(UTF_8));
      return;
    }

    final EncodingSession ctx = (EncodingSession) mSession;
    final Class cls = o.getClass();

    if (definition.getMappingClass().isEnum()) {
      final Enum enumer = (Enum) o;

      outstream.write(("{\"" + SerializationParts.ENCODED_TYPE + "\":\""
              + enumer.getDeclaringClass().getName() + "\""
              + ",\"" + SerializationParts.ENUM_STRING_VALUE + "\":\"" + enumer.name() + "\"}")
              .getBytes(UTF_8));

      return;
    }

    final boolean enc = ctx.hasObject(o);
    final String hash = ctx.getObject(o);

    if (enc) {
      /**
       * If this object is referencing a duplicate object in the graph, we only provide an ID reference.
       */

      outstream.write(("{\"" + SerializationParts.ENCODED_TYPE + "\":\"" + cls.getName()
              + "\",\"" + SerializationParts.OBJECT_ID + "\":\"" + hash + "\"}").getBytes(UTF_8));

      return;
    }

    int i = 0;
    boolean first = true;

    outstream.write(("{\"" + SerializationParts.ENCODED_TYPE + "\":\"" + cls.getName() + "\",\""
            + SerializationParts.OBJECT_ID + "\":\"" + hash + "\",").getBytes(UTF_8));

    for (final MemberMapping mapping : definition.getReadableMemberMappings()) {
      if (!first) {
        outstream.write(',');
      }

      i++;
      final Object v;

      if (mapping.getReadingMember() instanceof MetaField) {
        final Field field = ((MetaField) mapping.getReadingMember()).asField();
        field.setAccessible(true);

        try {
          v = field.get(o);
        }
        catch (Exception e) {
          throw new RuntimeException("error accessing field: " + field, e);
        }
      }
      else {
        final Method method = ((MetaMethod) mapping.getReadingMember()).asMethod();
        method.setAccessible(true);

        try {
          v = method.invoke(o);
        }
        catch (Exception e) {
          throw new RuntimeException("error calling getter: " + method, e);
        }
      }

      outstream.write(("\"" + mapping.getKey() + "\"").getBytes(UTF_8));
      outstream.write(':');

      if (v == null) {
        outstream.write("null".getBytes(UTF_8));
      }
      else {
        final DefinitionsFactory definitionsFactory = MappingContextSingleton.get().getDefinitionsFactory();

        if (definitionsFactory == null) {
          throw new RuntimeException("definition factory is null!");
        }

        final MappingDefinition definition1 = definitionsFactory.getDefinition(mapping.getType());

        if (definition1 == null) {
          throw new RuntimeException("no mapping definition for: " + mapping.getType().getFullyQualifiedName());
        }

        final Marshaller<Object> marshallerInstance = definition1.getMarshallerInstance();

        if (marshallerInstance == null) {
          throw new RuntimeException("no marshaller instance for: " + mapping.getType().getFullyQualifiedName());
        }

        outstream.write(
                marshallerInstance.marshall(v, ctx)
                        .getBytes(UTF_8)
        );
      }

      first = false;
    }

    if (i == 0) {
      outstream.write(("\"" + SerializationParts.INSTANTIATE_ONLY + "\":true").getBytes(UTF_8));
    }

    outstream.write('}');
  }

  public static Class getClassReference(final EJObject oMap) {
    try {
      return Thread.currentThread().getContextClassLoader()
              .loadClass(oMap.get(SerializationParts.ENCODED_TYPE).isString().stringValue());
    }
    catch (ClassNotFoundException e) {
      throw new RuntimeException("could not instantiate class", e);
    }
  }
}
