/*
 * pnmin.c
 * PNM file input.
 *
 * Copyright (C) 2002-2025 Cosmin Truta.
 *
 * Use, modification and distribution are subject
 * to the Boost Software License, Version 1.0.
 * See the accompanying file LICENSE_BSL_1_0.txt
 * or visit https://www.boost.org/LICENSE_1_0.txt
 *
 * SPDX-License-Identifier: BSL-1.0
 */


#include "pnmio.h"

#include <errno.h>
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>


/*
 * Checks if the character is a space: ' ', '\t', '\n' or '\r'.
 */
#define pnm_is_space(ch) \
   ((ch) == ' ' || (ch) == '\t' || (ch) == '\n' || (ch) == '\r')


/*
 * Checks if the character is a digit: '0' .. '9'.
 */
#define pnm_is_digit(ch) \
   ((ch) >= '0' && (ch) <= '9')


/*
 * Reads a character from a file stream.
 * Comment sequences starting with '#' are skipped until the end of line.
 * End of line sequences (LF, CR, CR+LF) are translated to '\n'.
 * Returns the character read, or EOF on input failure.
 */
static int pnm_fget_char(FILE *stream)
{
   int ch = getc(stream);

   /* Skip the comments. */
   if (ch == '#')
   {
      do
      {
         ch = getc(stream);
      } while (ch != EOF && ch != '\n' && ch != '\r');
   }

   /* Translate the line endings. */
   if (ch == '\r')
   {
      ch = getc(stream);
      if (ch != '\n')
      {
         ungetc(ch, stream);
         ch = '\n';
      }
   }

   return ch;
}


/*
 * Reads (scans) an unsigned integer from a file stream.
 * Returns 1 on success, 0 on matching failure, or EOF on input failure.
 */
static int pnm_fscan_uint(FILE *stream, unsigned int *value)
{
   int ch;
   unsigned int tmp;

   /* Skip the leading whitespaces. */
   do
   {
      ch = pnm_fget_char(stream);
   } while (pnm_is_space(ch));
   if (ch == EOF)
   {
      /* Input failure. */
      return EOF;
   }
   if (!pnm_is_digit(ch))
   {
      /* Matching failure. */
      ungetc(ch, stream);
      return 0;
   }

   /* Read the value. */
   *value = 0;
   do
   {
      tmp = *value * 10 + (ch - '0');
      if (tmp >= *value)
         *value = tmp;
      else
      {
         *value = UINT_MAX;
         errno = ERANGE;
      }
      ch = getc(stream);
   } while (pnm_is_digit(ch));

   /* Put back the trailing non-whitespace, if any. */
   if (!pnm_is_space(ch))
      ungetc(ch, stream);

   return 1;
}


/*
 * Reads a PNM header structure from a file stream and validates it.
 * Returns 1 on success, 0 on validation failure,
 * or -1 on input or matching failure.
 * Reading PAM ("P7") headers is not currently implemented.
 */
int pnm_fget_header(pnm_struct *pnm_ptr, FILE *stream)
{
   unsigned int format;
   int ch;

   /* Clear the PNM info structure. */
   memset(pnm_ptr, 0, sizeof(pnm_struct));

   /* Read the PNM file signature. */
   ch = getc(stream);
   if (ch == EOF)
   {
      /* Input failure. */
      return -1;
   }

   /* Treat any subsequent failure as a matching failure. */
   if (ch != 'P')
      return -1;
   ch = getc(stream);
   if (ch < '1' || ch > '9')
      return -1;
   format = (unsigned int)(ch - '0');

   /* Read the remaining text using pnm_fget_char instead of getc. */
   ch = pnm_fget_char(stream);
   if (!pnm_is_space(ch))
      return -1;

   /* Read the header. */
   pnm_ptr->format = format;
   if (format >= PNM_P1 && format <= PNM_P6)
   {
      pnm_ptr->depth = (format == PNM_P3 || format == PNM_P6) ? 3 : 1;
      if (pnm_fscan_uint(stream, &pnm_ptr->width) != 1
          || pnm_fscan_uint(stream, &pnm_ptr->height) != 1)
         return -1;
      if (format == PNM_P1 || format == PNM_P4)
         pnm_ptr->maxval = 1;
      else
      {
         if (pnm_fscan_uint(stream, &pnm_ptr->maxval) != 1)
            return -1;
      }
      return pnm_is_valid(pnm_ptr) ? 1 : 0;
   }
   else
   {
      /* TODO: if (format == PNM_P7) ... */
      return -1;
   }
}


/*
 * Reads an array of PNM sample values from a file stream.
 * The values are expected to be in the format specified by pnm_ptr->format.
 * The array length is pnm_ptr->depth * pnm_ptr->width * num_rows.
 * The validity check performed on the PNM structure is only partial.
 * Returns 1 on success, 0 on validation failure,
 * or -1 on input or matching failure.
 * If reading is incomplete, the remaining sample values are set to 0.
 */
