package org.molgenis.genomebrowser.service;

import static java.util.Objects.requireNonNull;
import static org.molgenis.genomebrowser.meta.GenomeBrowserAttributesMetadata.GENOMEBROWSERATTRIBUTES;
import static org.molgenis.genomebrowser.meta.GenomeBrowserSettingsMetadata.GENOMEBROWSERSETTINGS;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.molgenis.data.DataService;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.security.EntityTypeIdentity;
import org.molgenis.data.security.EntityTypePermission;
import org.molgenis.data.support.QueryImpl;
import org.molgenis.genomebrowser.GenomeBrowserTrack;
import org.molgenis.genomebrowser.meta.GenomeBrowserAttributes;
import org.molgenis.genomebrowser.meta.GenomeBrowserAttributesMetadata;
import org.molgenis.genomebrowser.meta.GenomeBrowserSettings;
import org.molgenis.genomebrowser.meta.GenomeBrowserSettingsMetadata;
import org.molgenis.security.core.UserPermissionEvaluator;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/** Service implements genomeBrowser specific business logic. */
@Service
@RequestMapping(GenomeBrowserService.URI)
public class GenomeBrowserService {
  public static final String URI = "/genomebrowser";

  private final DataService dataService;
  private final UserPermissionEvaluator userPermissionEvaluator;

  public GenomeBrowserService(
      DataService dataService, UserPermissionEvaluator userPermissionEvaluator) {
    this.dataService = requireNonNull(dataService);
    this.userPermissionEvaluator = requireNonNull(userPermissionEvaluator);
  }

  @Transactional(readOnly = true)
  @GetMapping(value = "/tracks", produces = APPLICATION_JSON_VALUE)
  @ResponseBody
  public List<String> getGenomeBrowserTracksAsString(@RequestParam String entityTypeId) {
    EntityType entityType = dataService.getEntityType(entityTypeId);
    return getTracksStringInternal(getGenomeBrowserTracks(entityType));
  }

  public Map<String, GenomeBrowserTrack> getGenomeBrowserTracks(EntityType entityType) {
    return hasPermission()
        ? getGenomeBrowserTracks(
            entityType, getDefaultGenomeBrowserAttributes().collect(Collectors.toList()))
        : Collections.emptyMap();
  }

  /** Get readable genome entities */
  @Transactional(readOnly = true)
  public List<String> getTracksString(Map<String, GenomeBrowserTrack> entityTracks) {
    return getTracksStringInternal(entityTracks);
  }

  private List<String> getTracksStringInternal(Map<String, GenomeBrowserTrack> entityTracks) {
    List<String> results = new ArrayList<>();
    if (hasPermission()) {
      Map<String, GenomeBrowserTrack> allTracks = new HashMap<>(entityTracks);
      for (GenomeBrowserTrack track : entityTracks.values()) {
        allTracks.putAll(getReferenceTracks(track));
      }
      results =
          allTracks.values().stream()
              .map(GenomeBrowserTrack::toTrackString)
              .collect(Collectors.toList());
    }
    return results;
  }

  private Map<String, GenomeBrowserTrack> getGenomeBrowserTracks(
      EntityType entityType, List<GenomeBrowserAttributes> defaultGenomeBrowserAttributes) {
    Map<String, GenomeBrowserTrack> settings = new HashMap<>();
    dataService
        .findAll(
            GENOMEBROWSERSETTINGS,
            new QueryImpl<GenomeBrowserSettings>()
                .eq(GenomeBrowserSettingsMetadata.ENTITY, entityType.getIdValue()),
            GenomeBrowserSettings.class)
        .forEach(
            referenceSettings ->
                settings.put(
                    referenceSettings.getIdentifier(),
                    GenomeBrowserTrack.create(referenceSettings)));
    if (settings.isEmpty()) {
      // if not check if attrs match any default config
      Collections.sort(defaultGenomeBrowserAttributes);
      for (GenomeBrowserAttributes genomeBrowserAttributes : defaultGenomeBrowserAttributes) {
        List<String> attributeNames = Lists.newArrayList(entityType.getAttributeNames());
        if (areAllAttributeAvailable(genomeBrowserAttributes, attributeNames)) {
          GenomeBrowserTrack genomeBrowserTrack =
              getDefaultGenomeBrowserSettingsEntity(entityType, genomeBrowserAttributes);
          settings.put(genomeBrowserTrack.getId(), genomeBrowserTrack);
          break;
        }
      }
    }
    return settings;
  }

