package gov.cms.grouper.snf;

import gov.cms.grouper.snf.lego.CsvHelper;
import gov.cms.grouper.snf.lego.SnfUtils;
import gov.cms.grouper.snf.model.CognitiveLevel;
import gov.cms.grouper.snf.model.table.BasicRow;
import gov.cms.grouper.snf.model.table.ClinicalCategoryMappingRow;
import gov.cms.grouper.snf.model.table.CmgForPtOtRow;
import gov.cms.grouper.snf.model.table.CognitiveLevelRow;
import gov.cms.grouper.snf.model.table.DiagnosisMasterRow;
import gov.cms.grouper.snf.model.table.NtaCmgRow;
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.SnfVersionRow;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
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.function.Supplier;
import java.util.stream.Collectors;

public class SnfTables {

  public static final Map<Integer, SnfVersionRow> versions = SnfTables.initVersions();

  private static Map<Integer, SnfVersionRow> initVersions() {
    final List<String> lines =
        SnfUtils.doOrDie(() -> SnfUtils.readFileFromClassPath("specTableCSV/VERSION.csv"));

    final CsvHelper<SnfVersionRow> helper =
        new CsvHelper<>(lines, SnfVersionRow.Builder.of(), true);
    Set<SnfVersionRow> items = helper.load();
    Map<Integer, SnfVersionRow> map = new HashMap<>();
    for (SnfVersionRow item : items) {
      map.put(item.getInternalVersion(), item);
    }
    return Collections.unmodifiableMap(map);
  }

  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 final Map<String, Set<ClinicalCategoryMappingRow>> clinicalCategoryMapping =
      SnfTables.initMap("specTableCSV/CLINICAL_CATEGORY_MAPPINGS.csv",
          (lines) -> CsvHelper.of(lines, ClinicalCategoryMappingRow.Builder.of(), true),
          ClinicalCategoryMappingRow::getPdsClinicalCategory, SnfTables.versions);

  public static final Map<CognitiveLevel, Set<CognitiveLevelRow>> cognitiveLeveltable =
      SnfTables.initMap("specTableCSV/CALC_OF_PDPM_LVL_FROM_BIMS.csv",
          (filePath) -> CsvHelper.of(filePath, CognitiveLevelRow.Builder.of(), true),
          CognitiveLevelRow::getCognitiveLevel, SnfTables.versions);


  protected static <KEY, T extends BasicRow> Map<KEY, Set<T>> initMap(Supplier<Set<T>> loader,
      Function<T, KEY> keyFunc, Map<Integer, SnfVersionRow> versionsTable) {

    Set<T> items = loader.get();
    for (T item : items) {
      item.populateVersion(versionsTable);
    }

    Supplier<Set<T>> newCollection = () -> new HashSet<>();
    Map<KEY, Set<T>> unmodifiableMap =
        SnfUtils.groupBy(SnfUtils.cast(SnfUtils.makeImmutableSet), items, keyFunc, newCollection);

    return unmodifiableMap;

  }

  private static <KEY, T extends BasicRow> Map<KEY, Set<T>> initMap(String filePath,
      Function<List<String>, CsvHelper<T>> helperFunc, Function<T, KEY> keyFunc,
      Map<Integer, SnfVersionRow> versionsTable) {
    Supplier<Set<T>> loader = () -> {
      List<String> lines = SnfUtils.doOrDie(() -> SnfUtils.readFileFromClassPath(filePath));
      CsvHelper<T> helper = helperFunc.apply(lines);
      Set<T> items = helper.load();
      return items;
    };

    return SnfTables.initMap(loader, keyFunc, versionsTable);
  }

  /**
   * 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 = SnfUtils.nullCheck2(items, 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 = SnfUtils.nullCheck2(items, 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> set : defMap.values()) {
      for (T item : set) {
        final boolean cond = condition.test(item);
        if (cond) {
          final OUT out = transformer.apply(item);
          results.add(out);
        }
      }
    }

    return results;
  }

  public static <KEY, T extends BasicRow, CHECK> T get(Map<KEY, Set<T>> defMap,
      BiFunction<T, CHECK, Boolean> selector, CHECK value) {
    Set<T> items =
        defMap.values().stream().flatMap(list -> list.stream()).collect(Collectors.toSet());
    T result = SnfUtils.nullCheck2(items, null, () -> items.stream()
        .filter((item) -> selector.apply(item, value)).findFirst().orElse(null));
    return result;
  }

  public static final Map<String, Set<CmgForPtOtRow>> cmgForPtOt =
      SnfTables.initMap("specTableCSV/CMG_FOR_PT_OT.csv",
          (filePath) -> CsvHelper.of(filePath, CmgForPtOtRow.Builder.of(), true),
          CmgForPtOtRow::getClinicalCategory, SnfTables.versions);

  public static final Map<String, Set<NtaCmgRow>> ntaCmgTable =
      SnfTables.initMap("specTableCSV/NTA_CMG.csv",
          (filePath) -> CsvHelper.of(filePath, NtaCmgRow.Builder.of(), true), NtaCmgRow::getCmg,
          SnfTables.versions);

  public static final Map<String, Set<NtaComorbidityRow>> ntaComorbidityTableByConditionOfService =
      SnfTables.initMap("specTableCSV/NTA_COMORBIDITY_SCORE_CALC.csv",
          (filePath) -> CsvHelper.of(filePath, NtaComorbidityRow.Builder.of(), true),
          NtaComorbidityRow::getConditionService, SnfTables.versions);


  public static final Map<String, Set<DiagnosisMasterRow>> diagnosisMasterTable =
      SnfTables.initMap("specTableCSV/DIAGNOSIS_MASTER.csv",
          (filePath) -> CsvHelper.of(filePath, DiagnosisMasterRow.Builder.of(), true),
          DiagnosisMasterRow::getCode, SnfTables.versions);

  public static final Map<Integer, Set<PerformanceRecodeRow>> performanceRecodeTable =
      SnfTables.initMap("specTableCSV/PERFORMANCE_RECODE.csv",
          (filePath) -> CsvHelper.of(filePath, PerformanceRecodeRow.Builder.of(), true),
          PerformanceRecodeRow::getPerformanceScore, SnfTables.versions);

  public static final Map<Integer, Set<SlpCmgRow>> slpCmgTable =
      SnfTables.initMap("specTableCSV/SLP_CMG.csv",
          (filePath) -> CsvHelper.of(filePath, SlpCmgRow.Builder.of(), true),
          SlpCmgRow::getPresenceOfAncSlCi, SnfTables.versions);

  public static final Map<String, Set<SlpComorbiditiesRow>> slpComorbiditiesTable =
      SnfTables.initMap("specTableCSV/SLP_COMORBIDITIES.csv",
          (filePath) -> CsvHelper.of(filePath, SlpComorbiditiesRow.Builder.of(), true),
          SlpComorbiditiesRow::getMdsItem, SnfTables.versions);

}
