package com.airlock.iam.base.infrastructure.mapper.jsonapi;

import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import ch.ergon.restal.jsonapi.document.data.ResourceObject.Builder;
import com.airlock.iam.base.infrastructure.mapper.entity.IdSupplier;
import java.util.Optional;
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MappingContext;
import ma.glasnost.orika.MappingException;

public final class JsonApiResourceMapper<T, R> implements JsonApiMapper<T, R> {
   private final MapperFacade orikaMapper;
   private T singleElement;
   private Class<R> targetType;
   private Optional<MappingContext> mappingContext = Optional.empty();
   private IdSupplier<T> idSupplier = t -> {
      throw new IllegalStateException("An ID supplier is required");
   };

   JsonApiResourceMapper(MapperFacade orikaMapper, Class<R> targetType) {
      this(orikaMapper, null, targetType);
   }

   public JsonApiResourceMapper(MapperFacade orikaMapper, T singleElement, Class<R> targetType) {
      this.orikaMapper = orikaMapper;
      this.singleElement = singleElement;
      this.targetType = targetType;
   }

   public JsonApiResourceMapper<T, R> withIdSupplier(IdSupplier<T> supplier) {
      this.idSupplier = supplier;
      return this;
   }

   public JsonApiResourceMapper<T, R> withMappingContext(MappingContext context) {
      this.mappingContext = Optional.of(context);
      return this;
   }

   public ResourceObject<R> toResourceObject() {
      return this.toResourceObject(this.singleElement);
   }

   public ResourceObject<R> toResourceObject(T element) {
      ResourceObjectMetaData metaData = this.metaDataOf(element);
      ResourceObject<R> resourceObject = this.toResourceObject(element, metaData).build();
      metaData.applyTo(resourceObject);
      return resourceObject;
   }

   private Builder<R> toResourceObject(T element, ResourceObjectMetaData metaData) {
      R mapped = this.map(element);
      Builder<R> builder = ResourceObject.builder(this.idSupplier.getId(element), mapped);
      metaData.applyTo(builder);
      return builder;
   }

   private <T, R> R map(T element) {
      return (R)(this.mappingContext.isPresent()
         ? this.orikaMapper.map(element, this.targetType, this.mappingContext.get())
         : this.orikaMapper.map(element, this.targetType));
   }

   private ResourceObjectMetaData metaDataOf(T element) {
      try {
         return this.mappingContext.isPresent()
            ? (ResourceObjectMetaData)this.orikaMapper.map(element, ResourceObjectMetaData.class, this.mappingContext.get())
            : (ResourceObjectMetaData)this.orikaMapper.map(element, ResourceObjectMetaData.class);
      } catch (MappingException e) {
         return new ResourceObjectMetaData();
      }
   }

   public ResourceDocument<R> toResourceDocument() {
      ResourceObjectMetaData metaData = this.metaDataOf(this.singleElement);
      return this.toResourceObject(this.singleElement, metaData).document();
   }
}
