/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

#include "winutils.h"

//----------------------------------------------------------------------------
// Function: GetMaskString
//
// Description:
//	Get the mask string that are used for output to the console.
//
// Returns:
//	TRUE: on success
//
// Notes:
//  The function only sets the existed permission in the mask string. If the
//  permission does not exist, the corresponding character in mask string is not
//  altered. The caller need to initilize the mask string to be all '-' to get
//  the correct mask string.
//
static BOOL GetMaskString(__in INT accessMask, __in_ecount(10) LPWSTR maskString)
{
  if(wcslen(maskString) != 10)
    return FALSE;

  if ((accessMask & UX_DIRECTORY) == UX_DIRECTORY)
    maskString[0] = L'd';
  else if ((accessMask & UX_SYMLINK) == UX_SYMLINK)
    maskString[0] = L'l';

  if ((accessMask & UX_U_READ) == UX_U_READ)
    maskString[1] = L'r';
  if ((accessMask & UX_U_WRITE) == UX_U_WRITE)
    maskString[2] = L'w';
  if ((accessMask & UX_U_EXECUTE) == UX_U_EXECUTE)
    maskString[3] = L'x';

  if ((accessMask & UX_G_READ) == UX_G_READ)
    maskString[4] = L'r';
  if ((accessMask & UX_G_WRITE) == UX_G_WRITE)
    maskString[5] = L'w';
  if ((accessMask & UX_G_EXECUTE) == UX_G_EXECUTE)
    maskString[6] = L'x';

  if ((accessMask & UX_O_READ) == UX_O_READ)
    maskString[7] = L'r';
  if ((accessMask & UX_O_WRITE) == UX_O_WRITE)
    maskString[8] = L'w';
  if ((accessMask & UX_O_EXECUTE) == UX_O_EXECUTE)
    maskString[9] = L'x';

  return TRUE;
}

//----------------------------------------------------------------------------
// Function: LsPrintLine
//
// Description:
//	Print one line of 'ls' command given all the information needed
//
// Returns:
//	None
//
// Notes:
//  if useSeparator is false, separates the output tokens with a space
//  character, otherwise, with a pipe character
//
static BOOL LsPrintLine(
  const INT mask,
  const DWORD hardlinkCount,
  LPCWSTR ownerName,
  LPCWSTR groupName,
  const FILETIME *lpFileWritetime,
  const LARGE_INTEGER fileSize,
  LPCWSTR path,
  BOOL useSeparator)
{
  // 'd' + 'rwx' for user, group, other
  static const size_t ck_ullMaskLen = 1 + 3 * 3;

  LPWSTR maskString = NULL;
  SYSTEMTIME stFileWriteTime;
  BOOL ret = FALSE;

  maskString = (LPWSTR)LocalAlloc(LPTR, (ck_ullMaskLen+1)*sizeof(WCHAR));
  if (maskString == NULL)
  {
    ReportErrorCode(L"LocalAlloc", GetLastError());
    return FALSE;
  }

  // Build mask string from mask mode
  if (FAILED(StringCchCopyW(maskString, (ck_ullMaskLen+1), L"----------")))
  {
    goto LsPrintLineEnd;
  }

  if (!GetMaskString(mask, maskString))
  {
    goto LsPrintLineEnd;
  }

  // Convert file time to system time
  if (!FileTimeToSystemTime(lpFileWritetime, &stFileWriteTime))
  {
    goto LsPrintLineEnd;
  }

  if (useSeparator)
  {
    fwprintf(stdout, L"%10s|%d|%s|%s|%lld|%3s|%2d|%4d|%s\n",
      maskString, hardlinkCount, ownerName, groupName, fileSize.QuadPart,
      MONTHS[stFileWriteTime.wMonth-1], stFileWriteTime.wDay,
      stFileWriteTime.wYear, path);
  }
  else
  {
    fwprintf(stdout, L"%10s %d %s %s %lld %3s %2d %4d %s\n",
      maskString, hardlinkCount, ownerName, groupName, fileSize.QuadPart,
      MONTHS[stFileWriteTime.wMonth-1], stFileWriteTime.wDay,
      stFileWriteTime.wYear, path);
  }

  ret = TRUE;

LsPrintLineEnd:
  LocalFree(maskString);

  return ret;
}

// List of command line options supported by "winutils ls"
enum CmdLineOption
{
  CmdLineOptionFollowSymlink = 0x1,  // "-L"
  CmdLineOptionSeparator = 0x2  // "-F"
  // options should be powers of 2 (aka next is 0x4)
};

static wchar_t* CurrentDir = L".";

