/****************************************************************************
 * tools/mkdeps.c
 *
 *   Copyright (C) 2012-2013 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <sys/stat.h>

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <ctype.h>
#include <libgen.h>
#include <errno.h>

#ifdef HOST_CYGWIN
#  include <sys/cygwin.h>
#endif

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define MAX_BUFFER  (4096)
#define MAX_EXPAND  (2048)
#define MAX_PATH    (512)

/* NAME_MAX is typically defined in limits.h */

#if !defined(NAME_MAX)

  /* FILENAME_MAX might be defined in stdio.h */

#  if defined(FILENAME_MAX)
#    define NAME_MAX FILENAME_MAX
#  else

  /* MAXNAMELEN might be defined in dirent.h */

#    include <dirent.h>
#    if defined(MAXNAMLEN)
#      define NAME_MAX MAXNAMLEN
#    else

  /* Lets not let a silly think like this stop us... just make something up */

#      define NAME_MAX 256
#    endif
#  endif
#endif

/****************************************************************************
 * Private Types
 ****************************************************************************/

enum slashmode_e
{
  MODE_FSLASH  = 0,
  MODE_BSLASH  = 1,
  MODE_DBLBACK = 2
};

/****************************************************************************
 * Private Data
 ****************************************************************************/

static char *g_cc        = NULL;
static char *g_cflags    = NULL;
static char *g_files     = NULL;
static char *g_altpath   = NULL;
static char *g_objpath   = NULL;
static char *g_suffix    = ".o";
static int   g_debug     = 0;
static bool  g_winnative = false;
#ifdef HOST_CYGWIN
static bool  g_winpath   = false;
#endif

static char g_command[MAX_BUFFER];
static char g_path[MAX_PATH];
#ifdef HOST_CYGWIN
static char g_expand[MAX_EXPAND];
static char g_dequoted[MAX_PATH];
static char g_posixpath[MAX_PATH];
#endif

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/* MinGW does not seem to provide strtok_r */

#ifndef HAVE_STRTOK_R
static char *MY_strtok_r(char *str, const char *delim, char **saveptr)
{
  char *pbegin;
  char *pend = NULL;

  /* Decide if we are starting a new string or continuing from
   * the point we left off.
   */

  if (str)
    {
      pbegin = str;
    }
  else if (saveptr && *saveptr)
    {
      pbegin = *saveptr;
    }
  else
    {
      return NULL;
    }

  /* Find the beginning of the next token */

  for (;
       *pbegin && strchr(delim, *pbegin) != NULL;
       pbegin++);

  /* If we are at the end of the string with nothing
   * but delimiters found, then return NULL.
   */

  if (!*pbegin)
    {
      return NULL;
    }

  /* Find the end of the token */

  for (pend = pbegin + 1;
       *pend && strchr(delim, *pend) == NULL;
       pend++);

  /* pend either points to the end of the string or to
   * the first delimiter after the string.
   */

  if (*pend)
    {
      /* Turn the delimiter into a null terminator */

      *pend++ = '\0';
    }

  /* Save the pointer where we left off and return the
   * beginning of the token.
   */

  if (saveptr)
    {
      *saveptr = pend;
    }

  return pbegin;
}

#undef strtok_r
#define strtok_r MY_strtok_r
#endif

static void append(char **base, char *str)
{
  char *oldbase;
  char *newbase;
  int alloclen;

  oldbase = *base;
  if (!oldbase)
    {
      newbase = strdup(str);
      if (!newbase)
        {
          fprintf(stderr, "ERROR: Failed to strdup %s\n", str);
          exit(EXIT_FAILURE);
        }
    }
  else
    {
      alloclen = strlen(oldbase) + strlen(str) + 2;
      newbase = (char *)malloc(alloclen);
      if (!newbase)
        {
          fprintf(stderr, "ERROR: Failed to allocate %d bytes\n", alloclen);
          exit(EXIT_FAILURE);
        }

      snprintf(newbase, alloclen, "%s %s\n", oldbase, str);
      free(oldbase);
   }

  *base = newbase;
}

