/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla.
 *
 * The Initial Developer of the Original Code is IBM Corporation.
 * Portions created by IBM Corporation are Copyright (C) 2003
 * IBM Corporation. All Rights Reserved.
 *
 * Contributor(s):
 *   Darin Fisher <darin@meer.net>
 *   Benjamin Smedberg <benjamin@smedbergs.us>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include <stdio.h>
#include <stdlib.h>
#ifdef XP_WIN
#include <windows.h>
#endif

#include "nsXULAppAPI.h"
#include "nsXPCOMGlue.h"
#include "nsRegisterGRE.h"
#include "nsAppRunner.h"
#include "nsINIParser.h"
#include "nsILocalFile.h"
#include "nsIXULAppInstall.h"
#include "nsCOMPtr.h"
#include "nsMemory.h"
#include "nsNativeCharsetUtils.h"
#include "nsBuildID.h"
#include "plstr.h"
#include "prprf.h"
#include "prenv.h"

/**
 * Output a string to the user.  This method is really only meant to be used to
 * output last-ditch error messages designed for developers NOT END USERS.
 *
 * @param isError
 *        Pass true to indicate severe errors.
 * @param fmt
 *        printf-style format string followed by arguments.
 */
static void Output(PRBool isError, const char *fmt, ... )
{
  va_list ap;
  va_start(ap, fmt);

#if defined(XP_WIN) && !MOZ_WINCONSOLE
  char *msg = PR_vsmprintf(fmt, ap);
  if (msg)
  {
    UINT flags = MB_OK;
    if (isError)
      flags |= MB_ICONERROR;
    else 
      flags |= MB_ICONINFORMATION;
    MessageBox(NULL, msg, "XULRunner", flags);
    PR_smprintf_free(msg);
  }
#else
  vfprintf(stderr, fmt, ap);
#endif

  va_end(ap);
}

/**
 * Return true if |arg| matches the given argument name.
 */
static PRBool IsArg(const char* arg, const char* s)
{
  if (*arg == '-')
  {
    if (*++arg == '-')
      ++arg;
    return !PL_strcasecmp(arg, s);
  }

#if defined(XP_WIN) || defined(XP_OS2)
  if (*arg == '/')
    return !PL_strcasecmp(++arg, s);
#endif

  return PR_FALSE;
}

/**
 * Version checking.
 */

static PRUint32 geckoVersion = 0;

static PRUint32 ParseVersion(const char *versionStr)
{
  PRUint16 major, minor;
  if (PR_sscanf(versionStr, "%hu.%hu", &major, &minor) != 2) {
    NS_WARNING("invalid version string");
    return 0;
  }

  return PRUint32(major) << 16 | PRUint32(minor);
}

static PRBool CheckMinVersion(const char *versionStr)
{
  PRUint32 v = ParseVersion(versionStr);
  return geckoVersion >= v;
}

static PRBool CheckMaxVersion(const char *versionStr)
{
  PRUint32 v = ParseVersion(versionStr);
  return geckoVersion <= v;
}

/**
 * Parse application data.
 */