//----------------------------------------------------------------------------
// Function: ParseCommandLine
//
// Description:
//   Parses the command line
//
// Returns:
//   TRUE on the valid command line, FALSE otherwise
//
BOOL ParseCommandLine(
  __in int argc,
  __in_ecount(argc) wchar_t *argv[],
  __deref_out PWSTR *path,
  __out int *optionsMask)
{
  int MaxOptions = 2; // Should be equal to the number of elems in CmdLineOption
  int i = 0;

  assert(optionsMask != NULL);
  assert(argv != NULL);
  assert(path != NULL);

  *optionsMask = 0;

  if (argc == 1)
  {
    // no path specified, assume "."
    *path = CurrentDir;
    return TRUE;
  }

  if (argc == 2)
  {
    // only path specified, no other options
    *path = argv[1];
    return TRUE;
  }

  if (argc > 2 + MaxOptions)
  {
    // too many parameters
    return FALSE;
  }

  for (i = 1; i < argc - 1; ++i)
  {
    if (wcscmp(argv[i], L"-L") == 0)
    {
      // Check if this option was already specified
      BOOL alreadySet = *optionsMask & CmdLineOptionFollowSymlink;
      if (alreadySet)
        return FALSE;

      *optionsMask |= CmdLineOptionFollowSymlink;
    }
    else if (wcscmp(argv[i], L"-F") == 0)
    {
      // Check if this option was already specified
      BOOL alreadySet = *optionsMask & CmdLineOptionSeparator;
      if (alreadySet)
        return FALSE;

      *optionsMask |= CmdLineOptionSeparator;
    }
    else
    {
      return FALSE;
    }
  }

  *path = argv[argc - 1];

  return TRUE;
}

//----------------------------------------------------------------------------
// Function: Ls
//
// Description:
//	The main method for ls command
//
// Returns:
//	0: on success
//
// Notes:
//
int Ls(__in int argc, __in_ecount(argc) wchar_t *argv[])
{
  LPWSTR pathName = NULL;
  LPWSTR longPathName = NULL;

  BY_HANDLE_FILE_INFORMATION fileInformation;

  LPWSTR ownerName = NULL;
  LPWSTR groupName = NULL;
  INT unixAccessMode = 0;
  DWORD dwErrorCode = ERROR_SUCCESS;

  LARGE_INTEGER fileSize;

  int ret = EXIT_FAILURE;
  int optionsMask = 0;

  if (!ParseCommandLine(argc, argv, &pathName, &optionsMask))
  {
    fwprintf(stderr, L"Incorrect command line arguments.\n\n");
    LsUsage(argv[0]);
    return EXIT_FAILURE;
  }

  assert(pathName != NULL);

  if (wcsspn(pathName, L"/?|><:*\"") != 0)
  {
    fwprintf(stderr, L"Incorrect file name format: %s\n", pathName);
    return EXIT_FAILURE;
  }

  // Convert the path the the long path
  //
  dwErrorCode = ConvertToLongPath(pathName, &longPathName);
  if (dwErrorCode != ERROR_SUCCESS)
  {
    ReportErrorCode(L"ConvertToLongPath", dwErrorCode);
    goto LsEnd;
  }

  dwErrorCode = GetFileInformationByName(
    longPathName, optionsMask & CmdLineOptionFollowSymlink, &fileInformation);
  if (dwErrorCode != ERROR_SUCCESS)
  {
    ReportErrorCode(L"GetFileInformationByName", dwErrorCode);
    goto LsEnd;
  }

  dwErrorCode = FindFileOwnerAndPermission(longPathName,
    optionsMask & CmdLineOptionFollowSymlink,
    &ownerName, &groupName, &unixAccessMode);
  if (dwErrorCode != ERROR_SUCCESS)
  {
    ReportErrorCode(L"FindFileOwnerAndPermission", dwErrorCode);
    goto LsEnd;
  }

  fileSize.HighPart = fileInformation.nFileSizeHigh;
  fileSize.LowPart = fileInformation.nFileSizeLow;

  // Print output using the input path name (not the long one)
  //
  if (!LsPrintLine(unixAccessMode,
    fileInformation.nNumberOfLinks,
    ownerName, groupName,
    &fileInformation.ftLastWriteTime,
    fileSize,
    pathName,
    optionsMask & CmdLineOptionSeparator))
    goto LsEnd;

  ret = EXIT_SUCCESS;

LsEnd:
  LocalFree(ownerName);
  LocalFree(groupName);
  LocalFree(longPathName);

  return ret;
}

void LsUsage(LPCWSTR program)
{
  fwprintf(stdout, L"\
Usage: %s [OPTIONS] [FILE]\n\
List information about the FILE (the current directory by default).\n\
Using long listing format and list directory entries instead of contents,\n\
and do not dereference symbolic links.\n\
Provides equivalent or similar function as 'ls -ld' on GNU/Linux.\n\
\n\
OPTIONS: -L dereference symbolic links\n\
         -F format the output by separating tokens with a pipe\n",
program);
}
