//===-- MICmdInvoker.cpp ----------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

// In-house headers:
#include "MICmdInvoker.h"
#include "MICmdBase.h"
#include "MICmdMgr.h"
#include "MICmnLog.h"
#include "MICmnStreamStdout.h"
#include "MIDriver.h"

//++
// Details: CMICmdInvoker constructor.
// Type:    Method.
// Args:    None.
// Return:  None.
// Throws:  None.
//--
CMICmdInvoker::CMICmdInvoker() : m_rStreamOut(CMICmnStreamStdout::Instance()) {}

//++
// Details: CMICmdInvoker destructor.
// Type:    Overridable.
// Args:    None.
// Return:  None.
// Throws:  None.
//--
CMICmdInvoker::~CMICmdInvoker() { Shutdown(); }

//++
// Details: Initialize resources for *this Command Invoker.
// Type:    Method.
// Args:    None.
// Return:  MIstatus::success - Functional succeeded.
//          MIstatus::failure - Functional failed.
// Throws:  None.
//--
bool CMICmdInvoker::Initialize() {
  m_clientUsageRefCnt++;

  if (m_bInitialized)
    return MIstatus::success;

  m_bInitialized = true;

  return MIstatus::success;
}

//++
// Details: Release resources for *this Stdin stream.
// Type:    Method.
// Args:    None.
// Return:  MIstatus::success - Functional succeeded.
//          MIstatus::failure - Functional failed.
// Throws:  None.
//--
bool CMICmdInvoker::Shutdown() {
  if (--m_clientUsageRefCnt > 0)
    return MIstatus::success;

  if (!m_bInitialized)
    return MIstatus::success;

  CmdDeleteAll();

  m_bInitialized = false;

  return MIstatus::success;
}

//++
// Details: Empty the map of invoked commands doing work. Command objects are
// deleted too.
// Type:    Method.
// Args:    None.
// Return:  None.
// Throws:  None.
//--
void CMICmdInvoker::CmdDeleteAll() {
  CMICmdMgr &rMgr = CMICmdMgr::Instance();
  MapCmdIdToCmd_t::const_iterator it = m_mapCmdIdToCmd.begin();
  while (it != m_mapCmdIdToCmd.end()) {
    const MIuint cmdId((*it).first);
    MIunused(cmdId);
    CMICmdBase *pCmd = (*it).second;
    const CMIUtilString &rCmdName(pCmd->GetCmdData().strMiCmd);
    MIunused(rCmdName);
    rMgr.CmdDelete(pCmd->GetCmdData());

    // Next
    ++it;
  }
  m_mapCmdIdToCmd.clear();
}

//++
// Details: Remove from the map of invoked commands doing work a command that
// has finished
//          its work. The command object is deleted too.
// Type:    Method.
// Args:    vId             - (R) Command object's unique ID.
//          vbYesDeleteCmd  - (R) True = Delete command object, false = delete
//          via the Command Manager.
// Return:  None.
// Throws:  None.
//--
bool CMICmdInvoker::CmdDelete(const MIuint vId,
                              const bool vbYesDeleteCmd /*= false*/) {
  CMICmdMgr &rMgr = CMICmdMgr::Instance();
  MapCmdIdToCmd_t::const_iterator it = m_mapCmdIdToCmd.find(vId);
  if (it != m_mapCmdIdToCmd.end()) {
    CMICmdBase *pCmd = (*it).second;
    if (vbYesDeleteCmd) {
      // Via registered interest command manager callback *this object to delete
      // the command
      m_mapCmdIdToCmd.erase(it);
      delete pCmd;
    } else
      // Notify other interested object of this command's pending deletion
      rMgr.CmdDelete(pCmd->GetCmdData());
  }

  if (m_mapCmdIdToCmd.empty())
    rMgr.CmdUnregisterForDeleteNotification(*this);

  return MIstatus::success;
}

//++
// Details: Add to the map of invoked commands doing work a command that is
// about to
//          start to do work.
// Type:    Method.
// Args:    vCmd    - (R) Command object.
// Return:  None.
// Throws:  None.
//--
bool CMICmdInvoker::CmdAdd(const CMICmdBase &vCmd) {
  if (m_mapCmdIdToCmd.empty()) {
    CMICmdMgr &rMgr = CMICmdMgr::Instance();
    rMgr.CmdRegisterForDeleteNotification(*this);
  }

  const MIuint &cmdId(vCmd.GetCmdData().id);
  MapCmdIdToCmd_t::const_iterator it = m_mapCmdIdToCmd.find(cmdId);
  if (it != m_mapCmdIdToCmd.end())
    return MIstatus::success;

  MapPairCmdIdToCmd_t pr(cmdId, const_cast<CMICmdBase *>(&vCmd));
  m_mapCmdIdToCmd.insert(pr);

  return MIstatus::success;
}

