package gov.cms.grouper.snf;

import gov.cms.grouper.snf.Data.Root;
import gov.cms.grouper.snf.model.enums.CognitiveLevel;
import gov.cms.grouper.snf.model.table.BasicRow;
import gov.cms.grouper.snf.model.table.ClinicalCategoryMappingRow;
import gov.cms.grouper.snf.model.table.ClinicalCategoryMasterRow;
import gov.cms.grouper.snf.model.table.CmgForPtOtRow;
import gov.cms.grouper.snf.model.table.CognitiveLevelRow;
import gov.cms.grouper.snf.model.table.NtaCmgRow;
import gov.cms.grouper.snf.model.table.NtaComorbidityMasterRow;
import gov.cms.grouper.snf.model.table.NtaComorbidityRow;
import gov.cms.grouper.snf.model.table.PerformanceRecodeRow;
import gov.cms.grouper.snf.model.table.SlpCmgRow;
import gov.cms.grouper.snf.model.table.SlpComorbiditiesRow;
import gov.cms.grouper.snf.model.table.SlpComorbidityMasterRow;
import gov.cms.grouper.snf.model.table.SnfVersionRow;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class SnfTables {

  public static void load() {
    try {
      InputStream inputStream = SnfTables.class.getResourceAsStream("/snf.bin");
      if (inputStream == null) throw new RuntimeException("snf.bin file is missing in resources!");

      Root rootData = Root.parseFrom(inputStream);
      versions = Collections.unmodifiableMap(
          rootData.getVersionList().stream().map(SnfVersionRow::build)
              .collect(Collectors.toMap(SnfVersionRow::getInternalVersion, v -> v)));
      clinicalCategoryMapping = buildMap(rootData.getClinicalCategoryMappingsList(),
          ClinicalCategoryMappingRow::build,
          ClinicalCategoryMappingRow::getPdsClinicalCategory);
      cognitiveLeveltable = buildMap(rootData.getCalcOfPdpmLvlFromBimsList(),
          CognitiveLevelRow::build,
          CognitiveLevelRow::getCognitiveLevel);
      cmgForPtOt = buildMap(rootData.getCmgForPtOtList(),
          CmgForPtOtRow::build,
          CmgForPtOtRow::getClinicalCategory);
      ntaCmgTable = buildMap(rootData.getNtaCmgList(),
          NtaCmgRow::build,
          NtaCmgRow::getCmg);
      ntaComorbidityTableByConditionOfService = buildMap(rootData.getNtaComorbidityScoreCalcList(),
          NtaComorbidityRow::build,
          NtaComorbidityRow::getConditionService);
      performanceRecodeTable = buildMap(rootData.getPerformanceRecodeList(),
          PerformanceRecodeRow::build,
          PerformanceRecodeRow::getPerformanceScore);
      slpCmgTable = buildMap(rootData.getSlpCmgList(),
          SlpCmgRow::build,
          SlpCmgRow::getPresenceOfAncSlCi);
      slpComorbiditiesTable = buildMap((rootData.getSlpComorbiditiesList()),
          SlpComorbiditiesRow::build,
          SlpComorbiditiesRow::getMdsItem);
      clinicalCategoryMasterTable = buildMap(rootData.getClinicalCategoryMasterList(),
          ClinicalCategoryMasterRow::build,
          ClinicalCategoryMasterRow::getCode);
      slpComorbidityMasterTable = buildMap(rootData.getSlpComorbidityMasterList(),
          SlpComorbidityMasterRow::build,
          SlpComorbidityMasterRow::getCode);
      ntaComorbidityMasterTable = buildMap(rootData.getNtaComorbidityMasterList(),
          NtaComorbidityMasterRow::build,
          NtaComorbidityMasterRow::getCode);

    } catch (IOException exc) {
      // This should never happen.
      throw new RuntimeException("Error loading SNF runtime data");
    }
  }

  private static <I, K, O> Map<K, Set<O>> buildMap(Collection<I> inputObjects, Function<I, O> mapInputToOutput,
      Function<O, K> groupingKey) {
    return Collections.unmodifiableMap(inputObjects.stream().map(mapInputToOutput)
        .collect(Collectors.groupingBy(groupingKey, Collectors.toSet())));
  }

  static {
    load();
  }

  public static Map<Integer, SnfVersionRow> versions;
  public static Map<String, Set<ClinicalCategoryMappingRow>> clinicalCategoryMapping;
  public static Map<CognitiveLevel, Set<CognitiveLevelRow>> cognitiveLeveltable;
  public static Map<String, Set<CmgForPtOtRow>> cmgForPtOt;
  public static Map<String, Set<NtaCmgRow>> ntaCmgTable;
  public static Map<String, Set<NtaComorbidityRow>> ntaComorbidityTableByConditionOfService;
  public static Map<Integer, Set<PerformanceRecodeRow>> performanceRecodeTable;
  public static Map<Integer, Set<SlpCmgRow>> slpCmgTable;
  public static Map<String, Set<SlpComorbiditiesRow>> slpComorbiditiesTable;
  public static Map<String, Set<ClinicalCategoryMasterRow>> clinicalCategoryMasterTable;
  public static Map<String, Set<SlpComorbidityMasterRow>> slpComorbidityMasterTable;
  public static Map<String, Set<NtaComorbidityMasterRow>> ntaComorbidityMasterTable;

  public static String getRecalculatedZ0100B() {
    String recalculatedZ0100B = "";
    Integer tmp = Integer.MIN_VALUE;

    for (Integer version : versions.keySet()) {
      if (version > tmp) {
        tmp = version;
        recalculatedZ0100B = versions.get(version).getVersion();
      }
    }

    return recalculatedZ0100B.substring(1);
  }

  public static Integer getDataVersion(LocalDate date) {
    if (date == null) return null;
    Integer version = null;

    for (Map.Entry<Integer, SnfVersionRow> row : versions.entrySet()) {
      if (row.getValue().isDate(date)) {
        version = row.getKey();
        break;
      }
    }

    return version;
  }

  public static String getItemSetVersion(LocalDate date) {
    String version = "";
    for (Map.Entry<Integer, SnfVersionRow> row : versions.entrySet()) {
      if (row.getValue().isDate(date)) {
        version = row.getValue().getItemSetVersion();
        break;
      }
    }

    return version;
  }

  /**
   * Given a data structure <code>defMap</code>, find and retrieve <code>key</code> based on the
   * <code>selector</code> predicate.
   *
   * @param defMap   the data structure used to perform look ups
   * @param key      the identifiable key of the data structure
   * @param selector the condition in which to filter by
   * @return An entry of the generic model or null
   */
  public static <KEY, T extends BasicRow, CHECK> T get(Map<KEY, Set<T>> defMap, KEY key,
      BiFunction<T, CHECK, Boolean> selector, CHECK value) {
    Set<T> items = defMap.get(key);
    T result = items == null ? null : items.stream()
        .filter((item) -> selector.apply(item, value)).findFirst().orElse(null);
    return result;
  }

  public static <KEY, T extends BasicRow, CHECK> Set<T> getAll(Map<KEY, Set<T>> defMap, KEY key,
      BiFunction<T, CHECK, Boolean> selector, CHECK value) {
    Set<T> items = defMap.get(key);
    Set<T> result = items == null ? null : items.stream()
        .filter((item) -> selector.apply(item, value)).collect(Collectors.toSet());
    return result;
  }

  public static <KEY, T extends BasicRow, OUT> List<OUT> selectAll(Map<KEY, Set<T>> defMap,
      Predicate<T> condition, Function<T, OUT> transformer) {
    final List<OUT> results = new ArrayList<>(20);
    for (Set<T> rows : defMap.values()) {
      for (T row : rows) {
        final boolean cond = condition.test(row);
        if (cond) {
          final OUT out = transformer.apply(row);
          results.add(out);
        }
      }
    }

    return results;
  }

  public static <K, T extends BasicRow, V> T get(Map<K, Set<T>> defMap,
      BiFunction<T, V, Boolean> selector, V value) {
    Set<T> items = defMap.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
    return items.stream().filter((item) -> selector.apply(item, value)).findFirst().orElse(null);
  }

}
