/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "Volume.h"
#include "VolumeCommand.h"
#include "VolumeManager.h"
#include "VolumeManagerLog.h"
#include "nsIVolume.h"
#include "nsXULAppAPI.h"

#include <vold/ResponseCode.h>

namespace mozilla {
namespace system {

#if DEBUG_VOLUME_OBSERVER
void
VolumeObserverList::Broadcast(Volume* const& aVolume)
{
  uint32_t size = mObservers.Length();
  for (uint32_t i = 0; i < size; ++i) {
    LOG("VolumeObserverList::Broadcast to [%u] %p volume '%s'",
        i, mObservers[i], aVolume->NameStr());
    mObservers[i]->Notify(aVolume);
  }
}
#endif

VolumeObserverList Volume::sEventObserverList;

// We have a feature where volumes can be locked when mounted. This
// is used to prevent a volume from being shared with the PC while
// it is actively being used (say for storing an update image)
//
// We use WakeLocks (a poor choice of name, but it does what we want)
// from the PowerManagerService to determine when we're locked.
// In particular we'll create a wakelock called volume-NAME-GENERATION
// (where NAME is the volume name, and GENERATION is its generation
// number), and if this wakelock is locked, then we'll prevent a volume
// from being shared.
//
// Implementation Details:
//
// Since the AutoMounter can only control when something gets mounted
// and not when it gets unmounted (for example: a user pulls the SDCard)
// and because Volume and nsVolume data structures are maintained on
// separate threads, we have the potential for some race conditions.
// We eliminate the race conditions by introducing the concept of a
// generation number. Every time a volume transitions to the Mounted
// state, it gets assigned a new generation number. Whenever the state
// of a Volume changes, we send the updated state and current generation
// number to the main thread where it gets updated in the nsVolume.
//
// Since WakeLocks can only be queried from the main-thread, the
// nsVolumeService looks for WakeLock status changes, and forwards
// the results to the IOThread.
//
// If the Volume (IOThread) receives a volume update where the generation
// number mismatches, then the update is simply ignored.
//
// When a Volume (IOThread) initially becomes mounted, we assume it to
// be locked until we get our first update from nsVolume (MainThread).
static int32_t sMountGeneration = 0;

static uint32_t sNextId = 1;

// We don't get media inserted/removed events at startup. So we
// assume it's present, and we'll be told that it's missing.
Volume::Volume(const nsCSubstring& aName)
  : mMediaPresent(true),
    mState(nsIVolume::STATE_INIT),
    mName(aName),
    mMountGeneration(-1),
    mMountLocked(true),  // Needs to agree with nsVolume::nsVolume
    mSharingEnabled(false),
    mFormatRequested(false),
    mMountRequested(false),
    mUnmountRequested(false),
    mCanBeShared(true),
    mIsSharing(false),
    mIsFormatting(false),
    mIsUnmounting(false),
    mIsRemovable(false),
    mIsHotSwappable(false),
    mId(sNextId++)
{
  DBG("Volume %s: created", NameStr());
}

void
Volume::Dump(const char* aLabel) const
{
  LOG("%s: Volume: %s (%d) is %s and %s @ %s gen %d locked %d",
      aLabel,
      NameStr(),
      Id(),
      StateStr(),
      MediaPresent() ? "inserted" : "missing",
      MountPoint().get(),
      MountGeneration(),
      (int)IsMountLocked());
  LOG("%s:   Sharing %s Mounting %s Formating %s Unmounting %s",
      aLabel,
      CanBeShared() ? (IsSharingEnabled() ? (IsSharing() ? "en-y" : "en-n")
                                          : "dis")
                    : "x",
      IsMountRequested() ? "req" : "n",
      IsFormatRequested() ? (IsFormatting() ? "req-y" : "req-n")
                          : (IsFormatting() ? "y" : "n"),
      IsUnmountRequested() ? (IsUnmounting() ? "req-y" : "req-n")
                           : (IsUnmounting() ? "y" : "n"));
}

void
Volume::ResolveAndSetMountPoint(const nsCSubstring& aMountPoint)
{
  nsCString mountPoint(aMountPoint);
  char realPathBuf[PATH_MAX];

  // Call realpath so that we wind up with a path which is compatible with
  // functions like nsVolumeService::GetVolumeByPath.

  if (realpath(mountPoint.get(), realPathBuf) < 0) {
    // The path we were handed doesn't exist. Warn about it, but use it
    // anyways assuming that the user knows what they're doing.

    ERR("ResolveAndSetMountPoint: realpath on '%s' failed: %d",
        mountPoint.get(), errno);
    mMountPoint = mountPoint;
  } else {
    mMountPoint = realPathBuf;
  }
  DBG("Volume %s: Setting mountpoint to '%s'", NameStr(), mMountPoint.get());
}

void Volume::SetFakeVolume(const nsACString& aMountPoint)
{
  this->mMountLocked = false;
  this->mCanBeShared = false;
  ResolveAndSetMountPoint(aMountPoint);
  SetState(nsIVolume::STATE_MOUNTED);
}

void
Volume::SetIsSharing(bool aIsSharing)
{
  if (aIsSharing == mIsSharing) {
    return;
  }
  mIsSharing = aIsSharing;
  LOG("Volume %s: IsSharing set to %d state %s",
      NameStr(), (int)mIsSharing, StateStr(mState));
  sEventObserverList.Broadcast(this);
}

void
Volume::SetIsFormatting(bool aIsFormatting)
{
  if (aIsFormatting == mIsFormatting) {
    return;
  }
  mIsFormatting = aIsFormatting;
  LOG("Volume %s: IsFormatting set to %d state %s",
      NameStr(), (int)mIsFormatting, StateStr(mState));
  if (mIsFormatting) {
    sEventObserverList.Broadcast(this);
  }
}

void
Volume::SetIsUnmounting(bool aIsUnmounting)
{
  if (aIsUnmounting == mIsUnmounting) {
    return;
  }
  mIsUnmounting = aIsUnmounting;
  LOG("Volume %s: IsUnmounting set to %d state %s",
      NameStr(), (int)mIsUnmounting, StateStr(mState));
  sEventObserverList.Broadcast(this);
}

void
Volume::SetIsRemovable(bool aIsRemovable)
{
  if (aIsRemovable == mIsRemovable) {
    return;
  }
  mIsRemovable = aIsRemovable;
  if (!mIsRemovable) {
    mIsHotSwappable = false;
  }
  LOG("Volume %s: IsRemovable set to %d state %s",
      NameStr(), (int)mIsRemovable, StateStr(mState));
  sEventObserverList.Broadcast(this);
}

void
Volume::SetIsHotSwappable(bool aIsHotSwappable)
{
  if (aIsHotSwappable == mIsHotSwappable) {
    return;
  }
  mIsHotSwappable = aIsHotSwappable;
  if (mIsHotSwappable) {
    mIsRemovable = true;
  }
  LOG("Volume %s: IsHotSwappable set to %d state %s",
      NameStr(), (int)mIsHotSwappable, StateStr(mState));
  sEventObserverList.Broadcast(this);
}

bool
Volume::BoolConfigValue(const nsCString& aConfigValue, bool& aBoolValue)
{
  if (aConfigValue.EqualsLiteral("1") ||
      aConfigValue.LowerCaseEqualsLiteral("true")) {
    aBoolValue = true;
    return true;
  }
  if (aConfigValue.EqualsLiteral("0") ||
      aConfigValue.LowerCaseEqualsLiteral("false")) {
    aBoolValue = false;
    return true;
  }
  return false;
}

void
Volume::SetConfig(const nsCString& aConfigName, const nsCString& aConfigValue)
{
  if (aConfigName.LowerCaseEqualsLiteral("removable")) {
    bool value = false;
    if (BoolConfigValue(aConfigValue, value)) {
      SetIsRemovable(value);
    } else {
      ERR("Volume %s: invalid value '%s' for configuration '%s'",
          NameStr(), aConfigValue.get(), aConfigName.get());
    }
    return;
  }
  if (aConfigName.LowerCaseEqualsLiteral("hotswappable")) {
    bool value = false;
    if (BoolConfigValue(aConfigValue, value)) {
      SetIsHotSwappable(value);
    } else {
      ERR("Volume %s: invalid value '%s' for configuration '%s'",
          NameStr(), aConfigValue.get(), aConfigName.get());
    }
    return;
  }
  ERR("Volume %s: invalid config '%s'", NameStr(), aConfigName.get());
}

void
Volume::SetMediaPresent(bool aMediaPresent)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  // mMediaPresent is slightly redunant to the state, however
  // when media is removed (while Idle), we get the following:
  //    631 Volume sdcard /mnt/sdcard disk removed (179:0)
  //    605 Volume sdcard /mnt/sdcard state changed from 1 (Idle-Unmounted) to 0 (No-Media)
  //
  // And on media insertion, we get:
  //    630 Volume sdcard /mnt/sdcard disk inserted (179:0)
  //    605 Volume sdcard /mnt/sdcard state changed from 0 (No-Media) to 2 (Pending)
  //    605 Volume sdcard /mnt/sdcard state changed from 2 (Pending) to 1 (Idle-Unmounted)
  //
  // On media removal while the media is mounted:
  //    632 Volume sdcard /mnt/sdcard bad removal (179:1)
  //    605 Volume sdcard /mnt/sdcard state changed from 4 (Mounted) to 5 (Unmounting)
  //    605 Volume sdcard /mnt/sdcard state changed from 5 (Unmounting) to 1 (Idle-Unmounted)
  //    631 Volume sdcard /mnt/sdcard disk removed (179:0)
  //    605 Volume sdcard /mnt/sdcard state changed from 1 (Idle-Unmounted) to 0 (No-Media)
  //
  // When sharing with a PC, it goes Mounted -> Idle -> Shared
  // When unsharing with a PC, it goes Shared -> Idle -> Mounted
  //
  // The AutoMounter needs to know whether the media is present or not when
  // processing the Idle state.

