/** \file
 * \brief File Access - Buffer Management
 *
 * See Copyright Notice in im_lib.h
 * $Id: im_filebuffer.cpp,v 1.2 2009/08/13 22:34:25 scuri Exp $
 */

#include <stdlib.h>
#include <string.h>
#include <assert.h>

#include "im.h"
#include "im_format.h"
#include "im_util.h"
#include "im_complex.h"
#include "im_color.h"


int imFileLineSizeAligned(int width, int bpp, int align)
{
  if (align == 4)
    return ((width * bpp + 31) / 32) * 4;
  else if (align == 2)
    return ((width * bpp + 15) / 16) * 2;
  else
    return (width * bpp + 7) / 8;
}

template <class T> 
static void iDoFillLineBuffer(int width, int height, int line, int plane,  
                              int file_color_mode, T* line_buffer, 
                              int user_color_mode, const T* data)
{
  // (writing) from data to file
  // will handle packing and alpha

  int file_depth = imColorModeDepth(file_color_mode);  
  int data_depth = imColorModeDepth(user_color_mode);
  int data_plane_size = width*height;  // This will be used in UNpacked data

  if (imColorModeIsPacked(user_color_mode))
    data += line*width*data_depth;
  else
    data += line*width;

  for (int x = 0; x < width; x++)
  {
    int x_data_offset = x*data_depth;    // This will be used in packed data

    if (imColorModeIsPacked(file_color_mode))
    {
      int x_file_offset = x*file_depth;  // This will be used in packed data

      // file is packed
      // NO color space conversion, color_space must match
      // Ignore alpha if necessary.
      int depth = IM_MIN(file_depth, data_depth);      
      for (int d = 0; d < depth; d++)
      {
        if (imColorModeIsPacked(user_color_mode))
          line_buffer[x_file_offset + d] = data[x_data_offset + d];
        else
          line_buffer[x_file_offset + d] = data[d*data_plane_size + x];
      }
    }
    else
    {
      // file NOT packed, copy just one plane
      // NO color space conversion, color_space must match

      if (plane >= imColorModeDepth(user_color_mode))
        return;

      if (imColorModeIsPacked(user_color_mode))
        line_buffer[x] = data[x_data_offset + plane];
      else
        line_buffer[x] = data[plane*data_plane_size + x];
    }
  }
}

template <class T> 
static void iDoFillData(int width, int height, int line, int plane,  
                              int file_color_mode, const T* line_buffer, 
                              int user_color_mode, T* data)
{
  // (reading) from file to data
  // will handle packing and alpha

  int file_depth = imColorModeDepth(file_color_mode);
  int data_depth = imColorModeDepth(user_color_mode);
  int data_plane_size = width*height;  // This will be used in UNpacked data

  if (imColorModeIsPacked(user_color_mode))
    data += line*width*data_depth;
  else
    data += line*width;

  for (int x = 0; x < width; x++)
  {
    int x_data_offset = x*data_depth;    // This will be used in packed data

    if (imColorModeIsPacked(file_color_mode))
    {
      int x_file_offset = x*file_depth;  // This will be used in packed data

      // file is packed
      // NO color space conversion, color_space must match
      // ignore alpha if necessary.
      int depth = IM_MIN(file_depth, data_depth);      
      for (int d = 0; d < depth; d++)
      {
        if (imColorModeIsPacked(user_color_mode))
          data[x_data_offset + d] = line_buffer[x_file_offset + d];
        else
          data[d*data_plane_size + x] = line_buffer[x_file_offset + d];
      }
    }
    else
    {
      // file NOT packed, copy just one plane
      // NO color space conversion, color_space must match

      if (plane >= imColorModeDepth(user_color_mode))
        return;

      if (imColorModeIsPacked(user_color_mode))
        data[x_data_offset + plane] = line_buffer[x];
      else
        data[plane*data_plane_size + x] = line_buffer[x];
    }
  }
}

