/****************************************************************************
 * tools/initialconfig.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you 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.
 *
 ****************************************************************************/

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

#define _GNU_SOURCE 1
#include <sys/stat.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>

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

#define MAX_LINE           512
#define MAX_ARCHITECTURES  32
#define MAX_MCUS           64
#define MAX_BOARDS         128

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

typedef int (*direntcb_t)(const char *dirpath, struct dirent *entry,
                          void *arg);

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

#ifdef CONFIG_WINDOWS_NATIVE
static char        g_delim       = '\\';      /* Delimiter to use when forming paths */
#else
static char        g_delim       = '/';       /* Delimiter to use when forming paths */
#endif

static const char  g_archdir[]   = "arch";    /* Architecture directory */
static const char  g_configdir[] = "boards";  /* Board configuration directory */

static char       *g_arch[MAX_ARCHITECTURES]; /* List of architecture names */
static int         g_narch;                   /* Number of architecture names */
static char       *g_selected_arch;           /* Selected architecture name */
static char       *g_selected_family;         /* Selected architecture family name */

static char       *g_mcu[MAX_MCUS];           /* List of MCU names */
static int         g_nmcu;                    /* Number of MCU names */
static char       *g_selected_mcu;            /* Selected MCU name */

static char       *g_board[MAX_BOARDS];       /* List of board names */
static int         g_nboard;                  /* Number of board names */
static char       *g_selected_board;          /* Selected board name */

static char        g_line[MAX_LINE + 1];      /* Line read from config file */

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

/****************************************************************************
 * Name: skip_space
 *
 * Description:
 *   Skip over any spaces
 *
 ****************************************************************************/

static char *skip_space(char *ptr)
{
  while (*ptr && isspace((int)*ptr)) ptr++;
  return ptr;
}

/****************************************************************************
 * Name: find_name_end
 *
 * Description:
 *   Find the end of a variable string
 *
 ****************************************************************************/

static char *find_name_end(char *ptr)
{
  while (*ptr && (isalnum((int)*ptr) || *ptr == '_')) ptr++;
  return ptr;
}

/****************************************************************************
 * Name: find_value_end
 *
 * Description:
 *   Find the end of a value string
 *
 ****************************************************************************/

static char *find_value_end(char *ptr)
{
  while (*ptr && !isspace((int)*ptr))
    {
      if (*ptr == '"')
        {
          do ptr++; while (*ptr && *ptr != '"');
          if (*ptr) ptr++;
        }
      else
        {
          do ptr++; while (*ptr && !isspace((int)*ptr) && *ptr != '"');
        }
    }

  return ptr;
}

/****************************************************************************
 * Name: read_line
 *
 * Description:
 *   Read the next line from the configuration file
 *
 ****************************************************************************/

static char *read_line(FILE *stream)
{
  char *ptr;

  for (; ; )
    {
      g_line[MAX_LINE] = '\0';
      if (!fgets(g_line, MAX_LINE, stream))
        {
          return NULL;
        }
      else
        {
          ptr = skip_space(g_line);
          if (*ptr && *ptr != '#' && *ptr != '\n')
            {
              return ptr;
            }
        }
    }
}

/****************************************************************************
 * Name: parse_line
 *
 * Description:
 *   Parse the line from the configuration file into a variable name
 *   string and a value string.
 *
 ****************************************************************************/

static void parse_line(char *ptr, char **varname, char **varval)
{
  /* Skip over any leading spaces */

  ptr = skip_space(ptr);

  /* The first no-space is the beginning of the variable name */

  *varname = skip_space(ptr);
  *varval = NULL;

  /* Parse to the end of the variable name */

  ptr = find_name_end(ptr);

  /* An equal sign is expected next, perhaps after some white space */

  if (*ptr && *ptr != '=')
    {
      /* Some else follows the variable name.  Terminate the variable
       * name and skip over any spaces.
       */

      *ptr = '\0';
       ptr = skip_space(ptr + 1);
    }

  /* Verify that the equal sign is present */

  if (*ptr == '=')
    {
      /* Make sure that the variable name is terminated (this was already
       * done if the name was followed by white space.
       */

      *ptr = '\0';

      /* The variable value should follow =, perhaps separated by some
       * white space.
       */

      ptr = skip_space(ptr + 1);
      if (*ptr)
        {
          /* Yes.. a variable follows.  Save the pointer to the start
           * of the variable string.
           */

          *varval = ptr;

          /* Find the end of the variable string and make sure that it
           * is terminated.
           */

          ptr = find_value_end(ptr);
          *ptr = '\0';
        }
    }
}