//++
// Details: Having previously had the potential command validated and found
// valid now
//          get the command executed.
//          If the Functionality returns MIstatus::failure call
//          GetErrorDescription().
//          This function is used by the application's main thread.
// Type:    Method.
// Args:    vCmd    - (RW) Command object.
// Return:  MIstatus::success - Functionality succeeded.
//          MIstatus::failure - Functionality failed.
// Throws:  None.
//--
bool CMICmdInvoker::CmdExecute(CMICmdBase &vCmd) {
  bool bOk = CmdAdd(vCmd);

  if (bOk) {
    vCmd.AddCommonArgs();
    if (!vCmd.ParseArgs()) {
      // Report command execution failed
      const SMICmdData cmdData(vCmd.GetCmdData());
      CmdStdout(cmdData);
      CmdCauseAppExit(vCmd);
      CmdDelete(cmdData.id);

      // Proceed to wait or execute next command
      return MIstatus::success;
    }
  }

  if (bOk && !vCmd.Execute()) {
    // Report command execution failed
    const SMICmdData cmdData(vCmd.GetCmdData());
    CmdStdout(cmdData);
    CmdCauseAppExit(vCmd);
    CmdDelete(cmdData.id);

    // Proceed to wait or execute next command
    return MIstatus::success;
  }

  bOk = CmdExecuteFinished(vCmd);

  return bOk;
}

//++
// Details: Called when a command has finished its Execution() work either
// synchronously
//          because the command executed was the type a non event type or
//          asynchronously
//          via the command's callback (because of an SB Listener event). Needs
//          to be called
//          so that *this invoker call do some house keeping and then proceed to
//          call
//          the command's Acknowledge() function.
// Type:    Method.
// Args:    vCmd    - (R) Command object.
// Return:  MIstatus::success - Functionality succeeded.
//          MIstatus::failure - Functionality failed.
// Throws:  None.
//--
bool CMICmdInvoker::CmdExecuteFinished(CMICmdBase &vCmd) {
  // Command finished now get the command to gather it's information and form
  // the MI
  // Result record
  if (!vCmd.Acknowledge()) {
    // Report command acknowledge functionality failed
    const SMICmdData cmdData(vCmd.GetCmdData());
    CmdStdout(cmdData);
    CmdCauseAppExit(vCmd);
    CmdDelete(cmdData.id);

    // Proceed to wait or execute next command
    return MIstatus::success;
  }

  // Retrieve the command's latest data/information. Needed for commands of the
  // event type so have
  // a record of commands pending finishing execution.
  const CMIUtilString &rMIResultRecord(vCmd.GetMIResultRecord());
  SMICmdData cmdData(
      vCmd.GetCmdData()); // Make a copy as the command will be deleted soon
  cmdData.strMiCmdResultRecord = rMIResultRecord; // Precautionary copy as the
                                                  // command might forget to do
                                                  // this
  if (vCmd.HasMIResultRecordExtra()) {
    cmdData.bHasResultRecordExtra = true;
    const CMIUtilString &rMIExtra(vCmd.GetMIResultRecordExtra());
    cmdData.strMiCmdResultRecordExtra =
        rMIExtra; // Precautionary copy as the command might forget to do this
  }

  // Send command's MI response to the client
  bool bOk = CmdStdout(cmdData);

  // Delete the command object as do not require anymore
  bOk = bOk && CmdDelete(vCmd.GetCmdData().id);

  return bOk;
}

//++
// Details: If the MI Driver is not operating via a client i.e. Eclipse check
// the command
//          on failure suggests the application exits. A command can be such
//          that a
//          failure cannot the allow the application to continue operating.
// Args:    vCmd    - (R) Command object.
// Return:  None.
// Return:  None.
// Throws:  None.
//--
void CMICmdInvoker::CmdCauseAppExit(const CMICmdBase &vCmd) const {
  if (vCmd.GetExitAppOnCommandFailure()) {
    CMIDriver &rDriver(CMIDriver::Instance());
    if (rDriver.IsDriverDebuggingArgExecutable()) {
      rDriver.SetExitApplicationFlag(true);
    }
  }
}

//++
// Details: Write to stdout and the Log file the command's MI formatted result.
// Type:    vCmdData    - (R) A command's information.
// Return:  MIstatus::success - Functionality succeeded.
//          MIstatus::failure - Functionality failed.
// Return:  None.
// Throws:  None.
//--
bool CMICmdInvoker::CmdStdout(const SMICmdData &vCmdData) const {
  bool bOk = m_pLog->WriteLog(vCmdData.strMiCmdAll);
  const bool bLock = bOk && m_rStreamOut.Lock();
  bOk = bOk && bLock &&
        m_rStreamOut.WriteMIResponse(vCmdData.strMiCmdResultRecord);
  if (bOk && vCmdData.bHasResultRecordExtra) {
    bOk = m_rStreamOut.WriteMIResponse(vCmdData.strMiCmdResultRecordExtra);
  }
  bOk = bLock && m_rStreamOut.Unlock();

  return bOk;
}

//++
// Details: Required by the CMICmdMgr::ICmdDeleteCallback. *this object is
// registered
//          with the Command Manager to receive callbacks when a command is
//          being deleted.
//          An object, *this invoker, does not delete a command object itself
//          but calls
//          the Command Manager to delete a command object. This function is the
//          Invoker's
//          called.
//          The Invoker owns the command objects and so can delete them but must
//          do it
//          via the manager so other objects can be notified of the deletion.
// Type:    Method.
// Args:    vCmd    - (RW) Command.
// Return:  None.
// Throws:  None.
//--
void CMICmdInvoker::Delete(SMICmdData &vCmd) { CmdDelete(vCmd.id, true); }
