/**
 * A PageProviderIMpl
 *
 * <p>TODO: this may be moved entirely into PagingStore as there's an one-to-one relationship here
 * However I want to keep this isolated as much as possible during development
 *
 * @author <a href="mailto:[email protected]">Clebert Suconic</a>
 */
public class PageCursorProviderImpl implements PageCursorProvider {
  // Constants -----------------------------------------------------

  private static final Logger log = Logger.getLogger(PageCursorProviderImpl.class);

  boolean isTrace = log.isTraceEnabled();

  // Attributes ----------------------------------------------------

  private final PagingStore pagingStore;

  private final StorageManager storageManager;

  // This is the same executor used at the PageStoreImpl. One Executor per pageStore
  private final Executor executor;

  private final SoftValueHashMap<Long, PageCache> softCache;

  private final ConcurrentMap<Long, PageSubscription> activeCursors =
      new ConcurrentHashMap<Long, PageSubscription>();

  // Static --------------------------------------------------------

  // Constructors --------------------------------------------------

  public PageCursorProviderImpl(
      final PagingStore pagingStore,
      final StorageManager storageManager,
      final Executor executor,
      final int maxCacheSize) {
    this.pagingStore = pagingStore;
    this.storageManager = storageManager;
    this.executor = executor;
    this.softCache = new SoftValueHashMap<Long, PageCache>(maxCacheSize);
  }

  // Public --------------------------------------------------------

  public PagingStore getAssociatedStore() {
    return pagingStore;
  }

  public synchronized PageSubscription createSubscription(
      long cursorID, Filter filter, boolean persistent) {
    if (log.isDebugEnabled()) {
      log.debug(
          this.pagingStore.getAddress()
              + " creating subscription "
              + cursorID
              + " with filter "
              + filter,
          new Exception("trace"));
    }
    PageSubscription activeCursor = activeCursors.get(cursorID);
    if (activeCursor != null) {
      throw new IllegalStateException("Cursor " + cursorID + " had already been created");
    }

    activeCursor =
        new PageSubscriptionImpl(
            this, pagingStore, storageManager, executor, filter, cursorID, persistent);
    activeCursors.put(cursorID, activeCursor);
    return activeCursor;
  }

  /* (non-Javadoc)
   * @see org.hornetq.core.paging.cursor.PageCursorProvider#createCursor()
   */
  public synchronized PageSubscription getSubscription(long cursorID) {
    return activeCursors.get(cursorID);
  }

  public PagedMessage getMessage(final PagePosition pos) throws Exception {
    PageCache cache = getPageCache(pos);

    if (pos.getMessageNr() >= cache.getNumberOfMessages()) {
      // sanity check, this should never happen unless there's a bug
      throw new IllegalStateException("Invalid messageNumber passed = " + pos + " on " + cache);
    }

    return cache.getMessage(pos.getMessageNr());
  }

  public PagedReference newReference(
      final PagePosition pos, final PagedMessage msg, final PageSubscription subscription) {
    return new PagedReferenceImpl(pos, msg, subscription);
  }

  /**
   * No need to synchronize this method since the private getPageCache will have a synchronized call
   */
  public PageCache getPageCache(PagePosition pos) {
    return getPageCache(pos.getPageNr());
  }

  public PageCache getPageCache(final long pageId) {
    try {
      boolean needToRead = false;
      PageCache cache = null;
      synchronized (softCache) {
        if (pageId > pagingStore.getCurrentWritingPage()) {
          return null;
        }

        cache = softCache.get(pageId);
        if (cache == null) {
          if (!pagingStore.checkPageFileExists((int) pageId)) {
            return null;
          }

          cache = createPageCache(pageId);
          needToRead = true;
          // anyone reading from this cache will have to wait reading to finish first
          // we also want only one thread reading this cache
          cache.lock();
          if (isTrace) {
            log.trace("adding " + pageId + " into cursor = " + this.pagingStore.getAddress());
          }
          softCache.put(pageId, cache);
        }
      }

      // Reading is done outside of the synchronized block, however
      // the page stays locked until the entire reading is finished
      if (needToRead) {
        Page page = null;
        try {
          page = pagingStore.createPage((int) pageId);

          storageManager.beforePageRead();
          page.open();

          List<PagedMessage> pgdMessages = page.read(storageManager);
          cache.setMessages(pgdMessages.toArray(new PagedMessage[pgdMessages.size()]));
        } finally {
          try {
            if (page != null) {
              page.close();
            }
          } catch (Throwable ignored) {
          }
          storageManager.afterPageRead();
          cache.unlock();
        }
      }

      return cache;
    } catch (Exception e) {
      throw new RuntimeException(
          "Couldn't complete paging due to an IO Exception on Paging - " + e.getMessage(), e);
    }
  }