template <class T> 
static inline void iConvertColor2RGB(T* data, int color_space, int data_type)
{
  T zero, max = (T)imColorMax(data_type);

  // These are identical procedures to iDoConvert2RGB in "im_filebuffer.cpp".

  switch (color_space)
  {
  case IM_XYZ: 
    {
      // to increase precision do intermediate conversions in float

      // scale to 0-1
      float c0 = imColorReconstruct(data[0], max);
      float c1 = imColorReconstruct(data[1], max);
      float c2 = imColorReconstruct(data[2], max);

      // result is still 0-1
      imColorXYZ2RGB(c0, c1, c2, 
                     c0, c1, c2, 1.0f);

      // do gamma correction then scale back to 0-max
      data[0] = imColorQuantize(imColorTransfer2Nonlinear(c0), max);
      data[1] = imColorQuantize(imColorTransfer2Nonlinear(c1), max);
      data[2] = imColorQuantize(imColorTransfer2Nonlinear(c2), max);
    }
    break;
  case IM_YCBCR: 
    zero = (T)imColorZero(data_type);
    imColorYCbCr2RGB(data[0], data[1], data[2], 
                      data[0], data[1], data[2], zero, max);
    break;
  case IM_CMYK: 
    imColorCMYK2RGB(data[0], data[1], data[2], data[3], 
                    data[0], data[1], data[2], max);
    break;
  case IM_LUV:
  case IM_LAB:
    {
      // to increase precision do intermediate conversions in float
      // scale to 0-1 and -0.5/+0.5
      float c0 = imColorReconstruct(data[0], max);
      float c1 = imColorReconstruct(data[1], max) - 0.5f;
      float c2 = imColorReconstruct(data[2], max) - 0.5f;

      if (color_space == IM_LUV)
        imColorLuv2XYZ(c0, c1, c2,  // conversion in-place
                       c0, c1, c2);
      else
        imColorLab2XYZ(c0, c1, c2,  // conversion in-place
                       c0, c1, c2);

      imColorXYZ2RGB(c0, c1, c2,    // conversion in-place
                     c0, c1, c2, 1.0f);

      // do gamma correction then scale back to 0-max
      data[0] = imColorQuantize(imColorTransfer2Nonlinear(c0), max);
      data[1] = imColorQuantize(imColorTransfer2Nonlinear(c1), max);
      data[2] = imColorQuantize(imColorTransfer2Nonlinear(c2), max);
    }
    break;
  }
}

// These functions will be always converting RGB -> RGB  (0-max) -> (0-255)

static inline imbyte iConvertType2Byte(const imbyte& data)
  { return data; }

static inline imbyte iConvertType2Byte(const imushort& data)
  { return imColorQuantize(imColorReconstruct(data, (imushort)65535), (imbyte)255); }

static inline imbyte iConvertType2Byte(const int& data)
  { return imColorQuantize(imColorReconstruct(data, 16777215), (imbyte)255); }

static inline imbyte iConvertType2Byte(const float& data)
  { return imColorQuantize(data, (imbyte)255); }

// Fake float to avoid erros in the color conversion template rotines.
// Since the color conversion use the double value, they are invalid,
// so the automatic conversion to bitmap for complex images works only for RGB.
static inline imbyte iConvertType2Byte(const double& data)
{ 
  imcfloat* fdata = (imcfloat*)&data;
  return imColorQuantize(cpxmag(*fdata), (imbyte)255); 
}

