/** @file
  Miscellaneous functions.

Copyright (c) 2005 - 2018, Intel Corporation. All rights reserved.<BR>
SPDX-License-Identifier: BSD-2-Clause-Patent


**/

#include "Fat.h"
UINT8  mMonthDays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

/**

  Create the task

  @param  IFile                 - The instance of the open file.
  @param  Token                 - A pointer to the token associated with the transaction.

  @return FAT_TASK *            - Return the task instance.

**/
FAT_TASK *
FatCreateTask (
  FAT_IFILE          *IFile,
  EFI_FILE_IO_TOKEN  *Token
  )
{
  FAT_TASK  *Task;

  Task = AllocateZeroPool (sizeof (*Task));
  if (Task != NULL) {
    Task->Signature   = FAT_TASK_SIGNATURE;
    Task->IFile       = IFile;
    Task->FileIoToken = Token;
    InitializeListHead (&Task->Subtasks);
    InitializeListHead (&Task->Link);
  }

  return Task;
}

/**

  Destroy the task.

  @param  Task                  - The task to be destroyed.

**/
VOID
FatDestroyTask (
  FAT_TASK  *Task
  )
{
  LIST_ENTRY   *Link;
  FAT_SUBTASK  *Subtask;

  Link = GetFirstNode (&Task->Subtasks);
  while (!IsNull (&Task->Subtasks, Link)) {
    Subtask = CR (Link, FAT_SUBTASK, Link, FAT_SUBTASK_SIGNATURE);
    Link    = FatDestroySubtask (Subtask);
  }

  FreePool (Task);
}

/**

  Wait all non-blocking requests complete.

  @param  IFile                 - The instance of the open file.

**/
VOID
FatWaitNonblockingTask (
  FAT_IFILE  *IFile
  )
{
  BOOLEAN  TaskQueueEmpty;

  do {
    EfiAcquireLock (&FatTaskLock);
    TaskQueueEmpty = IsListEmpty (&IFile->Tasks);
    EfiReleaseLock (&FatTaskLock);
  } while (!TaskQueueEmpty);
}

/**

  Remove the subtask from subtask list.

  @param  Subtask               - The subtask to be removed.

  @return LIST_ENTRY *          - The next node in the list.

**/
LIST_ENTRY *
FatDestroySubtask (
  FAT_SUBTASK  *Subtask
  )
{
  LIST_ENTRY  *Link;

  gBS->CloseEvent (Subtask->DiskIo2Token.Event);

  Link = RemoveEntryList (&Subtask->Link);
  FreePool (Subtask);

  return Link;
}

