// Copyright (C) 2010, Guy Barrand. All rights reserved.
// See the file tools.license for terms.

#ifndef tools_wps
#define tools_wps

#include <ctime>

#include <string>
#include <ostream>
#include <cstring>
#include "sprintf"

namespace tools {

class wps {
  typedef unsigned char Uchar;
  typedef unsigned int  Uint;

public:
  typedef float VCol;
  typedef bool(*rgb_func)(void*,unsigned int,unsigned int,VCol&,VCol&,VCol&);
public:
  wps(std::ostream& a_out):m_out(a_out){
    m_deviceWidth = (8.5f-1.f)*72*METAFILE_SCALE(); // 540 * METAFILE_SCALE
    m_deviceHeight = 11*72*METAFILE_SCALE();        // 792 * METAFILE_SCALE
    m_pageNumber = 0;
    m_file = 0;
    m_fname.clear();
    m_string.clear();
    m_gsave = 0;
    m_buffer = new Uchar[METAFILE_RECORD_LENGTH()+1];
    m_number = 0;
  }

  virtual ~wps(){
    if(m_file) close_file();
    m_string.clear();
    if(m_gsave) {
      m_out << "tools::wps::~wps :"
            << " bad gsave/grestore balance : " << m_gsave
            << std::endl;
    }
    m_gsave = 0;
    delete [] m_buffer;
  }
private:
  wps(const wps& a_from):m_out(a_from.m_out){}
  wps& operator=(const wps&){return *this;}
public:
  bool open_file(const std::string& a_name,bool a_anonymous = false){
    if(m_file) return false;

    m_file = ::fopen(a_name.c_str(),"wb");
    if(!m_file) return false;
    m_fname = a_name;

    m_number = 0;
    m_buffer[METAFILE_RECORD_LENGTH()]= '\0';
    m_pageNumber = 0;
    // Header :
    PrintFLN("%%!PS-Adobe-2.0");
    if(a_anonymous) { //usefull for integration tests.
    } else {
      PrintFLN("%%%%Creator: tools::wps.");
      PrintFLN("%%%%CreationDate: %s",get_date());
      PrintFLN("%%%%Title: %s",m_fname.c_str());
    }
    PrintFLN("%%%%Pages: (atend)");
    PrintFLN("%%%%BoundingBox: 0 0 %d %d",(int)m_deviceWidth,(int)m_deviceHeight);
    PrintFLN("%%%%DocumentFonts: Courier-Bold");
    PrintFLN("%%%%DocumentPaperSizes: a4");
    PrintFLN("%%%%EndComments");
    // postscript :
    PS_SAVE();
    PrintFLN("%%%%EndProlog");

    return true;
  }

  bool close_file(){
    if(!m_file) return false;
    PS_RESTORE();
    PrintFLN("%%%%Trailer");
    PrintFLN("%%%%Pages: %d",m_pageNumber);
    PrintFLN("%%%%EOF");
    ::fclose(m_file);
    m_file = 0;
    m_fname.clear();
    return true;
  }

  void PS_PAGE_SCALE(float a_width,float a_height,bool a_portrait = true){
    //NOTE : no check done on [a_width,a_height]<=0

    PS_SCALE(1/METAFILE_SCALE(),1/METAFILE_SCALE());
    PS_TRANSLATE(m_deviceWidth/20,m_deviceHeight/30);

    float scale;
    if(m_deviceWidth<=m_deviceHeight) {
      scale = (a_height<=a_width?m_deviceWidth/a_width:m_deviceWidth/a_height);
    } else {
      scale = (a_height<=a_width?m_deviceHeight/a_width:m_deviceHeight/a_height);
    }

   {float xtra,ytra;
    if(a_portrait) {
      xtra = (m_deviceWidth  - scale * a_width)/2;
      ytra = (m_deviceHeight - scale * a_height)/2;
    } else {
      PS_TRANSLATE(m_deviceWidth,0);
      PS_ROTATE(90);
      xtra = (m_deviceHeight - scale * a_width)/2;
      ytra = (m_deviceWidth  - scale * a_height)/2;
    }
    PS_TRANSLATE(xtra,ytra);}

    PS_SCALE(scale,scale);
  }

  void PS_SCALE(float a_x,float a_y) {
    in_buffer("%.2f %.2f scale ",a_x,a_y);
  }

  void PS_TRANSLATE(float a_x,float a_y){
    in_buffer("%.2f %.2f translate ",a_x,a_y);
  }

  void PS_ROTATE(float a_x){in_buffer("%.2f rotate ",a_x);}

  void PS_SAVE(){in_buffer("gsave ");m_gsave++;}

  void PS_RESTORE(){in_buffer("grestore ");m_gsave--;}

  void PS_BEGIN_PAGE(){
    m_pageNumber++;
    PrintFLN("%%%%Page: %d %d",m_pageNumber,m_pageNumber);
    PS_SAVE();
  }