/****************************************************************************
 * Name: find_variable
 *
 * Description:
 *   Return true if the selected variable exists.  Also return the value of
 *   the variable.
 *
 ****************************************************************************/

static bool find_variable(const char *configpath, const char *varname,
                          char **varvalue)
{
  FILE *stream;
  char *tmpname;
  char *tmpvalue;
  char *ptr;

  stream = fopen(configpath, "r");
  if (!stream)
    {
       fprintf(stderr, "ERROR: failed to open %s for reading: %s\n",
               configpath, strerror(errno));
       exit(EXIT_FAILURE);
    }

  /* Loop until the entire file has been parsed. */

  do
    {
      /* Read the next line from the file */

      ptr = read_line(stream);
      if (ptr)
        {
          /* Parse the line into a variable and a value field */

          tmpname = NULL;
          tmpvalue = NULL;
          parse_line(ptr, &tmpname, &tmpvalue);

          /* Make sure that both a variable name and value name were found. */

          if (tmpname == NULL || tmpvalue == NULL)
            {
              continue;
            }

          /* Check if this the variable name and value we are looking for */

          if (strcmp(varname, tmpname) == 0)
            {
              /* Yes.. return the value of the variable */

              *varvalue = tmpvalue;
              fclose(stream);
              return true;
            }
        }
    }
  while (ptr);

  /* Return failure */

  fclose(stream);
  return false;
}

/****************************************************************************
 * Name: check_variable
 *
 * Description:
 *   Return true if the selected variable exists in the configuration file
 *   and has the specified value.
 *
 ****************************************************************************/

static bool check_variable(const char *configpath, const char *varname,
                           const char *varvalue)
{
  char *tmpvalue;

  if (find_variable(configpath, varname, &tmpvalue))
    {
      /* The variable name exists.  Does it have a value?  Does the value
       * match varvalue?
       */

      if (tmpvalue != NULL && strcmp(varvalue, tmpvalue) == 0)
        {
          /* Yes.. return success */

          return true;
        }
    }

  /* Return failure */

  return false;
}

/****************************************************************************
 * Name: test_filepath
 *
 * Description:
 *   Test if a regular file exists at this path.
 *
 ****************************************************************************/

static bool test_filepath(const char *filepath)
{
  struct stat statbuf;
  int ret;

  ret = stat(filepath, &statbuf);
  if (ret < 0)
    {
      return false;
    }

  return S_ISREG(statbuf.st_mode);
}

/****************************************************************************
 * Name: test_dirpath
 *
 * Description:
 *   Test if a regular file exists at this path.
 *
 ****************************************************************************/

static bool test_dirpath(const char *filepath)
{
  struct stat statbuf;
  int ret;

  ret = stat(filepath, &statbuf);
  if (ret < 0)
    {
      return false;
    }

  return S_ISDIR(statbuf.st_mode);
}

/****************************************************************************
 * Name: foreach_dirent
 *
 * Description:
 *   Given a directory path, call the provided function for each entry in
 *   the directory.
 *
 ****************************************************************************/

