/*

  xmunipack - fits image display

  Copyright © 2018-9 F.Hroch (hroch@physics.muni.cz)

  This file is part of Munipack.

  Munipack is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  Munipack is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with Munipack.  If not, see <http://www.gnu.org/licenses/>.

*/


#include "fits.h"
#include <cfloat>
#include <wx/wx.h>
#include <algorithm>

#ifdef __WXDEBUG__
#include <wx/debug.h>
#include <wx/txtstrm.h>
#include <wx/wfstream.h>
#endif


#undef STOPWATCH


using namespace std;

// ---- FitsBaseDisplay  ------

FitsBaseDisplay::FitsBaseDisplay(): shrink(1), flags(0) {}

void FitsBaseDisplay::SetShrink(int s)
{
  flags = flags | OP_TUNE_ZOOM;
  shrink = s;
}

void FitsBaseDisplay::SetItt(const FitsItt& i)
{
  flags = flags | OP_TUNE_ITT;
  itt = i;
}

void FitsBaseDisplay::SetTone(const FitsTone& t)
{
  flags = flags | OP_TUNE_SCALE;
  tone = t;
}

void FitsBaseDisplay::SetBlack(double b)
{
  flags = flags | OP_TUNE_SCALE;
  tone.SetBlack(b);
}
void FitsBaseDisplay::SetSense(double s)
{
  flags = flags | OP_TUNE_SCALE;
  tone.SetSense(s);
}
void FitsBaseDisplay::SetQblack(double b)
{
  flags = flags | OP_TUNE_SCALE;
  tone.SetQblack(b);
}
void FitsBaseDisplay::SetRsense(double r)
{
  flags = flags | OP_TUNE_SCALE;
  tone.SetRsense(r);
}
void FitsBaseDisplay::SetAmp(double f)
{
  flags = flags | OP_TUNE_ITT;
  itt.SetAmp(f);
}
void FitsBaseDisplay::SetZero(double z)
{
  flags = flags | OP_TUNE_ITT;
  itt.SetZero(z);
}
void FitsBaseDisplay::SetItt(const wxString& t)
{
  flags = flags | OP_TUNE_ITT;
  itt.SetItt(t);
}

void FitsBaseDisplay::SetOperations(long f)
{
  flags = f;
  /*
  wxLogDebug("FitsBaseDisplay::SetOperations: %d %d %d %d %d",
	     (flags & OP_TUNE_ZOOM) == OP_TUNE_ZOOM,
	     (flags & OP_TUNE_SCALE) == OP_TUNE_SCALE,
	     (flags & OP_TUNE_ITT) == OP_TUNE_ITT,
	     (flags & OP_TUNE_COLOUR) == OP_TUNE_COLOUR,
	     (flags & OP_TUNE_PAL) == OP_TUNE_PAL);
  */
}



// ---- FitsGrayDisplay  ------

FitsGrayDisplay::FitsGrayDisplay(const FitsArray& a):
  FitsBaseDisplay(),original(a) {}

FitsGrayDisplay::FitsGrayDisplay(const FitsImage& a):
  FitsBaseDisplay()
{
  SetImage(a);
}

void FitsGrayDisplay::SetArray(const FitsArray& a)
{
  original = a;
}

void FitsGrayDisplay::SetImage(const FitsImage& a)
{
  wxASSERT(a.IsOk() && a.GetCount() == 1);
  const vector<FitsArray> array(a.GetArrays());
  original = array[0];
}


void FitsGrayDisplay::SetPalette(const FitsPalette& p)
{
  flags = flags | OP_TUNE_PAL;
  pal = p;
}

void FitsGrayDisplay::SetPalette(const wxString& p)
{
  flags = flags | OP_TUNE_PAL;
  pal.SetPalette(p);
}

void FitsGrayDisplay::SetInversePalette(bool b)
{
  flags = flags | OP_TUNE_PAL;
  pal.SetInverse(b);
}

int FitsGrayDisplay::GetWidth() const
{
  return original.GetWidth();
}

