/*
 * "lpq" command for CUPS.
 *
 * Copyright © 2021-2022 by OpenPrinting.
 * Copyright © 2007-2018 by Apple Inc.
 * Copyright © 1997-2006 by Easy Software Products.
 *
 * Licensed under Apache License v2.0.  See the file "LICENSE" for more
 * information.
 */

/*
 * Include necessary headers...
 */

#include <cups/cups-private.h>


/*
 * Local functions...
 */

static http_t	*connect_server(const char *, http_t *);
static int	show_jobs(const char *, http_t *, const char *,
		          const char *, const int, const int);
static void	show_printer(const char *, http_t *, const char *);
static void	usage(void) _CUPS_NORETURN;


/*
 * 'main()' - Parse options and commands.
 */

int
main(int  argc,				/* I - Number of command-line arguments */
     char *argv[])			/* I - Command-line arguments */
{
  int		i;			/* Looping var */
  http_t	*http;			/* Connection to server */
  const char	*opt,			/* Option pointer */
		*dest,			/* Desired printer */
		*user,			/* Desired user */
		*val;			/* Environment variable name */
  char		*instance;		/* Printer instance */
  int		id,			/* Desired job ID */
		all,			/* All printers */
		interval,		/* Reporting interval */
		longstatus;		/* Show file details */
  cups_dest_t	*named_dest;		/* Named destination */


  _cupsSetLocale(argv);

 /*
  * Check for command-line options...
  */

  http       = NULL;
  dest       = NULL;
  user       = NULL;
  id         = 0;
  interval   = 0;
  longstatus = 0;
  all        = 0;

  for (i = 1; i < argc; i ++)
  {
    if (argv[i][0] == '+')
    {
      interval = atoi(argv[i] + 1);
    }
    else if (!strcmp(argv[i], "--help"))
      usage();
    else if (argv[i][0] == '-')
    {
      for (opt = argv[i] + 1; *opt; opt ++)
      {
	switch (*opt)
	{
	  case 'E' : /* Encrypt */
#ifdef HAVE_TLS
	      cupsSetEncryption(HTTP_ENCRYPT_REQUIRED);

	      if (http)
		httpEncryption(http, HTTP_ENCRYPT_REQUIRED);
#else
	      _cupsLangPrintf(stderr, _("%s: Sorry, no encryption support."), argv[0]);
#endif /* HAVE_TLS */
	      break;

	  case 'U' : /* Username */
	      if (opt[1] != '\0')
	      {
		cupsSetUser(opt + 1);
		opt += strlen(opt) - 1;
	      }
	      else
	      {
		i ++;
		if (i >= argc)
		{
		  _cupsLangPrintf(stderr, _("%s: Error - expected username after \"-U\" option."), argv[0]);
		  return (1);
		}

		cupsSetUser(argv[i]);
	      }
	      break;

	  case 'P' : /* Printer */
	      if (opt[1] != '\0')
	      {
		dest = opt + 1;
		opt += strlen(opt) - 1;
	      }
	      else
	      {
		i ++;

		if (i >= argc)
		{
		  httpClose(http);

		  usage();
		}

		dest = argv[i];
	      }

	      if ((instance = strchr(dest, '/')) != NULL)
		*instance++ = '\0';

	      http = connect_server(argv[0], http);

	      if ((named_dest = cupsGetNamedDest(http, dest, instance)) == NULL)
	      {
		if (cupsLastError() == IPP_STATUS_ERROR_BAD_REQUEST ||
		    cupsLastError() == IPP_STATUS_ERROR_VERSION_NOT_SUPPORTED)
		  _cupsLangPrintf(stderr, _("%s: Error - add '/version=1.1' to server name."), argv[0]);
		else if (instance)
		  _cupsLangPrintf(stderr, _("%s: Error - unknown destination \"%s/%s\"."), argv[0], dest, instance);
		else
		  _cupsLangPrintf(stderr, _("%s: Unknown destination \"%s\"."), argv[0], dest);

		return (1);
	      }

	      cupsFreeDests(1, named_dest);
	      break;

	  case 'a' : /* All printers */
	      all = 1;
	      break;

	  case 'h' : /* Connect to host */
	      if (http)
	      {
		httpClose(http);
		http = NULL;
	      }

	      if (opt[1] != '\0')
	      {
		cupsSetServer(opt + 1);
		opt += strlen(opt) - 1;
	      }
	      else
	      {
		i ++;

		if (i >= argc)
		{
		  _cupsLangPrintf(stderr, _("%s: Error - expected hostname after \"-h\" option."), argv[0]);
		  return (1);
		}
		else
		  cupsSetServer(argv[i]);
	      }
	      break;

	  case 'l' : /* Long status */
	      longstatus = 1;
	      break;

	  default :
	      httpClose(http);

	      usage();
	}
      }
    }
    else if (isdigit(argv[i][0] & 255))
    {
      id = atoi(argv[i]);
    }
    else
    {
      user = argv[i];
    }
  }

  http = connect_server(argv[0], http);

  if (dest == NULL && !all)
  {
    if ((named_dest = cupsGetNamedDest(http, NULL, NULL)) == NULL)
    {
      if (cupsLastError() == IPP_STATUS_ERROR_BAD_REQUEST ||
          cupsLastError() == IPP_STATUS_ERROR_VERSION_NOT_SUPPORTED)
      {
	_cupsLangPrintf(stderr,
	                _("%s: Error - add '/version=1.1' to server name."),
			argv[0]);
        return (1);
      }

      val = NULL;

      if ((dest = getenv("LPDEST")) == NULL)
      {
	if ((dest = getenv("PRINTER")) != NULL)
	{
          if (!strcmp(dest, "lp"))
            dest = NULL;
	  else
	    val = "PRINTER";
	}
      }
      else
	val = "LPDEST";

      if (dest && val)
	_cupsLangPrintf(stderr,
	                _("%s: Error - %s environment variable names "
			  "non-existent destination \"%s\"."), argv[0], val,
			dest);
      else
	_cupsLangPrintf(stderr,
	                _("%s: Error - no default destination available."),
			argv[0]);
      httpClose(http);
      return (1);
    }

    dest = named_dest->name;
  }

 /*
  * Show the status in a loop...
  */

  for (;;)
  {
    if (dest)
      show_printer(argv[0], http, dest);

    i = show_jobs(argv[0], http, dest, user, id, longstatus);

    if (i && interval)
    {
      fflush(stdout);
      sleep((unsigned)interval);
    }
    else
      break;
  }

 /*
  * Close the connection to the server and return...
  */

  httpClose(http);

  return (0);
}


