/*
 * The MIT License
 *
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.lgooddatepicker.durationpicker_underconstruction;

import com.github.lgooddatepicker.zinternaltools.InternalUtilities;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

/** */
public class DurationConverter {

  /** Note: This function will return null if the string cannot be parsed for any reason. */
  public static Duration convertStringToDuration(String text, DurationConverterSettings settings) {
    if (text == null) {
      return null;
    }
    text = text.trim().toLowerCase();
    if (text.isEmpty()) {
      return null;
    }
    if (!text.contains(" ")) {
      for (int i = 0; i < text.length(); ++i) {
        char currentChar = text.charAt(i);
        if (!((Character.isDigit(currentChar)) || (currentChar == '.'))) {
          text =
              InternalUtilities.safeSubstring(text, 0, i)
                  + ' '
                  + InternalUtilities.safeSubstring(text, i, text.length());
          break;
        }
      }
    }
    String[] parts = text.split(" ");
    if (parts.length != 2) {
      return null;
    }
    String valueText = parts[0];
    String unitsText = parts[1];
    BigDecimal value;
    try {
      value = new BigDecimal(valueText);
    } catch (Exception e) {
      return null;
    }
    HashMap<DurationUnit, ArrayList<String>> parsingPrefixes = settings.translationsParsingPrefixes;

    for (String prefix : parsingPrefixes.get(DurationUnit.Year)) {
      if (unitsText.startsWith(prefix)) {
        value = value.multiply(new BigDecimal(DurationUnit.Year.inSeconds));
        return Duration.ofSeconds(value.longValue());
      }
    }
    // Note: Months must be parsed before minutes, otherwise the month prefixes will conflict
    // with the minute prefixes.
    for (String prefix : parsingPrefixes.get(DurationUnit.Month)) {
      if (unitsText.startsWith(prefix)) {
        value = value.multiply(new BigDecimal(DurationUnit.Month.inSeconds));
        return Duration.ofSeconds(value.longValue());
      }
    }
    for (String prefix : parsingPrefixes.get(DurationUnit.Week)) {
      if (unitsText.startsWith(prefix)) {
        value = value.multiply(new BigDecimal(DurationUnit.Week.inSeconds));
        return Duration.ofSeconds(value.longValue());
      }
    }
    for (String prefix : parsingPrefixes.get(DurationUnit.Day)) {
      if (unitsText.startsWith(prefix)) {
        value = value.multiply(new BigDecimal(DurationUnit.Day.inSeconds));
        return Duration.ofSeconds(value.longValue());
      }
    }
    for (String prefix : parsingPrefixes.get(DurationUnit.Hour)) {
      if (unitsText.startsWith(prefix)) {
        value = value.multiply(new BigDecimal(DurationUnit.Hour.inSeconds));
        return Duration.ofSeconds(value.longValue());
      }
    }
    for (String prefix : parsingPrefixes.get(DurationUnit.Minute)) {
      if (unitsText.startsWith(prefix)) {
        value = value.multiply(new BigDecimal(DurationUnit.Minute.inSeconds));
        return Duration.ofSeconds(value.longValue());
      }
    }
    for (String prefix : parsingPrefixes.get(DurationUnit.Second)) {
      if (unitsText.startsWith(prefix)) {
        value = value.multiply(new BigDecimal(DurationUnit.Second.inSeconds));
        BigDecimal fractionalPartInNanos = value.remainder(BigDecimal.ONE);
        fractionalPartInNanos = fractionalPartInNanos.multiply(new BigDecimal(1000000000));
        Duration result = Duration.ofSeconds(value.longValue());
        result = result.plusNanos(fractionalPartInNanos.longValue());
        return result;
      }
    }
    return null;
  }

