/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS.
 */
package org.opends.server.util;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.opends.server.api.DirectoryThread;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.schema.GeneralizedTimeSyntax;

/**
 * This class provides an application-wide timing service. It provides
 * the ability to retrieve the current time in various different formats
 * and resolutions.
 */
@org.opends.server.types.PublicAPI(
    stability = org.opends.server.types.StabilityLevel.UNCOMMITTED,
    mayInstantiate = false,
    mayExtend = false,
    mayInvoke = true)
public final class TimeThread
{

  /**
   * Timer job.
   */
  private static final class TimeInfo implements Runnable
  {

    /** The calendar holding the current time. */
    private GregorianCalendar calendar;

    /** The date for this time thread. */
    private Date date;

    /** The timestamp for this time thread in the generalized time format. */
    private String generalizedTime;

    /** The timestamp for this time thread in GMT. */
    private String gmtTimestamp;

    /** The date formatter that will be used to obtain the GMT timestamp. */
    private final SimpleDateFormat gmtTimestampFormatter;

    /** The current time in HHmm form as an integer. */
    private int hourAndMinute;

    /** The timestamp for this time thread in the local time zone. */
    private String localTimestamp;

    /** The date formatter that will be used to obtain the local timestamp. */
    private final SimpleDateFormat localTimestampFormatter;

    /** The current time in nanoseconds. */
    private volatile long nanoTime;

    /** The current time in milliseconds since the epoch. */
    private volatile long time;

    /**
     * A set of arbitrary formatters that should be maintained by this time
     * thread.
     */
    private final List<SimpleDateFormat> userDefinedFormatters;

    /**
     * A set of arbitrary formatted times, mapped from format string to the
     * corresponding formatted time representation.
     */
    private final Map<String, String> userDefinedTimeStrings;



    /**
     * Create a new job with the specified delay.
     */
    public TimeInfo()
    {
      userDefinedFormatters = new CopyOnWriteArrayList<>();
      userDefinedTimeStrings = new ConcurrentHashMap<>();

      TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");

      gmtTimestampFormatter = new SimpleDateFormat("yyyyMMddHHmmss'Z'");
      gmtTimestampFormatter.setTimeZone(utcTimeZone);

      localTimestampFormatter =
          new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z");

      // Populate initial values.
      run();
    }



    /** {@inheritDoc} */
    @Override
    public void run()
    {
      try
      {
        calendar = new GregorianCalendar();
        date = calendar.getTime();
        time = date.getTime();
        nanoTime = System.nanoTime();
        generalizedTime = GeneralizedTimeSyntax.format(date);
        localTimestamp = localTimestampFormatter.format(date);
        gmtTimestamp = gmtTimestampFormatter.format(date);
        hourAndMinute =
            calendar.get(Calendar.HOUR_OF_DAY) * 100
                + calendar.get(Calendar.MINUTE);

        for (SimpleDateFormat format : userDefinedFormatters)
        {
          userDefinedTimeStrings.put(format.toPattern(), format.format(date));
        }
      }
      catch (Exception e)
      {
        logger.traceException(e);
      }
    }
  }

  /**
   * Thread factory used by the scheduled execution service.
   */
  private static final class TimeThreadFactory implements
      ThreadFactory
  {

    /** {@inheritDoc} */
    @Override
    public Thread newThread(Runnable r)
    {
      Thread t = new DirectoryThread(r, "Time Thread");
      t.setDaemon(true);
      return t;
    }

  }



  /** The singleton instance. */
  private static TimeThread INSTANCE = new TimeThread();

  /** The tracer object for the debug logger. */
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();



