public class PageEvent {

  public static final String TAG = Constants.getTag(PageEvent.class);

  private final EventType mEventType;
  private final String mViewSession;
  private final int[] mPages;
  private final Orientation mOrientation;
  private final Clock mClock;
  private boolean mCollected = false;
  private boolean mStarted = false;
  private boolean mStopped = false;
  private long mStart = 0;
  private long mStop = 0;
  private ArrayList<PageEvent> mSubEvents = new ArrayList<PageEvent>();

  public static PageEvent view(String viewSession, int[] pages, Orientation orientation, Clock c) {
    return new PageEvent(EventType.VIEW, viewSession, orientation, pages, c);
  }

  public static PageEvent zoom(String viewSession, int[] pages, Orientation orientation, Clock c) {
    return new PageEvent(EventType.ZOOM, viewSession, orientation, pages, c);
  }

  public PageEvent(
      EventType type, String viewSession, Orientation orientation, int[] pages, Clock c) {
    mEventType = type;
    mViewSession = viewSession;
    mPages = pages;
    mOrientation = orientation;
    mClock = c;
  }

  /**
   * Start the timer for this event. This method can be called multiple times, only the first time
   * will set the start time.
   */
  public void start() {
    if (!isStarted()) {
      mStart = mClock.now();
    }
    mStarted = true;
  }

  /**
   * Stop the time for this event. If the event haven't been started yet, the start and stop time
   * will be the same.
   */
  public void stop() {
    if (isActive()) {
      mStop = mClock.now();
    } else if (!isStarted()) {
      long now = mClock.now();
      mStart = now;
      mStop = now;
    }
    mStopped = true;
  }

  /**
   * Get the relative duration of this event, this will subtract the duration of sub-events. If the
   * event is still active, the duration up till this point in time is returned, else it's the
   * duration from {@link #start()} till {@link #stop()}'
   *
   * @return The relative duration of this event
   */
  public long getDuration() {
    long delta = 0;
    for (PageEvent e : getSubEvents()) {
      delta += e.getDuration();
    }
    return getDurationAbsolute() - delta;
  }

  /**
   * Get the absolute duration of this event, this does not subtract duration of sub-events. If the
   * event is still active, the duration up till this point in time is returned, else it's the
   * duration from {@link #start()} till {@link #stop()}'
   *
   * @return The absolute duration of this event
   */
  public long getDurationAbsolute() {
    return (isActive() ? mClock.now() : mStop) - mStart;
  }

  /** @return {@code true} if the event has been started, but not yet stopped, else {@code false} */
  public boolean isActive() {
    return mStarted && !mStopped;
  }

  /** @return {@code true} if the event has been started, else {@code false} */
  public boolean isStarted() {
    return mStarted;
  }

  /** @return {@code true} if the event has been stopped, else {@code false} */
  public boolean isStopped() {
    return mStopped;
  }

  public void reset() {
    mStarted = false;
    mStopped = false;
    mStart = 0;
    mStop = 0;
    for (PageEvent e : mSubEvents) {
      e.reset();
    }
    mSubEvents.clear();
  }

  public EventType getType() {
    return mEventType;
  }

  public String getViewSession() {
    return mViewSession;
  }

  public int[] getPages() {
    return mPages;
  }

  public long getStart() {
    return mStart;
  }

  public long getStop() {
    return mStop;
  }

  public Clock getClock() {
    return mClock;
  }

  private Orientation getOrientation() {
    return mOrientation;
  }

  public boolean isCollected() {
    return mCollected;
  }

  public void setCollected(boolean collected) {
    mCollected = collected;
  }

  public void addSubEvent(PageEvent e) {
    mSubEvents.add(e);
  }

  public List<PageEvent> getSubEvents() {
    return mSubEvents;
  }

  public List<PageEvent> getSubEventsRecursive() {
    if (mSubEvents.isEmpty()) {
      return mSubEvents;
    }
    ArrayList<PageEvent> tmp = new ArrayList<PageEvent>(mSubEvents);
    for (PageEvent e : mSubEvents) {
      tmp.addAll(e.getSubEventsRecursive());
    }
    return tmp;
  }