  /**
   * This function will return an empty string if the duration is null or less than zero. This
   * function will never return null.
   */
  public static String convertStringFromDuration(
      Duration duration, DurationConverterSettings settings) {
    if ((duration == null) || (duration.isNegative())) {
      return "";
    }
    long seconds = duration.getSeconds();

    HashSet<DurationUnit> usedUnits =
        getUsedDurationUnitSet(settings.smallestUsedUnit, settings.largestUsedUnit);
    if (usedUnits.isEmpty()) {
      throw new RuntimeException(
          "convertStringFromDuration(), The \"used duration unit\" "
              + "variables do not allow any units to be used.");
    }
    boolean hasNoNanos = (duration.getNano() == 0);

    DurationUnit smallestUnit = settings.smallestUsedUnit;
    HashMap<DurationUnit, String> unitsSingular = settings.translationsUnitsSingular;
    HashMap<DurationUnit, String> unitsPlural = settings.translationsUnitsPlural;
    HashMap<DurationUnit, Boolean> pluralRules = settings.pluralUnitsMap;
    int oneYear = DurationUnit.Year.inSeconds;
    int oneMonth = DurationUnit.Month.inSeconds;
    int oneWeek = DurationUnit.Week.inSeconds;
    int oneDay = DurationUnit.Day.inSeconds;
    int oneHour = DurationUnit.Hour.inSeconds;
    int oneMinute = DurationUnit.Minute.inSeconds;
    int thirtyMinutes = DurationUnit.Second.thirtyMinutesInSeconds;

    String result = "";
    long value;
    boolean yearIsBestChoice =
        usedUnits.contains(DurationUnit.Year)
            && hasNoNanos
            && (seconds >= oneYear)
            && (seconds % oneYear == 0);
    if (yearIsBestChoice || (DurationUnit.Year == smallestUnit)) {
      value = seconds / oneYear;
      result += value + " ";
      result +=
          ((pluralRules.get(DurationUnit.Year) && (value != 1))
              ? unitsPlural.get(DurationUnit.Year)
              : unitsSingular.get(DurationUnit.Year));
      return result;
    }
    boolean monthIsBestChoice =
        usedUnits.contains(DurationUnit.Month)
            && hasNoNanos
            && (seconds >= oneMonth)
            && (seconds % oneMonth == 0);
    if (monthIsBestChoice || (DurationUnit.Month == smallestUnit)) {
      value = seconds / oneMonth;
      result += value + " ";
      result +=
          ((pluralRules.get(DurationUnit.Month) && (value != 1))
              ? unitsPlural.get(DurationUnit.Month)
              : unitsSingular.get(DurationUnit.Month));
      return result;
    }
    boolean weekIsBestChoice =
        usedUnits.contains(DurationUnit.Week)
            && hasNoNanos
            && (seconds >= oneWeek)
            && (seconds % oneWeek == 0);
    if (weekIsBestChoice || (DurationUnit.Week == smallestUnit)) {
      value = seconds / oneWeek;
      result += value + " ";
      result +=
          ((pluralRules.get(DurationUnit.Week) && (value != 1))
              ? unitsPlural.get(DurationUnit.Week)
              : unitsSingular.get(DurationUnit.Week));
      return result;
    }
    boolean dayIsBestChoice =
        usedUnits.contains(DurationUnit.Day)
            && hasNoNanos
            && (seconds >= oneDay)
            && (seconds % oneDay == 0);
    if (dayIsBestChoice || (DurationUnit.Day == smallestUnit)) {
      value = seconds / oneDay;
      result += value + " ";
      result +=
          ((pluralRules.get(DurationUnit.Day) && (value != 1))
              ? unitsPlural.get(DurationUnit.Day)
              : unitsSingular.get(DurationUnit.Day));
      return result;
    }

    boolean hourDecimalIsBestChoice =
        settings.hoursCanUseThirtyMinuteDecimals
            && usedUnits.contains(DurationUnit.Hour)
            && hasNoNanos
            && (seconds >= oneHour)
            && (seconds <= settings.hoursMaximumValueForDecimalsInSeconds)
            && (seconds % thirtyMinutes == 0)
            && (seconds % oneHour != 0);
    // Note, hourDecimal is never forced to be used as the "smallest used unit".
    if (hourDecimalIsBestChoice) {
      value = seconds / oneHour;
      // The full value is always "X.5" hour(s), due to the hourDecimalIsBestChoice criteria.
      String decimalString = ".5";
      result += value + decimalString + ' ';
      result +=
          ((pluralRules.get(DurationUnit.Hour))
              ? unitsPlural.get(DurationUnit.Hour)
              : unitsSingular.get(DurationUnit.Hour));
      return result;
    }

    boolean hourIntegerIsBestChoice =
        usedUnits.contains(DurationUnit.Hour)
            && hasNoNanos
            && (seconds >= oneHour)
            && (seconds % oneHour == 0);
    if (hourIntegerIsBestChoice || (DurationUnit.Hour == smallestUnit)) {
      value = seconds / oneHour;
      result += value + " ";
      result +=
          ((pluralRules.get(DurationUnit.Hour) && (value != 1))
              ? unitsPlural.get(DurationUnit.Hour)
              : unitsSingular.get(DurationUnit.Hour));
      return result;
    }

    boolean minuteIsBestChoice =
        usedUnits.contains(DurationUnit.Minute)
            && hasNoNanos
            && (seconds >= oneMinute)
            && (seconds % oneMinute == 0);
    if (minuteIsBestChoice || (DurationUnit.Minute == smallestUnit)) {
      value = seconds / oneMinute;
      result += value + " ";
      result +=
          ((pluralRules.get(DurationUnit.Minute) && (value != 1))
              ? unitsPlural.get(DurationUnit.Minute)
              : unitsSingular.get(DurationUnit.Minute));
      return result;
    }

    boolean secondIsBestChoice = usedUnits.contains(DurationUnit.Second);
    if (secondIsBestChoice || (DurationUnit.Second == smallestUnit)) {
      value = seconds;
      int nanosecondsFromDuration = duration.getNano();
      boolean includeNanoseconds = (nanosecondsFromDuration != 0);
      BigDecimal nanosecondsInSeconds = BigDecimal.valueOf(nanosecondsFromDuration, 9);
      BigDecimal secondsWithNanosAdded = nanosecondsInSeconds.add(new BigDecimal(value));
      secondsWithNanosAdded = secondsWithNanosAdded.stripTrailingZeros();
      result += ((includeNanoseconds) ? secondsWithNanosAdded.toPlainString() : value);
      result += " ";
      result +=
          (pluralRules.get(DurationUnit.Second) && ((value != 1) || (includeNanoseconds)))
              ? unitsPlural.get(DurationUnit.Second)
              : unitsSingular.get(DurationUnit.Second);
      return result;
    }
    throw new RuntimeException(
        "convertStringFromDuration(), "
            + "The duration unit could not be chosen. (This should not happen.)");
  }

