/* Copyright (c) 2009 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 com.google.appengine.demos.sticky.client.model;

import java.util.HashMap;
import java.util.Map;

import com.google.appengine.demos.sticky.client.model.Model.StatusObserver;
import com.google.appengine.demos.sticky.client.model.Service.AccessDeniedException;
import com.google.appengine.demos.sticky.client.model.Service.GetNotesResult;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * Controls all aspects of loading the set of {@link Note}s associated with the
 * selected {@link Surface}. This class takes care of performing (and possibly
 * retrying) a query for the initial set of Notes and then continues polling the
 * server for updates.
 * 
 * @author knorton@google.com (Kelly Norton)
 */
class NoteLoader {

  /**
   * Controls the initial load of notes from the server and will retry on
   * failure.
   */
  private class InitialLoader extends RetryTimer implements
      AsyncCallback<Service.GetNotesResult> {

    private final int id;

    public InitialLoader() {
      id = activeId;
      start();
    }

    public void onFailure(Throwable caught) {
      model.onServerFailed(caught instanceof Service.AccessDeniedException);
      if (isActiveSession(id)) {
        retryLater();
      }
    }

    public void onSuccess(GetNotesResult result) {
      model.onServerSucceeded();
      if (!isActiveSession(id)) {
        return;
      }

      model.getStatusObserver().onTaskFinished();

      timestamp = result.getTimestamp();
      final Note[] notes = result.getNotes();
      for (int i = 0, n = notes.length; i < n; ++i) {
        final Note note = notes[i];
        note.initialize(model);
        notesCache.put(note.getKey(), note);
      }
      model.notifySurfaceNotesReceived(notes);

      startPolling();
    }

    private void start() {
      model.getService().getNotes(model.getSelectedSurface().getKey(), null,
          this);
    }

    @Override
    protected void retry() {
      if (isActiveSession(id)) {
        start();
      }
    }
  }

  /**
   * Controls the polling calls to the server.
   */
  private class Poller extends Timer implements
      AsyncCallback<Service.GetNotesResult> {
    private final int id;

    public Poller() {
      this.id = activeId;
      scheduleRepeating(interval);
    }

    public void onFailure(Throwable caught) {
      model.onServerFailed(caught instanceof AccessDeniedException);

      if (!isActiveSession(id)) {
        cancel();
      }
    }

    public void onSuccess(GetNotesResult result) {
      model.onServerSucceeded();

      if (!isActiveSession(id)) {
        cancel();
        return;
      }

      timestamp = result.getTimestamp();

      final Note[] notes = result.getNotes();
      for (int i = 0, n = notes.length; i < n; ++i) {
        final Note note = notes[i];
        final Note existing = notesCache.get(note.getKey());
        // If note does not exist, send a noted created notification.
        // Otherwise, update the note and allow it to notify its observer that
        // it changed.
        if (existing == null) {
          note.initialize(model);
          notesCache.put(note.getKey(), note);
          model.notifyNoteCreated(note);
        } else {
          existing.update(note);
        }
      }
    }

    @Override
    public void run() {
      if (!isActiveSession(id)) {
        cancel();
        return;
      }
      final Surface surface = model.getSelectedSurface();
      if (surface.hasKey()) {
        model.getService().getNotes(model.getSelectedSurface().getKey(),
            timestamp, this);
      }
    }
  }

  private final Model model;

  private final int interval;

  private String timestamp;

  private int activeId;

  private Map<String, Note> notesCache = new HashMap<String, Note>();

  /**
   * Creates a new loader that is bound to the given model.
   * 
   * @param model the model to which this loader is bound
   * @param interval the time to wait between polls to the server
   */
  public NoteLoader(Model model, int interval) {
    this.model = model;
    this.interval = interval;
  }

  /**
   * Add a note to the loading cache so the {@link Model} can properly manage
   * duplicate objects.
   * 
   * @param key
   *          the notes key
   * @param note
   *          the note
   */
  public void cacheNote(String key, Note note) {
    assert key != null;
    notesCache.put(key, note);
  }

  /**
   * Forces the loader to cancel the active poller, reload the initial load and
   * start restart polling. This is called by the {@link Model} when a new
   * surface is selected.
   */
  public void reset() {
    activeId++;
    notesCache.clear();
    timestamp = null;

    final StatusObserver statusObserver = model.getStatusObserver();
    statusObserver.onTaskStarted("Loading '"
        + model.getSelectedSurface().getTitle() + "' ...");

    // If the surface has not been saved, we know there are no notes
    // associated with it, so it is safe to just synthesize an event on the
    // client.
    if (model.getSelectedSurface().hasKey()) {
      startInitialLoad();
    } else {
      statusObserver.onTaskFinished();
      model.notifySurfaceNotesReceived(new Note[0]);
      startPolling();
    }
  }

  private boolean isActiveSession(int id) {
    return id == activeId;
  }

  private void startInitialLoad() {
    new InitialLoader();
  }

  private void startPolling() {
    new Poller();
  }
}