/**

  Execute the task.

  @param  IFile                 - The instance of the open file.
  @param  Task                  - The task to be executed.

  @retval EFI_SUCCESS           - The task was executed successfully.
  @return other                 - An error occurred when executing the task.

**/
EFI_STATUS
FatQueueTask (
  IN FAT_IFILE  *IFile,
  IN FAT_TASK   *Task
  )
{
  EFI_STATUS   Status;
  LIST_ENTRY   *Link;
  LIST_ENTRY   *NextLink;
  FAT_SUBTASK  *Subtask;

  //
  // Sometimes the Task doesn't contain any subtasks, signal the event directly.
  //
  if (IsListEmpty (&Task->Subtasks)) {
    Task->FileIoToken->Status = EFI_SUCCESS;
    gBS->SignalEvent (Task->FileIoToken->Event);
    FreePool (Task);
    return EFI_SUCCESS;
  }

  EfiAcquireLock (&FatTaskLock);
  InsertTailList (&IFile->Tasks, &Task->Link);
  EfiReleaseLock (&FatTaskLock);

  Status = EFI_SUCCESS;
  //
  // Use NextLink to store the next link of the list, because Link might be remove from the
  // doubly-linked list and get freed in the end of current loop.
  //
  // Also, list operation APIs like IsNull() and GetNextNode() are avoided during the loop, since
  // they may check the validity of doubly-linked lists by traversing them. These APIs cannot
  // handle list elements being removed during the traverse.
  //
  for ( Link = GetFirstNode (&Task->Subtasks), NextLink = GetNextNode (&Task->Subtasks, Link)
        ; Link != &Task->Subtasks
        ; Link = NextLink, NextLink = Link->ForwardLink
        )
  {
    Subtask = CR (Link, FAT_SUBTASK, Link, FAT_SUBTASK_SIGNATURE);
    if (Subtask->Write) {
      Status = IFile->OFile->Volume->DiskIo2->WriteDiskEx (
                                                IFile->OFile->Volume->DiskIo2,
                                                IFile->OFile->Volume->MediaId,
                                                Subtask->Offset,
                                                &Subtask->DiskIo2Token,
                                                Subtask->BufferSize,
                                                Subtask->Buffer
                                                );
    } else {
      Status = IFile->OFile->Volume->DiskIo2->ReadDiskEx (
                                                IFile->OFile->Volume->DiskIo2,
                                                IFile->OFile->Volume->MediaId,
                                                Subtask->Offset,
                                                &Subtask->DiskIo2Token,
                                                Subtask->BufferSize,
                                                Subtask->Buffer
                                                );
    }

    if (EFI_ERROR (Status)) {
      break;
    }
  }

  if (EFI_ERROR (Status)) {
    EfiAcquireLock (&FatTaskLock);
    //
    // Remove all the remaining subtasks when failure.
    // We shouldn't remove all the tasks because the non-blocking requests have
    // been submitted and cannot be canceled.
    //
    while (!IsNull (&Task->Subtasks, Link)) {
      Subtask = CR (Link, FAT_SUBTASK, Link, FAT_SUBTASK_SIGNATURE);
      Link    = FatDestroySubtask (Subtask);
    }

    if (IsListEmpty (&Task->Subtasks)) {
      RemoveEntryList (&Task->Link);
      FreePool (Task);
    } else {
      //
      // If one or more subtasks have been already submitted, set FileIoToken
      // to NULL so that the callback won't signal the event.
      //
      Task->FileIoToken = NULL;
    }

    EfiReleaseLock (&FatTaskLock);
  }

  return Status;
}

/**

  Set the volume as dirty or not.

  @param  Volume                - FAT file system volume.
  @param  IoMode                - The access mode.
  @param  DirtyValue            - Set the volume as dirty or not.

  @retval EFI_SUCCESS           - Set the new FAT entry value successfully.
  @return other                 - An error occurred when operation the FAT entries.

**/
EFI_STATUS
FatAccessVolumeDirty (
  IN FAT_VOLUME  *Volume,
  IN IO_MODE     IoMode,
  IN VOID        *DirtyValue
  )
{
  UINTN  WriteCount;

  WriteCount = Volume->FatEntrySize;
  return FatDiskIo (Volume, IoMode, Volume->FatPos + WriteCount, WriteCount, DirtyValue, NULL);
}

/**
  Invoke a notification event.

  @param  Event                 Event whose notification function is being invoked.
  @param  Context               The pointer to the notification function's context,
                                which is implementation-dependent.

**/
VOID
EFIAPI
FatOnAccessComplete (
  IN  EFI_EVENT  Event,
  IN  VOID       *Context
  )
{
  EFI_STATUS   Status;
  FAT_SUBTASK  *Subtask;
  FAT_TASK     *Task;

  //
  // Avoid someone in future breaks the below assumption.
  //
  ASSERT (EfiGetCurrentTpl () == FatTaskLock.Tpl);

  Subtask = (FAT_SUBTASK *)Context;
  Task    = Subtask->Task;
  Status  = Subtask->DiskIo2Token.TransactionStatus;

  ASSERT (Task->Signature    == FAT_TASK_SIGNATURE);
  ASSERT (Subtask->Signature == FAT_SUBTASK_SIGNATURE);

  //
  // Remove the task unconditionally
  //
  FatDestroySubtask (Subtask);

  //
  // Task->FileIoToken is NULL which means the task will be ignored (just recycle the subtask and task memory).
  //
  if (Task->FileIoToken != NULL) {
    if (IsListEmpty (&Task->Subtasks) || EFI_ERROR (Status)) {
      Task->FileIoToken->Status = Status;
      gBS->SignalEvent (Task->FileIoToken->Event);
      //
      // Mark Task->FileIoToken to NULL so that the subtasks belonging to the task will be ignored.
      //
      Task->FileIoToken = NULL;
    }
  }

  if (IsListEmpty (&Task->Subtasks)) {
    RemoveEntryList (&Task->Link);
    FreePool (Task);
  }
}