int FitsGrayDisplay::GetHeight() const
{
  return original.GetHeight();
}

float *FitsGrayDisplay::sRGBGamma(long n, const float *array) const
{
  const float q = 1.0 / 2.4;
  const float a = 0.055;

  float *f = new float[n];
  for(long i = 0; i < n; i++) {
    float r = array[i];
    if( r < 0.0031308f )
      f[i] = 12.92f*r;
    else
      f[i] = (1 + a)*powf(r,q) - a;
  }
  return f;
}


float *FitsGrayDisplay::AdobeGamma(long n, const float *array) const
{
  const float q = 1.0 / 2.19921875;

  float *f = new float[n];
  for(long i = 0; i < n; i++)
    f[i] = powf(array[i],q);
  return f;
}

// ---- FitsColourDisplay  ------

FitsColourDisplay::FitsColourDisplay(const vector<FitsArray>& o):
  FitsBaseDisplay(),originals(o)
{
  wxASSERT(o.size() == 3);
  for(size_t i = 0; i < 3; i++)
    wxASSERT(o[i].IsOk());
}

FitsColourDisplay::FitsColourDisplay(const FitsArray& o):
  FitsBaseDisplay()
{
  SetArray(o);
}

void FitsColourDisplay::SetArray(const FitsArray& o)
{
  wxASSERT(o.IsOk() && o.Naxis() == 3);
  for(int k = 0; k < o.Naxis(); k++)
    originals.push_back(o.Plane(k));
}

FitsColourDisplay::FitsColourDisplay(const FitsImage& o):
  FitsBaseDisplay(),originals(o.GetArrays())
{
  SetImage(o);
}

void FitsColourDisplay::SetImage(const FitsImage& o)
{
  wxASSERT(o.IsOk() && o.GetCount() == 3);
  originals = o.GetArrays();
}

FitsImage FitsColourDisplay::GetShrinked() const
{
  return FitsImage(zoomed);
}

void FitsColourDisplay::SetColour(const FitsColor& c)
{
  flags = flags | OP_TUNE_COLOUR;
  colour = c;
}

void FitsColourDisplay::SetSaturation(double s)
{
  flags = flags | OP_TUNE_COLOUR;
  colour.SetSaturation(s);
}

void FitsColourDisplay::SetHue(double h)
{
  flags = flags | OP_TUNE_COLOUR;
  colour.SetHue(h);
}

void FitsColourDisplay::SetNiteThresh(double t)
{
  flags = flags | OP_TUNE_COLOUR;
  colour.SetNiteThresh(t);
}
void FitsColourDisplay::SetNiteWidth(double w)
{
  flags = flags | OP_TUNE_COLOUR;
  colour.SetNiteWidth(w);
}

void FitsColourDisplay::SetLevel(int n, double b)
{
  flags = flags | OP_TUNE_COLOUR;
  colour.SetLevel(n,b);
}

void FitsColourDisplay::SetNiteVision(bool b)
{
  flags = flags | OP_TUNE_COLOUR;
  colour.SetNiteVision(b);
}

int FitsColourDisplay::GetWidth() const
{
  if( ! originals.empty() )
    return originals[0].GetWidth();
  else
    return 0;
}

int FitsColourDisplay::GetHeight() const
{
  if( ! originals.empty() )
    return originals[0].GetHeight();
  else
    return 0;
}


// ---- FitsDisplay  ------


FitsDisplay::FitsDisplay(const FitsArray& array)
{
  wxASSERT(array.IsOk());
  if( array.IsColour() )
    display = new FitsColourDisplay(array);
  else
    display = new FitsGrayDisplay(array);
}

FitsDisplay::FitsDisplay(const FitsImage& i)
{
  wxASSERT(i.IsOk());
  if( i.IsColour() )
    display = new FitsColourDisplay(i);
  else
    display = new FitsGrayDisplay(i);
}

FitsDisplay::FitsDisplay(const FitsDisplay& display)
{
  wxFAIL_MSG("FitsDisplay ----- WE ARE REALY NEED COPY CONSTRUCTOR -----");
}