static void show_usage(const char *progname, const char *msg, int exitcode)
{
  if (msg)
    {
      fprintf(stderr, "\n");
      fprintf(stderr, "%s:\n", msg);
    }

  fprintf(stderr, "\n");
  fprintf(stderr, "%s  [OPTIONS] CC -- CFLAGS -- file [file [file...]]\n",
          progname);
  fprintf(stderr, "\n");
  fprintf(stderr, "Where:\n");
  fprintf(stderr, "  CC\n");
  fprintf(stderr, "    A variable number of arguments that define how to execute the compiler\n");
  fprintf(stderr, "  CFLAGS\n");
  fprintf(stderr, "    The compiler compilation flags\n");
  fprintf(stderr, "  file\n");
  fprintf(stderr, "    One or more C files whose dependencies will be checked.  Each file is expected\n");
  fprintf(stderr, "    to reside in the current directory unless --dep-path is provided on the command line\n");
  fprintf(stderr, "\n");
  fprintf(stderr, "And [OPTIONS] include:\n");
  fprintf(stderr, "  --dep-debug\n");
  fprintf(stderr, "    Enable script debug\n");
  fprintf(stderr, "  --dep-path <path>\n");
  fprintf(stderr, "    Do not look in the current directory for the file.  Instead, look in <path> to see\n");
  fprintf(stderr, "    if the file resides there.  --dep-path may be used multiple times to specify\n");
  fprintf(stderr, "    multiple alternative location\n");
  fprintf(stderr, "  --obj-path <path>\n");
  fprintf(stderr, "    The final objects will not reside in this path but, rather, at the path provided by\n");
  fprintf(stderr, "    <path>.  if provided multiple time, only the last --obj-path will be used.\n");
  fprintf(stderr, "  --obj-suffix <suffix>\n");
  fprintf(stderr, "    If and object path is provided, then the extension will be assumed to be .o.  This\n");
  fprintf(stderr, "    default suffix can be overrided with this command line option.\n");
  fprintf(stderr, "  --winnative\n");
  fprintf(stderr, "    By default, a POSIX-style environment is assumed (e.g., Linux, Cygwin, etc.)  This option is\n");
  fprintf(stderr, "    inform the tool that is working in a pure Windows native environment.\n");
#ifdef HOST_CYGWIN
  fprintf(stderr, "  --winpaths\n");
  fprintf(stderr, "    This option is useful when using a Windows native toolchain in a POSIX environment (such\n");
  fprintf(stderr, "    such as Cygwin).  In this case, will CC generates dependency lists using Windows paths\n");
  fprintf(stderr, "    (e.g., C:\\blablah\\blabla).\n");
#endif
  fprintf(stderr, "  --help\n");
  fprintf(stderr, "    Shows this message and exits\n");
  exit(exitcode);
}

