/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package consulo.application.util;

import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.win32.StdCallLibrary;
import consulo.application.CommonBundle;
import consulo.application.util.mac.foundation.Foundation;
import consulo.application.util.mac.foundation.ID;
import consulo.logging.Logger;
import consulo.util.jna.JnaLoader;
import consulo.util.lang.Clock;
import consulo.util.lang.StringUtil;
import consulo.util.lang.SyncDateFormat;

import jakarta.annotation.Nonnull;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class DateFormatUtil {
  private static final Logger LOG = Logger.getInstance(DateFormatUtil.class);

  public static final long SECOND = 1000;
  public static final long MINUTE = SECOND * 60;
  public static final long HOUR = MINUTE * 60;
  public static final long DAY = HOUR * 24;
  public static final long WEEK = DAY * 7;
  public static final long MONTH = DAY * 30;
  public static final long YEAR = DAY * 365;
  public static final long DAY_FACTOR = 24L * 60 * 60 * 1000;

  // do not expose this constants - they are very likely to be changed in future
  private static final SyncDateFormat DATE_FORMAT;
  private static final SyncDateFormat TIME_FORMAT;
  private static final SyncDateFormat TIME_WITH_SECONDS_FORMAT;
  private static final SyncDateFormat DATE_TIME_FORMAT;
  private static final SyncDateFormat ABOUT_DATE_FORMAT;
  private static final SyncDateFormat ISO8601_FORMAT;

  static {
    SyncDateFormat[] formats = getDateTimeFormats();
    DATE_FORMAT = formats[0];
    TIME_FORMAT = formats[1];
    TIME_WITH_SECONDS_FORMAT = formats[2];
    DATE_TIME_FORMAT = formats[3];

    ABOUT_DATE_FORMAT = new SyncDateFormat(DateFormat.getDateInstance(DateFormat.LONG, Locale.US));

    @SuppressWarnings("SpellCheckingInspection") DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
    iso8601.setTimeZone(TimeZone.getTimeZone("UTC"));
    ISO8601_FORMAT = new SyncDateFormat(iso8601);
  }

  private static final long[] DENOMINATORS = {YEAR, MONTH, WEEK, DAY, HOUR, MINUTE};
  private enum Period {YEAR, MONTH, WEEK, DAY, HOUR, MINUTE}
  private static final Period[] PERIODS = {Period.YEAR, Period.MONTH, Period.WEEK, Period.DAY, Period.HOUR, Period.MINUTE};

  private DateFormatUtil() { }

  public static long getDifferenceInDays(@Nonnull Date startDate, @Nonnull Date endDate) {
    return (endDate.getTime() - startDate.getTime() + DAY_FACTOR - 1000) / DAY_FACTOR;
  }

  @Nonnull
  public static SyncDateFormat getDateFormat() {
    return DATE_FORMAT;
  }

  @Nonnull
  public static SyncDateFormat getTimeFormat() {
    return TIME_FORMAT;
  }

  @Nonnull
  public static SyncDateFormat getTimeWithSecondsFormat() {
    return TIME_WITH_SECONDS_FORMAT;
  }

  @Nonnull
  public static SyncDateFormat getDateTimeFormat() {
    return DATE_TIME_FORMAT;
  }

  @Nonnull
  public static SyncDateFormat getIso8601Format() {
    return ISO8601_FORMAT;
  }

  @Nonnull
  public static String formatTime(@Nonnull Date time) {
    return formatTime(time.getTime());
  }

  @Nonnull
  public static String formatTime(long time) {
    return getTimeFormat().format(time);
  }

  @Nonnull
  public static String formatTimeWithSeconds(@Nonnull Date time) {
    return formatTimeWithSeconds(time.getTime());
  }

  @Nonnull
  public static String formatTimeWithSeconds(long time) {
    return getTimeWithSecondsFormat().format(time);
  }

  @Nonnull
  public static String formatDate(@Nonnull Date time) {
    return formatDate(time.getTime());
  }

  @Nonnull
  public static String formatDate(long time) {
    return getDateFormat().format(time);
  }

  @Nonnull
  public static String formatPrettyDate(@Nonnull Date date) {
    return formatPrettyDate(date.getTime());
  }

  @Nonnull
  public static String formatPrettyDate(long time) {
    return doFormatPretty(time, false);
  }

  @Nonnull
  public static String formatDateTime(Date date) {
    return formatDateTime(date.getTime());
  }

  @Nonnull
  public static String formatDateTime(long time) {
    return getDateTimeFormat().format(time);
  }

  @Nonnull
  public static String formatPrettyDateTime(@Nonnull Date date) {
    return formatPrettyDateTime(date.getTime());
  }

  @Nonnull
  public static String formatPrettyDateTime(long time) {
    return doFormatPretty(time, true);
  }

  public static boolean isPrettyFormattingPossible(long time) {
    return doFormatPretty(time, true) != null;
  }

  @Nonnull
  private static String doFormatPretty(long time, boolean formatTime) {
    long currentTime = Clock.getTime();

    Calendar c = Calendar.getInstance();
    c.setTimeInMillis(currentTime);

    int currentYear = c.get(Calendar.YEAR);
    int currentDayOfYear = c.get(Calendar.DAY_OF_YEAR);

    c.setTimeInMillis(time);

    int year = c.get(Calendar.YEAR);
    int dayOfYear = c.get(Calendar.DAY_OF_YEAR);

    if (formatTime) {
      long delta = currentTime - time;
      if (delta <= HOUR && delta >= 0) {
        return CommonBundle.message("date.format.minutes.ago", (int)Math.rint(delta / (double)MINUTE));
      }
    }

    boolean isToday = currentYear == year && currentDayOfYear == dayOfYear;
    if (isToday) {
      String result = CommonBundle.message("date.format.today");
      return formatTime ? result + " " + TIME_FORMAT.format(time) : result;
    }

    boolean isYesterdayOnPreviousYear =
            (currentYear == year + 1) && currentDayOfYear == 1 && dayOfYear == c.getActualMaximum(Calendar.DAY_OF_YEAR);
    boolean isYesterday = isYesterdayOnPreviousYear || (currentYear == year && currentDayOfYear == dayOfYear + 1);

    if (isYesterday) {
      String result = CommonBundle.message("date.format.yesterday");
      return formatTime ? result + " " + TIME_FORMAT.format(time) : result;
    }

    return formatTime ? DATE_TIME_FORMAT.format(time) : DATE_FORMAT.format(time);
  }

  @Nonnull
  public static String formatDuration(long delta) {
    StringBuilder buf = new StringBuilder();
    for (int i = 0; i < DENOMINATORS.length; i++) {
      long denominator = DENOMINATORS[i];
      int n = (int)(delta / denominator);
      if (n != 0) {
        buf.append(composeDurationMessage(PERIODS[i], n));
        buf.append(' ');
        delta = delta % denominator;
      }
    }

    if (buf.length() == 0) return CommonBundle.message("date.format.less.than.a.minute");
    return buf.toString().trim();
  }

  @SuppressWarnings("Duplicates")
  private static String composeDurationMessage(final Period period, final int n) {
    switch (period) {
      case DAY:
        return CommonBundle.message("date.format.n.days", n);
      case MINUTE:
        return CommonBundle.message("date.format.n.minutes", n);
      case HOUR:
        return CommonBundle.message("date.format.n.hours", n);
      case MONTH:
        return CommonBundle.message("date.format.n.months", n);
      case WEEK:
        return CommonBundle.message("date.format.n.weeks", n);
      default:
        return CommonBundle.message("date.format.n.years", n);
    }
  }

  @Nonnull
  public static String formatFrequency(long time) {
    return CommonBundle.message("date.frequency", formatBetweenDates(time, 0));
  }

  @Nonnull
  public static String formatBetweenDates(long d1, long d2) {
    long delta = Math.abs(d1 - d2);
    if (delta == 0) return CommonBundle.message("date.format.right.now");

    int n = -1;
    int i;
    for (i = 0; i < DENOMINATORS.length; i++) {
      long denominator = DENOMINATORS[i];
      if (delta >= denominator) {
        n = (int)(delta / denominator);
        break;
      }
    }

    if (d2 > d1) {
      if (n <= 0) {
        return CommonBundle.message("date.format.a.few.moments.ago");
      }
      else {
        return someTimeAgoMessage(PERIODS[i], n);
      }
    }
    else if (d2 < d1) {
      if (n <= 0) {
        return CommonBundle.message("date.format.in.a.few.moments");
      }
      else {
        return composeInSomeTimeMessage(PERIODS[i], n);
      }
    }

    return "";
  }

  @Nonnull
  public static String formatAboutDialogDate(@Nonnull Date date) {
    return ABOUT_DATE_FORMAT.format(date);
  }

  // helpers

  @SuppressWarnings("Duplicates")
  private static String someTimeAgoMessage(final Period period, final int n) {
    switch (period) {
      case DAY:
        return CommonBundle.message("date.format.n.days.ago", n);
      case MINUTE:
        return CommonBundle.message("date.format.n.minutes.ago", n);
      case HOUR:
        return CommonBundle.message("date.format.n.hours.ago", n);
      case MONTH:
        return CommonBundle.message("date.format.n.months.ago", n);
      case WEEK:
        return CommonBundle.message("date.format.n.weeks.ago", n);
      default:
        return CommonBundle.message("date.format.n.years.ago", n);
    }
  }

  @SuppressWarnings("Duplicates")
  private static String composeInSomeTimeMessage(final Period period, final int n) {
    switch (period) {
      case DAY:
        return CommonBundle.message("date.format.in.n.days", n);
      case MINUTE:
        return CommonBundle.message("date.format.in.n.minutes", n);
      case HOUR:
        return CommonBundle.message("date.format.in.n.hours", n);
      case MONTH:
        return CommonBundle.message("date.format.in.n.months", n);
      case WEEK:
        return CommonBundle.message("date.format.in.n.weeks", n);
      default:
        return CommonBundle.message("date.format.in.n.years", n);
    }
  }

  private static SyncDateFormat[] getDateTimeFormats() {
    DateFormat[] formats = new DateFormat[4];

    boolean loaded = false;
    try {
      if (SystemInfo.isWin7OrNewer && JnaLoader.isLoaded()) {
        loaded = getWindowsFormats(formats);
      }
      else if (SystemInfo.isMac) {
        loaded = getMacFormats(formats);
      }
      else if (SystemInfo.isUnix) {
        loaded = getUnixFormats(formats);
      }
    }
    catch (Throwable t) {
      LOG.info(t);
    }

    if (!loaded) {
      formats[0] = DateFormat.getDateInstance(DateFormat.SHORT);
      formats[1] = DateFormat.getTimeInstance(DateFormat.SHORT);
      formats[2] = DateFormat.getTimeInstance(DateFormat.MEDIUM);
      formats[3] = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
    }

    SyncDateFormat[] synced = new SyncDateFormat[4];
    for (int i = 0; i < formats.length; i++) {
      synced[i] = new SyncDateFormat(formats[i]);
    }
    return synced;
  }

  private static boolean getMacFormats(DateFormat[] formats) {
    final int MacFormatterNoStyle = 0;
    final int MacFormatterShortStyle = 1;
    final int MacFormatterMediumStyle = 2;
    final int MacFormatterBehavior_10_4 = 1040;

    ID autoReleasePool = Foundation.invoke("NSAutoreleasePool", "new");
    try {
      ID dateFormatter = Foundation.invoke("NSDateFormatter", "new");
      Foundation.invoke(dateFormatter, Foundation.createSelector("setFormatterBehavior:"), MacFormatterBehavior_10_4);

      formats[0] = invokeFormatter(dateFormatter, MacFormatterNoStyle, MacFormatterShortStyle);  // short date
      formats[1] = invokeFormatter(dateFormatter, MacFormatterShortStyle, MacFormatterNoStyle);  // short time
      formats[2] = invokeFormatter(dateFormatter, MacFormatterMediumStyle, MacFormatterNoStyle);  // medium time
      formats[3] = invokeFormatter(dateFormatter, MacFormatterShortStyle, MacFormatterShortStyle);  // short date/time

      return true;
    }
    finally {
      Foundation.invoke(autoReleasePool, Foundation.createSelector("release"));
    }
  }

  private static DateFormat invokeFormatter(ID dateFormatter, int timeStyle, int dateStyle) {
    Foundation.invoke(dateFormatter, Foundation.createSelector("setTimeStyle:"), timeStyle);
    Foundation.invoke(dateFormatter, Foundation.createSelector("setDateStyle:"), dateStyle);
    String format = Foundation.toStringViaUTF8(Foundation.invoke(dateFormatter, Foundation.createSelector("dateFormat")));
    assert format != null;
    return new SimpleDateFormat(format.trim());
  }

  private static boolean getUnixFormats(DateFormat[] formats) {
    String localeStr = System.getenv("LC_TIME");
    if (localeStr == null) return false;

    localeStr = localeStr.trim();
    int p = localeStr.indexOf('.');
    if (p > 0) localeStr = localeStr.substring(0, p);
    p = localeStr.indexOf('@');
    if (p > 0) localeStr = localeStr.substring(0, p);

    Locale locale;
    p = localeStr.indexOf('_');
    if (p < 0) {
      locale = new Locale(localeStr);
    }
    else {
      locale = new Locale(localeStr.substring(0, p), localeStr.substring(p + 1));
    }

    formats[0] = DateFormat.getDateInstance(DateFormat.SHORT, locale);
    formats[1] = DateFormat.getTimeInstance(DateFormat.SHORT, locale);
    formats[2] = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale);
    formats[3] = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);

    return true;
  }

  @SuppressWarnings("SpellCheckingInspection")
  private interface Kernel32 extends StdCallLibrary {
    String LOCALE_NAME_USER_DEFAULT = null;

    int LOCALE_SSHORTDATE  = 0x0000001F;
    int LOCALE_SSHORTTIME  = 0x00000079;
    int LOCALE_STIMEFORMAT = 0x00001003;

    int GetLocaleInfoEx(String localeName, int lcType, Pointer lcData, int dataSize);
    int GetLastError();
  }

  private static boolean getWindowsFormats(DateFormat[] formats) {
    Kernel32 kernel32 = (Kernel32)Native.loadLibrary("Kernel32", Kernel32.class);
    int dataSize = 128, rv;
    Memory data = new Memory(dataSize);

    rv = kernel32.GetLocaleInfoEx(Kernel32.LOCALE_NAME_USER_DEFAULT, Kernel32.LOCALE_SSHORTDATE, data, dataSize);
    assert rv > 1 : kernel32.GetLastError();
    String shortDate = fixWindowsFormat(new String(data.getCharArray(0, rv - 1)));

    rv = kernel32.GetLocaleInfoEx(Kernel32.LOCALE_NAME_USER_DEFAULT, Kernel32.LOCALE_SSHORTTIME, data, dataSize);
    assert rv > 1 : kernel32.GetLastError();
    String shortTime = fixWindowsFormat(new String(data.getCharArray(0, rv - 1)));

    rv = kernel32.GetLocaleInfoEx(Kernel32.LOCALE_NAME_USER_DEFAULT, Kernel32.LOCALE_STIMEFORMAT, data, dataSize);
    assert rv > 1 : kernel32.GetLastError();
    String mediumTime = fixWindowsFormat(new String(data.getCharArray(0, rv - 1)));

    formats[0] = new SimpleDateFormat(shortDate);
    formats[1] = new SimpleDateFormat(shortTime);
    formats[2] = new SimpleDateFormat(mediumTime);
    formats[3] = new SimpleDateFormat(shortDate + " " + shortTime);

    return true;
  }

  private static String fixWindowsFormat(String format) {
    format = format.replaceAll("g+", "G");
    format = StringUtil.replace(format, "tt", "a");
    return format;
  }
}