static int foreach_dirent(const char *dirpath, direntcb_t cb, void *arg)
{
  DIR *dirp;
  struct dirent *entry;
  int ret;

  dirp = opendir(dirpath);
  if (dirp == NULL)
    {
      fprintf(stderr, "ERROR: Failed to open directory '%s': %s\n",
              dirpath, strerror(errno));
      exit(EXIT_FAILURE);
    }

  for (; ; )
    {
      /* To distinguish between end of stream and error, set
       * errno to 0 and verify whether its value changed if
       * readdir returned NULL.
       */

      errno = 0;

      entry = readdir(dirp);
      if (entry == NULL && errno != 0)
        {
          fprintf(stderr,
                  "ERROR: Failed to read directory '%s' entry: %s\n",
                  dirpath, strerror(errno));
           closedir(dirp);
           exit(EXIT_FAILURE);
        }

      if (entry == NULL)
        {
          break;
        }

      /* Skip over the . and .. hard links */

      if (strcmp(entry->d_name, ".") == 0 ||
          strcmp(entry->d_name, "..") == 0)
        {
          continue;
        }

      ret = cb(dirpath, entry, arg);
      if (ret != 0)
        {
          break;
        }
    }

  closedir(dirp);
  return ret;
}

/****************************************************************************
 * Name: enum_architectures
 *
 * Description:
 *   Enumerate all architecture directory names.
 *
 ****************************************************************************/

static int enum_architectures(const char *dirpath, struct dirent *entry,
                              void *arg)
{
  int ret;
  char *archpath;
  char *testpath;

  /* All architecture directories should contain a Kconfig file, an include/
   * directory, and a src/ directory.
   */

  ret = asprintf(&archpath, "%s%c%s", dirpath, g_delim, entry->d_name);
  if (ret < 0)
    {
      fprintf(stderr,
              "ERROR: asprintf() failed to archpath\n");
      return ret;
    }

  ret = asprintf(&testpath, "%s%cKconfig", archpath, g_delim);
  if (ret < 0)
    {
      fprintf(stderr,
              "ERROR: asprintf() failed to testpath\n");
      return ret;
    }

  if (test_filepath(testpath))
    {
      free(testpath);

      ret = asprintf(&testpath, "%s%cinclude", archpath, g_delim);
      if (ret < 0)
        {
          fprintf(stderr,
                  "ERROR: asprintf() failed to testpath/include\n");
          return ret;
        }

      if (test_dirpath(testpath))
        {
          free(testpath);

          ret = asprintf(&testpath, "%s%csrc", archpath, g_delim);
          if (ret < 0)
            {
              fprintf(stderr,
                      "ERROR: asprintf() failed to testpath/src\n");
              return ret;
            }

          if (test_dirpath(testpath))
            {
              if (g_narch >= MAX_ARCHITECTURES)
                {
                  fprintf(stderr,
                         "ERROR: Too many architecture directories found\n");
                  exit(EXIT_FAILURE);
                }

              g_arch[g_narch] = strdup(entry->d_name);
              g_narch++;
            }
        }
    }

  free(testpath);
  free(archpath);
  return 0;
}

/****************************************************************************
 * Name: enum_mcus
 *
 * Description:
 *   Enumerate all MCU directory names.
 *
 ****************************************************************************/

static int enum_mcus(const char *dirpath, struct dirent *entry, void *arg)
{
  int ret;
  char *mcupath;
  char *testpath;

  /* All MCU directories should contain a Kconfig and a Make.defs file. */

  ret = asprintf(&mcupath, "%s%c%s", dirpath, g_delim, entry->d_name);
  if (ret < 0)
    {
      fprintf(stderr,
              "ERROR: asprintf() failed to mcupath\n");
      return ret;
    }

  ret = asprintf(&testpath, "%s%cKconfig", mcupath, g_delim);
  if (ret < 0)
    {
      fprintf(stderr,
              "ERROR: asprintf() failed to archpath/Kconfig\n");
      return ret;
    }

  if (test_filepath(testpath))
    {
      free(testpath);
      ret = asprintf(&testpath, "%s%cMake.defs", mcupath, g_delim);
      if (ret < 0)
        {
          fprintf(stderr,
                  "ERROR: asprintf() failed to testpath/Make.defs\n");
          return ret;
        }

      if (test_filepath(testpath))
        {
          if (g_nmcu >= MAX_MCUS)
            {
              fprintf(stderr,
                      "ERROR: Too many MCU directories found\n");
              exit(EXIT_FAILURE);
            }

          g_mcu[g_nmcu] = strdup(entry->d_name);
          g_nmcu++;
        }
    }

  free(testpath);
  free(mcupath);
  return 0;
}

