/*
 * Copyright (c) 2020 Red Hat, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA. 
 *
 * $Id: //eng/vdo-releases/aluminum/src/c++/vdo/base/actionManager.c#8 $
 */

#include "actionManager.h"

#include "memoryAlloc.h"

#include "adminState.h"
#include "completion.h"
#include "types.h"

/** An action to be performed in each of a set of zones */
typedef struct action Action;
struct action {
  /** Whether this structure is in use */
  bool              inUse;
  /** The admin operation associated with this action */
  AdminStateCode    operation;
  /**
   * The method to run on the initiator thread before the action is applied to
   * each zone.
   **/
  ActionPreamble   *preamble;
  /** The action to be performed in each zone */
  ZoneAction       *zoneAction;
  /**
   * The method to run on the initiator thread after the action has been
   * applied to each zone
   **/
  ActionConclusion *conclusion;
  /** The object to notify when the action is complete */
  VDOCompletion    *parent;
  /** The action specific context */
  void             *context;
  /** The action to perform after this one */
  Action           *next;
};

struct actionManager {
  /** The completion for performing actions */
  VDOCompletion     completion;
  /** The state of this action manager */
  AdminState        state;
  /** The two action slots*/
  Action            actions[2];
  /** The current action slot */
  Action           *currentAction;
  /** The number of zones in which an action is to be applied */
  ZoneCount         zones;
  /** A function to schedule a default next action */
  ActionScheduler  *scheduler;
  /**
   * A function to get the id of the thread on which to apply an action to a
   * zone
   **/
  ZoneThreadGetter *getZoneThreadID;
  /** The ID of the thread on which actions may be initiated */
  ThreadID          initiatorThreadID;
  /** Opaque data associated with this action manager */
  void             *context;
  /** The zone currently being acted upon */
  ZoneCount         actingZone;
};

/**
 * Convert a generic VDOCompletion to a ActionManager.
 *
 * @param completion The completion to convert
 *
 * @return The completion as a ActionManager
 **/
static inline ActionManager *asActionManager(VDOCompletion *completion)
{
  STATIC_ASSERT(offsetof(ActionManager, completion) == 0);
  assertCompletionType(completion->type, ACTION_COMPLETION);
  return (ActionManager *) completion;
}

/**
 * An action scheduler which does not schedule an action.
 *
 * <p>Implements ActionScheduler.
 **/
static bool noDefaultAction(void *context __attribute__((unused)))
{
  return false;
}

/**
 * A default preamble which does nothing.
 *
 * <p>Implements ActionPreamble
 **/
static void noPreamble(void          *context __attribute__((unused)),
                       VDOCompletion *completion)
{
  completeCompletion(completion);
}

/**
 * A default conclusion which does nothing.
 *
 * <p>Implements ActionConclusion.
 **/
static int noConclusion(void *context __attribute__((unused))) {
  return VDO_SUCCESS;
}

/**********************************************************************/
int makeActionManager(ZoneCount          zones,
                      ZoneThreadGetter  *getZoneThreadID,
                      ThreadID           initiatorThreadID,
                      void              *context,
                      ActionScheduler   *scheduler,
                      PhysicalLayer     *layer,
                      ActionManager    **managerPtr)
{
  ActionManager *manager;
  int result = ALLOCATE(1, ActionManager, __func__, &manager);
  if (result != VDO_SUCCESS) {
    return result;
  }

  *manager = (ActionManager) {
    .zones             = zones,
    .scheduler         = ((scheduler == NULL) ? noDefaultAction : scheduler),
    .getZoneThreadID   = getZoneThreadID,
    .initiatorThreadID = initiatorThreadID,
    .context           = context,
  };

  manager->actions[0].next = &manager->actions[1];
  manager->currentAction = manager->actions[1].next = &manager->actions[0];

  result = initializeEnqueueableCompletion(&manager->completion,
                                           ACTION_COMPLETION, layer);
  if (result != VDO_SUCCESS) {
    freeActionManager(&manager);
    return result;
  }

  *managerPtr = manager;
  return VDO_SUCCESS;
}

/**********************************************************************/
void freeActionManager(ActionManager **managerPtr)
{
  ActionManager *manager = *managerPtr;
  if (manager == NULL) {
    return;
  }

  destroyEnqueueable(&manager->completion);
  FREE(manager);
  *managerPtr = NULL;
}

/**********************************************************************/
AdminStateCode getCurrentManagerOperation(ActionManager *manager)
{
  return manager->state.state;
}

/**********************************************************************/
void *getCurrentActionContext(ActionManager *manager)
{
  return (manager->currentAction->inUse
          ? manager->currentAction->context : NULL);
}

/**********************************************************************/
static void finishActionCallback(VDOCompletion *completion);
static void applyToZone(VDOCompletion *completion);

/**
 * Get the thread ID for the current zone.
 *
 * @param manager  The action manager
 *
 * @return The ID of the thread on which to run actions for the current zone
 **/
static ThreadID getActingZoneThreadID(ActionManager *manager)
{
  return manager->getZoneThreadID(manager->context, manager->actingZone);
}

/**
 * Prepare the manager's completion to run on the next zone.
 *
 * @param manager  The action manager
 **/
static void prepareForNextZone(ActionManager *manager)
{
  prepareForRequeue(&manager->completion, applyToZone,
                    preserveErrorAndContinue, getActingZoneThreadID(manager),
                    manager->currentAction->parent);
}

/**
 * Prepare the manager's completion to run the conclusion on the initiator
 * thread.
 *
 * @param manager  The action manager
 **/
