package org.springframework.cloud.netflix.eureka.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.function.Supplier;

import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.netflix.appinfo.DataCenterInfo;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.LeaseInfo;
import com.netflix.discovery.converters.EurekaJacksonCodec;
import com.netflix.discovery.converters.EurekaJacksonCodec.InstanceInfoDeserializer;
import com.netflix.discovery.converters.EurekaJacksonCodec.InstanceInfoSerializer;
import com.netflix.discovery.converters.wrappers.CodecWrappers.LegacyJacksonJson;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;

import static com.netflix.discovery.converters.wrappers.CodecWrappers.getCodecName;

/**
 * @author Spencer Gibb
 */
public class CloudJacksonJson extends LegacyJacksonJson {

	protected final CloudJacksonCodec codec = new CloudJacksonCodec();

	public CloudJacksonCodec getCodec() {
		return codec;
	}

	@Override
	public String codecName() {
		return getCodecName(LegacyJacksonJson.class);
	}

	@Override
	public <T> String encode(T object) throws IOException {
		return this.codec.writeToString(object);
	}

	@Override
	public <T> void encode(T object, OutputStream outputStream) throws IOException {
		this.codec.writeTo(object, outputStream);
	}

	@Override
	public <T> T decode(String textValue, Class<T> type) throws IOException {
		return this.codec.readValue(type, textValue);
	}

	@Override
	public <T> T decode(InputStream inputStream, Class<T> type) throws IOException {
		return this.codec.readValue(type, inputStream);
	}

	static class CloudJacksonCodec extends EurekaJacksonCodec {
		private static final Version VERSION = new Version(1, 1, 0, null, null, null);

		@SuppressWarnings("deprecation")
		public CloudJacksonCodec() {
			super();

			ObjectMapper mapper = new ObjectMapper();
			mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

			SimpleModule module = new SimpleModule("eureka1.x", VERSION);
			module.addSerializer(DataCenterInfo.class, new DataCenterInfoSerializer());
			module.addSerializer(InstanceInfo.class, new CloudInstanceInfoSerializer());
			module.addSerializer(Application.class, new ApplicationSerializer());
			module.addSerializer(Applications.class, new ApplicationsSerializer(
					this.getVersionDeltaKey(), this.getAppHashCodeKey()));

			// TODO: Watch if this causes problems
			// module.addDeserializer(DataCenterInfo.class,
			// 		new DataCenterInfoDeserializer());
			module.addDeserializer(LeaseInfo.class, new LeaseInfoDeserializer());
			module.addDeserializer(InstanceInfo.class,
					new CloudInstanceInfoDeserializer(mapper));
			module.addDeserializer(Application.class,
					new ApplicationDeserializer(mapper));
			module.addDeserializer(Applications.class, new ApplicationsDeserializer(
					mapper, this.getVersionDeltaKey(), this.getAppHashCodeKey()));

			mapper.registerModule(module);

			HashMap<Class<?>, Supplier<ObjectReader>> readers = new HashMap<>();
			readers.put(InstanceInfo.class, ()-> mapper.reader().withType(InstanceInfo.class)
					.withRootName("instance"));
			readers.put(Application.class, ()-> mapper.reader().withType(Application.class)
					.withRootName("application"));
			readers.put(Applications.class, ()-> mapper.reader().withType(Applications.class)
					.withRootName("applications"));
			setField("objectReaderByClass", readers);

			HashMap<Class<?>, ObjectWriter> writers = new HashMap<>();
			writers.put(InstanceInfo.class, mapper.writer().withType(InstanceInfo.class)
					.withRootName("instance"));
			writers.put(Application.class, mapper.writer().withType(Application.class)
					.withRootName("application"));
			writers.put(Applications.class, mapper.writer().withType(Applications.class)
					.withRootName("applications"));
			setField("objectWriterByClass", writers);

			setField("mapper", mapper);
		}

		void setField(String name, Object value) {
			Field field = ReflectionUtils.findField(EurekaJacksonCodec.class, name);
			ReflectionUtils.makeAccessible(field);
			ReflectionUtils.setField(field, this, value);
		}
	}

	static class CloudInstanceInfoSerializer extends InstanceInfoSerializer {
		@Override
		public void serialize(final InstanceInfo info, JsonGenerator jgen,
				SerializerProvider provider) throws IOException {

			InstanceInfo updated = updateIfNeeded(info);
			super.serialize(updated, jgen, provider);
		}
	}

	static InstanceInfo updateIfNeeded(final InstanceInfo info) {
		if (info.getInstanceId() == null && info.getMetadata() != null) {
			String instanceId = info.getMetadata().get("instanceId");
			if (StringUtils.hasText(instanceId)) {
				// backwards compatibility for Angel
				if (StringUtils.hasText(info.getHostName()) && !instanceId.startsWith(info.getHostName())) {
					instanceId = info.getHostName()+":"+instanceId;
				}
				return new InstanceInfo.Builder(info).setInstanceId(instanceId).build();
			}
		}
		return info;
	}

	static class CloudInstanceInfoDeserializer extends InstanceInfoDeserializer {

		protected CloudInstanceInfoDeserializer(ObjectMapper mapper) {
			super(mapper);
		}

		@Override
		public InstanceInfo deserialize(JsonParser jp, DeserializationContext context) throws IOException {
			InstanceInfo info = super.deserialize(jp, context);
			InstanceInfo updated = updateIfNeeded(info);
			return updated;
		}
	}
}
