/*
 * Copyright 2000-2015 JetBrains s.r.o.
 *
 * 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.intellij.util.xmlb;

import com.intellij.openapi.util.JDOMExternalizableStringList;
import com.intellij.openapi.util.Pair;
import com.intellij.util.xmlb.annotations.CollectionBean;
import org.jdom.Content;
import org.jdom.Element;
import org.jdom.Text;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

class XmlSerializerImpl {
  private static Reference<Map<Pair<Type, MutableAccessor>, Binding>> ourBindings;

  @NotNull
  static Element serialize(@NotNull Object object, @NotNull SerializationFilter filter) throws XmlSerializationException {
    try {
      Class<?> aClass = object.getClass();
      Binding binding = getClassBinding(aClass, aClass, null);
      if (binding instanceof BeanBinding) {
        // top level expects not null (null indicates error, empty element will be omitted)
        return ((BeanBinding)binding).serialize(object, true, filter);
      }
      else {
        //noinspection ConstantConditions
        return (Element)binding.serialize(object, null, filter);
      }
    }
    catch (XmlSerializationException e) {
      throw e;
    }
    catch (Exception e) {
      throw new XmlSerializationException("Can't serialize instance of " + object.getClass(), e);
    }
  }

  @Nullable
  static Element serializeIfNotDefault(@NotNull Object object, @NotNull SerializationFilter filter) {
    Class<?> aClass = object.getClass();
    Binding binding = getClassBinding(aClass, aClass, null);
    assert binding != null;
    return (Element)binding.serialize(object, null, filter);
  }

  @Nullable
  static Binding getBinding(@NotNull Type type) {
    return getClassBinding(typeToClass(type), type, null);
  }

  @Nullable
  static Binding getBinding(@NotNull MutableAccessor accessor) {
    Type type = accessor.getGenericType();
    return getClassBinding(typeToClass(type), type, accessor);
  }

  @NotNull
  static Class<?> typeToClass(@NotNull Type type) {
    if (type instanceof Class) {
      return (Class<?>)type;
    }
    else if (type instanceof TypeVariable) {
      Type bound = ((TypeVariable)type).getBounds()[0];
      return bound instanceof Class ? (Class)bound : (Class<?>)((ParameterizedType)bound).getRawType();
    }
    else if (type instanceof WildcardType) {
      return (Class<?>)((WildcardType) type).getUpperBounds()[0];
    }
    else {
      return (Class<?>)((ParameterizedType)type).getRawType();
    }
  }

  @Nullable
  static synchronized Binding getClassBinding(@NotNull Class<?> aClass, @NotNull Type originalType, @Nullable MutableAccessor accessor) {
    if (aClass.isPrimitive() ||
        aClass == String.class ||
        aClass == Integer.class ||
        aClass == Long.class ||
        aClass == Boolean.class ||
        aClass == Double.class ||
        aClass == Float.class ||
        aClass.isEnum() ||
        Date.class.isAssignableFrom(aClass)) {
      return null;
    }

    Pair<Type, MutableAccessor> key = Pair.create(originalType, accessor);
    Map<Pair<Type, MutableAccessor>, Binding> map = getBindingCacheMap();
    Binding binding = map.get(key);
    if (binding == null) {
      binding = getNonCachedClassBinding(aClass, accessor, originalType);
      map.put(key, binding);
      try {
        binding.init(originalType);
      }
      catch (XmlSerializationException e) {
        map.remove(key);
        throw e;
      }
    }
    return binding;
  }

  @NotNull
  private static Map<Pair<Type, MutableAccessor>, Binding> getBindingCacheMap() {
    Map<Pair<Type, MutableAccessor>, Binding> map = com.intellij.reference.SoftReference.dereference(ourBindings);
    if (map == null) {
      map = new ConcurrentHashMap<Pair<Type, MutableAccessor>, Binding>();
      ourBindings = new SoftReference<Map<Pair<Type, MutableAccessor>, Binding>>(map);
    }
    return map;
  }

  @NotNull
  private static Binding getNonCachedClassBinding(@NotNull Class<?> aClass, @Nullable MutableAccessor accessor, @NotNull Type originalType) {
    if (aClass.isArray()) {
      if (Element.class.isAssignableFrom(aClass.getComponentType())) {
        assert accessor != null;
        return new JDOMElementBinding(accessor);
      }
      else {
        return new ArrayBinding(aClass, accessor);
      }
    }
    if (Collection.class.isAssignableFrom(aClass) && originalType instanceof ParameterizedType) {
      if (accessor != null) {
        CollectionBean listBean = accessor.getAnnotation(CollectionBean.class);
        if (listBean != null) {
          return new CompactCollectionBinding(accessor);
        }
      }
      return new CollectionBinding((ParameterizedType)originalType, accessor);
    }
    if (accessor != null) {
      if (Map.class.isAssignableFrom(aClass) && originalType instanceof ParameterizedType) {
        return new MapBinding(accessor);
      }
      if (Element.class.isAssignableFrom(aClass)) {
        return new JDOMElementBinding(accessor);
      }
      //noinspection deprecation
      if (JDOMExternalizableStringList.class == aClass) {
        return new CompactCollectionBinding(accessor);
      }
    }
    return new BeanBinding(aClass, accessor);
  }

  @Nullable
  static Object convert(@Nullable String value, @NotNull Class<?> valueClass) {
    if (value == null) {
      return null;
    }
    else if (valueClass == String.class) {
      return value;
    }
    else if (valueClass == int.class || valueClass == Integer.class) {
      return Integer.parseInt(value);
    }
    else if (valueClass == boolean.class || valueClass == Boolean.class) {
      return Boolean.parseBoolean(value);
    }
    else if (valueClass == double.class || valueClass == Double.class) {
      return Double.parseDouble(value);
    }
    else if (valueClass == float.class || valueClass == Float.class) {
      return Float.parseFloat(value);
    }
    else if (valueClass == long.class || valueClass == Long.class) {
      return Long.parseLong(value);
    }
    else if (valueClass.isEnum()) {
      for (Object enumConstant : valueClass.getEnumConstants()) {
        if (enumConstant.toString().equals(value)) {
          return enumConstant;
        }
      }
      return null;
    }
    else if (Date.class.isAssignableFrom(valueClass)) {
      try {
        return new Date(Long.parseLong(value));
      }
      catch (NumberFormatException e) {
        return new Date(0);
      }
    }
    else {
      return value;
    }
  }

  static void doSet(@NotNull Object host, @Nullable String value, @NotNull MutableAccessor accessor, @NotNull Class<?> valueClass) {
    if (value == null) {
      accessor.set(host, null);
    }
    else if (valueClass == String.class) {
      accessor.set(host, value);
    }
    else if (valueClass == int.class) {
      accessor.setInt(host, Integer.parseInt(value));
    }
    else if (valueClass == boolean.class) {
      accessor.setBoolean(host, Boolean.parseBoolean(value));
    }
    else if (valueClass == double.class) {
      accessor.setDouble(host, Double.parseDouble(value));
    }
    else if (valueClass == float.class) {
      accessor.setFloat(host, Float.parseFloat(value));
    }
    else if (valueClass == long.class) {
      accessor.setLong(host, Long.parseLong(value));
    }
    else if (valueClass == short.class) {
      accessor.setShort(host, Short.parseShort(value));
    }
    else if (valueClass.isEnum()) {
      Object deserializedValue = null;
      for (Object enumConstant : valueClass.getEnumConstants()) {
        if (enumConstant.toString().equals(value)) {
          deserializedValue = enumConstant;
        }
      }
      accessor.set(host, deserializedValue);
    }
    else if (Date.class.isAssignableFrom(valueClass)) {
      try {
        accessor.set(host, new Date(Long.parseLong(value)));
      }
      catch (NumberFormatException e) {
        accessor.set(host, new Date(0));
      }
    }
    else {
      Object deserializedValue = value;
      if (valueClass == Boolean.class) {
        deserializedValue = Boolean.parseBoolean(value);
      }
      else if (valueClass == Integer.class) {
        deserializedValue = Integer.parseInt(value);
      }
      else if (valueClass == Short.class) {
        deserializedValue = Short.parseShort(value);
      }
      else if (valueClass == Long.class) {
        deserializedValue = Long.parseLong(value);
      }
      else if (valueClass == Double.class) {
        deserializedValue = Double.parseDouble(value);
      }
      else if (valueClass == Float.class) {
        deserializedValue = Float.parseFloat(value);
      }
      accessor.set(host, deserializedValue);
    }
  }

  @NotNull
  static String convertToString(@NotNull Object value) {
    if (value instanceof Date) {
      return Long.toString(((Date)value).getTime());
    }
    else {
      return value.toString();
    }
  }

  @NotNull
  static String getTextValue(@NotNull Element element, @NotNull String defaultText) {
    List<Content> content = element.getContent();
    int size = content.size();
    StringBuilder builder = null;
    for (int i = 0; i < size; i++) {
      Content child = content.get(i);
      if (child instanceof Text) {
        String value = child.getValue();
        if (builder == null && i == (size - 1)) {
          return value;
        }

        if (builder == null) {
          builder = new StringBuilder();
        }
        builder.append(value);
      }
    }
    return builder == null ? defaultText : builder.toString();
  }
}
