package gw.rating.flow.domain.util

uses com.google.common.base.Stopwatch
uses com.google.gdata.util.common.base.Pair
uses com.guidewire.rating.flow.domain.util.RateRoutinesJarGenerationStatus
uses com.guidewire.rating.flow.domain.util.RateRoutinesJarUpdater
uses gw.api.startable.IStartablePlugin
uses gw.api.startable.StartablePluginCallbackHandler
uses gw.api.startable.StartablePluginState
uses gw.api.system.PCConfigParameters
uses gw.api.system.PCLoggerCategory
uses gw.rating.flow.util.JarFileWriter

uses java.io.File
uses java.nio.file.Files
uses java.nio.file.Paths
uses java.nio.file.StandardCopyOption
uses java.util.concurrent.Semaphore
uses java.util.concurrent.TimeUnit
uses java.util.concurrent.locks.ReentrantLock

/**
 * Takes care of keeping the rate routines JAR up-to-date.
 */
final class RateRoutinesJarUpdateHandler implements IStartablePlugin {
  static final var _logger = PCLoggerCategory.RATEFLOW

  static final var _semaphore = new Semaphore(0)
  static final var _lock = new ReentrantLock()
  static var _started = false
  static var _stopRequested = false
  static var _updaterThread: Thread = null
  static var _lastUpdateTime: long as LastUpdateTime = 0

  construct() {
  }

  override function start(pluginCallbackHandler : StartablePluginCallbackHandler, serverStarting : boolean) {
    if (_started) {
      return
    }

    if (not (PCConfigParameters.EnableRateRoutinesJar.Value and PCConfigParameters.RateRoutinesJarFile.Value.HasContent) or _started) {
      return
    }

    _updaterThread = new Thread(new JarUpdater(), "RateRoutinesJarUpdaterThread")
    _updaterThread.setDaemon(true)
    _updaterThread.start()
    _started = true
    _stopRequested = false
    _lastUpdateTime = 0

    _logger.info("RateRoutinesJarUpdateHandler started")
  }

  override function stop(serverShuttingDown : boolean) {
    if (not _started) {
      return
    }

    _stopRequested = true
    enqueue()  // To wake up the updater thread so it can quit
    _updaterThread = null
    _started = false
    _lastUpdateTime = 0

    _logger.info("RateRoutinesJarUpdateHandler received stop request")
  }

  override property get State() : StartablePluginState {
    return _updaterThread?.Alive ? Started : Stopped
  }

  /**
   * Asynchronous: queues the update request and returns immediately.
   */
  public static function enqueue() {
    if (not _started) {
      _logger.warn("Rate routines JAR update request received and queued, but updater is not running")
    }

    _semaphore.release(1)  // Request an update as soon as possible
  }

  /**
   * Synchronous.
   *
   * @param jarFile JAR to update
   * @param status process status is reported via this; not <code>null</code> only when called by UI
   */
  @SuppressWarnings("HiddenPackageReference")
  public static function update(jarFile: File, status: RateRoutinesJarGenerationStatus) {
    try {
      _lock.lock()
      var stopwatch = Stopwatch.createStarted()

      new RateRoutinesJarUpdater(jarFile, status).update()  // Ignore return

      _lastUpdateTime = Date.Now.Time

      var duration = stopwatch.elapsed(TimeUnit.SECONDS)

      _logger.info("Rate routines JAR update finished at ${new Date(_lastUpdateTime)} in ${duration} seconds")
    } finally {
      _lock.unlock()
    }
  }

  /**
   * Synchronous.<br>
   *
   * The update JAR is RateRoutinesJarFile if it doesn't already exist or if RateRoutinesUpdateJarFile is not specified.
   * Otherwise the update JAR is RateRoutinesUpdateJarFile.
   */
  @SuppressWarnings("HiddenPackageReference")
  public static function update() {
    var updateAndBootstrapJarFilePaths = UpdateAndBootstrapJarFilePaths

    var updateJarFilePath = updateAndBootstrapJarFilePaths.First
    var bootstrapJarFilePath = updateAndBootstrapJarFilePaths.Second

    var updateJarFile = new File(updateJarFilePath)
    if (bootstrapJarFilePath == null) {
      if (not updateJarFile.exists()) {
        JarFileWriter.createJarFile(updateJarFile)
      } // else we're already bootstrapped
    } else {
      Files.copy(Paths.get(bootstrapJarFilePath, {}), updateJarFile.toPath(), {StandardCopyOption.REPLACE_EXISTING})
    }

    update(updateJarFile, :status = null)
  }

  /**
   * Should only be called after calling {@link #enqueue} or {@link #update}.<br>
   * <b>Caveat User:</b> This is intended solely for internal testing purposes.
   */
  internal static function waitForCompletion() {
    _lock.lock()  // Blocks until no update is in progress
    _lock.unlock()
  }

  /**
   * Gets the path to use for the update JAR and the JAR to bootstrap from (if available).
   * <p>
   * If {@link PCConfigParameters#RateRoutinesJarFile} doesn't exist, or if
   * {@link PCConfigParameters#RateRoutinesUpdateJarFile} isn't specified, it becomes the update JAR. Else
   * {@link PCConfigParameters#RateRoutinesUpdateJarFile} becomes the update JAR.
   * <p>
   * The bootstrap JAR is the JAR to use as a starting point and copied to the chosen update JAR.
   *
   * @return update and bootstrap file paths; latter may be <code>null</code>
   */
  private static property get UpdateAndBootstrapJarFilePaths(): Pair<String, String> {
    var updateJarFilePath: String
    var bootstrapJarFilePath: String = null

    if (PCConfigParameters.RateRoutinesUpdateJarFile.Value.HasContent and
        new File(PCConfigParameters.RateRoutinesJarFile.Value).exists()) {
      updateJarFilePath = PCConfigParameters.RateRoutinesUpdateJarFile.Value

      if (new File(PCConfigParameters.RateRoutinesJarFile.Value).exists()) {  // Paranoia
        bootstrapJarFilePath = PCConfigParameters.RateRoutinesJarFile.Value
      }
    } else {
      // Either no update JAR specified, or the base JAR itself doesn't exist
      updateJarFilePath = PCConfigParameters.RateRoutinesJarFile.Value
    }

    return new Pair<String, String>(updateJarFilePath, bootstrapJarFilePath)
  }

  private class JarUpdater implements Runnable {
    override function run() {
      while (true) {
        _semaphore.acquireUninterruptibly()
        var permitCount = _semaphore.drainPermits()  // In case there were multiple update requests

        if (_stopRequested) {
          return
        }

        _logger.info("Rate routines JAR update processing ${permitCount + 1} request(s)")

        try {
          update()
        } catch(ex) {
          _logger.error("Exception encountered updating JAR ${UpdateAndBootstrapJarFilePaths.First}", ex)
        }
      }
    }
  }
}