// Copyright 2017 Google Inc.
//
// 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 codeu.chat.util;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;

// TIMELINE
//
// The timeline is a time ordered collection of executable units. This is used
// when work needs to be ordered by time. The timeline manages its own threads
// and there is no way to know outside of the code that is executed when the
// code has been executed.
public final class Timeline {

  private final static Logger.Log LOG = Logger.newLog(Timeline.class);

  private static final class Event implements Comparable<Event> {

    public final long time;
    public final Runnable callback;

    public Event(long time, Runnable callback) {
      this.time = time;
      this.callback = callback;
    }

    @Override
    public int compareTo(Event other) {
      return Long.compare(time, other.time);
    }
  }

  private final BlockingQueue<Event> backlog = new PriorityBlockingQueue<>();
  private final BlockingQueue<Runnable> todo = new LinkedBlockingQueue<>();

  private boolean running = true;

  // This thread is used to track the time of events and moves events from the
  // "backlog" queue to the "todo" queue when it is time to execute. They are
  // separated to allow this thread to be safely interrupted.
  private final Thread scheduler = new Thread() {
    @Override
    public void run() {
      while (running) {

        Event next;

        try {
          next = backlog.take();
        } catch (InterruptedException ex) {
          // Rather than try to handle the exception here, set "next"
          // to null and let the normal flow handle the case.
          next = null;
        }

        long sleep = 0;

        if (next != null) {

          final long now = System.currentTimeMillis();

          // Check which queue the event should be added to. If it
          // is time to execute, it should be added to the "todo"
          // queue. If it is not time, it should be added back to the
          // "backlog".
          // If the item is added back to the backlog, we know how long
          // it will be until it will be executed. That means we can sleep
          // until then.
          if (next.time <= now) {
            forceAdd(todo, next.callback);
            sleep = 0;
          } else {
            // Put it back (it's not time).
            forceAdd(backlog, next);
            sleep = next.time - now;
          }
        }

        if (sleep > 0) {
          try {
            Thread.sleep(sleep);
          } catch (InterruptedException ex) {
            // There are two cases this will happen:
            //  1. A new item was added and we are being woken to
            //     check if we need to update the time.
            //  2. It is time to exit and we need to wake-up so that
            //     we can check that "running" is "false".
          }
        }
      }
    }
  };

  // This thread is used to run the code that was given to the time line. This
  // worker does not need to know anything about the time. Once an event gets to
  // here - it is considered "on time" and will be executed.
  private final Thread executor = new Thread() {
    @Override
    public void run() {
      while (running) {
        try {
          todo.take().run();
        } catch (Exception ex) {
          // Catch all exceptions here to stop any rogue action from
          // take down the timeline.
          LOG.warning(
              "An exception was seen on the timeline (%s)",
              ex.toString());
        }
      }
    }
  };

  public Timeline() {
    scheduler.start();
    executor.start();
  }

  // SCHEDULE NOW
  //
  // Add an event to the timeline so that it will occur as soon as possible.
  public void scheduleNow(Runnable callback) {
    scheduleAt(System.currentTimeMillis(), callback);
  }

  // SCHEDULE IN
  //
  // Add an event to the timeline so that it will occur in approximately in a
  // set amount of milliseconds.
  public void scheduleIn(long ms, Runnable callback) {
    scheduleAt(System.currentTimeMillis() + ms, callback);
  }

  // SCHEDULE AT
  //
  // Add an event to the timeline so that will occur approximately at a fixed
  // point in time.
  public void scheduleAt(long timeMs, Runnable callback) {
    final Event event = new Event(timeMs, callback);
    forceAdd(backlog, event);
    scheduler.interrupt();  // wake it up
  }

  // STOP
  //
  // Tell the timeline to shutdown. This is a non-blocking call.
  public void stop() {
    running = false;

    // Interrupt does not force a thread to exit. It signals the
    // thead that it is time to stop execution. As the threads may
    // be sleeping, this will force them awake.
    executor.interrupt();
    scheduler.interrupt();
  }

  // JOIN
  //
  // Wait for the timeline to shutdown. This is a blocking call.
  public void join() {
    forceJoin(executor);
    forceJoin(scheduler);
  }

  private static void forceJoin(Thread thread) {
    while (true) {
      try {
        thread.join();
        break;
      } catch (InterruptedException ex) {
        // Do nothing - allow this to try again.
      }
    }
  }

  private static <T> void forceAdd(BlockingQueue<T> queue, T value) {
    while (!queue.offer(value)) {
      LOG.warning("Failed to add to queue, trying again...");
    }
  }
}