template <class T> 
static void iDoFillDataBitmap(int width, int height, int line, int plane, int data_type,
                              int file_color_mode, const T* line_buffer, 
                              int user_color_mode, imbyte* data)
{
  // (reading) from file to data
  // will handle packing, alpha, color space conversion to RGB and data_type to BYTE

  int file_depth = imColorModeDepth(file_color_mode);
  int data_depth = imColorModeDepth(user_color_mode);
  int copy_alpha = imColorModeHasAlpha(file_color_mode) && imColorModeHasAlpha(user_color_mode);
  int data_plane_size = width*height;  // This will be used in UNpacked data

  if (imColorModeIsPacked(user_color_mode))
    data += line*width*data_depth;
  else
    data += line*width;

  for (int x = 0; x < width; x++)
  {
    int x_data_offset = x*data_depth;    // This will be used in packed data

    if (imColorModeIsPacked(file_color_mode))
    {
      int x_file_offset = x*file_depth;  // This will be used in packed data

      if (imColorModeMatch(file_color_mode, user_color_mode))
      {
        // file is packed
        // same color space components   (in this case means RGB)
        // ignore alpha if necessary.
        int depth = IM_MIN(file_depth, data_depth);      
        for (int d = 0; d < depth; d++)
        {
          if (imColorModeIsPacked(user_color_mode))
            data[x_data_offset + d] = iConvertType2Byte(line_buffer[x_file_offset + d]);
          else
            data[d*data_plane_size + x] = iConvertType2Byte(line_buffer[x_file_offset + d]);
        }
      }
      else
      {
        // file is packed
        // but different color space components
        // only to RGB conversions are accepted

        if (imColorModeSpace(user_color_mode) != IM_RGB)
          return;

        T src_data[4];
        src_data[0] = line_buffer[x_file_offset];
        src_data[1] = line_buffer[x_file_offset + 1];
        src_data[2] = line_buffer[x_file_offset + 2];
        if (imColorModeSpace(file_color_mode) == IM_CMYK)
          src_data[3] = line_buffer[x_file_offset + 3];

        // Do conversion in place
        iConvertColor2RGB(src_data, imColorModeSpace(file_color_mode), data_type);

        if (imColorModeIsPacked(user_color_mode))
        {
          data[x_data_offset] = iConvertType2Byte(src_data[0]);
          data[x_data_offset + 1] = iConvertType2Byte(src_data[1]);
          data[x_data_offset + 2] = iConvertType2Byte(src_data[2]);

          if (copy_alpha)
          {
            if (imColorModeSpace(file_color_mode) == IM_CMYK)
              data[x_data_offset + 3] = iConvertType2Byte(line_buffer[x_file_offset + 4]);
            else
              data[x_data_offset + 3] = iConvertType2Byte(line_buffer[x_file_offset + 3]);
          }
        }
        else
        {
          data[x] = iConvertType2Byte(src_data[0]);
          data[data_plane_size + x] = iConvertType2Byte(src_data[1]);
          data[2*data_plane_size + x] = iConvertType2Byte(src_data[2]);

          if (copy_alpha)
          {
            if (imColorModeSpace(file_color_mode) == IM_CMYK)
              data[3*data_plane_size + x] = iConvertType2Byte(line_buffer[x_file_offset + 4]);
            else
              data[3*data_plane_size + x] = iConvertType2Byte(line_buffer[x_file_offset + 3]);
          }
        }
      }
    }
    else
    {
      // file NOT packed, copy just one plane
      // NO color space conversion possible now

      if (plane >= imColorModeDepth(user_color_mode))
        return;

      if (imColorModeIsPacked(user_color_mode))
        data[x_data_offset + plane] = iConvertType2Byte(line_buffer[x]);
      else
        data[plane*data_plane_size + x] = iConvertType2Byte(line_buffer[x]);
    }
  }
}

static void iFileExpandBits(imFile* ifile)
{
  // conversion will be done in place in backward order (from end to start)

  if (abs(ifile->convert_bpp) < 8)
  {
    imbyte* byte_buffer = (imbyte*)ifile->line_buffer;
    imbyte* bit_buffer = (imbyte*)ifile->line_buffer;

    byte_buffer += ifile->width-1; 
    int bpp = ifile->convert_bpp;
    int expand_range = imColorModeSpace(ifile->file_color_mode) == IM_GRAY? 1: 0;

    for (int i=ifile->width-1; i >= 0; i--)
    {
      if (bpp == 1)
        *byte_buffer = (imbyte)((bit_buffer[i / 8] >> (7 - i % 8)) & 0x01);
      else if (bpp == 4)
        *byte_buffer = (imbyte)((bit_buffer[i / 2] >> ((1 - i % 2) * 4)) & 0x0F);
      else if (bpp == 2)
        *byte_buffer = (imbyte)((bit_buffer[i / 4] >> ((3 - i % 4) * 2)) & 0x03);

      if (expand_range)   /* if convert_bpp<0 then only expand its range */
      {
        if (bpp == 4 || bpp == -4)
          *byte_buffer *= 17;
        else if (bpp == 2 || bpp == -2)
          *byte_buffer *= 85;
      }

      byte_buffer--;
    }
  }
  else if (ifile->convert_bpp == 12)
  {
    imushort* ushort_buffer = (imushort*)ifile->line_buffer;
    imbyte* bit_buffer = (imbyte*)ifile->line_buffer;

    for (int i=ifile->width-1; i >= 0; i--)
    {
      int byte_index = (3*i)/2;
      if (i%2)
        ushort_buffer[i] = (bit_buffer[byte_index] << 4) | (bit_buffer[byte_index+1] & 0x0F);
      else
        ushort_buffer[i] = ((bit_buffer[byte_index] & 0x0F) << 8) | (bit_buffer[byte_index+1]);
    }
  }
}