  if (mMediaPresent == aMediaPresent) {
    return;
  }

  LOG("Volume: %s media %s", NameStr(), aMediaPresent ? "inserted" : "removed");
  mMediaPresent = aMediaPresent;
  sEventObserverList.Broadcast(this);
}

void
Volume::SetSharingEnabled(bool aSharingEnabled)
{
  mSharingEnabled = aSharingEnabled;

  LOG("SetSharingMode for volume %s to %d canBeShared = %d",
      NameStr(), (int)mSharingEnabled, (int)mCanBeShared);
  sEventObserverList.Broadcast(this);
}

void
Volume::SetFormatRequested(bool aFormatRequested)
{
  mFormatRequested = aFormatRequested;

  LOG("SetFormatRequested for volume %s to %d CanBeFormatted = %d",
      NameStr(), (int)mFormatRequested, (int)CanBeFormatted());
}

void
Volume::SetMountRequested(bool aMountRequested)
{
  mMountRequested = aMountRequested;

  LOG("SetMountRequested for volume %s to %d CanBeMounted = %d",
      NameStr(), (int)mMountRequested, (int)CanBeMounted());
}

void
Volume::SetUnmountRequested(bool aUnmountRequested)
{
  mUnmountRequested = aUnmountRequested;

  LOG("SetUnmountRequested for volume %s to %d CanBeMounted = %d",
      NameStr(), (int)mUnmountRequested, (int)CanBeMounted());
}

void
Volume::SetState(Volume::STATE aNewState)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
  if (aNewState == mState) {
    return;
  }
  if (aNewState == nsIVolume::STATE_MOUNTED) {
    mMountGeneration = ++sMountGeneration;
    LOG("Volume %s (%u): changing state from %s to %s @ '%s' (%d observers) "
        "mountGeneration = %d, locked = %d",
        NameStr(), mId, StateStr(mState),
        StateStr(aNewState), mMountPoint.get(), sEventObserverList.Length(),
        mMountGeneration, (int)mMountLocked);
  } else {
    LOG("Volume %s (%u): changing state from %s to %s (%d observers)",
        NameStr(), mId, StateStr(mState),
        StateStr(aNewState), sEventObserverList.Length());
  }