static void parse_args(int argc, char **argv)
{
  char *args = NULL;
  int argidx;

  /* Accumulate CFLAGS up to "--" */

  for (argidx = 1; argidx < argc; argidx++)
    {
      if (strcmp(argv[argidx], "--") == 0)
        {
          g_cc = g_cflags;
          g_cflags = args;
          args = NULL;
        }
      else if (strcmp(argv[argidx], "--dep-debug") == 0)
        {
          g_debug++;
        }
      else if (strcmp(argv[argidx], "--dep-path") == 0)
        {
          argidx++;
          if (argidx >= argc)
            {
              show_usage(argv[0], "ERROR: Missing argument to --dep-path", EXIT_FAILURE);
            }

          if (args)
            {
              append(&args, argv[argidx]);
            }
          else
            {
              append(&g_altpath, argv[argidx]);
            }
        }
      else if (strcmp(argv[argidx], "--obj-path") == 0)
        {
          argidx++;
          if (argidx >= argc)
            {
              show_usage(argv[0], "ERROR: Missing argument to --obj-path", EXIT_FAILURE);
            }

          g_objpath = argv[argidx];
        }
      else if (strcmp(argv[argidx], "--obj-suffix") == 0)
        {
          argidx++;
          if (argidx >= argc)
            {
              show_usage(argv[0], "ERROR: Missing argument to --obj-suffix", EXIT_FAILURE);
            }

          g_suffix = argv[argidx];
        }
      else if (strcmp(argv[argidx], "--winnative") == 0)
        {
          g_winnative = true;
        }
#ifdef HOST_CYGWIN
      else if (strcmp(argv[argidx], "--winpath") == 0)
        {
          g_winpath = true;
        }
#endif
      else if (strcmp(argv[argidx], "--help") == 0)
        {
          show_usage(argv[0], NULL, EXIT_SUCCESS);
        }
      else
        {
          append(&args, argv[argidx]);
        }
    }

  /* The final thing accumulated is the list of files */

  g_files = args;

  /* If no paths were specified, then look in the current directory only */

  if (!g_altpath)
    {
      g_altpath = strdup(".");
    }

  if (g_debug)
    {
      fprintf(stderr, "SELECTIONS\n");
      fprintf(stderr, "  CC             : [%s]\n", g_cc ? g_cc : "(None)");
      fprintf(stderr, "  CFLAGS         : [%s]\n", g_cflags ? g_cflags : "(None)");
      fprintf(stderr, "  FILES          : [%s]\n", g_files ? g_files : "(None)");
      fprintf(stderr, "  PATHS          : [%s]\n", g_altpath ? g_altpath : "(None)");
      if (g_objpath)
        {
          fprintf(stderr, "  OBJDIR         : [%s]\n", g_objpath);
          fprintf(stderr, "  SUFFIX         : [%s]\n", g_suffix);
        }
      else
        {
          fprintf(stderr, "  OBJDIR         : (None)\n");
        }

#ifdef HOST_CYGWIN
      fprintf(stderr, "  Windows Paths  : [%s]\n", g_winpath ? "TRUE" : "FALSE");
#endif
      fprintf(stderr, "  Windows Native : [%s]\n", g_winnative ? "TRUE" : "FALSE");
    }

  /* Check for required parameters */

  if (!g_cc)
    {
      show_usage(argv[0], "ERROR: No compiler specified", EXIT_FAILURE);
    }

  if (!g_files)
    {
      /* Don't report an error -- this happens normally in some configurations */

      printf("# No files specified for dependency generataion\n");
      exit(EXIT_SUCCESS);
    }

#ifdef HOST_CYGWIN
  if (g_winnative && g_winpath)
    {
      show_usage(argv[0], "ERROR: Both --winnative and --winpath makes no sense", EXIT_FAILURE);
    }
#endif
}

static const char *do_expand(const char *argument)
{
#ifdef HOST_CYGWIN
  if (g_winpath)
    {
      const char *src;
      char *dest;
      int len;

      src  = argument;
      dest = g_expand;
      len  = 0;

      while (*src && len < MAX_EXPAND)
        {
          if (*src == '\\')
            {
              /* Copy backslash */

              *dest++ = *src++;
              if (++len >= MAX_EXPAND)
                {
                  break;
                }

              /* Already expanded? */

              if (*src == '\\')
                {
                  /* Yes... just copy all consecutive backslashes */

                  do
                    {
                      *dest++ = *src++;
                      if (++len >= MAX_EXPAND)
                        {
                          break;
                        }
                    }
                  while (*src == '\\');
                }
              else
                {
                  /* No.. expeand */

                  *dest++ = '\\';
                  if (++len >= MAX_EXPAND)
                    {
                      break;
                    }
                }
            }
          else
          {
            *dest++ = *src++;
            len++;
          }
        }

      if (*src)
        {
          fprintf(stderr, "ERROR: Truncated during expansion string is too long [%lu/%u]\n",
                  (unsigned long)strlen(argument), MAX_EXPAND);
          exit(EXIT_FAILURE);
        }

      *dest = '\0';
      return g_expand;
    }
  else
#endif
    {
      return argument;
    }
}

#ifdef HOST_CYGWIN
static bool dequote_path(const char *winpath)
{
  char *dest = g_dequoted;
  const char *src = winpath;
  int len = 0;
  bool quoted = false;

  while (*src && len < MAX_PATH)
    {
      if (src[0] != '\\' || (src[1] != ' ' && src[1] != '(' && src[1] != ')'))
        {
          *dest++ = *src;
          len++;
        }
      else
        {
          quoted = true;
        }

      src++;
    }

  if (*src || len >= MAX_PATH)
    {
      fprintf(stderr, "# ERROR: Path truncated\n");
      exit(EXIT_FAILURE);
    }

  *dest = '\0';
  return quoted;
}
#endif