static void prepareForConclusion(ActionManager *manager)
{
  prepareForRequeue(&manager->completion, finishActionCallback,
                    preserveErrorAndContinue, manager->initiatorThreadID,
                    manager->currentAction->parent);
}

/**
 * Perform an action on the next zone if there is one.
 *
 * @param completion  The action completion
 **/
static void applyToZone(VDOCompletion *completion)
{
  ActionManager *manager = asActionManager(completion);
  ASSERT_LOG_ONLY((getCallbackThreadID() == getActingZoneThreadID(manager)),
                  "applyToZone() called on acting zones's thread");

  ZoneCount zone = manager->actingZone++;
  if (manager->actingZone == manager->zones) {
    // We are about to apply to the last zone. Once that is finished,
    // we're done, so go back to the initiator thread and finish up.
    prepareForConclusion(manager);
  } else {
    // Prepare to come back on the next zone
    prepareForNextZone(manager);
  }

  manager->currentAction->zoneAction(manager->context, zone, completion);
}

/**
 * The error handler for preamble errors.
 *
 * @param completion  The manager completion
 **/
static void handlePreambleError(VDOCompletion *completion)
{
  // Skip the zone actions since the preamble failed.
  completion->callback = finishActionCallback;
  preserveErrorAndContinue(completion);
}

/**
 * Launch the current action.
 *
 * @param manager  The action manager
 **/
static void launchCurrentAction(ActionManager *manager)
{
  Action *action = manager->currentAction;
  int     result = startOperation(&manager->state, action->operation);
  if (result != VDO_SUCCESS) {
    if (action->parent != NULL) {
      setCompletionResult(action->parent, result);
    }

    // We aren't going to run the preamble, so don't run the conclusion
    action->conclusion = noConclusion;
    finishActionCallback(&manager->completion);
    return;
  }

  if (action->zoneAction == NULL) {
    prepareForConclusion(manager);
  } else {
    manager->actingZone = 0;
    prepareForRequeue(&manager->completion, applyToZone, handlePreambleError,
                      getActingZoneThreadID(manager),
                      manager->currentAction->parent);
  }

  action->preamble(manager->context, &manager->completion);
}

/**
 * Attempt to schedule a next action.
 *
 * @param manager  The action manager
 *
 * @return <code>true</code> if an action was scheduled.
 **/
static bool scheduleDefaultAction(ActionManager *manager)
{
  // Don't schedule a default action if we are operating or not in normal
  // operation.
  return ((manager->state.state == ADMIN_STATE_NORMAL_OPERATION)
          && manager->scheduler(manager->context));
}

/**
 * Finish an action now that it has been applied to all zones. This
 * callback is registered in applyToZone().
 *
 * @param completion  The action manager completion
 **/
static void finishActionCallback(VDOCompletion *completion)
{
  ActionManager *manager        = asActionManager(completion);
  Action         action         = *(manager->currentAction);
  manager->currentAction->inUse = false;
  manager->currentAction        = manager->currentAction->next;

  // We need to check this now to avoid use-after-free issues if running the
  // conclusion or notifying the parent results in the manager being freed.
  bool hasNextAction = (manager->currentAction->inUse
                        || scheduleDefaultAction(manager));
  int result = action.conclusion(manager->context);
  finishOperation(&manager->state);
  if (action.parent != NULL) {
    finishCompletion(action.parent, result);
  }

  if (hasNextAction) {
    launchCurrentAction(manager);
  }
}

/**********************************************************************/
bool scheduleAction(ActionManager    *manager,
                    ActionPreamble   *preamble,
                    ZoneAction       *zoneAction,
                    ActionConclusion *conclusion,
                    VDOCompletion    *parent)
{
  return scheduleOperation(manager, ADMIN_STATE_OPERATING, preamble,
                           zoneAction, conclusion, parent);
}

/**********************************************************************/
bool scheduleOperation(ActionManager    *manager,
                       AdminStateCode    operation,
                       ActionPreamble   *preamble,
                       ZoneAction       *zoneAction,
                       ActionConclusion *conclusion,
                       VDOCompletion    *parent)
{
  return scheduleOperationWithContext(manager, operation, preamble, zoneAction,
                                      conclusion, NULL, parent);
}

/**********************************************************************/
bool scheduleOperationWithContext(ActionManager    *manager,
                                  AdminStateCode    operation,
                                  ActionPreamble   *preamble,
                                  ZoneAction       *zoneAction,
                                  ActionConclusion *conclusion,
                                  void             *context,
                                  VDOCompletion    *parent)
{
  ASSERT_LOG_ONLY((getCallbackThreadID() == manager->initiatorThreadID),
                  "action initiated from correct thread");
  Action *action;
  if (!manager->currentAction->inUse) {
    action = manager->currentAction;
  } else if (!manager->currentAction->next->inUse) {
    action = manager->currentAction->next;
  } else {
    if (parent != NULL) {
      finishCompletion(parent, VDO_COMPONENT_BUSY);
    }

    return false;
  }

  *action = (Action) {
    .inUse      = true,
    .operation  = operation,
    .preamble   = (preamble == NULL) ? noPreamble : preamble,
    .zoneAction = zoneAction,
    .conclusion = (conclusion == NULL) ? noConclusion : conclusion,
    .context    = context,
    .parent     = parent,
    .next       = action->next,
  };

  if (action == manager->currentAction) {
    launchCurrentAction(manager);
  }

  return true;
}