  public void addPageCache(PageCache cache) {
    synchronized (softCache) {
      softCache.put(cache.getPageId(), cache);
    }
  }

  public int getCacheMaxSize() {
    return softCache.getMaxEelements();
  }

  public void setCacheMaxSize(final int size) {
    softCache.setMaxElements(size);
  }

  public int getCacheSize() {
    synchronized (softCache) {
      return softCache.size();
    }
  }

  public void processReload() throws Exception {
    for (PageSubscription cursor : this.activeCursors.values()) {
      cursor.processReload();
    }

    cleanup();
  }

  public void stop() {
    for (PageSubscription cursor : activeCursors.values()) {
      cursor.stop();
    }

    Future future = new Future();

    executor.execute(future);

    while (!future.await(10000)) {
      log.warn("Waiting cursor provider " + this + " to finish executors" + executor);
    }
  }

  public void flushExecutors() {
    for (PageSubscription cursor : activeCursors.values()) {
      cursor.flushExecutors();
    }

    Future future = new Future();

    executor.execute(future);

    while (!future.await(10000)) {
      log.warn("Waiting cursor provider " + this + " to finish executors " + executor);
    }
  }

  public void close(PageSubscription cursor) {
    activeCursors.remove(cursor.getId());

    scheduleCleanup();
  }