/****************************************************************************
 * Name: enum_board_configurations
 *
 * Description:
 *   Enumerate all configurations for boards find the configuration
 *   directory for the selected MCU.
 *
 ****************************************************************************/

static int enum_board_configurations(const char *dirpath,
                                     struct dirent *entry, void *arg)
{
  char *configpath;
  char *varvalue;
  int ret = 0;

  /* All board directories should contain a defconfig file. */

  ret = asprintf(&configpath, "%s%c%s%cdefconfig",
                 dirpath, g_delim, entry->d_name, g_delim);
  if (ret < 0)
    {
      fprintf(stderr,
              "ERROR: asprintf() failed to configpath\n");
      return ret;
    }

  if (test_filepath(configpath))
    {
      /* We don't want all board configurations, we only want the name of
       * the board that includes a configuration with:
       *
       *   CONFIG_ARCH_CHIP="xxxx"
       *
       * Where xxxx is the selected MCU name.
       */

      ret = asprintf(&varvalue, "\"%s\"", g_selected_mcu);
      if (ret < 0)
        {
          fprintf(stderr,
                  "ERROR: asprintf() failed to varvalue\n");
          return ret;
        }

      if (check_variable(configpath, "CONFIG_ARCH_CHIP", varvalue))
        {
          /* Found it... add the board name to the list of boards for the
           * selected MCU.
           */

          if (g_nboard >= MAX_BOARDS)
            {
              fprintf(stderr,
                      "ERROR: Too many board configurations found\n");
              exit(EXIT_FAILURE);
            }

          g_board[g_nboard] = strdup(arg);
          g_nboard++;

          /* If we have not yet extracted the architecture family, then do
           * that here.
           */

          if (g_selected_family == NULL)
            {
              char *family;

              if (find_variable(configpath, "CONFIG_ARCH_FAMILY", &family))
                {
                  g_selected_family = strdup(family);
                }
            }

          /* Stop the enumeration if we find a match.  Continue if not...
           * that is because one board might possible support multiple
           * architectures.
           */

          ret = 1;
        }

      free(varvalue);
    }

  free(configpath);
  return ret;
}

/****************************************************************************
 * Name: enum_boards
 *
 * Description:
 *   Enumerate all boards find the configuration directory for the selected
 *   MCU.
 *
 ****************************************************************************/

static int enum_boards(const char *dirpath, struct dirent *entry, void *arg)
{
  int ret = 0;
  char *boardpath;
  char *testpath;

  /* All board directories should contain a Kconfig file, an include/
   * directory, and a src/ directory.
   */

  ret = asprintf(&boardpath, "%s%c%s", dirpath, g_delim, entry->d_name);
  if (ret < 0)
    {
      fprintf(stderr,
              "ERROR: asprintf() failed to boardpath\n");
      return ret;
    }

  ret = asprintf(&testpath, "%s%cKconfig", boardpath, g_delim);
  if (ret < 0)
    {
      fprintf(stderr,
              "ERROR: asprintf() failed to testpath\n");
      return ret;
    }

  if (test_filepath(testpath))
    {
      free(testpath);
      ret = asprintf(&testpath, "%s%cinclude", boardpath, g_delim);
      if (ret < 0)
        {
          fprintf(stderr,
                  "ERROR: asprintf() failed to testpath\n");
          return ret;
        }

      if (test_dirpath(testpath))
        {
          free(testpath);
          ret = asprintf(&testpath, "%s%csrc", boardpath, g_delim);
          if (ret < 0)
            {
              fprintf(stderr,
                      "ERROR: asprintf() failed to archpath\n");
              return ret;
            }

          if (test_dirpath(testpath))
            {
              /* Enumerate the board configurations */

              foreach_dirent(boardpath, enum_board_configurations,
                             entry->d_name);
            }
        }
    }

  free(testpath);
  free(boardpath);
  return 0;
}

/****************************************************************************
 * Name: list_select
 *
 * Description:
 *   Select one value from a list.
 *
 ****************************************************************************/