static void iFileCompactBits(imFile* ifile)
{
  // conversion will be done in place
  imbyte* byte_buffer = (imbyte*)ifile->line_buffer;
  imbyte* bit_buffer = (imbyte*)ifile->line_buffer;

  if (ifile->convert_bpp == 1)
  {
    for (int i = 0; i < ifile->width; i++)
    {
      if (*byte_buffer)
        bit_buffer[i / 8] |=  (0x01 << (7 - (i % 8)));
      else
        bit_buffer[i / 8] &= ~(0x01 << (7 - (i % 8)));

      byte_buffer++;
    }
  }
  else  // -1 == expand 1 to 255
  {
    for (int i = 0; i < ifile->width; i++)
    {
      if (*byte_buffer)
        *byte_buffer = 255;

      byte_buffer++;
    }
  }
}

template <class SRC, class DST> 
static void iDoSwitchInt(int count, const SRC* src_data, DST* dst_data, int offset)
{
  for (int i = 0; i < count; i++)
  {
    *dst_data++ = (DST)((int)*src_data++ + offset);
  }
}

template <class SRC, class DST> 
static void iDoSwitchReal(int count, const SRC* src_data, DST* dst_data)
{
  for (int i = 0; i < count; i++)
  {
    *dst_data++ = (DST)(*src_data++);
  }
}

static void iFileSwitchFromType(imFile* ifile)
{
  int line_count = imImageLineCount(ifile->width, ifile->file_color_mode);
  switch(ifile->file_data_type)
  {
  case IM_BYTE:    // Source is char
    iDoSwitchInt(line_count, (const char*)ifile->line_buffer, (imbyte*)ifile->line_buffer, 128);
    break;
  case IM_USHORT:  // Source is short
    iDoSwitchInt(line_count, (const short*)ifile->line_buffer, (imushort*)ifile->line_buffer, 32768);
    break;
  case IM_INT:     // Source is uint
    iDoSwitchInt(line_count, (const unsigned int*)ifile->line_buffer, (int*)ifile->line_buffer, -8388608);
    break;
  case IM_FLOAT:   // Source is double
    iDoSwitchReal(line_count, (const double*)ifile->line_buffer, (float*)ifile->line_buffer);
    break;
  case IM_CFLOAT:  // Source is complex double
    iDoSwitchReal(2*line_count, (const double*)ifile->line_buffer, (float*)ifile->line_buffer);
    break;
  }
}

static void iFileSwitchToType(imFile* ifile)
{
  int line_count = imImageLineCount(ifile->width, ifile->file_color_mode);
  switch(ifile->file_data_type)
  {
  case IM_BYTE:    // Destiny is char
    iDoSwitchInt(line_count, (const imbyte*)ifile->line_buffer, (char*)ifile->line_buffer, -128);
    break;
  case IM_USHORT:  // Destiny is short
    iDoSwitchInt(line_count, (const imushort*)ifile->line_buffer, (short*)ifile->line_buffer, -32768);
    break;
  case IM_INT:     // Destiny is uint
    iDoSwitchInt(line_count, (const int*)ifile->line_buffer, (unsigned int*)ifile->line_buffer, 8388608);
    break;
  case IM_FLOAT:   // Destiny is double
    iDoSwitchReal(line_count, (const float*)ifile->line_buffer, (double*)ifile->line_buffer);
    break;
  case IM_CFLOAT:  // Destiny is complex double
    iDoSwitchReal(2*line_count, (const float*)ifile->line_buffer, (double*)ifile->line_buffer);
    break;
  }
}