  switch (aNewState) {
     case nsIVolume::STATE_NOMEDIA:
       // Cover the startup case where we don't get insertion/removal events
       mMediaPresent = false;
       mIsSharing = false;
       mUnmountRequested = false;
       mMountRequested = false;
       mIsUnmounting = false;
       break;

     case nsIVolume::STATE_MOUNTED:
     case nsIVolume::STATE_MOUNT_FAIL:
       mMountRequested = false;
       mIsFormatting = false;
       mIsSharing = false;
       mIsUnmounting = false;
       break;

     case nsIVolume::STATE_FORMATTING:
       mFormatRequested = false;
       mIsFormatting = true;
       mIsSharing = false;
       mIsUnmounting = false;
       break;

     case nsIVolume::STATE_SHARED:
     case nsIVolume::STATE_SHAREDMNT:
       // Covers startup cases. Normally, mIsSharing would be set to true
       // when we issue the command to initiate the sharing process, but
       // it's conceivable that a volume could already be in a shared state
       // when b2g starts.
       mIsSharing = true;
       mIsUnmounting = false;
       mIsFormatting = false;
       break;

     case nsIVolume::STATE_UNMOUNTING:
       mIsUnmounting = true;
       mIsFormatting = false;
       mIsSharing = false;
       break;

     case nsIVolume::STATE_IDLE: // Fall through
     case nsIVolume::STATE_CHECKMNT: // Fall through
     default:
       break;
  }
  mState = aNewState;
  sEventObserverList.Broadcast(this);
}

void
Volume::SetMountPoint(const nsCSubstring& aMountPoint)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  if (mMountPoint.Equals(aMountPoint)) {
    return;
  }
  ResolveAndSetMountPoint(aMountPoint);
}

void
Volume::StartMount(VolumeResponseCallback* aCallback)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  StartCommand(new VolumeActionCommand(this, "mount", "", aCallback));
}