  /**
   * Retrieves a <CODE>Calendar</CODE> containing the time at the last
   * update.
   *
   * @return A <CODE>Calendar</CODE> containing the time at the last
   *         update.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static Calendar getCalendar() throws IllegalStateException
  {
    checkState();
    return INSTANCE.timeInfo.calendar;
  }



  /**
   * Retrieves a <CODE>Date</CODE> containing the time at the last
   * update.
   *
   * @return A <CODE>Date</CODE> containing the time at the last update.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static Date getDate() throws IllegalStateException
  {
    checkState();
    return INSTANCE.timeInfo.date;
  }



  /**
   * Retrieves a string containing a normalized representation of the
   * current time in a generalized time format. The timestamp will look
   * like "20050101000000.000Z".
   *
   * @return A string containing a normalized representation of the
   *         current time in a generalized time format.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static String getGeneralizedTime() throws IllegalStateException
  {
    checkState();
    return INSTANCE.timeInfo.generalizedTime;
  }



  /**
   * Retrieves a string containing the current time in GMT. The
   * timestamp will look like "20050101000000Z".
   *
   * @return A string containing the current time in GMT.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static String getGMTTime() throws IllegalStateException
  {
    checkState();
    return INSTANCE.timeInfo.gmtTimestamp;
  }



  /**
   * Retrieves an integer containing the time in HHmm format at the last
   * update. It will be calculated as "(hourOfDay*100) + minuteOfHour".
   *
   * @return An integer containing the time in HHmm format at the last
   *         update.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static int getHourAndMinute() throws IllegalStateException
  {
    checkState();
    return INSTANCE.timeInfo.hourAndMinute;
  }



  /**
   * Retrieves a string containing the current time in the local time
   * zone. The timestamp format will look like
   * "01/Jan/2005:00:00:00 -0600".
   *
   * @return A string containing the current time in the local time
   *         zone.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static String getLocalTime() throws IllegalStateException
  {
    checkState();
    return INSTANCE.timeInfo.localTimestamp;
  }



  /**
   * Retrieves the time in nanoseconds from the most precise available system
   * timer. The value returned represents nanoseconds since some fixed but
   * arbitrary time.
   *
   * @return The time in nanoseconds from some fixed but arbitrary time.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static long getNanoTime() throws IllegalStateException
  {
    checkState();
    return INSTANCE.timeInfo.nanoTime;
  }



  /**
   * Retrieves the time in milliseconds since the epoch at the last
   * update.
   *
   * @return The time in milliseconds since the epoch at the last
   *         update.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static long getTime() throws IllegalStateException
  {
    checkState();
    return INSTANCE.timeInfo.time;
  }



  /**
   * Retrieves the current time formatted using the given format string.
   * <p>
   * The first time this method is used with a given format string, it
   * will be used to create a formatter that will generate the time
   * string. That formatter will then be put into a list so that it will
   * be maintained automatically for future use.
   *
   * @param formatString
   *          The string that defines the format of the time string to
   *          retrieve.
   * @return The formatted time string.
   * @throws IllegalArgumentException
   *           If the provided format string is invalid.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static String getUserDefinedTime(String formatString)
      throws IllegalArgumentException, IllegalStateException
  {
    checkState();

    String timeString =
        INSTANCE.timeInfo.userDefinedTimeStrings.get(formatString);

    if (timeString == null)
    {
      SimpleDateFormat formatter = new SimpleDateFormat(formatString);
      timeString = formatter.format(INSTANCE.timeInfo.date);
      INSTANCE.timeInfo.userDefinedTimeStrings.put(formatString,
          timeString);
      INSTANCE.timeInfo.userDefinedFormatters.add(formatter);
    }

    return timeString;
  }



  /**
   * Removes the user-defined time formatter from this time thread so
   * that it will no longer be maintained. This is a safe operation
   * because if the same format string is used for multiple purposes
   * then it will be added back the next time a time is requested with
   * the given format.
   *
   * @param formatString
   *          The format string for the date formatter to remove.
   * @throws IllegalStateException
   *           If the time service has not been started.
   */
  public static void removeUserDefinedFormatter(String formatString)
    throws IllegalStateException
  {
    checkState();

    INSTANCE.timeInfo.userDefinedFormatters.remove(new SimpleDateFormat(
        formatString));
    INSTANCE.timeInfo.userDefinedTimeStrings.remove(formatString);
  }



  /**
   * Starts the time service if it has not already been started.
   */
  public static void start()
  {
    if (INSTANCE == null)
    {
      INSTANCE = new TimeThread();
    }
  }



  /**
   * Stops the time service if it has not already been stopped.
   */
  public static void stop()
  {
    if (INSTANCE != null)
    {
      INSTANCE.scheduler.shutdown();
      INSTANCE = null;
    }
  }



  /** Ensures that the time service has been started. */
  private static void checkState() throws IllegalStateException
  {
    if (INSTANCE == null)
    {
      throw new IllegalStateException("Time service not started");
    }
  }



  /** The scheduler. */
  private final ScheduledExecutorService scheduler =
      Executors.newSingleThreadScheduledExecutor(new TimeThreadFactory());

  /** The current time information. */
  private final TimeInfo timeInfo = new TimeInfo();



  /**
   * Creates a new instance of this time service and starts it.
   */
  private TimeThread()
  {
    this.scheduler.scheduleWithFixedDelay(timeInfo, 0, 200,
        TimeUnit.MILLISECONDS);
  }
}