/**

  General disk access function.

  @param  Volume                - FAT file system volume.
  @param  IoMode                - The access mode (disk read/write or cache access).
  @param  Offset                - The starting byte offset to read from.
  @param  BufferSize            - Size of Buffer.
  @param  Buffer                - Buffer containing read data.
  @param  Task                    point to task instance.

  @retval EFI_SUCCESS           - The operation is performed successfully.
  @retval EFI_VOLUME_CORRUPTED  - The access is
  @return Others                - The status of read/write the disk

**/
EFI_STATUS
FatDiskIo (
  IN     FAT_VOLUME  *Volume,
  IN     IO_MODE     IoMode,
  IN     UINT64      Offset,
  IN     UINTN       BufferSize,
  IN OUT VOID        *Buffer,
  IN     FAT_TASK    *Task
  )
{
  EFI_STATUS            Status;
  EFI_DISK_IO_PROTOCOL  *DiskIo;
  EFI_DISK_READ         IoFunction;
  FAT_SUBTASK           *Subtask;

  //
  // Verify the IO is in devices range
  //
  Status = EFI_VOLUME_CORRUPTED;
  if (Offset + BufferSize <= Volume->VolumeSize) {
    if (CACHE_ENABLED (IoMode)) {
      //
      // Access cache
      //
      Status = FatAccessCache (Volume, CACHE_TYPE (IoMode), RAW_ACCESS (IoMode), Offset, BufferSize, Buffer, Task);
    } else {
      //
      // Access disk directly
      //
      if (Task == NULL) {
        //
        // Blocking access
        //
        DiskIo     = Volume->DiskIo;
        IoFunction = (IoMode == ReadDisk) ? DiskIo->ReadDisk : DiskIo->WriteDisk;
        Status     = IoFunction (DiskIo, Volume->MediaId, Offset, BufferSize, Buffer);
      } else {
        //
        // Non-blocking access
        //
        Subtask = AllocateZeroPool (sizeof (*Subtask));
        if (Subtask == NULL) {
          Status = EFI_OUT_OF_RESOURCES;
        } else {
          Subtask->Signature  = FAT_SUBTASK_SIGNATURE;
          Subtask->Task       = Task;
          Subtask->Write      = (BOOLEAN)(IoMode == WriteDisk);
          Subtask->Offset     = Offset;
          Subtask->Buffer     = Buffer;
          Subtask->BufferSize = BufferSize;
          Status              = gBS->CreateEvent (
                                       EVT_NOTIFY_SIGNAL,
                                       TPL_NOTIFY,
                                       FatOnAccessComplete,
                                       Subtask,
                                       &Subtask->DiskIo2Token.Event
                                       );
          if (!EFI_ERROR (Status)) {
            InsertTailList (&Task->Subtasks, &Subtask->Link);
          } else {
            FreePool (Subtask);
          }
        }
      }
    }
  }

  if (EFI_ERROR (Status)) {
    Volume->DiskError = TRUE;
    DEBUG ((DEBUG_ERROR, "FatDiskIo: error %r\n", Status));
  }

  return Status;
}

/**

  Lock the volume.

**/
VOID
FatAcquireLock (
  VOID
  )
{
  EfiAcquireLock (&FatFsLock);
}

/**

  Lock the volume.
  If the lock is already in the acquired state, then EFI_ACCESS_DENIED is returned.
  Otherwise, EFI_SUCCESS is returned.

  @retval EFI_SUCCESS           - The volume is locked.
  @retval EFI_ACCESS_DENIED     - The volume could not be locked because it is already locked.

**/
EFI_STATUS
FatAcquireLockOrFail (
  VOID
  )
{
  return EfiAcquireLockOrFail (&FatFsLock);
}

/**

  Unlock the volume.

**/
VOID
FatReleaseLock (
  VOID
  )
{
  EfiReleaseLock (&FatFsLock);
}

/**

  Free directory entry.

  @param  DirEnt                - The directory entry to be freed.

**/
VOID
FatFreeDirEnt (
  IN FAT_DIRENT  *DirEnt
  )
{
  if (DirEnt->FileString != NULL) {
    FreePool (DirEnt->FileString);
  }

  FreePool (DirEnt);
}