  @Override
  public String toString() {
    return "";
  }

  public JSONObject toDebugJSON() {
    JSONObject o = toJSON();
    try {
      o.put("start", mStart);
      o.put("stop", mStop);
      o.put("clock", mClock.getClass().getSimpleName());
      o.put("sub-event-count", mSubEvents.size());
      o.put("durationAbs", getDurationAbsolute());
    } catch (JSONException e) {
      SgnLog.d(TAG, e.getMessage(), e);
    }
    return o;
  }

  public JSONObject toJSON() {
    try {
      JSONObject o = new JSONObject();
      o.put("type", mEventType.toString());
      o.put("ms", getDuration());
      o.put("orientation", mOrientation.toString());
      o.put("pages", PageflipUtils.join(",", mPages));
      o.put("view_session", mViewSession);
      return o;
    } catch (JSONException e) {
      SgnLog.d(TAG, e.getMessage(), e);
    }
    return new JSONObject();
  }
}
public class MemoryCache implements Cache {

  public static final String TAG = Constants.getTag(MemoryCache.class);

  /** On average we've measured a Cache.Item from the ETA API to be around 4kb */
  private static final int AVG_ITEM_SIZE = 4096;

  /** Max cache size - init to 1mb */
  private int mMaxItems = 256;

  /** Perceent of cache to remove on cleanup */
  private int mPercentToClean = 20;

  private Map<String, Item> mCache;

  public MemoryCache() {

    setLimit(Runtime.getRuntime().maxMemory() / 8);

    float loadFactor = 1.5f; // Quicker lookup times
    boolean accessOrder = true; // Enable LRU ordering
    mCache =
        Collections.synchronizedMap(
            new LinkedHashMap<String, Item>(mMaxItems, loadFactor, accessOrder));
  }

  /**
   * Set the percentage of cache to clean out when memory limit is hit
   *
   * @param percentToClean A percentage between 0 and 100 (default is 20)
   */
  public void setCleanLimit(int percentToClean) {
    if (percentToClean <= 0 || 100 <= percentToClean) {
      throw new IllegalArgumentException("Percent a number between 0-100");
    }
    mPercentToClean = percentToClean;
  }

  /**
   * Set the limit on memory this Cache may use.
   *
   * @param maxMemLimit The limit in bytes
   */
  public void setLimit(long maxMemLimit) {
    if (maxMemLimit > Runtime.getRuntime().maxMemory()) {
      throw new IllegalArgumentException("maxMemLimit cannot be more than max heap size");
    }
    mMaxItems = (int) (maxMemLimit / AVG_ITEM_SIZE);
    SgnLog.v(
        TAG, "New memory limit: " + maxMemLimit / 1024 + "kb (approx " + mMaxItems + " items)");
  }

  public void put(Request<?> request, Response<?> response) {

    // If the request is cacheable
    if (request.getMethod() == Method.GET
        && request.isCacheable()
        && !request.isCacheHit()
        && response.cache != null) {

      request.addEvent("add-response-to-cache");
      synchronized (MemoryCache.class) {
        mCache.putAll(response.cache);
        checkSize();
      }
    }
  }

  private void checkSize() {

    int size = mCache.size();

    if (size > mMaxItems) {

      float percentToRemove = (float) mPercentToClean / (float) 100;
      int itemsToRemove = (int) (size * percentToRemove);

      // least recently accessed item will be the first one iterated
      Iterator<Entry<String, Cache.Item>> it = mCache.entrySet().iterator();
      while (it.hasNext()) {
        it.next();
        it.remove();
        if (itemsToRemove-- == 0) {
          break;
        }
      }

      SgnLog.d(TAG, "Cleaned " + TAG + " new size: " + mCache.size());
    }
  }

  public Cache.Item get(String key) {

    synchronized (MemoryCache.class) {
      Cache.Item c = mCache.get(key);
      if (c == null) {
        return null;
      } else if (c.isExpired()) {
        mCache.remove(key);
        return null;
      }
      return c;
    }
  }

  public void clear() {
    synchronized (MemoryCache.class) {
      mCache.clear();
    }
  }
}