/*
 * 'connect_server()' - Connect to the server as necessary...
 */

static http_t *				/* O - New HTTP connection */
connect_server(const char *command,	/* I - Command name */
               http_t     *http)	/* I - Current HTTP connection */
{
  if (!http)
  {
    http = httpConnectEncrypt(cupsServer(), ippPort(),
	                      cupsEncryption());

    if (http == NULL)
    {
      _cupsLangPrintf(stderr, _("%s: Unable to connect to server."), command);
      exit(1);
    }
  }

  return (http);
}


/*
 * 'show_jobs()' - Show jobs.
 */

static int				/* O - Number of jobs in queue */
show_jobs(const char *command,		/* I - Command name */
          http_t     *http,		/* I - HTTP connection to server */
          const char *dest,		/* I - Destination */
	  const char *user,		/* I - User */
	  const int  id,		/* I - Job ID */
	  const int  longstatus)	/* I - 1 if long report desired */
{
  ipp_t		*request,		/* IPP Request */
		*response;		/* IPP Response */
  ipp_attribute_t *attr;		/* Current attribute */
  const char	*jobdest,		/* Pointer into job-printer-uri */
		*jobuser,		/* Pointer to job-originating-user-name */
		*jobname;		/* Pointer to job-name */
  ipp_jstate_t	jobstate;		/* job-state */
  int		jobid,			/* job-id */
		jobsize,		/* job-k-octets */
		jobcount,		/* Number of jobs */
		jobcopies,		/* Number of copies */
		rank;			/* Rank of job */
  char		resource[1024];		/* Resource string */
  char		rankstr[255];		/* Rank string */
  char		namestr[1024];		/* Job name string */
  static const char * const jobattrs[] =/* Job attributes we want to see */
		{
		  "copies",
		  "job-id",
		  "job-k-octets",
		  "job-name",
		  "job-originating-user-name",
		  "job-printer-uri",
		  "job-priority",
		  "job-state"
		};
  static const char * const ranks[10] =	/* Ranking strings */
		{
		  "th",
		  "st",
		  "nd",
		  "rd",
		  "th",
		  "th",
		  "th",
		  "th",
		  "th",
		  "th"
		};


  if (http == NULL)
    return (0);

 /*
  * Build an IPP_GET_JOBS or IPP_GET_JOB_ATTRIBUTES request, which requires
  * the following attributes:
  *
  *    attributes-charset
  *    attributes-natural-language
  *    job-uri or printer-uri
  *    requested-attributes
  *    requesting-user-name
  */

  request = ippNewRequest(id ? IPP_GET_JOB_ATTRIBUTES : IPP_GET_JOBS);

  if (id)
  {
    snprintf(resource, sizeof(resource), "ipp://localhost/jobs/%d", id);
    ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "job-uri",
                 NULL, resource);
  }
  else if (dest)
  {
    httpAssembleURIf(HTTP_URI_CODING_ALL, resource, sizeof(resource), "ipp",
                     NULL, "localhost", 0, "/printers/%s", dest);

    ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
                 NULL, resource);
  }
  else
    ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
                 NULL, "ipp://localhost/");

  if (user)
  {
    ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME,
                 "requesting-user-name", NULL, user);
    ippAddBoolean(request, IPP_TAG_OPERATION, "my-jobs", 1);
  }
  else
    ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME,
                 "requesting-user-name", NULL, cupsUser());

  ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
                "requested-attributes",
                (int)(sizeof(jobattrs) / sizeof(jobattrs[0])), NULL, jobattrs);

 /*
  * Do the request and get back a response...
  */

  jobcount = 0;

  if ((response = cupsDoRequest(http, request, "/")) != NULL)
  {
    if (response->request.status.status_code > IPP_OK_CONFLICT)
    {
      _cupsLangPrintf(stderr, "%s: %s", command, cupsLastErrorString());
      ippDelete(response);
      return (0);
    }

    rank = 1;

   /*
    * Loop through the job list and display them...
    */

    for (attr = response->attrs; attr != NULL; attr = attr->next)
    {
     /*
      * Skip leading attributes until we hit a job...
      */

      while (attr != NULL && attr->group_tag != IPP_TAG_JOB)
        attr = attr->next;

      if (attr == NULL)
        break;

     /*
      * Pull the needed attributes from this job...
      */

      jobid       = 0;
      jobsize     = 0;
      jobstate    = IPP_JOB_PENDING;
      jobname     = "unknown";
      jobuser     = "unknown";
      jobdest     = NULL;
      jobcopies   = 1;

      while (attr != NULL && attr->group_tag == IPP_TAG_JOB)
      {
        if (!strcmp(attr->name, "job-id") &&
	    attr->value_tag == IPP_TAG_INTEGER)
	  jobid = attr->values[0].integer;

        if (!strcmp(attr->name, "job-k-octets") &&
	    attr->value_tag == IPP_TAG_INTEGER)
	  jobsize = attr->values[0].integer;

        if (!strcmp(attr->name, "job-state") &&
	    attr->value_tag == IPP_TAG_ENUM)
	  jobstate = (ipp_jstate_t)attr->values[0].integer;

        if (!strcmp(attr->name, "job-printer-uri") &&
	    attr->value_tag == IPP_TAG_URI)
	  if ((jobdest = strrchr(attr->values[0].string.text, '/')) != NULL)
	    jobdest ++;

        if (!strcmp(attr->name, "job-originating-user-name") &&
	    attr->value_tag == IPP_TAG_NAME)
	  jobuser = attr->values[0].string.text;

        if (!strcmp(attr->name, "job-name") &&
	    attr->value_tag == IPP_TAG_NAME)
	  jobname = attr->values[0].string.text;

        if (!strcmp(attr->name, "copies") &&
	    attr->value_tag == IPP_TAG_INTEGER)
	  jobcopies = attr->values[0].integer;

        attr = attr->next;
      }

     /*
      * See if we have everything needed...
      */

      if (jobdest == NULL || jobid == 0)
      {
        if (attr == NULL)
	  break;
	else
          continue;
      }

      if (!longstatus && jobcount == 0)
	_cupsLangPuts(stdout,
	              _("Rank    Owner   Job     File(s)"
		        "                         Total Size"));

      jobcount ++;

     /*
      * Display the job...
      */

      if (jobstate == IPP_JOB_PROCESSING)
	strlcpy(rankstr, "active", sizeof(rankstr));
      else
      {
       /*
        * Make the rank show the "correct" suffix for each number
	* (11-13 are the only special cases, for English anyways...)
	*/

	if ((rank % 100) >= 11 && (rank % 100) <= 13)
	  snprintf(rankstr, sizeof(rankstr), "%dth", rank);
	else
	  snprintf(rankstr, sizeof(rankstr), "%d%s", rank, ranks[rank % 10]);

	rank ++;
      }

      if (longstatus)
      {
        _cupsLangPuts(stdout, "\n");

        if (jobcopies > 1)
	  snprintf(namestr, sizeof(namestr), "%d copies of %s", jobcopies,
	           jobname);
	else
	  strlcpy(namestr, jobname, sizeof(namestr));

        _cupsLangPrintf(stdout, _("%s: %-33.33s [job %d localhost]"),
	                jobuser, rankstr, jobid);
        _cupsLangPrintf(stdout, _("        %-39.39s %.0f bytes"),
	                namestr, 1024.0 * jobsize);
      }
      else
        _cupsLangPrintf(stdout,
	                _("%-7s %-7.7s %-7d %-31.31s %.0f bytes"),
			rankstr, jobuser, jobid, jobname, 1024.0 * jobsize);

      if (attr == NULL)
        break;
    }

    ippDelete(response);
  }
  else
  {
    _cupsLangPrintf(stderr, "%s: %s", command, cupsLastErrorString());
    return (0);
  }

  if (jobcount == 0)
    _cupsLangPuts(stdout, _("no entries"));

  return (jobcount);
}


