/***************************************************************************
                       plotting_image.cpp  -  GDL routines for plotting
                             -------------------
    begin                : July 22 2002
    copyright            : (C) 2002-2011 by Marc Schellens et al.
    email                : m_schellens@users.sf.net
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program 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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "includefirst.hpp"
#include "plotting.hpp"
#include "math_utl.hpp"

namespace lib {

  using namespace std;

  class tv_image_call : public plotting_routine_call {
  private:
    BaseGDL* image;
    DLong trueColor;
    DByteGDL* byteImage;
    Guard<BaseGDL> byteImageGuard;
    DLong imageWidth, imageHeight;
    DLong xSize, ySize;
    DLong botLeftPixelX, botLeftPixelY;
    DLong channel;
    SizeT rank;
    bool doT3d;
    DDouble zPosition;

    bool handle_args(EnvT* e) {

      //T3D
      static int t3dIx = e->KeywordIx("T3D");
      doT3d = (e->BooleanKeywordSet(t3dIx) || T3Denabled());

      //note: Z (VALUE) will be used uniquely if Z is not effectively defined.
      // Then Z is useful only if (doT3d).
      static int zvIx = e->KeywordIx("Z");
      zPosition = 0.0; //it is NOT a zValue.
      if (doT3d) {
        e->AssureDoubleScalarKWIfPresent(zvIx, zPosition);
        //norm directly here, we are in 3D mode
        DDouble *sz;
        GetSFromPlotStructs(NULL, NULL, &sz);
        zPosition = zPosition * sz[1] + sz[0];
      }

      trueColor = 0;
      static int TRUEIx = e->KeywordIx("TRUE");
      static int ORDERIx = e->KeywordIx("ORDER");
      static int CHANNELIx = e->KeywordIx("CHANNEL");

      image = e->GetParDefined(0);
      rank = image->Rank();
      e->AssureLongScalarKWIfPresent(TRUEIx, trueColor);
      if (rank < 1 || rank > 3) e->Throw("Image array must have rank 1, 2 or 3");
      if (rank <= 2 && trueColor != 0) e->Throw("Array must have 3 dimensions: " + e->GetParString(0));
      if (trueColor < 0 || trueColor > 3) e->Throw("Value of TRUE keyword is out of allowed range.");
      // to be changed    if (trueColor == 1 && xwd->depth < 24) e->Throw("Device depth must be 24 or greater for trueColor color display");
      DLong orderVal = SysVar::TV_ORDER();
      e->AssureLongScalarKWIfPresent(ORDERIx, orderVal);

      channel = 0;
      if (e->NParam(0) == 4) e->AssureLongScalarPar(3, channel);
      e->AssureLongScalarKWIfPresent(CHANNELIx, channel);
      if (channel < 0 || channel > 3) e->Throw("Value of Channel is out of allowed range.");
// byte conversion apparently handle NaNs quite gracefully so the /NAN (hidden) option of TV can be safely ignored?
      if (rank == 1) {
        byteImage = static_cast<DByteGDL*> (image->Convert2(GDL_BYTE, BaseGDL::COPY));
        byteImageGuard.Init(byteImage);
        if (orderVal != 0) {
          byteImage->Reverse(0);
        }
        imageWidth = byteImage->Dim(0);
        imageHeight = 1;
      } else if (rank == 2) {
        byteImage = static_cast<DByteGDL*> (image->Convert2(GDL_BYTE, BaseGDL::COPY));
        byteImageGuard.Init(byteImage);
        if (orderVal != 0) {
          byteImage->Reverse(1);
        }
        imageWidth = byteImage->Dim(0);
        imageHeight = byteImage->Dim(1);
      } else if (rank == 3) {
        if (trueColor == 1) {
          byteImage = static_cast<DByteGDL*> (image->Convert2(GDL_BYTE, BaseGDL::COPY));
          byteImageGuard.Init(byteImage);
          if (byteImage->Dim(0) < 3) e->Throw("Array <BYTE     Array[" + i2s(byteImage->Dim(0)) + "," +
            i2s(byteImage->Dim(1)) + "," + i2s(byteImage->Dim(2))
            + "]> does not have enough elements.");
          if (orderVal != 0) {
            byteImage->Reverse(2);
          }
          imageWidth = byteImage->Dim(1);
          imageHeight = byteImage->Dim(2);
        }
        if (trueColor == 2) {
          byteImage = static_cast<DByteGDL*> (image->Convert2(GDL_BYTE, BaseGDL::COPY));
          byteImageGuard.Init(byteImage);
          if (byteImage->Dim(1) < 3) e->Throw("Array <BYTE     Array[" + i2s(byteImage->Dim(0)) + "," +
            i2s(byteImage->Dim(1)) + "," + i2s(byteImage->Dim(2))
            + "]> does not have enough elements.");
          if (orderVal != 0) {
            byteImage->Reverse(2);
          }
          imageWidth = byteImage->Dim(0);
          imageHeight = byteImage->Dim(2);
        }
        if (trueColor == 3) {
          byteImage = static_cast<DByteGDL*> (image->Convert2(GDL_BYTE, BaseGDL::COPY));
          byteImageGuard.Init(byteImage);
          if (byteImage->Dim(2) < 3) e->Throw("Array <BYTE     Array[" + i2s(byteImage->Dim(0)) + "," +
            i2s(byteImage->Dim(1)) + "," + i2s(byteImage->Dim(2))
            + "]> does not have enough elements.");
          if (orderVal != 0) {
            byteImage->Reverse(1);
          }
          imageWidth = byteImage->Dim(0);
          imageHeight = byteImage->Dim(1);
        }
        if (trueColor == 0) { // here we have a rank =3 which is indeed a disguised rank 2
          if (image->Dim(0) == 1) {
            imageWidth = image->Dim(1);
            imageHeight = image->Dim(2);
          } else if (image->Dim(1) == 1) {
            imageWidth = image->Dim(0);
            imageHeight = image->Dim(2);
          } else if (image->Dim(2) == 1) {
            imageWidth = image->Dim(0);
            imageHeight = image->Dim(1);
          }
          else { //passed a tricolor image whithout saying it!
            imageWidth = image->Dim(0);
            imageHeight = image->Dim(1);
            if (imageWidth == 3) {
              imageWidth = 1;
            } else {
              if (imageHeight == 3) imageHeight = 1;
            } //rather horrible patch
          }
          byteImage = static_cast<DByteGDL*> (image->Convert2(GDL_BYTE, BaseGDL::COPY));
          byteImageGuard.Init(byteImage);
          SizeT newdims[2];
          newdims[0] = imageWidth;
          newdims[1] = imageHeight;
          dimension dim(newdims, 2);
          (static_cast<BaseGDL*> (byteImage))->SetDim(dim);
          rank = 2;
          if (orderVal != 0) {
            byteImage->Reverse(1);
          }
        }
      }
      return false; //do not abort
    }

  private:

    bool prepareDrawArea(EnvT* e, GDLGStream * actStream) {

      COORDSYS coordinateSystem = NONE;
      static int DATAIx = e->KeywordIx("DATA");
      static int DEVICEIx = e->KeywordIx("DEVICE");
      static int NORMALIx = e->KeywordIx("NORMAL");
      static int INCHES = e->KeywordIx("INCHES");
      static int XSIZE = e->KeywordIx("XSIZE");
      static int YSIZE = e->KeywordIx("YSIZE");
      static int CENTIMETERS = e->KeywordIx("CENTIMETERS");

      //check presence of DATA,DEVICE and NORMAL options
      if (e->KeywordSet(DATAIx)) coordinateSystem = DATA;
      if (e->KeywordSet(DEVICEIx)) coordinateSystem = DEVICE;
      if (e->KeywordSet(NORMALIx)) coordinateSystem = NORMAL;

      SizeT nParam = e->NParam(1);

      DLong xPageSize = actStream->xPageSize();
      DLong yPageSize = actStream->yPageSize();
      DLong positionOnPage = 0;
      botLeftPixelX = 0;
      botLeftPixelY = 0;

      //DEVICE HAS FIXED PIXELS?
      bool fixedPixelSize = (((*static_cast<DLongGDL*> (SysVar::D()->GetTag(SysVar::D()->Desc()->TagIndex("FLAGS"), 0)))[0] & 1) == 0);

      //image size. In DEV, CM or INCHES
      PLFLT devx, devy, x, y;
      devx = 0;
      devy = 0;
      bool xSizeGiven, ySizeGiven;
      double aspect = static_cast<double> (imageWidth) / static_cast<double> (imageHeight);

      xSizeGiven = e->KeywordPresent(XSIZE);
      if (xSizeGiven) e->AssureDoubleScalarKWIfPresent(XSIZE, devx);
      ySizeGiven = e->KeywordPresent(YSIZE);
      if (ySizeGiven) e->AssureDoubleScalarKWIfPresent(YSIZE, devy);

      //interpret size:
      if (xSizeGiven || ySizeGiven) {
        PLFLT x1, y1, x2, y2;
        if (coordinateSystem == DATA) { //devx and devy interpreted as floats
          actStream->world2device(0, 0, x1, y1);
          actStream->world2device(devx, devy, x2, y2);
          devx = x2 - x1;
          devy = y2 - y1;
        } else if (coordinateSystem == NORMAL) { //devx and devy interpreted as floats
          actStream->NormedDeviceToDevice(0, 0, x1, y1);
          actStream->NormedDeviceToDevice(devx, devy, x2, y2);
          devx = (x2 - x1);
          devy = (y2 - y1);
        } else if (coordinateSystem == DEVICE) { //devx and devy must be interpreted as ints to mimic IDL . test all cases with xsize=0.5 to check. 
          //devx and devy must be interpreted as ints to mimic IDL in the DEVICE mode. 
          // if this mode is set, then devx < 1 (resp devy) will be as if the xsize (resp. ysize) option was a /NORM value.
          if (xSizeGiven && devx < 1) {
            actStream->NormedDeviceToDevice(0, 0, x1, y1);
            actStream->NormedDeviceToDevice(1, 0, x2, y2);
            devx = (x2 - x1);
          }
          if (ySizeGiven && devy < 1) {
            actStream->NormedDeviceToDevice(0, 0, x1, y1);
            actStream->NormedDeviceToDevice(0, 1, x2, y2);
            devy = (y2 - y1);
          }
        } else {
          if (e->KeywordSet(INCHES)) { //devx and devy interpreted as floats
            devx *= (INCHToMM);
            devy *= (INCHToMM);
            actStream->mm2device(devx, devy, x, y);
            devx = x;
            devy = y;
          } else if (e->KeywordSet(CENTIMETERS)) { //devx and devy interpreted as floats
            devx *= (10.);
            devy *= (10.);
            actStream->mm2device(devx, devy, x, y);
            devx = x;
            devy = y;
          }
        }
        if (xSizeGiven && !ySizeGiven) {
          devy = devx / aspect;
          ySizeGiven = true;
        }
        if (!xSizeGiven && ySizeGiven) {
          devx = devy*aspect;
          xSizeGiven = true;
        }
      } else {
        double pageaspect = xPageSize / yPageSize;
        if (aspect > pageaspect) {
          devx = xPageSize;
          devy = devx / aspect;
        } else {
          devy = yPageSize;
          devx = devy*aspect;
        }
      }

      if (nParam == 2) {
        int nPerPageX, nPerPageY, ix, iy;
        e->AssureLongScalarPar(1, positionOnPage);
        if (!fixedPixelSize && !(xSizeGiven && ySizeGiven)) e->Throw("XSIZE and/or YSIZE needed for proper positioning.");
        nPerPageX = xPageSize / imageWidth;
        nPerPageY = yPageSize / imageHeight;
        // AC 2011/11/06, bug 3433502
        if (nPerPageX > 0) {
          ix = positionOnPage % nPerPageX;
        } else {
          nPerPageX = 1;
          ix = 0;
        }
        if (nPerPageY > 0) {
          iy = (positionOnPage / nPerPageX) % nPerPageY;
        } else {
          nPerPageY = 1;
          iy = 0;
        }
        botLeftPixelX = imageWidth*ix;
        botLeftPixelY = yPageSize - imageHeight * (iy + 1);
      } else if (nParam >= 3) {
        PLFLT xLLf, yLLf;
        e->AssureDoubleScalarPar(1, xLLf); //may be DATA or NORM or DEVICE. DEV,NORM and DATA preferred on INCH and CENTIM. INCH and CM imply DEVICE
        e->AssureDoubleScalarPar(2, yLLf); //idem
        //convert to device Pixel:
        if (coordinateSystem == DATA) {
          SelfProjectXY(1, &xLLf, &yLLf, coordinateSystem); //here for eventual projection
          bool f = false;
          SelfConvertToNormXY(1, &xLLf, f, &yLLf, f, coordinateSystem); //input coordinates converted to NORMAL
          if (doT3d) SelfPDotTTransformXYZ(1, &xLLf, &yLLf, &zPosition); //T3D transform for point
          botLeftPixelX = xLLf*xPageSize;
          botLeftPixelY = yLLf*yPageSize;
        } else if (coordinateSystem == NORMAL) {
          actStream->NormedDeviceToDevice(xLLf, yLLf, x, y);
          botLeftPixelX = x;
          botLeftPixelY = y;
        } else if (coordinateSystem == DEVICE) {
          botLeftPixelX = xLLf;
          botLeftPixelY = yLLf;
        } else {
          if (e->KeywordSet(INCHES)) {
            xLLf *= (INCHToMM);
            yLLf *= (INCHToMM);
            actStream->mm2device(xLLf, yLLf, x, y);
            botLeftPixelX = x;
            botLeftPixelY = y;
          } else if (e->KeywordSet(CENTIMETERS)) {
            xLLf *= (10.);
            yLLf *= (10.);
            actStream->mm2device(xLLf, yLLf, x, y);
            botLeftPixelX = x;
            botLeftPixelY = y;
          } else {
            botLeftPixelX = xLLf;
            botLeftPixelY = yLLf;
          }
        }
      }

      xSize = devx;
      ySize = devy;

      int debug = 0;
      if (debug == 1) {
        std::cout << "==================== " << std::endl;
        std::cout << "trueColor " << trueColor << std::endl;
        std::cout << "Rank " << rank << std::endl;
        std::cout << "Channel " << channel << std::endl;
        std::cout << "xSize " << xSize << std::endl;
        std::cout << "ySize " << ySize << std::endl;
        std::cout << "botLeftPixelX " << botLeftPixelX << std::endl;
        std::cout << "botLeftPixelY " << botLeftPixelY << std::endl;
        std::cout << "imageWidth " << imageWidth << std::endl;
        std::cout << "imageHeight " << imageHeight << std::endl;
      }
      return false; //do not abort
    }

  private:

    void applyGraphics(EnvT* e, GDLGStream * actStream) {

      //pass physical position of image
      DLong devicebox[4] = {botLeftPixelX, xSize, botLeftPixelY, ySize};

      Guard<BaseGDL> chan_guard;
      bool tidy = false;
      DByte* im = &(*byteImage)[0];
      if (byteImage->Rank() == 3 && byteImage->Dim(0) == 4) {
        SizeT s = imageWidth * imageHeight * 3;
        im = (DByte*) malloc(s);
        for (SizeT i = 0, k = 0; i < s;) {
          im[i++] = (*byteImage)[k++];
          im[i++] = (*byteImage)[k++];
          im[i++] = (*byteImage)[k++];
          k++;
        }
        tidy = true;
      }
      if (channel == 0) {
        if (!actStream->PaintImage(im, imageWidth, imageHeight, devicebox, trueColor, channel)) e->Throw("device does not support Paint");
      } else if (rank == 3) {
        // Rank == 3 w/channel
        SizeT dims[2];
        dims[0] = imageWidth;
        dims[1] = imageHeight;
        dimension dim(dims, 2);
        DByteGDL* byteImage_chan = new DByteGDL(dim, BaseGDL::ZERO);
        for (SizeT i = (channel - 1); i < byteImage->N_Elements(); i += 3) {
          (*byteImage_chan)[i / 3] = im[i];
        }
        // Send just single channel
        if (!actStream->PaintImage(im, imageWidth, imageHeight, devicebox, trueColor, channel)) e->Throw("device does not support Paint");
        chan_guard.Init(byteImage_chan); // delete upon exit
      } else if (rank == 2) {
        // Rank = 2 w/channel
        if (!actStream->PaintImage(im, imageWidth, imageHeight, devicebox, trueColor, channel)) e->Throw("device does not support Paint");
      }
      if (tidy) free(im);
    }



  private:

    void post_call(EnvT*, GDLGStream * actStream)
    {
    }
  };

  void tv_image(EnvT* e) {
    tv_image_call tv_image;
    tv_image.call(e, 1);
  }

} // namespace