  /* (non-Javadoc)
   * @see org.hornetq.core.paging.cursor.PageCursorProvider#scheduleCleanup()
   */
  public void scheduleCleanup() {

    executor.execute(
        new Runnable() {
          public void run() {
            storageManager.setContext(storageManager.newSingleThreadContext());
            try {
              cleanup();
            } finally {
              storageManager.clearContext();
            }
          }

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

  public void cleanup() {
    ArrayList<Page> depagedPages = new ArrayList<Page>();

    while (true) {
      if (pagingStore.lock(100)) {
        break;
      }
      if (!pagingStore.isStarted()) return;
    }

    synchronized (this) {
      try {
        if (!pagingStore.isStarted()) {
          return;
        }

        if (pagingStore.getNumberOfPages() == 0) {
          return;
        }

        if (log.isDebugEnabled()) {
          log.debug("Asserting cleanup for address " + this.pagingStore.getAddress());
        }

        ArrayList<PageSubscription> cursorList = new ArrayList<PageSubscription>();
        cursorList.addAll(activeCursors.values());

        long minPage = checkMinPage(cursorList);

        if (minPage == pagingStore.getCurrentWritingPage()
            && pagingStore.getCurrentPage().getNumberOfMessages() > 0) {
          boolean complete = true;

          for (PageSubscription cursor : cursorList) {
            if (!cursor.isComplete(minPage)) {
              if (log.isDebugEnabled()) {
                log.debug("Cursor " + cursor + " was considered incomplete at page " + minPage);
              }

              complete = false;
              break;
            } else {
              if (log.isDebugEnabled()) {
                log.debug("Cursor " + cursor + "was considered **complete** at page " + minPage);
              }
            }
          }

          if (!pagingStore.isStarted()) {
            return;
          }

          if (complete) {

            if (log.isDebugEnabled()) {
              log.debug(
                  "Address "
                      + pagingStore.getAddress()
                      + " is leaving page mode as all messages are consumed and acknowledged from the page store");
            }

            pagingStore.forceAnotherPage();

            Page currentPage = pagingStore.getCurrentPage();

            storePositions(cursorList, currentPage);

            pagingStore.stopPaging();

            // This has to be called after we stopped paging
            for (PageSubscription cursor : cursorList) {
              cursor.scheduleCleanupCheck();
            }
          }
        }

        for (long i = pagingStore.getFirstPage(); i < minPage; i++) {
          Page page = pagingStore.depage();
          if (page == null) {
            break;
          }
          depagedPages.add(page);
        }

        if (pagingStore.getNumberOfPages() == 0
            || pagingStore.getNumberOfPages() == 1
                && pagingStore.getCurrentPage().getNumberOfMessages() == 0) {
          pagingStore.stopPaging();
        } else {
          if (log.isTraceEnabled()) {
            log.trace(
                "Couldn't cleanup page on address "
                    + this.pagingStore.getAddress()
                    + " as numberOfPages == "
                    + pagingStore.getNumberOfPages()
                    + " and currentPage.numberOfMessages = "
                    + pagingStore.getCurrentPage().getNumberOfMessages());
          }
        }
      } catch (Exception ex) {
        log.warn("Couldn't complete cleanup on paging", ex);
        return;
      } finally {
        pagingStore.unlock();
      }
    }

    try {
      for (Page depagedPage : depagedPages) {
        PageCache cache;
        PagedMessage[] pgdMessages;
        synchronized (softCache) {
          cache = softCache.get((long) depagedPage.getPageId());
        }

        if (isTrace) {
          log.trace("Removing page " + depagedPage.getPageId() + " from page-cache");
        }

        if (cache == null) {
          // The page is not on cache any more
          // We need to read the page-file before deleting it
          // to make sure we remove any large-messages pending
          storageManager.beforePageRead();

          List<PagedMessage> pgdMessagesList = null;
          try {
            depagedPage.open();
            pgdMessagesList = depagedPage.read(storageManager);
          } finally {
            try {
              depagedPage.close();
            } catch (Exception e) {
            }

            storageManager.afterPageRead();
          }
          depagedPage.close();
          pgdMessages = pgdMessagesList.toArray(new PagedMessage[pgdMessagesList.size()]);
        } else {
          pgdMessages = cache.getMessages();
        }

        depagedPage.delete(pgdMessages);

        synchronized (softCache) {
          softCache.remove((long) depagedPage.getPageId());
        }
      }
    } catch (Exception ex) {
      log.warn("Couldn't complete cleanup on paging", ex);
      return;
    }
  }

  /**
   * @param cursorList
   * @param currentPage
   * @throws Exception
   */
  private void storePositions(ArrayList<PageSubscription> cursorList, Page currentPage)
      throws Exception {
    try {
      // First step: Move every cursor to the next bookmarked page (that was just created)
      for (PageSubscription cursor : cursorList) {
        cursor.confirmPosition(new PagePositionImpl(currentPage.getPageId(), -1));
      }

      while (!storageManager.waitOnOperations(5000)) {
        log.warn("Couldn't complete operations on IO context " + storageManager.getContext());
      }
    } finally {
      for (PageSubscription cursor : cursorList) {
        cursor.enableAutoCleanup();
      }
    }
  }

  public void printDebug() {
    System.out.println("Debug information for PageCursorProviderImpl:");
    for (PageCache cache : softCache.values()) {
      System.out.println("Cache " + cache);
    }
  }

  // Package protected ---------------------------------------------

  // Protected -----------------------------------------------------

  /* Protected as we may let test cases to instrument the test */
  protected PageCacheImpl createPageCache(final long pageId) throws Exception {
    return new PageCacheImpl(pagingStore.createPage((int) pageId));
  }

  // Private -------------------------------------------------------

  /** This method is synchronized because we want it to be atomic with the cursors being used */
  private long checkMinPage(List<PageSubscription> cursorList) {
    long minPage = Long.MAX_VALUE;

    for (PageSubscription cursor : cursorList) {
      long firstPage = cursor.getFirstPage();
      if (log.isDebugEnabled()) {
        log.debug(
            this.pagingStore.getAddress()
                + " has a cursor "
                + cursor
                + " with first page="
                + firstPage);
      }
      if (firstPage < minPage) {
        minPage = firstPage;
      }
    }

    if (log.isDebugEnabled()) {
      log.debug(this.pagingStore.getAddress() + " has minPage=" + minPage);
    }

    return minPage;
  }

  // Inner classes -------------------------------------------------

}
  public void cleanup() {
    ArrayList<Page> depagedPages = new ArrayList<Page>();

    while (true) {
      if (pagingStore.lock(100)) {
        break;
      }
      if (!pagingStore.isStarted()) return;
    }

    synchronized (this) {
      try {
        if (!pagingStore.isStarted()) {
          return;
        }

        if (pagingStore.getNumberOfPages() == 0) {
          return;
        }

        if (log.isDebugEnabled()) {
          log.debug("Asserting cleanup for address " + this.pagingStore.getAddress());
        }

        ArrayList<PageSubscription> cursorList = new ArrayList<PageSubscription>();
        cursorList.addAll(activeCursors.values());

        long minPage = checkMinPage(cursorList);

        if (minPage == pagingStore.getCurrentWritingPage()
            && pagingStore.getCurrentPage().getNumberOfMessages() > 0) {
          boolean complete = true;

          for (PageSubscription cursor : cursorList) {
            if (!cursor.isComplete(minPage)) {
              if (log.isDebugEnabled()) {
                log.debug("Cursor " + cursor + " was considered incomplete at page " + minPage);
              }

              complete = false;
              break;
            } else {
              if (log.isDebugEnabled()) {
                log.debug("Cursor " + cursor + "was considered **complete** at page " + minPage);
              }
            }
          }

          if (!pagingStore.isStarted()) {
            return;
          }

          if (complete) {

            if (log.isDebugEnabled()) {
              log.debug(
                  "Address "
                      + pagingStore.getAddress()
                      + " is leaving page mode as all messages are consumed and acknowledged from the page store");
            }

            pagingStore.forceAnotherPage();

            Page currentPage = pagingStore.getCurrentPage();

            storePositions(cursorList, currentPage);

            pagingStore.stopPaging();

            // This has to be called after we stopped paging
            for (PageSubscription cursor : cursorList) {
              cursor.scheduleCleanupCheck();
            }
          }
        }

        for (long i = pagingStore.getFirstPage(); i < minPage; i++) {
          Page page = pagingStore.depage();
          if (page == null) {
            break;
          }
          depagedPages.add(page);
        }

        if (pagingStore.getNumberOfPages() == 0
            || pagingStore.getNumberOfPages() == 1
                && pagingStore.getCurrentPage().getNumberOfMessages() == 0) {
          pagingStore.stopPaging();
        } else {
          if (log.isTraceEnabled()) {
            log.trace(
                "Couldn't cleanup page on address "
                    + this.pagingStore.getAddress()
                    + " as numberOfPages == "
                    + pagingStore.getNumberOfPages()
                    + " and currentPage.numberOfMessages = "
                    + pagingStore.getCurrentPage().getNumberOfMessages());
          }
        }
      } catch (Exception ex) {
        log.warn("Couldn't complete cleanup on paging", ex);
        return;
      } finally {
        pagingStore.unlock();
      }
    }

    try {
      for (Page depagedPage : depagedPages) {
        PageCache cache;
        PagedMessage[] pgdMessages;
        synchronized (softCache) {
          cache = softCache.get((long) depagedPage.getPageId());
        }

        if (isTrace) {
          log.trace("Removing page " + depagedPage.getPageId() + " from page-cache");
        }

        if (cache == null) {
          // The page is not on cache any more
          // We need to read the page-file before deleting it
          // to make sure we remove any large-messages pending
          storageManager.beforePageRead();

          List<PagedMessage> pgdMessagesList = null;
          try {
            depagedPage.open();
            pgdMessagesList = depagedPage.read(storageManager);
          } finally {
            try {
              depagedPage.close();
            } catch (Exception e) {
            }

            storageManager.afterPageRead();
          }
          depagedPage.close();
          pgdMessages = pgdMessagesList.toArray(new PagedMessage[pgdMessagesList.size()]);
        } else {
          pgdMessages = cache.getMessages();
        }

        depagedPage.delete(pgdMessages);

        synchronized (softCache) {
          softCache.remove((long) depagedPage.getPageId());
        }
      }
    } catch (Exception ex) {
      log.warn("Couldn't complete cleanup on paging", ex);
      return;
    }
  }