static const char *convert_path(const char *path)
{
#ifdef HOST_CYGWIN
  if (g_winpath)
    {
      const char *retptr;
      ssize_t size;
      ssize_t ret;
      bool quoted;

      quoted = dequote_path(path);
      if (quoted)
        {
          retptr = g_posixpath;
        }
      else
        {
          retptr = &g_posixpath[1];
        }

      size = cygwin_conv_path(CCP_POSIX_TO_WIN_A | CCP_RELATIVE, g_dequoted,
                             NULL, 0);
      if (size > (MAX_PATH-3))
        {
          fprintf(stderr, "# ERROR: POSIX path too long: %lu\n",
                  (unsigned long)size);
          exit(EXIT_FAILURE);
        }

      ret = cygwin_conv_path(CCP_POSIX_TO_WIN_A | CCP_RELATIVE, g_dequoted,
                             &g_posixpath[1], MAX_PATH-3);
      if (ret < 0)
        {
          fprintf(stderr, "# ERROR: cygwin_conv_path '%s' failed: %s\n",
                  g_dequoted, strerror(errno));
          exit(EXIT_FAILURE);
        }

      if (quoted)
        {
          size++;
          g_posixpath[0] = '"';
          g_posixpath[size] = '"';
        }

      g_posixpath[size+1] = '\0';
      return retptr;
    }
  else
#endif
    {
      return path;
    }
}