void imFileLineBufferWrite(imFile* ifile, const void* data, int line, int plane)
{
  // (writing) from data to file

  if (imColorModeIsTopDown(ifile->file_color_mode) != imColorModeIsTopDown(ifile->user_color_mode))
    line = ifile->height-1 - line;

  if ((ifile->file_color_mode & 0x3FF) == 
      (ifile->user_color_mode & 0x3FF)) // compare only packing, alpha and color space
  {
    int data_offset = line*ifile->line_buffer_size;
    if (plane != 0)
      data_offset += plane*ifile->height*ifile->line_buffer_size;

    memcpy(ifile->line_buffer, (unsigned char*)data + data_offset, ifile->line_buffer_size);
  }
  else
  {
    switch(ifile->file_data_type)
    {
    case IM_BYTE:
      iDoFillLineBuffer(ifile->width, ifile->height, line, plane, 
                        ifile->file_color_mode, (imbyte*)ifile->line_buffer, 
                        ifile->user_color_mode, (const imbyte*)data);
      break;
    case IM_USHORT:
      iDoFillLineBuffer(ifile->width, ifile->height, line, plane,  
                        ifile->file_color_mode, (imushort*)ifile->line_buffer, 
                        ifile->user_color_mode, (const imushort*)data);
      break;
    case IM_INT:
      iDoFillLineBuffer(ifile->width, ifile->height, line, plane,  
                        ifile->file_color_mode, (int*)ifile->line_buffer, 
                        ifile->user_color_mode, (const int*)data);
      break;
    case IM_FLOAT:
      iDoFillLineBuffer(ifile->width, ifile->height, line, plane,  
                        ifile->file_color_mode, (float*)ifile->line_buffer, 
                        ifile->user_color_mode, (const float*)data);
      break;
    case IM_CFLOAT:
      iDoFillLineBuffer(ifile->width, ifile->height, line, plane,  
                        ifile->file_color_mode, (imcfloat*)ifile->line_buffer, 
                        ifile->user_color_mode, (const imcfloat*)data);
      break;
    }
  }

  if (ifile->convert_bpp)
    iFileCompactBits(ifile);

  if (ifile->switch_type)
    iFileSwitchToType(ifile);
}

void imFileLineBufferRead(imFile* ifile, void* data, int line, int plane)
{
  // (reading) from file to data

  if (imColorModeIsTopDown(ifile->file_color_mode) != imColorModeIsTopDown(ifile->user_color_mode))
    line = ifile->height-1 - line;

  if (ifile->convert_bpp)
    iFileExpandBits(ifile);

  if (ifile->switch_type)
    iFileSwitchFromType(ifile);

  if ((ifile->file_color_mode & 0x3FF) == (ifile->user_color_mode & 0x3FF) && // compare only packing, alpha and color space, ignore bottom up.
      ifile->file_data_type == ifile->user_data_type) // compare data type when reading
  {
    int data_offset = line*ifile->line_buffer_size;
    if (plane != 0)
      data_offset += plane*ifile->height*ifile->line_buffer_size;

    memcpy((unsigned char*)data + data_offset, ifile->line_buffer, ifile->line_buffer_size);
  }
  else
  {
    // now we have 2 conversions groups
    // one to convert only packing and alpha
    // and the other to convert packing, alpha, color space and data type
    int convert2bitmap = 0;
    if (imColorModeSpace(ifile->user_color_mode) != imColorModeSpace(ifile->file_color_mode) ||
        ifile->file_data_type != IM_BYTE)
      convert2bitmap = 1;

    switch(ifile->file_data_type)
    {
    case IM_BYTE:
      if (convert2bitmap)
        iDoFillDataBitmap(ifile->width, ifile->height, line, plane, ifile->file_data_type,
                          ifile->file_color_mode, (const imbyte*)ifile->line_buffer, 
                          ifile->user_color_mode, (imbyte*)data);
      else
        iDoFillData(ifile->width, ifile->height, line, plane, 
                    ifile->file_color_mode, (const imbyte*)ifile->line_buffer, 
                    ifile->user_color_mode, (imbyte*)data);
      break;
    case IM_USHORT:
      if (convert2bitmap)
        iDoFillDataBitmap(ifile->width, ifile->height, line, plane, ifile->file_data_type,
                          ifile->file_color_mode, (const imushort*)ifile->line_buffer, 
                          ifile->user_color_mode, (imbyte*)data);
      else
        iDoFillData(ifile->width, ifile->height, line, plane,  
                    ifile->file_color_mode, (const imushort*)ifile->line_buffer, 
                    ifile->user_color_mode, (imushort*)data);
      break;
    case IM_INT:
      if (convert2bitmap)
        iDoFillDataBitmap(ifile->width, ifile->height, line, plane, ifile->file_data_type,
                          ifile->file_color_mode, (const int*)ifile->line_buffer, 
                          ifile->user_color_mode, (imbyte*)data);
      else
        iDoFillData(ifile->width, ifile->height, line, plane,  
                    ifile->file_color_mode, (const int*)ifile->line_buffer, 
                    ifile->user_color_mode, (int*)data);
      break;
    case IM_FLOAT:
      if (convert2bitmap)
        iDoFillDataBitmap(ifile->width, ifile->height, line, plane, ifile->file_data_type,
                          ifile->file_color_mode, (const float*)ifile->line_buffer, 
                          ifile->user_color_mode, (imbyte*)data);
      else
        iDoFillData(ifile->width, ifile->height, line, plane,  
                    ifile->file_color_mode, (const float*)ifile->line_buffer, 
                    ifile->user_color_mode, (float*)data);
      break;
    case IM_CFLOAT:
      if (convert2bitmap)
        iDoFillDataBitmap(ifile->width, ifile->height, line, plane, ifile->file_data_type,
                          ifile->file_color_mode, (const double*)ifile->line_buffer, 
                          ifile->user_color_mode, (imbyte*)data);
      else
        iDoFillData(ifile->width, ifile->height, line, plane,  
                    ifile->file_color_mode, (const imcfloat*)ifile->line_buffer, 
                    ifile->user_color_mode, (imcfloat*)data);
      break;
    }
  }
}
           