int pnm_fget_values(const pnm_struct *pnm_ptr,
                    unsigned int *sample_values,
                    unsigned int num_rows,
                    FILE *stream)
{
   unsigned int format = pnm_ptr->format;
   unsigned int depth = pnm_ptr->depth;
   unsigned int width = pnm_ptr->width;
   unsigned int maxval = pnm_ptr->maxval;
   size_t row_length = (size_t)depth * (size_t)width;
   size_t num_samples = num_rows * row_length;
   int ch, ch8, ch16, ch24;
   int mask;
   size_t i, j;

   /* Read the sample values. */
   switch (format)
   {
   case PNM_P1:
      for (i = 0; i < num_samples; ++i)
      {
         do
         {
            ch = pnm_fget_char(stream);
         } while (pnm_is_space(ch));
         if (ch != '0' && ch != '1')
         {
            ungetc(ch, stream);
            break;
         }
         sample_values[i] = (ch == '0') ? 1 : 0;
      }
      break;
   case PNM_P2:
   case PNM_P3:
      for (i = 0; i < num_samples; ++i)
      {
         if (pnm_fscan_uint(stream, &sample_values[i]) != 1)
            break;
      }
      break;
   case PNM_P4:
      for (i = j = 0; i < num_samples;)
      {
         ch = getc(stream);
         if (ch == EOF)
            break;
         for (mask = 0x80; mask != 0; mask >>= 1)
         {
            sample_values[i++] = (ch & mask) ? 0 : 1;
            if (++j == row_length)
            {
               j = 0;
               break;
            }
         }
      }
      break;
   case PNM_P5:
   case PNM_P6:
   case PNM_P7:
      if (maxval <= 0xffU)
      {
         /* Read 1 byte per sample. */
         for (i = 0; i < num_samples; ++i)
         {
            ch = getc(stream);
            if (ch == EOF)
               break;
            sample_values[i] = (unsigned int)ch;
         }
      }
      else if (maxval <= 0xffffU)
      {
         /* Read 2 bytes per sample. */
         for (i = 0; i < num_samples; ++i)
         {
            ch8 = getc(stream);
            ch = getc(stream);
            if (ch == EOF)
               break;
            sample_values[i] = ((unsigned int)ch8 << 8) + (unsigned int)ch;
         }
      }
#if UINT_MAX > PNM_VALUE_MAX
      else if (maxval > PNM_VALUE_MAX)
      {
         errno = EINVAL;
         return 0;
      }
#endif
      else
      {
         /* Read 3 or 4 bytes per sample. */
         ch24 = 0;
         for (i = 0; i < num_samples; ++i)
         {
            if (maxval > 0xffffffU)
               ch24 = getc(stream);
            ch16 = getc(stream);
            ch8 = getc(stream);
            ch = getc(stream);
            if (ch == EOF)
               break;
            sample_values[i] = ((unsigned int)ch24 << 24)
                               + ((unsigned int)ch16 << 16)
                               + ((unsigned int)ch8 << 8)
                               + ((unsigned int)ch);
         }
      }
      break;
   default:
      errno = EINVAL;
      return 0;
   }

   /* Check the result. */
   if (i < num_samples)
   {
      memset(sample_values + i, 0, (num_samples - i) * sizeof(unsigned int));
      return -1;
   }
   return 1;
}


/*
 * Reads an array of sample bytes from a raw PNM file stream.
 * Multi-byte samples are stored in network order, as in the PNM stream.
 * The byte count is sample_size * pnm_ptr->depth * pnm_ptr->width * num_rows.
 * The validity check performed on the PNM structure is only partial.
 * Returns 1 on success, 0 on validation failure, or -1 on input failure.
 * If reading is incomplete, the remaining sample bytes are set to 0.
 */
int pnm_fget_bytes(const pnm_struct *pnm_ptr,
                   unsigned char *sample_bytes,
                   size_t sample_size,
                   unsigned int num_rows,
                   FILE *stream)
{
   unsigned int format = pnm_ptr->format;
   unsigned int depth = pnm_ptr->depth;
   unsigned int width = pnm_ptr->width;
   unsigned int maxval = pnm_ptr->maxval;
   size_t row_length = (size_t)depth * (size_t)width;
   size_t num_samples = num_rows * row_length;
   size_t raw_sample_size;
   int ch, mask;
   size_t i, j;

   /* Validate the given sample size. */
   if (maxval <= 0xffU)
      raw_sample_size = 1;
   else if (maxval <= 0xffffU)
      raw_sample_size = 2;
   else if (maxval <= 0xffffffU)
      raw_sample_size = 3;
#if UINT_MAX > PNM_VALUE_MAX
   else if (maxval > PNM_VALUE_MAX)
      raw_sample_size = !sample_size;
#endif
   else
      raw_sample_size = 4;
   if (raw_sample_size != sample_size)
   {
      errno = EINVAL;
      return 0;
   }

   /* Read the raw sample bytes. */
   switch (format)
   {
   case PNM_P4:
      for (i = j = 0; i < num_samples;)
      {
         ch = getc(stream);
         if (ch == EOF)
            break;
         for (mask = 0x80; mask != 0; mask >>= 1)
         {
            sample_bytes[i++] = (unsigned char)((ch & mask) ? 0 : 1);
            if (++j == row_length)
            {
               j = 0;
               break;
            }
         }
      }
      break;
   case PNM_P5:
   case PNM_P6:
   case PNM_P7:
      i = fread(sample_bytes, sample_size, num_samples, stream);
      break;
   default:
      errno = EINVAL;
      return 0;
   }

   /* Check the result. */
   if (i < num_samples)
   {
      memset(sample_bytes + i, 0, sample_size * num_samples - i);
      return -1;
   }
   return 1;
}