/**

  Free volume structure (including the contents of directory cache and disk cache).

  @param  Volume                - The volume structure to be freed.

**/
VOID
FatFreeVolume (
  IN FAT_VOLUME  *Volume
  )
{
  //
  // Free disk cache
  //
  if (Volume->CacheBuffer != NULL) {
    FreePool (Volume->CacheBuffer);
  }

  //
  // Free directory cache
  //
  FatCleanupODirCache (Volume);
  FreePool (Volume);
}

/**

  Translate EFI time to FAT time.

  @param  ETime                 - The time of EFI_TIME.
  @param  FTime                 - The time of FAT_DATE_TIME.

**/
VOID
FatEfiTimeToFatTime (
  IN  EFI_TIME       *ETime,
  OUT FAT_DATE_TIME  *FTime
  )
{
  //
  // ignores timezone info in source ETime
  //
  if (ETime->Year > 1980) {
    FTime->Date.Year = (UINT16)(ETime->Year - 1980);
  }

  if (ETime->Year >= 1980 + FAT_MAX_YEAR_FROM_1980) {
    FTime->Date.Year = FAT_MAX_YEAR_FROM_1980;
  }

  FTime->Date.Month        = ETime->Month;
  FTime->Date.Day          = ETime->Day;
  FTime->Time.Hour         = ETime->Hour;
  FTime->Time.Minute       = ETime->Minute;
  FTime->Time.DoubleSecond = (UINT16)(ETime->Second / 2);
}

/**

  Translate Fat time to EFI time.

  @param  FTime                 - The time of FAT_DATE_TIME.
  @param  ETime                 - The time of EFI_TIME..

**/
VOID
FatFatTimeToEfiTime (
  IN  FAT_DATE_TIME  *FTime,
  OUT EFI_TIME       *ETime
  )
{
  ETime->Year       = (UINT16)(FTime->Date.Year + 1980);
  ETime->Month      = (UINT8)FTime->Date.Month;
  ETime->Day        = (UINT8)FTime->Date.Day;
  ETime->Hour       = (UINT8)FTime->Time.Hour;
  ETime->Minute     = (UINT8)FTime->Time.Minute;
  ETime->Second     = (UINT8)(FTime->Time.DoubleSecond * 2);
  ETime->Nanosecond = 0;
  ETime->TimeZone   = EFI_UNSPECIFIED_TIMEZONE;
  ETime->Daylight   = 0;
}

/**

  Get Current FAT time.

  @param  FatNow                - Current FAT time.

**/
VOID
FatGetCurrentFatTime (
  OUT FAT_DATE_TIME  *FatNow
  )
{
  EFI_STATUS  Status;
  EFI_TIME    Now;

  Status = gRT->GetTime (&Now, NULL);
  if (!EFI_ERROR (Status)) {
    FatEfiTimeToFatTime (&Now, FatNow);
  } else {
    ZeroMem (&Now, sizeof (EFI_TIME));
    Now.Year  = 1980;
    Now.Month = 1;
    Now.Day   = 1;
    FatEfiTimeToFatTime (&Now, FatNow);
  }
}

/**

  Check whether a time is valid.

  @param  Time                  - The time of EFI_TIME.

  @retval TRUE                  - The time is valid.
  @retval FALSE                 - The time is not valid.

**/
BOOLEAN
FatIsValidTime (
  IN EFI_TIME  *Time
  )
{
  UINTN    Day;
  BOOLEAN  ValidTime;

  ValidTime = TRUE;

  //
  // Check the fields for range problems
  // Fat can only support from 1980
  //
  if ((Time->Year < 1980) ||
      (Time->Month < 1) ||
      (Time->Month > 12) ||
      (Time->Day < 1) ||
      (Time->Day > 31) ||
      (Time->Hour > 23) ||
      (Time->Minute > 59) ||
      (Time->Second > 59) ||
      (Time->Nanosecond > 999999999)
      )
  {
    ValidTime = FALSE;
  } else {
    //
    // Perform a more specific check of the day of the month
    //
    Day = mMonthDays[Time->Month - 1];
    if ((Time->Month == 2) && IS_LEAP_YEAR (Time->Year)) {
      Day += 1;
      //
      // 1 extra day this month
      //
    }

    if (Time->Day > Day) {
      ValidTime = FALSE;
    }
  }

  return ValidTime;
}