void imFileLineBufferInit(imFile* ifile)
{
  ifile->line_buffer_size = imImageLineSize(ifile->width, ifile->file_color_mode, ifile->file_data_type);

  if (ifile->switch_type && (ifile->file_data_type == IM_FLOAT || ifile->file_data_type == IM_CFLOAT))
    ifile->line_buffer_extra += ifile->line_buffer_size; // double the size at least

  if (ifile->line_buffer_size + ifile->line_buffer_extra > ifile->line_buffer_alloc)
  {
    ifile->line_buffer_alloc = ifile->line_buffer_size + ifile->line_buffer_extra;
    ifile->line_buffer = realloc(ifile->line_buffer, ifile->line_buffer_alloc);
  }
}

int imFileLineBufferCount(imFile* ifile)
{
  int count = ifile->height;
  if (!imColorModeIsPacked(ifile->file_color_mode))
  {
    if (imColorModeHasAlpha(ifile->file_color_mode) && imColorModeHasAlpha(ifile->user_color_mode))
      count *= imColorModeDepth(ifile->file_color_mode);
    else
      count *= imColorModeDepth(imColorModeSpace(ifile->file_color_mode));
  }
  return count;
}

void imFileLineBufferInc(imFile* ifile, int *row, int *plane)
{
  if (!imColorModeIsPacked(ifile->file_color_mode))
  {
    if (*row == ifile->height-1)
    {
      *row = 0;
      (*plane)++;
      return;
    }
  }

  (*row)++;
}

int imFileCheckConversion(imFile* ifile)
{
  if ((ifile->file_color_mode & 0x3FF) == (ifile->user_color_mode & 0x3FF) && // compare only packing, alpha and color space
      ifile->file_data_type == ifile->user_data_type)
    return 1;

  int user_color_space = imColorModeSpace(ifile->user_color_mode);
  int file_color_space = imColorModeSpace(ifile->file_color_mode);

  // NO color space conversion if file is not packed.
  if(user_color_space != file_color_space &&
     imColorModeDepth(file_color_space) > 1 &&
     !imColorModeIsPacked(ifile->file_color_mode))
    return 0;

  if (ifile->is_new)
  {
    // (writing) from data to file

    // NO data type conversions when writing.
    if (ifile->file_data_type != ifile->user_data_type)
      return 0;

    // NO color space conversions when writing. 
    // If there is a necessary conversion the format driver will do it.
    if (user_color_space != file_color_space)
      return 0;
   }
  else
  {
    // (reading) from file to data

    // Data type conversions only to byte
    if (ifile->file_data_type != ifile->user_data_type &&
        ifile->user_data_type != IM_BYTE)
      return 0;
  }

  return 1;
}