/* 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/. */

/**
 * This file can be used in the following contexts:
 *
 *  1. included from a non-osfile worker thread using importScript
 *   (it serves to define a synchronous API for that worker thread)
 *   (bug 707681)
 *
 *  2. included from the main thread using Components.utils.import
 *   (it serves to define the asynchronous API, whose implementation
 *    resides in the worker thread)
 *   (bug 729057)
 *
 * 3. included from the osfile worker thread using importScript
 *   (it serves to define the implementation of the asynchronous API)
 *   (bug 729057)
 */

{
  if (typeof Components != "undefined") {
    // We do not wish osfile_win.jsm to be used directly as a main thread
    // module yet. When time comes, it will be loaded by a combination of
    // a main thread front-end/worker thread implementation that makes sure
    // that we are not executing synchronous IO code in the main thread.

    throw new Error("osfile_win.jsm cannot be used from the main thread yet");
  }
  importScripts("resource://gre/modules/osfile/osfile_shared.jsm");

  (function(exports) {
     "use strict";
     if (!exports.OS) {
       exports.OS = {};
     }
     if (!exports.OS.Win) {
       exports.OS.Win = {};
     }
     if (exports.OS.Win.File) {
       return; // Avoid double-initialization
     }
     exports.OS.Win.File = {};

     let LOG = OS.Shared.LOG.bind(OS.Shared, "OS.Win.File");

     let libc = ctypes.open("kernel32.dll");
     if (!libc) {
       throw new Error("Could not open kernel32.dll");
     }

     /**
      * Initialize the Windows module.
      *
      * @param {function=} declareFFI
      */
     let init = function init(aDeclareFFI) {
       let declareFFI;
       if (aDeclareFFI) {
         declareFFI = aDeclareFFI.bind(null, libc);
       } else {
         declareFFI = OS.Shared.declareFFI.bind(null, libc);
       }

       // Shorthands
       let OSWin = exports.OS.Win;
       let WinFile = exports.OS.Win.File;
       if (!exports.OS.Types) {
         exports.OS.Types = {};
       }
       let Type = exports.OS.Shared.Type;
       let Types = Type;

       // Initialize types

       Types.HANDLE =
         new Type("HANDLE",
                  ctypes.voidptr_t);

       /**
        * A C integer holding INVALID_HANDLE_VALUE in case of error or
        * a file descriptor in case of success.
        */
       Types.maybe_HANDLE =
         new Type("maybe_HANDLE",
           Types.HANDLE.implementation,
           function (maybe) {
             if (ctypes.cast(maybe, ctypes.int).value == invalid_handle) {
               // Ensure that API clients can effectively compare against
               // Const.INVALID_HANDLE_VALUE. Without this cast,
               // == would always return |false|.
               return invalid_handle;
             }
             return ctypes.CDataFinalizer(maybe, _CloseHandle);
           });

       /**
        * A C integer holding INVALID_HANDLE_VALUE in case of error or
        * a file descriptor in case of success.
        */
       Types.maybe_find_HANDLE =
         new Type("maybe_find_HANDLE",
           Types.HANDLE.implementation,
           function (maybe) {
             if (ctypes.cast(maybe, ctypes.int).value == invalid_handle) {
               // Ensure that API clients can effectively compare against
               // Const.INVALID_HANDLE_VALUE. Without this cast,
               // == would always return |false|.
               return invalid_handle;
             }
             return ctypes.CDataFinalizer(maybe, _FindClose);
           });

       let invalid_handle = exports.OS.Constants.Win.INVALID_HANDLE_VALUE;

       Types.DWORD = Types.int32_t;

       /**
        * A C integer holding -1 in case of error or a positive integer
        * in case of success.
        */
       Types.negative_or_DWORD =
         new Type("negative_or_DWORD",
                  ctypes.int32_t);

       /**
        * A C integer holding 0 in case of error or a positive integer
        * in case of success.
        */
       Types.zero_or_DWORD =
         new Type("zero_or_DWORD",
                  ctypes.int32_t);

       /**
        * A C integer holding 0 in case of error, any other value in
        * case of success.
        */
       Types.zero_or_nothing =
         new Type("zero_or_nothing",
                  Types.bool.implementation);

       Types.FILETIME =
         new Type("FILETIME",
                  ctypes.StructType("FILETIME", [
                  { lo: Types.DWORD.implementation },
                  { hi: Types.DWORD.implementation }]));
              
       Types.FindData =
         new Type("FIND_DATA",
                  ctypes.StructType("FIND_DATA", [
                    { dwFileAttributes: ctypes.uint32_t },
                    { ftCreationTime:   Types.FILETIME.implementation },
                    { ftLastAccessTime: Types.FILETIME.implementation },
                    { ftLastWriteTime:  Types.FILETIME.implementation },
                    { nFileSizeHigh:    Types.DWORD.implementation },
                    { nFileSizeLow:     Types.DWORD.implementation },
                    { dwReserved0:      Types.DWORD.implementation },
                    { dwReserved1:      Types.DWORD.implementation },
                    { cFileName:        ctypes.ArrayType(ctypes.jschar, exports.OS.Constants.Win.MAX_PATH) },
                    { cAlternateFileName: ctypes.ArrayType(ctypes.jschar, 14) }
                      ]));
                  
       Types.SystemTime =
         new Type("SystemTime",
                  ctypes.StructType("SystemTime", [
                  { wYear:      ctypes.int16_t },
                  { wMonth:     ctypes.int16_t },
                  { wDayOfWeek: ctypes.int16_t },
                  { wDay:       ctypes.int16_t },
                  { wHour:      ctypes.int16_t },
                  { wMinute:    ctypes.int16_t },
                  { wSecond:    ctypes.int16_t },
                  { wMilliSeconds: ctypes.int16_t }
                  ]));

       // Special case: these functions are used by the
       // finalizer
       let _CloseHandle =
         libc.declare("CloseHandle", ctypes.winapi_abi,
                        /*return */ctypes.bool,
                        /*handle*/ ctypes.voidptr_t);

       WinFile.CloseHandle = function(fd) {
         return fd.dispose(); // Returns the value of |CloseHandle|.
       };

       let _FindClose =
         libc.declare("CloseHandle", ctypes.winapi_abi,
                        /*return */ctypes.bool,
                        /*handle*/ ctypes.voidptr_t);

       WinFile.FindClose = function(handle) {
         return handle.dispose(); // Returns the value of |CloseHandle|.
       };

       // Declare libc functions as functions of |OS.Win.File|

       WinFile.CopyFile =
         declareFFI("CopyFileW", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*sourcePath*/ Types.jschar.in_ptr,
                    /*destPath*/   Types.jschar.in_ptr,
                    /*bailIfExist*/Types.bool);

       WinFile.CreateFile =
         declareFFI("CreateFileW", ctypes.winapi_abi,
                    /*return*/  Types.maybe_HANDLE,
                    /*name*/    Types.jschar.in_ptr,
                    /*access*/  Types.DWORD,
                    /*share*/   Types.DWORD,
                    /*security*/Types.void_t.in_ptr,// FIXME: Implement?
                    /*creation*/Types.DWORD,
                    /*flags*/   Types.DWORD,
                    /*template*/Types.HANDLE);

       WinFile.DeleteFile =
         declareFFI("DeleteFileW", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*path*/   Types.jschar.in_ptr);

       WinFile.FileTimeToSystemTime =
         declareFFI("FileTimeToSystemTime", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*filetime*/Types.FILETIME.in_ptr,
                    /*systime*/ Types.SystemTime.out_ptr);

       WinFile.FindFirstFile =
         declareFFI("FindFirstFileW", ctypes.winapi_abi,
                    /*return*/ Types.maybe_find_HANDLE,
                    /*pattern*/Types.jschar.in_ptr,
                    /*data*/   Types.FindData.out_ptr);

       WinFile.FindNextFile =
         declareFFI("FindNextFileW", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*prev*/   Types.HANDLE,
                    /*data*/   Types.FindData.out_ptr);

       WinFile.FormatMessage =
         declareFFI("FormatMessageW", ctypes.winapi_abi,
                    /*return*/ Types.DWORD,
                    /*flags*/  Types.DWORD,
                    /*source*/ Types.void_t.in_ptr,
                    /*msgid*/  Types.DWORD,
                    /*langid*/ Types.DWORD,
                    /*buf*/    Types.jschar.out_ptr,
                    /*size*/   Types.DWORD,
                    /*Arguments*/Types.void_t.in_ptr
                   );

       WinFile.GetCurrentDirectory =
         declareFFI("GetCurrentDirectoryW", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_DWORD,
                    /*length*/ Types.DWORD,
                    /*buf*/    Types.jschar.out_ptr
                   );

       WinFile.MoveFileEx =
         declareFFI("MoveFileExW", ctypes.winapi_abi,
                    /*return*/   Types.zero_or_nothing,
                    /*sourcePath*/ Types.jschar.in_ptr,
                    /*destPath*/ Types.jschar.in_ptr,
                    /*flags*/    Types.DWORD
                   );

       WinFile.ReadFile =
         declareFFI("ReadFile", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*file*/   Types.HANDLE,
                    /*buffer*/ Types.char.out_ptr,
                    /*nbytes*/ Types.DWORD,
                    /*nbytes_read*/Types.DWORD.out_ptr,
                    /*overlapped*/Types.void_t.inout_ptr // FIXME: Implement?
         );

       WinFile.RemoveDirectory =
         declareFFI("RemoveDirectoryW", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*path*/   Types.jschar.in_ptr);

       WinFile.SetCurrentDirectory =
         declareFFI("SetCurrentDirectoryW", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*path*/   Types.jschar.in_ptr
                   );

       WinFile.SetEndOfFile =
         declareFFI("SetEndOfFile", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*file*/   Types.HANDLE);

       WinFile.SetFilePointer =
         declareFFI("SetFilePointer", ctypes.winapi_abi,
                    /*return*/ Types.negative_or_DWORD,
                    /*file*/   Types.HANDLE,
                    /*distlow*/Types.long,
                    /*disthi*/ Types.long.in_ptr,
                    /*method*/ Types.DWORD);

       WinFile.WriteFile =
         declareFFI("WriteFile", ctypes.winapi_abi,
                    /*return*/ Types.zero_or_nothing,
                    /*file*/   Types.HANDLE,
                    /*buffer*/ Types.char.in_ptr,
                    /*nbytes*/ Types.DWORD,
                    /*nbytes_wr*/Types.DWORD.out_ptr,
                    /*overlapped*/Types.void_t.inout_ptr // FIXME: Implement?
         );

        /**
         * Utility function: check that a path is not a UNC path,
         * as the current implementation of |Path| does not support
         * UNC grammars.
         *
         * We consider that any path starting with "\\\\" is a UNC path.
         */
        let ensureNotUNC = function ensureNotUNC(path) {
           if (!path) {
              throw new TypeError("Expecting a non-null path");
           }
           if (path.length >= 2 && path[0] == "\\" && path[1] == "\\") {
              throw new Error("Module Path cannot handle UNC-formatted paths yet: " + path);
           }
        };

        /**
         * Utility function: Remove any leading/trailing backslashes
         * from a string.
         */
        let trimBackslashes = function trimBackslashes(string) {
          return string.replace(/^\\+|\\+$/g,'');
        };

        /**
         * Handling native paths.
         *
         * This module contains a number of functions destined to simplify
         * working with native paths through a cross-platform API. Functions
         * of this module will only work with the following assumptions:
         *
         * - paths are valid;
         * - paths are defined with one of the grammars that this module can
         *   parse (see later);
         * - all path concatenations go through function |join|.
         *
         * Limitations of this implementation.
         *
         * Windows supports 6 distinct grammars for paths. For the moment, this
         * implementation supports the following subset:
         *
         * - drivename:backslash-separated components
         * - backslash-separated components
         *
         * Additionally, |normalize| can convert a path containing slash-
         * separated components to a path containing backslash-separated
         * components.
         */
       exports.OS.Win.Path = {
         /**
          * Return the final part of the path.
          * The final part of the path is everything after the last "\\".
          */
         basename: function basename(path) {
           ensureNotUNC(path);
           return path.slice(Math.max(path.lastIndexOf("\\"),
             path.lastIndexOf(":")) + 1);
         },

         /**
          * Return the directory part of the path.  The directory part
          * of the path is everything before the last "\\", including
          * the drive/server name.
          *
          * If the path contains no directory, return the drive letter,
          * or "." if the path contains no drive letter or if option
          * |winNoDrive| is set.
          *
          * @param {string} path The path.
          * @param {*=} options Platform-specific options controlling the behavior
          * of this function. This implementation supports the following options:
          *  - |winNoDrive| If |true|, also remove the letter from the path name.
          */
         dirname: function dirname(path, options) {
           ensureNotUNC(path);
           let noDrive = (options && options.winNoDrive);

           // Find the last occurrence of "\\"
           let index = path.lastIndexOf("\\");
           if (index == -1) {
             // If there is no directory component...
             if (!noDrive) {
               // Return the drive path if possible, falling back to "."
               return this.winGetDrive(path) || ".";
             } else {
               // Or just "."
               return ".";
             }
           }

           // Ignore any occurrence of "\\: immediately before that one
           while (index >= 0 && path[index] == "\\") {
             --index;
           }

           // Compute what is left, removing the drive name if necessary
           let start;
           if (noDrive) {
             start = (this.winGetDrive(path) || "").length;
           } else {
             start = 0;
           }
           return path.slice(start, index + 1);
         },

         /**
          * Join path components.
          * This is the recommended manner of getting the path of a file/subdirectory
          * in a directory.
          *
          * Example: Obtaining $TMP/foo/bar in an OS-independent manner
          *  var tmpDir = OS.Path.to("TmpD");
          *  var path = OS.Path.join(tmpDir, "foo", "bar");
          *
          * Under Windows, this will return "$TMP\foo\bar".
          */
         join: function join(path /*...*/) {
           let paths = [];
           let root;
           let absolute = false;
           for each(let subpath in arguments) {
             let drive = this.winGetDrive(subpath);
             let abs   = this.winIsAbsolute(subpath);
             if (drive) {
               root = drive;
               paths = [trimBackslashes(subpath.slice(drive.length))];
               absolute = abs;
             } else if (abs) {
               paths = [trimBackslashes(subpath)];
               absolute = true;
             } else {
               paths.push(trimBackslashes(subpath));
             }
           }
           let result = "";
           if (root) {
             result += root;
           }
           if (absolute) {
             result += "\\";
           }
           result += paths.join("\\");
           return result;
         },

         /**
          * Return the drive letter of a path, or |null| if the
          * path does not contain a drive letter.
          */
         winGetDrive: function winGetDrive(path) {
           ensureNotUNC(path);
           let index = path.indexOf(":");
           if (index <= 0) return null;
           return path.slice(0, index + 1);
         },

         /**
          * Return |true| if the path is absolute, |false| otherwise.
          *
          * We consider that a path is absolute if it starts with "\\"
          * or "driveletter:\\".
          */
         winIsAbsolute: function winIsAbsolute(path) {
           ensureNotUNC(path);
           return this._winIsAbsolute(path);
         },
         /**
          * As |winIsAbsolute|, but does not check for UNC.
          * Useful mostly as an internal utility, for normalization.
          */
         _winIsAbsolute: function _winIsAbsolute(path) {
           let index = path.indexOf(":");
           return path.length > index + 1 && path[index + 1] == "\\";
         },

         /**
          * Normalize a path by removing any unneeded ".", "..", "\\".
          * Also convert any "/" to a "\\".
          */
         normalize: function normalize(path) {
           let stack = [];

           // Remove the drive (we will put it back at the end)
           let root = this.winGetDrive(path);
           if (root) {
             path = path.slice(root.length);
           }

           // Remember whether we need to restore a leading "\\".
           let absolute = this._winIsAbsolute(path);

           // Normalize "/" to "\\"
           path = path.replace("/", "\\");

           // And now, fill |stack| from the components,
           // popping whenever there is a ".."
           path.split("\\").forEach(function loop(v) {
             switch (v) {
             case "":  case ".": // Ignore
               break;
             case "..":
               if (stack.length == 0) {
                 if (absolute) {
                   throw new Error("Path is ill-formed: attempting to go past root");
                 } else {
                  stack.push("..");
                 }
               } else {
                 stack.pop();
               }
               break;
             default:
               stack.push(v);
             }
           });

           // Put everything back together
           let result = stack.join("\\");
           if (absolute) {
             result = "\\" + result;
           }
           if (root) {
             result = root + result;
           }
           return result;
         },

         /**
          * Return the components of a path.
          * You should generally apply this function to a normalized path.
          *
          * @return {{
          *   {bool} absolute |true| if the path is absolute, |false| otherwise
          *   {array} components the string components of the path
          *   {string?} winDrive the drive or server for this path
          * }}
          *
          * Other implementations may add additional OS-specific informations.
          */
         split: function split(path) {
           return {
             absolute: this.winIsAbsolute(path),
             winDrive: this.winGetDrive(path),
             components: path.split("\\")
           };
         }
       };

       // Export useful stuff for extensibility

       exports.OS.Win.libc = libc;
       exports.OS.Win.declareFFI = declareFFI;
     };
     exports.OS.Win.File._init = init;
   })(this);
}
