/*---------------------------------------------------------------------------*
 *  PFileSystem.c  *
 *                                                                           *
 *  Copyright 2007, 2008 Nuance Communciations, Inc.                               *
 *                                                                           *
 *  Licensed 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 "ArrayList.h"
#include "LCHAR.h"
#include "PFileSystem.h"
#include "PFileSystemImpl.h"
#include "phashtable.h"
#include "plog.h"
#include "pmemory.h"


#define MTAG NULL

/**
 * Indicates if PFileSystem is initialized.
 */
extern ESR_BOOL PFileSystemCreated;

/**
 * [file path, PFileSystem*] mapping.
 */
extern PHashTable* PFileSystemPathMap;

/**
 * Current working directory.
 */
extern LCHAR PFileSystemCurrentDirectory[P_PATH_MAX];

PORTABLE_API ESR_ReturnCode PFileSystemCanonicalSlashes(LCHAR* path)
{
  ESR_ReturnCode rc;
  
  if (path == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  
  lstrtrim(path);
  CHKLOG(rc, lstrreplace(path, L('\\'), L('/')));
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

ESR_ReturnCode PFileSystemLinearToPathTokens(const LCHAR* path, LCHAR*** tokenArray, size_t* count)
{
  ESR_ReturnCode rc;
  const LCHAR* beginning;
  const LCHAR* ending;
  LCHAR linear[P_PATH_MAX];
  ArrayList* arrayList = NULL;
  LCHAR* value = NULL;
  size_t i;
  
  if (path == NULL || tokenArray == NULL || count == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  LSTRCPY(linear, path);
  CHKLOG(rc, PFileSystemCanonicalSlashes(linear));
  CHKLOG(rc, ArrayListCreate(&arrayList));
  beginning = linear;
  while (ESR_TRUE)
  {
    ending = LSTRCHR(beginning, L('/'));
    if (ending == NULL)
      ending = beginning + LSTRLEN(beginning);
    value = MALLOC(sizeof(LCHAR) * (ending - beginning + 1 + 1), MTAG);
    if (value == NULL)
    {
      rc = ESR_OUT_OF_MEMORY;
      PLogError(ESR_rc2str(rc));
      goto CLEANUP;
    }
    LSTRNCPY(value, beginning, ending - beginning + 1);
    value[ending-beginning+1] = L('\0');
    CHKLOG(rc, lstrtrim(value));
    if (LSTRLEN(value) == 0)
    {
      FREE(value);
      value = NULL;
    }
    else
    {
      CHKLOG(rc, arrayList->add(arrayList, value));
      value = NULL;
    }
    if (*ending == 0)
      break;
    beginning = ending + 1;
  }
  
  /* Build static token array */
  CHKLOG(rc, arrayList->getSize(arrayList, count));
  *tokenArray = MALLOC(*count * sizeof(LCHAR*), MTAG);
  if (*tokenArray == NULL)
  {
    rc = ESR_OUT_OF_MEMORY;
    goto CLEANUP;
  }
  for (i = 0; i < *count; ++i)
  {
    rc = arrayList->get(arrayList, i, (void**)(&(*tokenArray)[i]));
    if (rc != ESR_SUCCESS)
      goto CLEANUP;
  }
  rc = arrayList->destroy(arrayList);
  if (rc != ESR_SUCCESS)
    goto CLEANUP;
  return ESR_SUCCESS;
CLEANUP:
  FREE(value);
  if (arrayList != NULL)
  {
    ESR_ReturnCode cleanRC;
    
    cleanRC = arrayList->getSize(arrayList, count);
    if (cleanRC != ESR_SUCCESS)
      return rc;
    for (i = 0; i < *count; ++i)
    {
      cleanRC = arrayList->get(arrayList, 0, (void**)&value);
      if (cleanRC != ESR_SUCCESS)
        return rc;
      FREE(value);
      cleanRC = arrayList->remove(arrayList, 0);
      if (cleanRC != ESR_SUCCESS)
        return rc;
    }
    arrayList->destroy(arrayList);
  }
  return rc;
}

ESR_ReturnCode PFileSystemIsAbsolutePath(const LCHAR* path, ESR_BOOL* isAbsolute)
{
  LCHAR canonical[P_PATH_MAX];
  ESR_ReturnCode rc;
  
  if (isAbsolute == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  LSTRCPY(canonical, path);
  CHKLOG(rc, PFileSystemCanonicalSlashes(canonical));
  
  *isAbsolute = (canonical[0] == '/' ||
                 (LISALPHA(canonical[0]) && canonical[1] == ':' && canonical[2] == '/'));
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

ESR_ReturnCode PFileSystemGetAbsolutePath(LCHAR* path, size_t* len)
{
  ESR_ReturnCode rc;
#define MAX_PATH_TOKENS 20
  LCHAR** tokens = NULL;
  size_t tokenLen = 0, i;
  ESR_BOOL isAbsolute;
  
  if (path == NULL || len == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  CHKLOG(rc, PFileSystemIsAbsolutePath(path, &isAbsolute));
  
  /* Prefix relative paths with the current working directory */
  if (!isAbsolute)
  {
    LCHAR cwd[P_PATH_MAX];
    size_t len2;
    
    len2 = P_PATH_MAX;
    CHKLOG(rc, PFileSystemGetcwd(cwd, &len2));
    len2 = *len;
    CHKLOG(rc, lstrinsert(cwd, path, 0, &len2));
  }
  
  CHKLOG(rc, PFileSystemCanonicalSlashes(path));
  tokenLen = MAX_PATH_TOKENS;
  CHKLOG(rc, PFileSystemLinearToPathTokens(path, &tokens, &tokenLen));
  
  LSTRCPY(path, L(""));
  for (i = 0; i < tokenLen; ++i)
  {
    if (LSTRCMP(tokens[i], L("../")) == 0)
    {
      size_t len2;
      
      len2 = *len;
      passert(path[LSTRLEN(path)-1] == L('/'));
      CHKLOG(rc, PFileSystemGetParentDirectory(path, &len2));
    }
    else if (LSTRCMP(tokens[i], L("./")) == 0)
    {
      if (i > 0)
      {
        /* do nothing */
      }
      else
      {
        LSTRCPY(path, L("./"));
      }
    }
    else
      LSTRCAT(path, tokens[i]);
    FREE(tokens[i]);
    tokens[i] = NULL;
  }
  FREE(tokens);
  return ESR_SUCCESS;
CLEANUP:
  if (tokens != NULL)
  {
    for (i = 0; i < tokenLen; ++i)
    {
      FREE(tokens[i]);
      tokens[i] = NULL;
    }
  }
  return rc;
}

ESR_ReturnCode PFileSystemIsCreated(ESR_BOOL* isCreated)
{
  ESR_ReturnCode rc;
  
  if (isCreated == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  *isCreated = PFileSystemCreated;
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

/**
 * Given a path, returns the associated file-system and relative path.
 *
 * @param path Path to look up
 * @param fs [out] File-system which matches the path
 * @param relativePath [out] Relative path associated with match (should have length P_PATH_MAX)
 */
ESR_ReturnCode PFileSystemGetFS(const LCHAR* path, PFileSystem** fileSystem, LCHAR* relativePath)
{
  ESR_ReturnCode rc;
  PHashTableEntry* entry;
  LCHAR* key;
  PFileSystem* value;
  LCHAR* bestKey = NULL;
  PFileSystem* bestValue = NULL;
  
  CHKLOG(rc, PHashTableEntryGetFirst(PFileSystemPathMap, &entry));
  while (entry != NULL)
  {
    CHKLOG(rc, PHashTableEntryGetKeyValue(entry, (void **)&key, (void **)&value));
    if (LSTRSTR(path, key) == path)
    {
      /* File-system handles file path */
      
      if (bestKey == NULL || LSTRLEN(key) > LSTRLEN(bestKey))
      {
        /* Found a better match -- the new key is a subdirectory of the previous bestKey */
        bestKey = key;
        bestValue = value;
      }
    }
    CHKLOG(rc, PHashTableEntryAdvance(&entry));
  }
  if (bestKey == NULL)
  {
    rc = ESR_INVALID_STATE;
    PLogError(L("No file-system handles the specified path (%s)"), path);
    goto CLEANUP;
  }
  *fileSystem = bestValue;
  if (relativePath != NULL)
  {
    ESR_BOOL isAbsolute;
    
    CHKLOG(rc, PFileSystemIsAbsolutePath(path + LSTRLEN(bestKey), &isAbsolute));
    LSTRCPY(relativePath, L(""));
    if (!isAbsolute)
    {
      /* Make sure that the relative path is relative to the root of the file-system */
      LSTRCAT(relativePath, L("/"));
    }
    LSTRCAT(relativePath, path + LSTRLEN(bestKey));
  }
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

ESR_ReturnCode PFileSystemCreatePFile(const LCHAR* filename, ESR_BOOL littleEndian, PFile** self)
{
  ESR_ReturnCode rc;
  LCHAR absolutePath[P_PATH_MAX];
  PFileSystem* fileSystem;
  size_t len;
  
  if (filename == NULL || self == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  LSTRCPY(absolutePath, filename);
  lstrtrim(absolutePath);
  len = P_PATH_MAX;
  CHKLOG(rc, PFileSystemGetAbsolutePath(absolutePath, &len));
  CHKLOG(rc, PFileSystemGetFS(absolutePath, &fileSystem, NULL));
  rc = fileSystem->createPFile(fileSystem, absolutePath, littleEndian, self);
  if (rc == ESR_NO_MATCH_ERROR)
    rc = ESR_OPEN_ERROR;
  if (rc != ESR_SUCCESS)
  {
    PLogError("%s, %s, %s", ESR_rc2str(rc), filename, absolutePath);
    goto CLEANUP;
  }
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

ESR_ReturnCode PFileSystemMkdir(const LCHAR* path)
{
  ESR_ReturnCode rc;
  LCHAR absolutePath[P_PATH_MAX];
  PFileSystem* fileSystem;
  size_t len;
  
  if (path == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  LSTRCPY(absolutePath, path);
  lstrtrim(absolutePath);
  len = P_PATH_MAX;
  CHKLOG(rc, PFileSystemGetAbsolutePath(absolutePath, &len));
  CHKLOG(rc, PFileSystemGetFS(absolutePath, &fileSystem, NULL));
  CHK(rc, fileSystem->mkdir(fileSystem, absolutePath));
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

ESR_ReturnCode PFileSystemGetcwd(LCHAR* path, size_t* len)
{
  ESR_ReturnCode rc;
  
  if (path == NULL || len == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  if (LSTRLEN(PFileSystemCurrentDirectory) + 1 > *len)
  {
    rc = ESR_BUFFER_OVERFLOW;
    *len = LSTRLEN(PFileSystemCurrentDirectory) + 1;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  LSTRCPY(path, PFileSystemCurrentDirectory);
  /* Check function postcondition */
  passert(path[LSTRLEN(path)-1] == L('/'));
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

ESR_ReturnCode PFileSystemChdir(const LCHAR* path)
{
  ESR_ReturnCode rc;
  LCHAR absolutePath[P_PATH_MAX];
  PFileSystem* fileSystem;
  size_t len;
  
  if (path == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  LSTRCPY(absolutePath, path);
  /* Ensure path ends with '/' */
  if (absolutePath[LSTRLEN(absolutePath)-1] != L('/'))
    LSTRCAT(absolutePath, L("/"));
  lstrtrim(absolutePath);
  len = P_PATH_MAX;
  CHKLOG(rc, PFileSystemGetAbsolutePath(absolutePath, &len));
  CHKLOG(rc, PFileSystemGetFS(absolutePath, &fileSystem, NULL));
  rc = fileSystem->chdir(fileSystem, absolutePath);
  if (rc != ESR_SUCCESS)
  {
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  if (absolutePath[LSTRLEN(absolutePath)-1] != L('/'))
    LSTRCAT(absolutePath, L("/"));
  LSTRCPY(PFileSystemCurrentDirectory, absolutePath);
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

/**
 * PRECONDITION: Directory names must end with '/'
 */
ESR_ReturnCode PFileSystemGetParentDirectory(LCHAR* path, size_t* len)
{
  LCHAR* lastSlash;
  LCHAR clone[P_PATH_MAX];
  ESR_ReturnCode rc;
  size_t len2;
  
  if (path == NULL || len == NULL)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  LSTRCPY(clone, path);
  lstrtrim(clone);
  len2 = P_PATH_MAX;
  CHKLOG(rc, PFileSystemGetAbsolutePath(clone, &len2));
  
  /* 1.0 - Strip filename */
  lastSlash = LSTRRCHR(clone, L('/'));
  if (lastSlash == NULL)
  {
    /* path contains only a filename */
    LSTRCPY(path, L("../"));
    return ESR_SUCCESS;
  }
  else if (lastSlash < clone + LSTRLEN(clone) - 1)
  {
    
    *(lastSlash + 1) = L('\0');
    if (LSTRLEN(clone) > *len)
    {
      *len = LSTRLEN(clone);
      rc = ESR_BUFFER_OVERFLOW;
      goto CLEANUP;
    }
    LSTRCPY(path, clone);
    *len = LSTRLEN(path);
    return ESR_SUCCESS;
  }
  
  /* Get parent directory */
  if (lastSlash -clone + 2 == 3 && LSTRNCMP(clone, L("../"), 3) == 0)
  {
    LSTRCAT(clone, L("../"));
    if (LSTRLEN(clone) > *len)
    {
      *len = LSTRLEN(clone);
      rc = ESR_BUFFER_OVERFLOW;
      goto CLEANUP;
    }
    LSTRCPY(path, clone);
    *len = LSTRLEN(path);
    return ESR_SUCCESS;
  }
  if (lastSlash -clone + 1 == 2 && LSTRNCMP(clone, L("./"), 2) == 0)
  {
    if (LSTRLEN(L("../")) > *len)
    {
      *len = LSTRLEN(L("../"));
      rc = ESR_BUFFER_OVERFLOW;
      goto CLEANUP;
    }
    LSTRCPY(path, L("../"));
    *len = LSTRLEN(path);
    return ESR_SUCCESS;
  }
  else if (lastSlash == clone && LSTRNCMP(clone, L("/"), 1) == 0)
  {
    rc = ESR_INVALID_ARGUMENT;
    PLogError(ESR_rc2str(rc));
    goto CLEANUP;
  }
  *lastSlash = 0;
  lastSlash = LSTRRCHR(clone, L('/'));
  if (lastSlash != NULL)
  {
    *(lastSlash + 1) = 0;
    if (LSTRLEN(clone) > *len)
    {
      *len = LSTRLEN(clone);
      rc = ESR_BUFFER_OVERFLOW;
      goto CLEANUP;
    }
    LSTRCPY(path, clone);
    *len = LSTRLEN(path);
  }
  else
  {
    LSTRCPY(path, L(""));
    *len = 0;
  }
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}

ESR_ReturnCode PFileSystemIsDirectoryPath(const LCHAR* path, ESR_BOOL* isDirectory)
{
  LCHAR temp[P_PATH_MAX];
  ESR_ReturnCode rc;
  
  passert(isDirectory != NULL);
  LSTRCPY(temp, path);
  lstrtrim(temp);
  CHKLOG(rc, PFileSystemCanonicalSlashes(temp));
  *isDirectory = temp[LSTRLEN(temp)-1] == '/';
  return ESR_SUCCESS;
CLEANUP:
  return rc;
}