  void PS_END_PAGE(){
    in_buffer("showpage ");
    PS_RESTORE();
  }

  enum rgb_nbit {
    rgb_bw = 0,
    rgb_2 = 2,
    rgb_4 = 4,
    rgb_8 = 8
  };

  void PS_IMAGE(Uint a_width,Uint a_height,rgb_nbit a_nbit,rgb_func a_proc,void* a_tag){
    //NOTE : no check done on [a_width,a_height] being zero.

    Uint   row,col;
    VCol   dr,dg,db;
    Uchar  red,green,blue,b;

    PS_SAVE();
    in_buffer("%d %d scale ", a_width, a_height );
    bool status = true;
    if(a_nbit==rgb_bw) { //grey or black_white
      in_buffer   ("/picstr %d string def ",a_width);
      in_buffer   ("%d %d %d ",a_width,a_height,8);
      in_buffer   ("[ %d 0 0 -%d 0 %d ] ",a_width,a_height,a_height);
      in_buffer   ("{ currentfile picstr readhexstring pop } " );
      PrintFLN ("image " );
      for ( row = 0; row < a_height; row++ ) {
        for ( col = 0; col < a_width; col++) {
          status = a_proc(a_tag,col,row,dr,dg,db)?status:false;
          VCol fgrey = rgb2grey(dr,dg,db);
          Uchar grey = (Uchar) ( 255.0F * fgrey);
          WriteByte(grey);
        }
      }
      int nbhex = a_width * a_height * 2;
      PrintFLN ("%%%% nbhex digit          :%d ",nbhex);
      PrintFLN ("%%%% nbhex/record_length  :%d ",
                int(nbhex/METAFILE_RECORD_LENGTH()));
      PrintFLN ("%%%% nbhex%%record_length :%d ",
                int(nbhex%METAFILE_RECORD_LENGTH()));

    } else if(a_nbit==rgb_2) {
      int nbyte2 = (a_width   *  3)/4;
      nbyte2 /=3;
      nbyte2 *=3;
      Uint col_max = (nbyte2  *  4)/3;
      // 2 bit for r and g and b.
      // rgbs following each other.
      in_buffer   ("/rgbstr %d string def ",nbyte2);
      in_buffer   ("%d %d %d ",col_max,a_height,2);
      in_buffer   ("[ %d 0 0 -%d 0 %d ] ",col_max,a_height,a_height);
      in_buffer   ("{ currentfile rgbstr readhexstring pop } " );
      in_buffer   ("false 3 " );
      PrintFLN ("colorimage " );
      for ( row = 0; row < a_height; row++ ) {
        for ( col = 0; col < col_max; col+=4) {
          status  = a_proc(a_tag,col,row,dr,dg,db)?status:false;
          red     = (Uchar) ( 3.0F * dr);
          green   = (Uchar) ( 3.0F * dg);
          blue    = (Uchar) ( 3.0F * db);
          b       = red;
          b       = (b<<2)+green;
          b       = (b<<2)+blue;

          status  = a_proc(a_tag,col+1,row,dr,dg,db)?status:false;
          red     = (Uchar) ( 3.0F * dr);
          green   = (Uchar) ( 3.0F * dg);
          blue    = (Uchar) ( 3.0F * db);
          b       = (b<<2)+red;
          WriteByte(b);

          b       = green;
          b       = (b<<2)+blue;

          status  = a_proc(a_tag,col+2,row,dr,dg,db)?status:false;
          red     = (Uchar) ( 3.0F * dr);
          green   = (Uchar) ( 3.0F * dg);
          blue    = (Uchar) ( 3.0F * db);
          b     = (b<<2)+red;
          b     = (b<<2)+green;
          WriteByte(b);
          b       = blue;

          status  = a_proc(a_tag,col+3,row,dr,dg,db)?status:false;
          red     = (Uchar) ( 3.0F * dr);
          green   = (Uchar) ( 3.0F * dg);
          blue    = (Uchar) ( 3.0F * db);
          b     = (b<<2)+red;
          b     = (b<<2)+green;
          b     = (b<<2)+blue;
          WriteByte(b);
        }
      }

    } else if(a_nbit==rgb_4) {
      int nbyte4 = (a_width  * 3)/2;
      nbyte4 /=3;
      nbyte4 *=3;
      Uint col_max = (nbyte4 * 2)/3;
      // 4 bit for r and g and b.
      // rgbs following each other.
      in_buffer   ("/rgbstr %d string def ",nbyte4);
      in_buffer   ("%d %d %d ",col_max,a_height,4);
      in_buffer   ("[ %d 0 0 -%d 0 %d ] ",col_max,a_height,a_height);
      in_buffer   ("{ currentfile rgbstr readhexstring pop } " );
      in_buffer   ("false 3 " );
      PrintFLN ("colorimage " );
      for ( row = 0; row < a_height; row++ ) {
        for ( col = 0; col < col_max; col+=2) {
          status  = a_proc(a_tag,col,row,dr,dg,db)?status:false;
          red     = (Uchar) ( 15.0F * dr);
          green   = (Uchar) ( 15.0F * dg);
          in_buffer ("%x%x",red,green);
          blue    = (Uchar) ( 15.0F * db);

          status  = a_proc(a_tag,col+1,row,dr,dg,db)?status:false;
          red     = (Uchar) ( 15.0F * dr);
          in_buffer ("%x%x",blue,red);
          green   = (Uchar) ( 15.0F * dg);
          blue    = (Uchar) ( 15.0F * db);
          in_buffer ("%x%x",green,blue);
        }
      }

    } else if(a_nbit==rgb_8) {
      int nbyte8 = a_width   * 3;
      // 8 bit for r and g and b.
      in_buffer   ("/rgbstr %d string def ",nbyte8);
      in_buffer   ("%d %d %d ",a_width,a_height,8);
      in_buffer   ("[ %d 0 0 -%d 0 %d ] ",a_width,a_height,a_height);
      in_buffer   ("{ currentfile rgbstr readhexstring pop } " );
      in_buffer   ("false 3 " );
      PrintFLN   ("colorimage " );
      for ( row = 0; row < a_height; row++ ) {
        for ( col = 0; col < a_width; col++) {
          status     = a_proc(a_tag,col,row,dr,dg,db)?status:false;
          red        = (Uchar) ( 255.0F * dr);
          WriteByte (red);
          green      = (Uchar) ( 255.0F * dg);
          WriteByte (green);
          blue       = (Uchar) ( 255.0F * db);
          WriteByte (blue);
        }
      }
    } else {
      m_out << "PS_IMAGE :"
            << " unknown rgb nbit " << a_nbit
            << std::endl;
    }
    if(!status) {
      m_out << "PS_IMAGE :"
            << " problem to retrieve some pixel rgb."
            << std::endl;
    }
    PS_RESTORE();
  }

protected:
  static size_t METAFILE_RECORD_LENGTH() {return 80;}