char *list_select(char **list, unsigned nitems)
{
  char ch;
  int ndx;
  int i;

  /* Show the list */

  for (i = 0, ch = '1'; i < nitems; i++)
    {
      printf("  %c. %s\n", ch, list[i]);
      if (ch == '9')
        {
          ch = 'a';
        }
      else if (ch == 'z')
        {
          ch = 'A';
        }
      else
        {
          ch++;
        }
    }

  for (; ; )
    {
      bool input = false;

      printf("Enter [1");
      if (nitems > 1)
        {
          printf("-%c", nitems >= 9 ? '9' : '0' + nitems);
          if (nitems > 9)
            {
              printf(",a");
              if (nitems > 10)
                {
                  printf("-%c", 'a' + nitems - 10);
                  if (nitems > 35)
                    {
                      printf(",A");
                      if (nitems > 36)
                        {
                          printf("-%c", 'A' + nitems - 36);
                        }
                    }
                }
            }
        }

      printf("]: ");

      do
        {
          ch = getchar();
          if (ch >= '1' && ch <= '9')
            {
              ndx = ch - '1';
            }
          else if (ch >= 'a' && ch <= 'z')
            {
              ndx = ch - 'a' + 9;
            }
          else if (ch >= 'A' && ch <= 'Z')
            {
              ndx = ch - 'A' + 35;
            }
          else if (ch == '\n')
            {
              continue;
            }
          else
            {
              printf("Invalid selection: %c -- Try again\n", ch);
              input = true;
              continue;
            }

          if (ndx < nitems)
            {
              return list[ndx];
            }
          else
            {
              printf("Invalid selection: %c -- Try again\n", ch);
              input = true;
            }
        }
      while (!input);
    }
}

/****************************************************************************
 * Name: create_config
 *
 * Description:
 *   Generate a bogus .config file.  There is only sufficient information
 *   in this bogus .config to estable the correct symbolic links.
 *
 ****************************************************************************/

static void create_config(void)
{
  FILE *stream;

  stream = fopen(".config", "w");
  if (!stream)
    {
       fprintf(stderr, "ERROR: failed to open .config for writing: %s\n",
               strerror(errno));
       exit(EXIT_FAILURE);
    }

  fprintf(stream, "CONFIG_ARCH=\"%s\"\n", g_selected_arch);
  if (g_selected_family != NULL)
    {
      fprintf(stream, "CONFIG_ARCH_FAMILY=%s\n", g_selected_family);
    }

  fprintf(stream, "CONFIG_ARCH_CHIP=\"%s\"\n", g_selected_mcu);
  fprintf(stream, "CONFIG_ARCH_BOARD=\"%s\"\n", g_selected_board);

  fclose(stream);
}

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

/****************************************************************************
 * Name: main
 *
 * Description:
 *   Program entry point.
 *
 ****************************************************************************/

int main(int argc, char **argv)
{
  int ret;
  char *archpath;

  /* Enumerate all of the architectures */

  g_narch = 0;
  foreach_dirent(g_archdir, enum_architectures, NULL);

  /* Select an architecture */

  printf("Select an architecture:\n");
  g_selected_arch = list_select(g_arch, g_narch);

  /* Enumerate the MCUs for the selected architecture */

  g_nmcu = 0;
  ret = asprintf(&archpath, "%s%c%s%csrc",
                 g_archdir, g_delim, g_selected_arch, g_delim);
  if (ret < 0)
    {
      fprintf(stderr,
              "ERROR: asprintf() failed to archpath/src\n");
      return ret;
    }

  foreach_dirent(archpath, enum_mcus, NULL);

  /* Select an MCU */

  printf("Select an MCU for architecture=%s:\n", g_selected_arch);
  g_selected_mcu = list_select(g_mcu, g_nmcu);

  /* Enumerate the boards for the selected MCU */

  g_nboard = 0;
  foreach_dirent(g_configdir, enum_boards, NULL);

  /* Select an board */

  printf("Select a board for MCU=%s:\n", g_selected_mcu);
  g_selected_board = list_select(g_board, g_nboard);

  /* Then output a bogus .config file with enough information to establish
   * the correct symbolic links
   */

  create_config();
  return 0;
}