FitsDisplay& FitsDisplay::operator= (const FitsDisplay& other)
{
  wxFAIL_MSG("FitsDisplay ----- WE ARE REALY NEED ASSIGN CONSTRUCTOR -----");
  if( this != &other ) {
    ;
  }
  return *this;
}

FitsDisplay::~FitsDisplay()
{
  delete display;
}

FitsPalette FitsDisplay::GetPalette() const
{
  return pal;
}

FitsItt FitsDisplay::GetItt() const
{
  return itt;
}

FitsColor FitsDisplay::GetColor() const
{
  return color;
}

void FitsDisplay::SetShrink(int s)
{
  wxASSERT(display);
  display->SetShrink(s);
}

void FitsDisplay::SetShrink(int width, int height)
{
  wxASSERT(display);

  float s;
  int w = display->GetWidth();
  int h = display->GetHeight();

  if( w > h )
    s = float(w)/float(width);
  else
    s = float(h)/float(height);

  display->SetShrink(int(s+0.5));
}


void FitsDisplay::SetItt(const FitsItt& i)
{
  wxASSERT(display);
  itt = i;
  display->SetItt(i);
}

void FitsDisplay::SetTone(const FitsTone& t)
{
  wxASSERT(display);
  tone = t;
  display->SetTone(t);
}

void FitsDisplay::SetPalette(const FitsPalette& p)
{
  wxASSERT(display);
  pal = p;
  display->SetPalette(p);
}

void FitsDisplay::SetColor(const FitsColor& c)
{
  wxASSERT(display);
  color = c;
  display->SetColour(c);
}

void FitsDisplay::SetOperations(long flags)
{
  wxASSERT(display);
  display->SetOperations(flags);
}


FitsBitmap FitsDisplay::GetImage() const
{
  wxASSERT(display);
  display->SetOperations(OP_TUNE_SCALE | OP_TUNE_ITT | OP_TUNE_PAL |
			       OP_TUNE_COLOUR | OP_TUNE_RGB);
  display->Render();
  return display->GetBitmap();
}

void FitsGrayDisplay::Render()
{
  current = original;
  scaled = original;

#ifdef STOPWATCH
  wxStopWatch sw;
#endif

  if( (flags & OP_TUNE_ZOOM) == OP_TUNE_ZOOM || shrink > 1 ) {
    FitsGeometry g(original);
    zoomed = g.Shrink(shrink);
  }
  else
    zoomed = original;

#ifdef STOPWATCH
  wxLogDebug("Zooming took up to stop: %f sec",sw.Time()/1000.0);
  sw.Start();
#endif

  if( (flags & OP_TUNE_SCALE) == OP_TUNE_SCALE ) {
    float *f = tone.Scale(zoomed.Npixels(),zoomed.PixelData());
    scaled = FitsArray(zoomed,zoomed.Naxis(),zoomed.Naxes(),f);
    current = scaled;
  }

#ifdef STOPWATCH
  wxLogDebug("Scaling took up to stop: %f sec",sw.Time()/1000.0);
  sw.Start();
#endif

  if( (flags & OP_TUNE_ITT) == OP_TUNE_ITT ) {
    float *f = itt.Scale(scaled.Npixels(),scaled.PixelData());
    current = FitsArray(scaled,scaled.Naxis(),scaled.Naxes(),f);
  }

  if( (flags & OP_TUNE_RGB) == OP_TUNE_RGB ) {

    unsigned char *rgb;
    long npix = current.Npixels();
#ifdef __ADOBERGB__
    float *g = AdobeGamma(npix,current.PixelData());
#else  // __sRGB__
    float *g = sRGBGamma(npix,current.PixelData());
#endif

    if( (flags & OP_TUNE_PAL) == OP_TUNE_PAL )
      rgb = pal.RGB(npix,g);
    else {
      rgb = (unsigned char *) malloc(3*npix);
      for(long i = 0; i < npix; i++) {
	unsigned char d = fclip255(255.0f*g[i]);
	memset(rgb+3*i,d,3);
      }
    }
    delete[] g;

#ifdef STOPWATCH
    wxLogDebug("Gray to RGB took up to stop: %f sec",sw.Time()/1000.0);
    sw.Start();
#endif

    wxASSERT(rgb);
    bitmap = FitsBitmap(current.GetWidth(),current.GetHeight(),rgb);
  }
}