  Map<String, GenomeBrowserTrack> getReferenceTracks(GenomeBrowserTrack settings) {
    Map<String, GenomeBrowserTrack> result = new HashMap<>();
    if (hasPermission()
        && settings.getMolgenisReferenceMode()
            != GenomeBrowserSettings.MolgenisReferenceMode.NONE) {
      if (settings.getMolgenisReferenceMode()
          == GenomeBrowserSettings.MolgenisReferenceMode.CONFIGURED) {
        // Cannot be null due to nullableExpression on MolgenisReferenceTracks in
        // GenomeBrowserSettingsMetadata
        //noinspection ConstantConditions
        settings
            .getMolgenisReferenceTracks()
            .forEach(referenceTrack -> result.put(referenceTrack.getId(), referenceTrack));
      } else { // Mode == ALL
        // TODO Improve performance by rewriting to query that returns all genomic entities instead
        // of retrieving all entities and determining which one is genomic
        List<GenomeBrowserAttributes> defaultGenomeBrowserAttributes =
            getDefaultGenomeBrowserAttributes().collect(Collectors.toList());
        for (EntityType entityType :
            dataService.getMeta().getEntityTypes().collect(Collectors.toList())) {
          if (!entityType.isAbstract() && !entityType.equals(settings.getEntity())) {
            getGenomeBrowserTracks(entityType, defaultGenomeBrowserAttributes)
                .values()
                .forEach(
                    referenceSettings -> result.put(referenceSettings.getId(), referenceSettings));
          }
        }
      }
    }
    return result;
  }

  private boolean hasPermission() {
    return userPermissionEvaluator.hasPermission(
            new EntityTypeIdentity(GENOMEBROWSERSETTINGS), EntityTypePermission.READ_DATA)
        && userPermissionEvaluator.hasPermission(
            new EntityTypeIdentity(GENOMEBROWSERATTRIBUTES), EntityTypePermission.READ_DATA);
  }

  private Stream<GenomeBrowserAttributes> getDefaultGenomeBrowserAttributes() {
    return dataService.findAll(
        GenomeBrowserAttributesMetadata.GENOMEBROWSERATTRIBUTES,
        new QueryImpl<GenomeBrowserAttributes>().eq(GenomeBrowserAttributesMetadata.DEFAULT, true),
        GenomeBrowserAttributes.class);
  }

  private boolean isAttributeAvailable(String attributeName, Iterable<String> attributeNames) {
    return (attributeName == null || Iterables.contains(attributeNames, attributeName));
  }

  private boolean areAllAttributeAvailable(
      GenomeBrowserAttributes genomeBrowserAttributes, Iterable<String> attributeNames) {
    return isAttributeAvailable(genomeBrowserAttributes.getChrom(), attributeNames)
        && isAttributeAvailable(genomeBrowserAttributes.getPos(), attributeNames)
        && isAttributeAvailable(genomeBrowserAttributes.getAlt(), attributeNames)
        && isAttributeAvailable(genomeBrowserAttributes.getRef(), attributeNames)
        && isAttributeAvailable(genomeBrowserAttributes.getStop(), attributeNames);
  }

  private GenomeBrowserTrack getDefaultGenomeBrowserSettingsEntity(
      EntityType entityType, GenomeBrowserAttributes attrs) {
    return GenomeBrowserTrack.create(
        entityType.getIdValue().toString(),
        entityType.getLabel(),
        entityType.getLabelAttribute().getName(),
        entityType,
        GenomeBrowserSettings.TrackType.VARIANT,
        Collections.emptyList(),
        GenomeBrowserSettings.MolgenisReferenceMode.ALL,
        attrs,
        null,
        null,
        null,
        null,
        null);
  }
}