void
Volume::StartUnmount(VolumeResponseCallback* aCallback)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  StartCommand(new VolumeActionCommand(this, "unmount", "force", aCallback));
}

void
Volume::StartFormat(VolumeResponseCallback* aCallback)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  StartCommand(new VolumeActionCommand(this, "format", "", aCallback));
}

void
Volume::StartShare(VolumeResponseCallback* aCallback)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  StartCommand(new VolumeActionCommand(this, "share", "ums", aCallback));
}

void
Volume::StartUnshare(VolumeResponseCallback* aCallback)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  StartCommand(new VolumeActionCommand(this, "unshare", "ums", aCallback));
}

void
Volume::StartCommand(VolumeCommand* aCommand)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  VolumeManager::PostCommand(aCommand);
}

//static
void
Volume::RegisterVolumeObserver(Volume::EventObserver* aObserver, const char* aName)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  sEventObserverList.AddObserver(aObserver);

  DBG("Added Volume Observer '%s' @%p, length = %u",
      aName, aObserver, sEventObserverList.Length());

  // Send an initial event to the observer (for each volume)
  size_t numVolumes = VolumeManager::NumVolumes();
  for (size_t volIndex = 0; volIndex < numVolumes; volIndex++) {
    RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex);
    aObserver->Notify(vol);
  }
}

//static
void
Volume::UnregisterVolumeObserver(Volume::EventObserver* aObserver, const char* aName)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  sEventObserverList.RemoveObserver(aObserver);

  DBG("Removed Volume Observer '%s' @%p, length = %u",
      aName, aObserver, sEventObserverList.Length());
}

//static
void
Volume::UpdateMountLock(const nsACString& aVolumeName,
                        const int32_t& aMountGeneration,
                        const bool& aMountLocked)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName);
  if (!vol || (vol->mMountGeneration != aMountGeneration)) {
    return;
  }
  if (vol->mMountLocked != aMountLocked) {
    vol->mMountLocked = aMountLocked;
    DBG("Volume::UpdateMountLock for '%s' to %d\n", vol->NameStr(), (int)aMountLocked);
    sEventObserverList.Broadcast(vol);
  }
}

void
Volume::HandleVoldResponse(int aResponseCode, nsCWhitespaceTokenizer& aTokenizer)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());

  // The volume name will have already been parsed, and the tokenizer will point
  // to the token after the volume name
  switch (aResponseCode) {
    case ::ResponseCode::VolumeListResult: {
      // Each line will look something like:
      //
      //  sdcard /mnt/sdcard 1
      //
      nsDependentCSubstring mntPoint(aTokenizer.nextToken());
      SetMountPoint(mntPoint);
      nsresult errCode;
      nsCString state(aTokenizer.nextToken());
      if (state.EqualsLiteral("X")) {
        // Special state for creating fake volumes which can't be shared.
        mCanBeShared = false;
        SetState(nsIVolume::STATE_MOUNTED);
      } else {
        SetState((STATE)state.ToInteger(&errCode));
      }
      break;
    }

    case ::ResponseCode::VolumeStateChange: {
      // Format of the line looks something like:
      //
      //  Volume sdcard /mnt/sdcard state changed from 7 (Shared-Unmounted) to 1 (Idle-Unmounted)
      //
      // So we parse out the state after the string " to "
      while (aTokenizer.hasMoreTokens()) {
        nsAutoCString token(aTokenizer.nextToken());
        if (token.EqualsLiteral("to")) {
          nsresult errCode;
          token = aTokenizer.nextToken();
          STATE newState = (STATE)(token.ToInteger(&errCode));
          if (newState == nsIVolume::STATE_MOUNTED) {
            // We set the state to STATE_CHECKMNT here, and the once the
            // AutoMounter detects that the volume is actually accessible
            // then the AutoMounter will set the volume as STATE_MOUNTED.
            SetState(nsIVolume::STATE_CHECKMNT);
          } else {
            if (State() == nsIVolume::STATE_CHECKING && newState == nsIVolume::STATE_IDLE) {
              LOG("Mount of volume '%s' failed", NameStr());
              SetState(nsIVolume::STATE_MOUNT_FAIL);
            } else {
              SetState(newState);
            }
          }
          break;
        }
      }
      break;
    }

    case ::ResponseCode::VolumeDiskInserted:
      SetMediaPresent(true);
      break;

    case ::ResponseCode::VolumeDiskRemoved: // fall-thru
    case ::ResponseCode::VolumeBadRemoval:
      SetMediaPresent(false);
      break;

    default:
      LOG("Volume: %s unrecognized reponse code (ignored)", NameStr());
      break;
  }
}

} // namespace system
} // namespace mozilla