static int LoadAppData(const char* appDataFile, nsXREAppData* aResult,
                       nsCString& vendor, nsCString& name, nsCString& version,
                       nsCString& buildID, nsCString& appID,
                       nsCString& copyright)
{
  nsresult rv;

  nsCOMPtr<nsILocalFile> lf;
  XRE_GetFileFromPath(appDataFile, getter_AddRefs(lf));
  if (!lf)
    return 2;

  nsCOMPtr<nsIFile> appDir;
  rv = lf->GetParent(getter_AddRefs(appDir));
  if (NS_FAILED(rv))
    return 2;

  rv = CallQueryInterface(appDir, &aResult->directory);
  if (NS_FAILED(rv))
    return 2;

  nsINIParser parser;
  rv = parser.Init(lf);
  if (NS_FAILED(rv))
    return 2;

  // Gecko version checking
  //
  // TODO: If these version checks fail, then look for a compatible XULRunner
  //       version on the system, and launch it instead.

  nsCAutoString gkVersion;
  rv = parser.GetString("Gecko", "MinVersion", gkVersion);

  if (NS_FAILED(rv) || !CheckMinVersion(gkVersion.get())) {
    Output(PR_TRUE, "Error: Gecko MinVersion requirement not met.\n");
    return 1;
  }

  rv = parser.GetString("Gecko", "MaxVersion", gkVersion);
  if (NS_SUCCEEDED(rv) && !CheckMaxVersion(gkVersion.get())) {
    Output(PR_TRUE, "Error: Gecko MaxVersion requirement not met.\n");
    return 1;
  }

  PRUint32 i;

  // Read string-valued fields
  const struct
  {
    const char *key;
    const char **fill;
    nsCString  *buf;
    PRBool      required;
  } string_fields[] = {
    { "Vendor",    &aResult->vendor,    &vendor,    PR_FALSE },
    { "Name",      &aResult->name,      &name,      PR_TRUE  },
    { "Version",   &aResult->version,   &version,   PR_FALSE },
    { "BuildID",   &aResult->buildID,   &buildID,   PR_TRUE  },
    { "ID",        &aResult->ID,        &appID,     PR_FALSE },
    { "Copyright", &aResult->copyright, &copyright, PR_FALSE }
  };
  for (i = 0; i < NS_ARRAY_LENGTH(string_fields); ++i) {
    rv = parser.GetString("App", string_fields[i].key,
                          *string_fields[i].buf);
    if (NS_SUCCEEDED(rv)) {
      *string_fields[i].fill = string_fields[i].buf->get();
    }
    else if (string_fields[i].required) {
      Output(PR_TRUE, "Error: %x: No \"%s\" field.\n",
             rv, string_fields[i].key);
      return 1;
    }
  }

  // Read boolean-valued fields
  const struct {
    const char* key;
    PRUint32 flag;
  } boolean_fields[] = {
    { "EnableProfileMigrator",  NS_XRE_ENABLE_PROFILE_MIGRATOR  },
    { "EnableExtensionManager", NS_XRE_ENABLE_EXTENSION_MANAGER }
  };
  char buf[6]; // large enough to hold "false"
  aResult->flags = 0;
  for (i = 0; i < NS_ARRAY_LENGTH(boolean_fields); ++i) {
    rv = parser.GetString("XRE", boolean_fields[i].key, buf, sizeof(buf));
    // accept a truncated result since we are only interested in the
    // first character.  this is designed to allow the possibility of
    // expanding these boolean attributes to express additional options.
    if ((NS_SUCCEEDED(rv) || rv == NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) &&
        (buf[0] == '1' || buf[0] == 't' || buf[0] == 'T')) {
      aResult->flags |= boolean_fields[i].flag;
    }
  } 

#ifdef DEBUG
  printf("---------------------------------------------------------\n");
  printf("     Vendor %s\n", aResult->vendor);
  printf("       Name %s\n", aResult->name);
  printf("    Version %s\n", aResult->version);
  printf("    BuildID %s\n", aResult->buildID);
  printf("  Copyright %s\n", aResult->copyright);
  printf("      Flags %08x\n", aResult->flags);
  printf("---------------------------------------------------------\n");
#endif

  return 0;
}