  static float METAFILE_SCALE() {return 1.0f;}

  static char* get_date(){
    char*  string;
    time_t d;
    ::time(&d);
    string = ::ctime(&d);
    string[24] = '\0';
    return string;
  }

  static VCol rgb2grey(VCol a_red,VCol a_green,VCol a_blue){
    return (0.30f * a_red + 0.59f * a_green + 0.11f * a_blue);
  }

  bool in_buffer(const char* a_format,...){
    va_list args;
    va_start(args,a_format);
    bool status = vsprintf(m_string,2048,a_format,args);
    va_end(args);
    if(!status) {
      m_out << "tools::wps::in_buffer : overflow." << std::endl;
      return false;
    }

    size_t length = m_string.size();
    if(length>METAFILE_RECORD_LENGTH()) {
      m_out << "tools::wps::in_buffer : overflow." << std::endl;
      return false;
    }

    size_t nlength = m_number + length;
    if(nlength>METAFILE_RECORD_LENGTH()) {
      m_buffer[m_number] = '\0';
      if(::fprintf(m_file,"%s\n",(char*)m_buffer)<0) {
        m_out << "tools::wps::in_buffer : fprintf failed." << std::endl;
      }
      m_number = 0;
      nlength = length;
    }
    Uchar* pointer = m_buffer + m_number;
    ::strcpy((char*)pointer,m_string.c_str());
    m_number = nlength;
    return true;
  }

  bool PrintFLN(const char* a_format,...){
    va_list args;
    va_start(args,a_format);
    bool status = vsprintf(m_string,2048,a_format,args);
    va_end(args);
    if(!status) {
      m_out << "tools::wps::PrintFLN : overflow." << std::endl;
      return false;
    }

    // put buffer in file :
    if(m_number>0) {
      m_buffer[m_number] = '\0';
      if(::fprintf(m_file,"%s\n",(char*)m_buffer)<0) {
        m_out << "tools::wps::PrintFLN : fprintf failed." << std::endl;
      }
      m_number = 0;
    }
    if(::fprintf(m_file,"%s\n",m_string.c_str())<0) {
      m_out << "tools::wps::PrintFLN : fprintf failed." << std::endl;
    }

    return true;
  }

  void WriteByte(Uchar a_byte){
    Uchar h = a_byte / 16;
    Uchar l = a_byte % 16;
    in_buffer("%x%x",h,l);
  }

protected:
  std::ostream& m_out;
  float         m_deviceWidth;
  float         m_deviceHeight;
  unsigned int  m_pageNumber;
  FILE*         m_file;
  std::string   m_fname;
  std::string   m_string;
  int           m_gsave;
  Uchar*        m_buffer;
  size_t        m_number;
};

}

#endif