FitsImage FitsGrayDisplay::GetImage() const
{
  return FitsImage(current);
}

FitsImage FitsColourDisplay::GetImage() const
{
  return FitsImage(scaled);
}

void FitsColourDisplay::Render()
{
  // This part does color transformation from an CIE 1931 XYZ color space
  // to a display color space. Optionaly, colour parameters are tuned.
  //
  // Digital cameras usually produces colors in sRGB or AdobeRGB.
  //
  // Astronomical filter series usually stores data in their proper systems.
  // Plain usage leads to a deformation of colors.
  // Documents
  //         http://www.fho-emden.de/~hoffmann/ciexyz29082000.pdf
  //     and http://www.fho-emden.de/~hoffmann/cielab03022003.pdf
  // detaily describes the general transformation.
  //
  // Therefore, we are converting the input to the XYZ color space.
  // (XYZ should be obtained from any RAW digital photography with dcraw).
  // (XYZ should be obtained from Landolt's BVR by a custom transformation.)

  // http://en.wikipedia.org/wiki/RGB_color_spaces
  // http://en.wikipedia.org/wiki/CIE_1931_color_space
  // http://en.wikipedia.org/wiki/D65

  // Luminosity scaled XYZ is transformed to some RGB.
  // (Adobe RGB (Mac) or sRGB is not directly supported by wxWidgets.)

#ifdef __WXDEBUG__
  wxASSERT(originals.size() == 3);
  wxASSERT(originals.size() == (size_t) colour.GetBands());
  for(size_t i = 0; i < originals.size(); i++) {
    wxASSERT(originals[i].IsOk());
    if( i > 0 ) {
      wxASSERT(originals[i].Naxis() == originals[i-1].Naxis());
      for(size_t j = 0; j < (size_t) originals[i].Naxis(); j++)
	wxASSERT(originals[i].Naxes(j) == originals[i-1].Naxes(j));
    }
  }
#endif

  for(size_t i = 1; i < originals.size(); i++)
    wxASSERT(originals[i-1].Npixels() == originals[i].Npixels());

#ifdef STOPWATCH
  wxStopWatch sw;
#endif

  // pre-zoom
  if( (flags & OP_TUNE_ZOOM) == OP_TUNE_ZOOM || shrink > 1 ) {
    zoomed.clear();
    for(size_t k = 0; k < originals.size(); k++) {
      FitsGeometry g(originals[k]);
      zoomed.push_back(g.Shrink(shrink));
    }
  }
  else
    zoomed = originals;

#ifdef STOPWATCH
  wxLogDebug("Zooming took up to stop: %f sec",sw.Time()/1000.0);
  sw.Start();
#endif

  // scale
  if( (flags & OP_TUNE_SCALE) == OP_TUNE_SCALE ) {
    scaled.clear();
    for(size_t k = 0; k < zoomed.size(); k++) {
      float *f = tone.Scale(zoomed[k].Npixels(),zoomed[k].PixelData());
      scaled.push_back(FitsArray(zoomed[k],zoomed[k].Naxis(),
				 zoomed[k].Naxes(),f));
    }
  }
  else
    scaled = zoomed;

#ifdef STOPWATCH
  wxLogDebug("Scaling took up to stop: %f sec",sw.Time()/1000.0);
  sw.Start();
#endif

  if( (flags & OP_TUNE_RGB) == OP_TUNE_RGB ) {

    long npix = scaled[0].Npixels();
    float *X = new float[npix];
    float *Y = new float[npix];
    float *Z = new float[npix];
    const float *X0 = scaled[2].PixelData();
    const float *Y0 = scaled[1].PixelData();
    const float *Z0 = scaled[0].PixelData();

    // proper scaling
    for(long i = 0; i < npix; i++) {
      X[i] = 100 * X0[i];
      Y[i] = 100 * Y0[i];
      Z[i] = 100 * Z0[i];
    }

    if( (flags & OP_TUNE_ITT) == OP_TUNE_ITT ||
	(flags & OP_TUNE_COLOUR) == OP_TUNE_COLOUR ) {

      float *L = new float[npix];
      float *a = new float[npix];
      float *b = new float[npix];

      colour.XYZ_Lab(npix,X,Y,Z,L,a,b);

      // scale luminosity
      if( (flags & OP_TUNE_ITT) == OP_TUNE_ITT ) {
	for(long i = 0; i < npix; i++) L[i] = L[i] / 100.0;
	float *xL= itt.Scale(npix,L);
	for(long i = 0; i < npix; i++) L[i] = 100.0 * xL[i];
	delete[] xL;
      }

      // tune saturation and hue
      if( (flags & OP_TUNE_COLOUR) == OP_TUNE_COLOUR )
	colour.TuneColors(npix,L,a,b);

      // convert back to XYZ
      colour.Lab_XYZ(npix,L,a,b,X,Y,Z);

      // apply night vision
      if( colour.GetNiteVision() )
	colour.NiteVision(npix,L,X,Y,Z);

      delete[] L;
      delete[] a;
      delete[] b;

    } // OP_TUNE_COLOUR

  // XYZ to RGB
#ifdef __ADOBERGB__
    unsigned char *rgb = XYZ_AdobeRGB(npix,X,Y,Z);
#else  // __sRGB__
    unsigned char *rgb = XYZ_sRGB(npix,X,Y,Z);
#endif

#ifdef STOPWATCH
    wxLogDebug("XYZ to RGB took up to stop: %f sec",sw.Time()/1000.0);
#endif

    wxASSERT(rgb);
    bitmap = FitsBitmap(scaled[0].GetWidth(),scaled[0].GetHeight(),rgb);

    delete[] X;
    delete[] Y;
    delete[] Z;
  } // OP_.._RGB
}