static void Usage()
{
    // display additional information (XXX make localizable?)
    Output(PR_FALSE,
           "Mozilla XULRunner %s\n\n"
           "Usage: " XULRUNNER_PROGNAME " [OPTIONS]\n"
           "       " XULRUNNER_PROGNAME " APP-FILE [APP-OPTIONS...]\n"
           "\n"
           "OPTIONS\n"
           "      --app                  specify APP-FILE (optional)\n"
           "  -h, --help                 show this message\n"
           "  -v, --version              show version\n"
           "  --gre-version              print the GRE version string on stdout\n"
           "  --register-global          register this GRE in the machine registry\n"
           "  --register-user            register this GRE in the user registry\n"
           "  --unregister-global        unregister this GRE formerly registered with\n"
           "                             --register-global\n"
           "  --unregister-user          unregister this GRE formely registered with\n"
           "                             --register-user\n"
           "  --find-gre <version>       Find a GRE with version <version> and print\n"
           "                             the path on stdout\n"
           "  --install-app <application> [<destination> [<directoryname>]]\n"
           "                             Install a XUL application.\n"
           "\n"
           "APP-FILE\n"
           "  Application initialization file.\n"
           "\n"
           "APP-OPTIONS\n"
           "  Application specific options.\n",
           GRE_BUILD_ID);
}

static nsresult
GetXULRunnerDir(const char *argv0, nsIFile* *aResult)
{
  nsresult rv;

  nsCOMPtr<nsILocalFile> appFile;
  rv = XRE_GetBinaryPath(argv0, getter_AddRefs(appFile));
  if (NS_FAILED(rv)) {
    Output(PR_TRUE, "Could not find XULRunner application path.\n");
    return rv;
  }

  rv = appFile->GetParent(aResult);
  if (NS_FAILED(rv)) {
    Output(PR_TRUE, "Could not find XULRunner installation dir.\n");
  }
  return rv;
}

static int
InstallXULApp(nsIFile* aXULRunnerDir,
              const char *aAppLocation,
              const char *aInstallTo,
              const char *aLeafName)
{
  nsCOMPtr<nsILocalFile> appLocation;
  nsCOMPtr<nsILocalFile> installTo;
  nsAutoString leafName;

  nsresult rv = XRE_GetFileFromPath(aAppLocation, getter_AddRefs(appLocation));
  if (NS_FAILED(rv))
    return 2;

  if (aInstallTo) {
    rv = XRE_GetFileFromPath(aInstallTo, getter_AddRefs(installTo));
    if (NS_FAILED(rv))
      return 2;
  }

  if (aLeafName)
    NS_CopyNativeToUnicode(nsDependentCString(aLeafName), leafName);

  rv = NS_InitXPCOM2(nsnull, aXULRunnerDir, nsnull);
  if (NS_FAILED(rv))
    return 3;

  {
    // Scope our COMPtr to avoid holding XPCOM refs beyond xpcom shutdown
    nsCOMPtr<nsIXULAppInstall> install
      (do_GetService("@mozilla.org/xulrunner/app-install-service;1"));
    if (!install) {
      rv = NS_ERROR_FAILURE;
    }
    else {
      rv = install->InstallApplication(appLocation, installTo, leafName);
    }
  }

  NS_ShutdownXPCOM(nsnull);

  if (NS_FAILED(rv))
    return 3;

  return 0;
}

static const GREProperty kGREProperties[] = {
  { "xulrunner", "true" }
#ifdef MOZ_JAVAXPCOM
  , { "javaxpcom", "1" }
#endif
};