  private static HashSet<DurationUnit> getUsedDurationUnitSet(
      DurationUnit smallestUnit, DurationUnit largestUnit) {
    HashSet<DurationUnit> result = new HashSet<>();
    for (DurationUnit unit : DurationUnit.values()) {
      if (unit.compareTo(smallestUnit) >= 0 && unit.compareTo(largestUnit) <= 0) {
        result.add(unit);
      }
    }
    return result;
  }

  public static void main(String[] args) {
    DurationConverterSettings settings = new DurationConverterSettings();
    settings.smallestUsedUnit = DurationUnit.Second;
    settings.largestUsedUnit = DurationUnit.Year;
    settings.pluralUnitsMap = settings.getSimplePluralUnitsMap(true);

    String s1 = convertStringFromDuration(null, settings);
    String s2 = convertStringFromDuration(Duration.ofSeconds(-1), settings);
    String s3 = convertStringFromDuration(Duration.ofSeconds(0), settings);
    String s4 = convertStringFromDuration(Duration.ofSeconds(1), settings);
    String s5 = convertStringFromDuration(Duration.ofSeconds(1).plusNanos(2), settings);
    Duration m1 = convertStringToDuration("", settings);
    Duration m2 = convertStringToDuration(" ", settings);
    Duration m3 = convertStringToDuration("gobbldy gook", settings);
    Duration m4 = convertStringToDuration("gobbldy gook 55", settings);
    Duration m5 = convertStringToDuration("5.5g", settings);
    Duration m6 = convertStringToDuration("5.5m", settings);
    Duration m7 = convertStringToDuration("3.62h", settings);
    String ss7 = convertStringFromDuration(m7, settings);
    Duration m8 = convertStringToDuration("5.5d", settings);
    Duration m9 = convertStringToDuration("5.5w", settings);
    for (Duration duration = Duration.ofSeconds(0).plusNanos(0);
        duration.compareTo(Duration.ofDays(1000)) <= 0;
        duration = duration.plusHours(1)) {
      String textDuration = convertStringFromDuration(duration, settings);
      Duration durationParsed = convertStringToDuration(textDuration, settings);
      System.out.println(
          "Duration: "
              + duration.toString()
              + ", Text: "
              + textDuration
              + ", DurationParsed: "
              + ((durationParsed == null) ? "null" : durationParsed.toString()));
      if (durationParsed == null || (!durationParsed.equals(duration))) {
        throw new RuntimeException("");
      }
    }
  }
}