unsigned char *FitsColourDisplay::XYZ_sRGB(long n, const float *X,
					   const float *Y, const float *Z) const
{
  // http://en.wikipedia.org/wiki/SRGB

  wxASSERT(n > 0);

  unsigned char *rgb = (unsigned char *) malloc(3*n);
  unsigned char *p = rgb;

  for(long i = 0; i < n; i++) {
    float x = X[i] / 100.0;
    float y = Y[i] / 100.0;
    float z = Z[i] / 100.0;
    float r = sRGBGamma( 3.2406f*x - 1.5372f*y - 0.4986f*z);
    float g = sRGBGamma(-0.9689f*x + 1.8758f*y + 0.0415f*z);
    float b = sRGBGamma( 0.0557f*x - 0.2040f*y + 1.0570f*z);
    *p++ = fclip255(255.0f*r);
    *p++ = fclip255(255.0f*g);
    *p++ = fclip255(255.0f*b);
  }
  return rgb;
}

unsigned char *FitsColourDisplay::XYZ_AdobeRGB(long n, const float *X,
					 const float *Y, const float *Z) const
{
  // http://en.wikipedia.org/wiki/Adobe_RGB_color_space
  // http://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf

  wxASSERT(n > 0);
  unsigned char *rgb = (unsigned char *) malloc(3*n);
  unsigned char *p = rgb;

  for(long i = 0; i < n; i++) {
    float x = X[i] / 100.0;
    float y = Y[i] / 100.0;
    float z = Z[i] / 100.0;
    float r = AdobeGamma( 2.04159f*x - 0.56501f*y - 0.34473f*z);
    float g = AdobeGamma(-0.96924f*x + 1.87597f*y + 0.04156f*z);
    float b = AdobeGamma( 0.01344f*x - 0.11836f*y + 1.01517f*z);
    *p++ = fclip255(255.0f*r);
    *p++ = fclip255(255.0f*g);
    *p++ = fclip255(255.0f*b);
  }
  return rgb;
}