int main(int argc, char* argv[])
{
  if (argc > 1 && (IsArg(argv[1], "h") ||
                   IsArg(argv[1], "help") ||
                   IsArg(argv[1], "?")))
  {
    Usage();
    return 0;
  }

  if (argc == 2 && (IsArg(argv[1], "v") || IsArg(argv[1], "version")))
  {
    Output(PR_FALSE, "Mozilla XULRunner %s\n", GRE_BUILD_ID);
    return 0;
  }

  if (argc > 1) {
    PRBool registerGlobal = IsArg(argv[1], "register-global");
    PRBool registerUser   = IsArg(argv[1], "register-user");
    if (registerGlobal || registerUser) {
      if (argc != 2) {
        Usage();
        return 1;
      }

      nsCOMPtr<nsIFile> regDir;
      nsresult rv = GetXULRunnerDir(argv[0], getter_AddRefs(regDir));
      if (NS_FAILED(rv))
        return 2;

      return RegisterXULRunner(registerGlobal, regDir,
                               kGREProperties,
                               NS_ARRAY_LENGTH(kGREProperties)) ? 0 : 2;
    }

    registerGlobal = IsArg(argv[1], "unregister-global");
    registerUser   = IsArg(argv[1], "unregister-user");
    if (registerGlobal || registerUser) {
      if (argc != 2) {
        Usage();
        return 1;
      }

      nsCOMPtr<nsIFile> regDir;
      nsresult rv = GetXULRunnerDir(argv[0], getter_AddRefs(regDir));
      if (NS_FAILED(rv))
        return 2;
      UnregisterXULRunner(registerGlobal, regDir);
      return 0;
    }

    if (IsArg(argv[1], "find-gre")) {
      if (argc != 3) {
        Usage();
        return 1;
      }

      char path[MAXPATHLEN];
      static const GREVersionRange vr = {
        argv[2], PR_TRUE,
        argv[2], PR_TRUE
      };
      static const GREProperty kProperties[] = {
        { "xulrunner", "true" }
      };

      nsresult rv = GRE_GetGREPathWithProperties(&vr, 1, kProperties,
                                                 NS_ARRAY_LENGTH(kProperties),
                                                 path, sizeof(path));
      if (NS_FAILED(rv))
        return 1;

      printf("%s\n", path);
      return 0;
    }

    if (IsArg(argv[1], "gre-version")) {
      if (argc != 2) {
        Usage();
        return 1;
      }

      printf("%s\n", GRE_BUILD_ID);
      return 0;
    }

    if (IsArg(argv[1], "install-app")) {
      if (argc < 3 || argc > 5) {
        Usage();
        return 1;
      }

      char *appLocation = argv[2];

      char *installTo = nsnull;
      if (argc > 3) {
        installTo = argv[3];
        if (!*installTo) // left blank?
          installTo = nsnull;
      }

      char *leafName = nsnull;
      if (argc > 4) {
        leafName = argv[4];
        if (!*leafName)
          leafName = nsnull;
      }

      nsCOMPtr<nsIFile> regDir;
      nsresult rv = GetXULRunnerDir(argv[0], getter_AddRefs(regDir));
      if (NS_FAILED(rv))
        return 2;

      return InstallXULApp(regDir, appLocation, installTo, leafName);
    }
  }

  geckoVersion = ParseVersion(GRE_BUILD_ID);

  const char *appDataFile = PR_GetEnv("XUL_APP_FILE");

  if (!(appDataFile && *appDataFile)) {
    if (argc < 2) {
      Usage();
      return 1;
    }

    if (IsArg(argv[1], "app")) {
      if (argc == 2) {
        Usage();
        return 1;
      }
      argv[1] = argv[0];
      ++argv;
      --argc;
    }

    appDataFile = argv[1];
    argv[1] = argv[0];
    ++argv;
    --argc;

    static char kAppEnv[MAXPATHLEN];
    PR_snprintf(kAppEnv, MAXPATHLEN, "XUL_APP_FILE=%s", appDataFile);
    PR_SetEnv(kAppEnv);
  }

  nsCAutoString vendor, name, version, buildID, appID, copyright;

  nsXREAppData appData = { sizeof(nsXREAppData), 0 };

  int rv = LoadAppData(appDataFile, &appData,
                       vendor, name, version, buildID, appID, copyright);
  if (!rv)
    rv = XRE_main(argc, argv, &appData);

  NS_IF_RELEASE(appData.directory);

  return rv;
}

#if defined( XP_WIN ) && defined( WIN32 ) && !defined(__GNUC__)
// We need WinMain in order to not be a console app.  This function is
// unused if we are a console application.
int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR args, int )
{
  // Do the real work.
  return main( __argc, __argv );
}
#endif
