package gov.cms.grouper.snf.lego;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class SnfUtils {

  /**
   * basic helper to do simple if else condition
   *
   * @param check the function to run for condition check
   * @param trueExec if true execute this function
   * @param falseExec if false execute this function
   * @return result of the correct function
   */
  public static <OUT> OUT ifElse(Supplier<Boolean> check, Supplier<OUT> trueExec,
      Supplier<OUT> falseExec) {
    boolean con = check.get();
    OUT result = null;
    if (trueExec != null && con == true) {
      result = trueExec.get();
    } else if (falseExec != null && con == false) {
      result = falseExec.get();
    }
    return result;
  }

  public static boolean isBlank(String input) {
    boolean result = false;
    if (input == null || input.trim().length() == 0) {
      result = true;
    }
    return result;
  }

  public static String clean(final String input) {
    return SnfUtils.clean(input, null);
  }

  public static String clean(final String input, final String blankValue) {
    return SnfUtils.ifElse(() -> SnfUtils.isBlank(input), () -> blankValue, () -> input.trim());
  }

  public static <T, OUT> OUT nullCheck(final T input, final OUT nullValue, final OUT nonNullValue) {
    return SnfUtils.ifElse(() -> input == null, () -> nullValue, () -> nonNullValue);
  }

  public static <T, OUT> OUT nullCheck(final T input, final OUT nullValue) {
    return SnfUtils.nullCheck(input, nullValue, null);
  }

  public static <T, OUT> OUT nullCheck2(final T input, Supplier<OUT> nullExec,
      Supplier<OUT> nonNullExec) {
    OUT result = SnfUtils.ifElse(() -> (input == null), nullExec, nonNullExec);
    return result;
  }


  public static <OUT> OUT tryExec(SupplierWithException<OUT> exec, boolean ignoreExcception) {
    OUT result = null;
    try {
      result = exec.get();
    } catch (Throwable th) {
      if (ignoreExcception == false) {
        throw new RuntimeException(th);
      }
    }
    return result;
  }

  public static void tryExec(BlockWithException exec, boolean ignoreExcception) {
    try {
      exec.exec();
    } catch (Throwable th) {
      if (ignoreExcception == false) {
        throw new RuntimeException(th);
      }
    }
  }

  public static void doOrDie(BlockWithException exec) {
    tryExec(exec, false);
  }

  public static void tryIgnoreExcption(BlockWithException exec) {
    tryExec(exec, true);
  }


  public static <OUT> OUT doOrDie(SupplierWithException<OUT> exec) {
    return tryExec(exec, false);
  }

  public static <OUT> OUT tryIgnoreExcption(SupplierWithException<OUT> exec) {
    return tryExec(exec, true);
  }


  public static LocalDate parseDate(DateTimeFormatter formatter, String input) {
    try {
      return LocalDate.parse(SnfUtils.clean(input), formatter);
    } catch (Throwable th) {
      return null;
    }
  }

  public static Integer parse(String input, Integer defaultValue) {
    try {
      return Integer.parseInt(SnfUtils.clean(input));
    } catch (Exception exception) {
      return defaultValue;
    }
  }

  @SuppressWarnings("unchecked")
  public static <INT, OUT> OUT cast(INT in) {
    return (OUT) in;
  }

  @SuppressWarnings("unchecked")
  public static <T> Set<T> toSet(T... items) {
    return SnfUtils.toSet(true, items);
  }

  @SuppressWarnings("unchecked")
  public static <T> Set<T> toSet(boolean immutable, T... items) {
    return SnfUtils.toSet(immutable, Arrays.asList(items));
  }

  public static <T> Set<T> toSet(Collection<T> items) {
    return SnfUtils.toSet(true, items);
  }

  public static <T> Set<T> toSet(boolean immutable, Collection<T> items) {
    Set<T> set = new HashSet<>(items);
    if (immutable) {
      set = Collections.unmodifiableSet(set);
    }
    return set;
  }


  @SuppressWarnings("unchecked")
  public static <T extends Comparable<T>> SortedSet<T> toOrderedSet(boolean immutable, T... items) {
    return SnfUtils.toOrderedSet(immutable, SnfComparator.nullHigh(), items);
  }

  @SuppressWarnings("unchecked")
  public static <T extends Comparable<T>> SortedSet<T> toOrderedSet(T... items) {
    return SnfUtils.toOrderedSet(true, SnfComparator.nullHigh(), items);
  }


  @SuppressWarnings("unchecked")
  public static <T extends Comparable<T>> SortedSet<T> toOrderedSet(boolean immutable,
      Comparator<T> order, T... items) {
    return SnfUtils.toOrderedSet(order, Arrays.asList(items), immutable);
  }

  public static <T extends Comparable<T>> SortedSet<T> toOrderedSet(Collection<T> items) {
    return SnfUtils.toOrderedSet(items, true);
  }

  public static <T extends Comparable<T>> SortedSet<T> toOrderedSet(Collection<T> items,
      boolean immutable) {
    return SnfUtils.toOrderedSet(SnfComparator.nullHigh(), items, immutable);
  }


  public static <T extends Comparable<T>> SortedSet<T> toOrderedSet(Comparator<T> order,
      Collection<T> items, boolean immutable) {
    TreeSet<T> result = new TreeSet<>(order);
    result.addAll(items);
    return SnfUtils.ifElse(() -> immutable, () -> Collections.unmodifiableSortedSet(result),
        () -> result);
  }

  public static <T> Predicate<T> and(Collection<Predicate<T>> conditions) {
    Predicate<T> result = (item) -> {
      boolean out = false;
      for (Predicate<T> cond : conditions) {
        if (cond.test(item) == false) {
          out = false;
          break;
        } else {
          out = true;
        }
      }
      return out;
    };

    return result;
  }

  public static <T> Predicate<T> or(Collection<Predicate<T>> conditions) {
    Predicate<T> result = (item) -> {
      boolean out = false;
      for (Predicate<T> cond : conditions) {
        if (cond.test(item) == true) {
          out = true;
          break;
        }
      }
      return out;
    };

    return result;
  }

  public static <T> boolean allContains(Set<T> subList, Set<T> inThisList) {
    boolean result = true;
    for (T item : subList) {
      if (inThisList.contains(item) == false) {
        result = false;
        break;
      }
    }
    return result;
  }

  public static <T> boolean containsAny(Set<T> subList, Set<T> inThisList) {
    boolean result = false;
    for (T item : subList) {
      if (inThisList.contains(item)) {
        result = true;
        break;
      }
    }
    return result;
  }

  public static <T> Supplier<T> of(T item) {
    return () -> item;
  }

  public static boolean meetFirstNConditions(List<Supplier<Boolean>> conditions, int n) {
    int meet = 0;
    boolean result = meet >= n;
    for (Supplier<Boolean> con : conditions) {
      boolean c = con.get();
      if (c) {
        meet++;
      }
      result = meet >= n;
      if (result) {
        break;
      }
    }
    return result;
  }

  @SafeVarargs
  public static <T> List<T> concatList(Collection<T>... collections) {
    List<T> result = new ArrayList<>(collections.length * 10);
    for (Collection<T> c : collections) {
      result.addAll(c);
    }
    return result;
  }

  @SafeVarargs
  public static <T> Set<T> concat(Collection<T>... collections) {
    Set<T> result = new HashSet<>(10 * collections.length);
    for (Collection<T> c : collections) {
      result.addAll(c);
    }
    return result;
  }

  public static List<String> readFileFromClassPath(String path) throws Exception {
    List<String> result = new ArrayList<>();
    try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(SnfUtils.class.getClassLoader().getResourceAsStream(path)))) {
      String line = reader.readLine();
      while (line != null) {
        result.add(line);
        line = reader.readLine();
      }
    } catch (Exception ex) {
      throw ex;
    }
    return result;
  }

  public static boolean any(Boolean... items) {
    boolean result = false;
    for (Boolean item : items) {
      if (Boolean.TRUE.equals(item)) {
        result = true;
        break;
      }
    }
    return result;
  }

  public static Function<Set<?>, Set<?>> makeImmutableSet =
      (set) -> Collections.unmodifiableSet(set);

  public static <KEY, T, C extends Collection<T>> Map<KEY, C> groupBy(Function<C, C> makeImmutable,
      Collection<T> items, Function<T, KEY> keyFunc, Supplier<C> createCollection) {
    Map<KEY, C> map = new HashMap<>(items.size());
    for (T item : items) {
      final KEY key = keyFunc.apply(item);
      C collection = map.get(key);
      if (collection == null) {
        collection = createCollection.get();
        map.put(key, collection);
      }
      collection.add(item);
    }

    if (makeImmutable != null) {
      Map<KEY, C> copy = new HashMap<>(items.size());
      for (Entry<KEY, C> entry : map.entrySet()) {
        final KEY key = entry.getKey();
        C collection = entry.getValue();
        collection = makeImmutable.apply(collection);
        copy.put(key, collection);
      }
      map = copy;
    }

    return Collections.unmodifiableMap(map);
  }

  public static String trimICDCodes(String code) {
    return code.replaceAll("[\\.^]", "").trim();
  }

}
