/*
 * ClientStateUpdater.java
 *
 * Copyright (C) 2009-12 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.studio.client.workbench;

import com.google.inject.Inject;

import org.rstudio.core.client.Barrier.Token;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.TimeBufferedCommand;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.workbench.events.*;
import org.rstudio.studio.client.workbench.model.ClientState;
import org.rstudio.studio.client.workbench.model.WorkbenchServerOperations;

public class ClientStateUpdater extends TimeBufferedCommand
{
   /**
    * "Client state" collectively refers to the set of stuff that the
    * client would like to remember for the next time we connect.
    *
    * Components that would like to participate in saving client state should
    * add a handler to SaveClientStateEvent on EventBus, and in
    * that handler, add values to the ClientState if they have changed.
    * Values that are unchanged since the last update need not be added,
    * as the values that are put in ClientState will be merged with previous
    * values.
    *
    * The SaveClientStateEvent fires either passively (on a timer) or actively
    * (on request [though in fact also on a timer, just a shorter one]). Any
    * component can request that SaveClientStateEvent be fired: simply fire
    * PushClientStateEvent on the EventBus.
    */
   @Inject
   public ClientStateUpdater(EventBus events,
                             WorkbenchServerOperations server)
   {
      super(INITIAL_INTERVAL_MILLIS, PASSIVE_INTERVAL_MILLIS, ACTIVE_INTERVAL_MILLIS);
      events_ = events;
      server_ = server;

      events_.addHandler(PushClientStateEvent.TYPE, new PushClientStateHandler()
      {
         public void onPushClientState(PushClientStateEvent event)
         {
            // Don't allow active pushes until after the initial interval
            // has elapsed. This lets us avoid storms of requests during
            // startup.
            if (lastExecuted_ != null)
               nudge();
         }
      });

      events_.addHandler(LastChanceSaveEvent.TYPE, new LastChanceSaveHandler()
      {
         public void onLastChanceSave(LastChanceSaveEvent event)
         {
            // We're quitting. Save client state one more time.
            barrierToken_ = event.acquire();
            try
            {
               nudge();
            }
            catch(Exception ex)
            {
               Debug.log("Exception on scheduling client state save for shutdown: ");
               Debug.logException(ex);
               if (barrierToken_ != null)
                  barrierToken_.release();
            }
         }
      });
   }

   @Override
   protected void performAction(final boolean shouldSchedulePassive)
   {
      ClientState state = ClientState.create();
      try
      {
         events_.fireEvent(new SaveClientStateEvent(state));
      }
      catch (Exception e)
      {
         onComplete(shouldSchedulePassive);
         return;
      }

      if (state.isEmpty())
      {
         onComplete(shouldSchedulePassive);
         return;
      }

      try
      {
         server_.updateClientState(
               state.getTemporaryData(),
               state.getPersistentData(),
               state.getProjectPersistentData(),
               new ServerRequestCallback<Void>() {
                  @Override
                  public void onError(ServerError error)
                  {
                     onComplete(shouldSchedulePassive);
                  }

                  @Override
                  public void onResponseReceived(Void response)
                  {
                     onComplete(shouldSchedulePassive);
                  }
               });
      }
      catch (Exception ex)
      {
         Debug.log("Exception updating client state: ");
         Debug.logException(ex);
         
         // complete (ensure barrier token is released)
         onComplete(shouldSchedulePassive);
      }
   }

   private void onComplete(boolean shouldSchedulePassive)
   {
      if (barrierToken_ != null)
         barrierToken_.release();
      if (shouldSchedulePassive)
         schedulePassive();
   }

   private static final int INITIAL_INTERVAL_MILLIS = 2000;
   private static final int PASSIVE_INTERVAL_MILLIS = 5000;
   private static final int ACTIVE_INTERVAL_MILLIS = Desktop.isDesktop()
                                                     ? 100
                                                     : 350;
   private final EventBus events_;
   private final WorkbenchServerOperations server_;
   private Token barrierToken_;
}