static void do_dependency(const char *file)
{
  static const char moption[] = " -M ";
  struct stat buf;
  char *alloc;
  char *altpath;
  char *path;
  char *lasts;
  char separator;
  int cmdlen;
  int pathlen;
  int filelen;
  int totallen;
  int ret;

  /* Initialize the separator */

#ifdef HOST_CYGWIN
  separator =  (g_winnative || g_winpath) ? '\\' : '/';
#else
  separator =  g_winnative ? '\\' : '/';
#endif

  /* Copy the compiler into the command buffer */

  cmdlen = strlen(g_cc);
  if (cmdlen >= MAX_BUFFER)
    {
      fprintf(stderr, "ERROR: Compiler string is too long [%d/%d]: %s\n",
              cmdlen, MAX_BUFFER, g_cc);
      exit(EXIT_FAILURE);
    }

  strcpy(g_command, g_cc);

  /* Copy " -MT " */

  if (g_objpath)
    {
      char tmp[NAME_MAX+6];
      char *dupname;
      char *objname;
      char *dotptr;
      const char *expanded;

      dupname = strdup(file);
      if (!dupname)
        {
          fprintf(stderr, "ERROR: Failed to dup: %s\n", file);
          exit(EXIT_FAILURE);
        }

      objname = basename(dupname);
      dotptr  = strrchr(objname, '.');
      if (dotptr)
        {
          *dotptr = '\0';
        }

      snprintf(tmp, NAME_MAX+6, " -MT %s%c%s%s ",
               g_objpath, separator, objname, g_suffix);
      expanded = do_expand(tmp);

      cmdlen += strlen(expanded);
      if (cmdlen >= MAX_BUFFER)
        {
          fprintf(stderr, "ERROR: Option string is too long [%d/%d]: %s\n",
                  cmdlen, MAX_BUFFER, moption);
          exit(EXIT_FAILURE);
        }

      strcat(g_command, expanded);
      free(dupname);
    }

  /* Copy " -M " */

  cmdlen += strlen(moption);
  if (cmdlen >= MAX_BUFFER)
    {
      fprintf(stderr, "ERROR: Option string is too long [%d/%d]: %s\n",
              cmdlen, MAX_BUFFER, moption);
      exit(EXIT_FAILURE);
    }

  strcat(g_command, moption);

  /* Copy the CFLAGS into the command buffer */

  if (g_cflags)
    {
      const char *expanded;

      expanded = do_expand(g_cflags);
      cmdlen += strlen(expanded);

      if (cmdlen >= MAX_BUFFER)
        {
          fprintf(stderr, "ERROR: CFLAG string is too long [%d/%d]: %s\n",
                  cmdlen, MAX_BUFFER, g_cflags);
          exit(EXIT_FAILURE);
        }

      strcat(g_command, expanded);
    }

  /* Add a space */

  g_command[cmdlen] = ' ';
  cmdlen++;
  g_command[cmdlen] = '\0';

  /* Make a copy of g_altpath. We need to do this because at least the version
   * of strtok_r above does modify it.
   */

  alloc = strdup(g_altpath);
  if (!alloc)
    {
      fprintf(stderr, "ERROR: Failed to strdup paths\n");
      exit(EXIT_FAILURE);
    }

  altpath = alloc;

  /* Try each path.  This loop will continue until each path has been tried
   * (failure) or until stat() finds the file
   */

  while ((path = strtok_r(altpath, " ", &lasts)) != NULL)
    {
      const char *expanded;
      const char *converted;

      /* Create a full path to the file */

      pathlen = strlen(path);
      if (pathlen >= MAX_PATH)
        {
          fprintf(stderr, "ERROR: Path is too long [%d/%d]: %s\n",
                  pathlen, MAX_PATH, path);
          exit(EXIT_FAILURE);
        }

      strcpy(g_path, path);

      if (g_path[pathlen] != '\0')
        {
          fprintf(stderr, "ERROR: Missing NUL terminator\n");
          exit(EXIT_FAILURE);
        }

      if (g_path[pathlen-1] != separator)
        {
          g_path[pathlen] = separator;
          g_path[pathlen+1] = '\0';
          pathlen++;
        }

      filelen = strlen(file);
      pathlen += filelen;
      if (pathlen >= MAX_PATH)
        {
          fprintf(stderr, "ERROR: Path+file is too long [%d/%d]\n",
                  pathlen, MAX_PATH);
          exit(EXIT_FAILURE);
        }

      strcat(g_path, file);

      /* Check that a file actually exists at this path */

      if (g_debug)
        {
          fprintf(stderr, "Trying path=%s file=%s fullpath=%s\n",
                  path, file, g_path);
        }

      converted = convert_path(g_path);
      ret = stat(converted, &buf);
      if (ret < 0)
        {
          altpath = NULL;
          continue;
        }

      if (!S_ISREG(buf.st_mode))
        {
          fprintf(stderr, "ERROR: File %s exists but is not a regular file\n",
                  g_path);
          exit(EXIT_FAILURE);
        }

      /* Append the expanded path to the command */

      expanded = do_expand(g_path);
      pathlen  = strlen(expanded);
      totallen = cmdlen + pathlen;

      if (totallen >= MAX_BUFFER)
        {
          fprintf(stderr, "ERROR: Path string is too long [%d/%d]: %s\n",
                  totallen, MAX_BUFFER, g_path);
          exit(EXIT_FAILURE);
        }

      strcat(g_command, expanded);

      /* Okay.. we have everything.  Create the dependency.  One a failure
       * to start the compiler, system() will return -1;  Otherwise, the
       * returned value from the compiler is in WEXITSTATUS(ret).
       */

      if (g_debug)
        {
          fprintf(stderr, "Executing: %s\n", g_command);
        }

      ret = system(g_command);
#ifdef WEXITSTATUS
      if (ret < 0 || WEXITSTATUS(ret) != 0)
        {
          if (ret < 0)
            {
              fprintf(stderr, "ERROR: system failed: %s\n", strerror(errno));
            }
          else
            {
              fprintf(stderr, "ERROR: %s failed: %d\n", g_cc, WEXITSTATUS(ret));
            }

          fprintf(stderr, "       command: %s\n", g_command);
          exit(EXIT_FAILURE);
        }
#else
      if (ret < 0)
        {
          fprintf(stderr, "ERROR: system failed: %s\n", strerror(errno));
          fprintf(stderr, "       command: %s\n", g_command);
          exit(EXIT_FAILURE);
        }
#endif

      /* We don't really know that the command succeeded... Let's assume that it did */

      free(alloc);
      return;
    }

   printf("# ERROR: File \"%s\" not found at any location\n", file);
   exit(EXIT_FAILURE);
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

int main(int argc, char **argv, char **envp)
{
  char *lasts;
  char *files;
  char *file;

  /* Parse command line parameters */

  parse_args(argc, argv);

  /* Then generate dependencies for each path on the command line.  NOTE
   * strtok_r will clobber the files list.  But that is okay because we are
   * only going to traverse it once.
   */

  files = g_files;
  while ((file = strtok_r(files, " ", &lasts)) != NULL)
    {
      /* Check if we need to do path conversions for a Windows-natvive tool
       * being using in a POSIX/Cygwin environment.
       */

      do_dependency(file);
      files = NULL;
    }

  return EXIT_SUCCESS;
}
