/**
 *  @file
 *
 *  @brief Mounts a File System
 *  @ingroup FileSystemTypesAndMount
 */

/*
 *  COPYRIGHT (c) 1989-2010.
 *  On-Line Applications Research Corporation (OAR).
 *
 *  Copyright (c) 2010-2012 embedded brains GmbH.
 *
 *  The license and distribution terms for this file may be
 *  found in the file LICENSE in this distribution or at
 *  http://www.rtems.org/license/LICENSE.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdlib.h>
#include <string.h>

#include <rtems/libio_.h>

RTEMS_CHAIN_DEFINE_EMPTY(rtems_filesystem_mount_table);

const rtems_filesystem_limits_and_options_t rtems_filesystem_default_pathconf = {
   5,    /* link_max: count */
   128,  /* max_canon: max formatted input line size */
   7,    /* max_input: max input line size */
   255,  /* name_max: max name */
   255,  /* path_max: max path */
   1024, /* pipe_buf: pipe buffer size */
   0,    /* posix_async_io: async IO supported on fs, 0=no, 1=yes */
   0 ,   /* posix_chown_restrictions: can chown: 0=no, 1=yes */
   1,    /* posix_no_trunc: error on filenames > max name, 0=no, 1=yes */
   0,    /* posix_prio_io: priority IO, 0=no, 1=yes */
   0,    /* posix_sync_io: file can be sync'ed, 0=no, 1=yes */
   0     /* posix_vdisable: special char processing, 0=no, 1=yes */
};

static rtems_filesystem_mount_table_entry_t *alloc_mount_table_entry(
  const char *source_or_null,
  const char *target_or_null,
  const char *filesystemtype,
  size_t *target_length_ptr
)
{
  const char *target = target_or_null != NULL ? target_or_null : "/";
  size_t filesystemtype_size = strlen( filesystemtype ) + 1;
  size_t source_size = source_or_null != NULL ?
    strlen( source_or_null ) + 1 : 0;
  size_t target_size = strlen( target ) + 1;
  size_t size = sizeof( rtems_filesystem_mount_table_entry_t )
    + filesystemtype_size + source_size + target_size
    + sizeof( rtems_filesystem_global_location_t );
  rtems_filesystem_mount_table_entry_t *mt_entry = calloc( 1, size );

  if ( mt_entry != NULL ) {
    rtems_filesystem_global_location_t *mt_fs_root =
      (rtems_filesystem_global_location_t *)
        ((char *) mt_entry + sizeof( *mt_entry ));
    char *str = (char *) mt_fs_root + sizeof( *mt_fs_root );

    memcpy( str, filesystemtype, filesystemtype_size );
    mt_entry->type = str;
    str += filesystemtype_size;

    if ( source_or_null != NULL ) {
      memcpy( str, source_or_null, source_size );
      mt_entry->dev = str;
      str += source_size;
    }

    memcpy( str, target, target_size );
    mt_entry->target = str;

    mt_entry->mounted = true;
    mt_entry->mt_fs_root = mt_fs_root;
    mt_entry->pathconf_limits_and_options = &rtems_filesystem_default_pathconf;

    mt_fs_root->location.mt_entry = mt_entry;
    mt_fs_root->reference_count = 1;

    rtems_chain_initialize(
      &mt_entry->location_chain,
      mt_fs_root,
      1,
      sizeof(*mt_fs_root)
    );
  }

  *target_length_ptr = target_size - 1;

  return mt_entry;
}

static int register_subordinate_file_system(
  rtems_filesystem_mount_table_entry_t *mt_entry,
  const char *target
)
{
  int rv = 0;
  rtems_filesystem_eval_path_context_t ctx;
  int eval_flags = RTEMS_FS_PERMS_RWX
    | RTEMS_FS_FOLLOW_LINK;
  rtems_filesystem_location_info_t *currentloc =
    rtems_filesystem_eval_path_start( &ctx, target, eval_flags );

  if ( !rtems_filesystem_location_is_instance_root( currentloc ) ) {
    rtems_filesystem_location_info_t targetloc;
    rtems_filesystem_global_location_t *mt_point_node;

    rtems_filesystem_eval_path_extract_currentloc( &ctx, &targetloc );
    mt_point_node = rtems_filesystem_location_transform_to_global( &targetloc );
    mt_entry->mt_point_node = mt_point_node;
    rv = (*mt_point_node->location.mt_entry->ops->mount_h)( mt_entry );
    if ( rv == 0 ) {
      rtems_filesystem_mt_lock();
      rtems_chain_append_unprotected(
        &rtems_filesystem_mount_table,
        &mt_entry->mt_node
      );
      rtems_filesystem_mt_unlock();
    } else {
      rtems_filesystem_global_location_release( mt_point_node, true );
    }
  } else {
    rtems_filesystem_eval_path_error( &ctx, EBUSY );
    rv = -1;
  }

  rtems_filesystem_eval_path_cleanup( &ctx );

  return rv;
}

static int register_root_file_system(
  rtems_filesystem_mount_table_entry_t *mt_entry
)
{
  int rv = 0;

  rtems_filesystem_mt_lock();
  if ( rtems_chain_is_empty( &rtems_filesystem_mount_table ) ) {
    rtems_chain_append_unprotected(
      &rtems_filesystem_mount_table,
      &mt_entry->mt_node
    );
  } else {
    errno = EINVAL;
    rv = -1;
  }
  rtems_filesystem_mt_unlock();

  if ( rv == 0 ) {
    rtems_filesystem_global_location_t *new_fs_root =
      rtems_filesystem_global_location_obtain( &mt_entry->mt_fs_root );
    rtems_filesystem_global_location_t *new_fs_current =
      rtems_filesystem_global_location_obtain( &mt_entry->mt_fs_root );

    rtems_filesystem_global_location_assign(
      &rtems_filesystem_root,
      new_fs_root
    );
    rtems_filesystem_global_location_assign(
      &rtems_filesystem_current,
      new_fs_current
    );
  }

  return rv;
}

int mount(
  const char                 *source,
  const char                 *target,
  const char                 *filesystemtype,
  rtems_filesystem_options_t options,
  const void                 *data
)
{
  int rv = 0;

  if (
    options == RTEMS_FILESYSTEM_READ_ONLY
      || options == RTEMS_FILESYSTEM_READ_WRITE
  ) {
    rtems_filesystem_fsmount_me_t fsmount_me_h =
      rtems_filesystem_get_mount_handler( filesystemtype );

    if ( fsmount_me_h != NULL ) {
      size_t target_length = 0;
      rtems_filesystem_mount_table_entry_t *mt_entry = alloc_mount_table_entry(
        source,
        target,
        filesystemtype,
        &target_length
      );

      if ( mt_entry != NULL ) {
        mt_entry->writeable = options == RTEMS_FILESYSTEM_READ_WRITE;

        rv = (*fsmount_me_h)( mt_entry, data );
        if ( rv == 0 ) {
          if ( target != NULL ) {
            rv = register_subordinate_file_system( mt_entry, target );
          } else {
            rv = register_root_file_system( mt_entry );
          }

          if ( rv != 0 ) {
            (*mt_entry->ops->fsunmount_me_h)( mt_entry );
          }
        }

        if ( rv != 0 ) {
          free( mt_entry );
        }
      } else {
        errno = ENOMEM;
        rv = -1;
      }
    } else {
      errno = EINVAL;
      rv = -1;
    }
  } else {
    errno = EINVAL;
    rv = -1;
  }

  return rv;
}