/*
 * 'show_printer()' - Show printer status.
 */

static void
show_printer(const char *command,	/* I - Command name */
             http_t     *http,		/* I - HTTP connection to server */
             const char *dest)		/* I - Destination */
{
  ipp_t		*request,		/* IPP Request */
		*response;		/* IPP Response */
  ipp_attribute_t *attr;		/* Current attribute */
  ipp_pstate_t	state;			/* Printer state */
  char		uri[HTTP_MAX_URI];	/* Printer URI */


  if (http == NULL)
    return;

 /*
  * Build an IPP_GET_PRINTER_ATTRIBUTES request, which requires the following
  * attributes:
  *
  *    attributes-charset
  *    attributes-natural-language
  *    printer-uri
  */

  request = ippNewRequest(IPP_GET_PRINTER_ATTRIBUTES);

  httpAssembleURIf(HTTP_URI_CODING_ALL, uri, sizeof(uri), "ipp", NULL,
                   "localhost", 0, "/printers/%s", dest);
  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI,
               "printer-uri", NULL, uri);

 /*
  * Do the request and get back a response...
  */

  if ((response = cupsDoRequest(http, request, "/")) != NULL)
  {
    if (response->request.status.status_code > IPP_OK_CONFLICT)
    {
      _cupsLangPrintf(stderr, "%s: %s", command, cupsLastErrorString());
      ippDelete(response);
      return;
    }

    if ((attr = ippFindAttribute(response, "printer-state", IPP_TAG_ENUM)) != NULL)
      state = (ipp_pstate_t)attr->values[0].integer;
    else
      state = IPP_PRINTER_STOPPED;

    switch (state)
    {
      case IPP_PRINTER_IDLE :
          _cupsLangPrintf(stdout, _("%s is ready"), dest);
	  break;
      case IPP_PRINTER_PROCESSING :
          _cupsLangPrintf(stdout, _("%s is ready and printing"),
	                  dest);
	  break;
      case IPP_PRINTER_STOPPED :
          _cupsLangPrintf(stdout, _("%s is not ready"), dest);
	  break;
    }

    ippDelete(response);
  }
  else
    _cupsLangPrintf(stderr, "%s: %s", command, cupsLastErrorString());
}


/*
 * 'usage()' - Show program usage.
 */

static void
usage(void)
{
  _cupsLangPuts(stderr, _("Usage: lpq [options] [+interval]"));
  _cupsLangPuts(stdout, _("Options:"));
  _cupsLangPuts(stdout, _("-a                      Show jobs on all destinations"));
  _cupsLangPuts(stdout, _("-E                      Encrypt the connection to the server"));
  _cupsLangPuts(stdout, _("-h server[:port]        Connect to the named server and port"));
  _cupsLangPuts(stdout, _("-l                      Show verbose (long) output"));
  _cupsLangPuts(stdout, _("-P destination          Show status for the specified destination"));
  _cupsLangPuts(stdout, _("-U username             Specify the username to use for authentication"));

  exit(1);
}
