/***************************************************************************
                          SIMPLY POWERFUL TOOLKIT (SPTK)
                          CExcelSheet.cpp  -  description
                             -------------------
    begin                : Mon Jan 19 2004
    copyright            : (C) 2000-2006 by Alexey S.Parshin
    email                : alexeyp@gmail.com
 ***************************************************************************/

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

   This library 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 Library
   General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; if not, write to the Free Software Foundation, 
   Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.

   Please report all bugs and problems to "alexeyp@gmail.com"
 ***************************************************************************/

#include <string.h>
#include <stdio.h>
#include <sptk3/CException.h>
#include <sptk3/excel/CExcelBook.h>
#include <sptk3/excel/CExcelRowRecord.h>
#include <sptk3/excel/CExcelIndex.h>

using namespace std;
using namespace sptk;

#ifdef _DEBUG
// Debug messages level 0..3
#define DEBUG 0
#endif

CExcelSheet::CExcelSheet(const char *name,int pos,CExcelBook *book)
: m_recordsBuffer(1024)
{
   m_parentBook = book;
   m_sheetName = name;
   m_sheetPosition = pos;
   m_BIFFversion = 0;
   m_lastComputedPosition = 0;
   resize(1,1);
}

void CExcelSheet::clearData() {
   m_recordsBuffer.reset();
   for (unsigned i = 0; i < rows.size(); i++) {
      for (unsigned j= 0;  j < rows[i].size(); j++) {
         CExcelCell& cell = rows[i][j];
         cell.setNull();
      }
   }
}

void CExcelSheet::resize(unsigned height,unsigned width) {

   m_columns.resize(width);

   unsigned oldHeight = rows.size();

   if (height > 65535)
      throw CException("Can't have more than 65535 rows in a spreadsheet.\nYou've requested "+int2string(height)+" rows");

   rows.resize(height);

   unsigned oldWidth = rows[0].size();

   int maxLetter = 'Z' - 'A';
   static CStrings columnNames;
   for (unsigned c = columnNames.size(); c < width; c++) {
      int letter1 = c / maxLetter;
      int letter2 = c % maxLetter;
      string fieldName;
      if (letter1)
         fieldName += 'A' + letter1;
      fieldName += 'A' + letter2;
      columnNames.push_back(fieldName);
   }

   unsigned firstShortRow = 0;
   if (oldWidth == width)
      firstShortRow = oldHeight;

   unsigned i = 0;
   try {
      for (i = firstShortRow; i < height; i++) {
         CExcelRow& excelRow = rows[i];
         excelRow.resize(width);
      }
   }
   catch (exception& e) {
      printf("Can't resize the row %i: %s\n",i,e.what());
   }
   catch (...) {
      printf("Can't resize the row %i\n",i);
   }
}

int CExcelSheet::setValue(unsigned row,unsigned col,CVariant value,const CExcelCellFormat *format) {
   if (row >= rowCount()) resize(row + 1,colCount());
   if (col >= colCount()) resize(rowCount(),col+1);
   rows[row][col] = CExcelCell(value,format);
   return 1;
}

int CExcelSheet::setRKValue(unsigned row,unsigned col,double number,CVariantType rkType,const CExcelCellFormat *format) {
   const CExcelNumericFormat& dataFormat = format->dataFormat();
   bool generalFormat = true;
   if (&dataFormat) {
      switch (dataFormat.kind()) {
         case VAR_INT:
            generalFormat = false;
            setValue(row,col,int(number),format);
            break;
         case VAR_DATE:
         case VAR_DATE_TIME:
            generalFormat = false;
            setValue(row,col,CDateTime(number),format);
            break;
         default:
            break;
      }
   }

   if (generalFormat) {
      switch (rkType) {
         case VAR_INT:
            setValue(row,col,int(number),format);
            break;
         default:
            setValue(row,col,number,format);
            break;
      }
   }
   return 1;
}

char * CExcelSheet::format_double(double d,int ixf) {
   int ifmt = m_parentBook->xfRecords[ixf].formatIndex();
   CVariantType format = m_parentBook->numericFormat(ifmt).kind();
   if (format & VAR_DATE || format & VAR_DATE_TIME) {
      CDateTime   dt(d);
      std::string     s;
      if ( dt.date() ) // append date
         s += dt.dateString();
      if ( (double)dt != (int)dt ) { // append time
         if (!s.empty()) {
            s += " ";
         }
         s += dt.timeString();
      }
      strcpy(m_format_buffer,s.c_str());
   } else {
      sprintf(m_format_buffer,"%.10g",d);
   }
   return m_format_buffer;
}

void CExcelSheet::process_item (int rectype, int reclen, char *rec) {
   unsigned short    row = 0, col = 0, len, ixf;
   unsigned          sst;
   double            number;
   unsigned short    firstColumn, lastColumn;
   char             *ptr;
   CVariantType      rkType;

   switch (rectype) {

      case BOF:
         m_BIFFversion = CExcelRecord::getshort(rec,0);
         break;

      case DIMENSIONS:
         {
            unsigned maxrow = 0;
            unsigned maxcol = 0;
            if (m_BIFFversion >= 0x600 && reclen > 10) {
               maxrow = CExcelRecord::getint(rec,4);
               maxcol = CExcelRecord::getshort(rec,10);
            } else {
               maxrow = CExcelRecord::getint(rec,2);
               maxcol = CExcelRecord::getshort(rec,6);
            }
            if (maxrow > 0) maxrow--;
            if (maxcol > 0) maxcol--;
            if (maxrow > 0 && maxcol > 0)
               resize(maxrow,maxcol);
         }
         break;

      case LABEL:
         {
            row = CExcelRecord::getshort(rec,0);
            col = CExcelRecord::getshort(rec,2);
            ixf = CExcelRecord::getshort(rec,4);
            len = CExcelRecord::getshort(rec,6);
            const CExcelCellFormat& format = m_parentBook->xfRecords[ixf];
            ptr = rec + 8;
            if (len && *ptr == 0)
               ptr++;
            char ch = ptr[len];
            ptr[len] = 0;
            setValue(row,col,ptr,&format);
            ptr[len] = ch;
         }
         break;

      case LABELSST:
         {
            row = CExcelRecord::getshort(rec,0);
            col = CExcelRecord::getshort(rec,2);
            ixf = CExcelRecord::getshort(rec,4);
            sst = CExcelRecord::getint(rec,6);
            if (sst < m_parentBook->sharedStringTable.size()) {
               const char *str = m_parentBook->sharedStringTable[sst].c_str();
               const CExcelCellFormat& format = m_parentBook->xfRecords[ixf];
               setValue(row,col,str,&format);
            } else {
               throw CException("The SST index is out of range, SST="+int2string(sst)+"\n");
            }
         }
         break;

      case RK:
         {
            row = CExcelRecord::getshort(rec,0);
            col = CExcelRecord::getshort(rec,2);
            ixf = CExcelRecord::getshort(rec,4);
            number = CExcelRecord::RK2double(CExcelRecord::getlong(rec,6),rkType);
            const CExcelCellFormat& format = m_parentBook->xfRecords[ixf];
            setRKValue(row,col,number,rkType,&format);
         }
         break;

      case MULRK:
         {
            row = CExcelRecord::getshort(rec,0);
            firstColumn = CExcelRecord::getshort(rec,2);
            lastColumn = CExcelRecord::getshort(rec,reclen - 2);
            int offset = 4;
            for (col = firstColumn; col <= lastColumn; col++) {
               ixf = CExcelRecord::getshort(rec,offset);
               offset += 2;
               number = CExcelRecord::RK2double(CExcelRecord::getlong(rec,offset),rkType);
               const CExcelCellFormat& format = m_parentBook->xfRecords[ixf];
               setRKValue(row,col,number,rkType,&format);
               offset += 4;
            }
         }
         break;

      case NUMBER:
         {
            row = CExcelRecord::getshort(rec,0);
            col = CExcelRecord::getshort(rec,2);
            ixf = CExcelRecord::getshort(rec,4);
            number = CExcelRecord::getdouble(rec,6);
            const CExcelCellFormat& format = m_parentBook->xfRecords[ixf];
            setValue(row,col,number,&format);
         }
         break;

      case FORMULA1:
      case FORMULA2:
         row = CExcelRecord::getshort(rec,0);
         col = CExcelRecord::getshort(rec,2);
         if (((unsigned char)rec[12]==0xFF)&&(unsigned char)rec[13]==0xFF) {
            // not a floating point value
            if (rec[6]==1) {
               // boolean
               char buf[2]="0";
               buf[0]+=rec[9];
               setValue(row,col,buf,0L);
            } 
            else if (rec[6]==2) {
               // error
               char buf[8];
               switch ( rec[8] ) {
                  case 0x00:
                     strncpy(buf,"#NULL!",8);
                     break;
                  case 0x07:
                     strncpy(buf,"#DIV/0!",8);
                     break;
                  case 0x0f:
                     strncpy(buf,"#VALUE!",8);
                     break;
                  case 0x17:
                     strncpy(buf,"#REF!",8);
                     break;
                  case 0x1d:
                     strncpy(buf,"#NAME?",8);
                     break;
                  case 0x24:
                     strncpy(buf,"#NUM!",8);
                     break;
                  case 0x2a:
                     strncpy(buf,"#N/A!",8);
                     break;
                  default:
                     strncpy(buf,"#ERR",8);
               }

               setValue(row,col,buf,0L);
            } 
            else if (rec[6]==0) {
                  // saved_reference=prow->cells+col;
            }
         } else {
            //setText(row,col,format_double(reorderBytes(*(double *)(rec+6)),0));
            setValue(row,col,CExcelRecord::getdouble(rec,6),0L);
         }
         break;			

      case STRING:
         {
            len = CExcelRecord::getshort(rec,0);
            ptr = rec + 2;
            char ch = ptr[len];
            ptr[len] = 0;
            setValue(row,col,ptr,0L);
            ptr[len] = ch;
         }
         break;
   }
}

int CExcelSheet::parseWorkSheet(CBuffer& excelData,unsigned& pos) {
   short rectype, reclen;

   bool BOFfound = false;
   unsigned endpos = excelData.bytes();
   char *data = excelData.data();
   //unsigned bofpos = pos;
   while (pos < endpos) {
      rectype = CExcelRecord::getshort(data,pos);
      if (rectype == 0x809) {
         BOFfound = true;
         break;
      }
      pos += 4;
   }

   if (!BOFfound)
      return 0;

#if DEBUG > 1
   FILE *logFile = fopen("tmp/"+name()+"("+m_parentBook->name()+").log","wt");
#endif

   std::string operation("Reading sheet "+name());

   int   started = 0;
   char *curr = 0L;

   int   percentage = -1;
   while (pos < endpos) {
      rectype = CExcelRecord::getshort(data,pos);
      reclen  = CExcelRecord::getshort(data,pos+2);
      pos += 4;

      if (!started) {
         if (rectype != BOF) return 0;
         started = 1;
      }

      if ( rectype == MSEOF ) break;

      int perc = (pos * 100) / endpos;

      if (perc != percentage) {
         percentage = perc;
         if (m_parentBook)
            m_parentBook->do_callback(operation.c_str(),percentage);
      }


#if DEBUG > 1
      if (logFile) {
         fprintf(logFile,"%06x (%04x) %-15s \treclen = %-3i ",pos,rectype,CExcelRecord::gettypename(rectype),reclen);
         for (int b = 0; b < reclen; b++) {
            unsigned char ch = data[pos+b];
            fprintf(logFile," %02x",ch);
         }
         fprintf(logFile,"\n");
      }
#endif

      if ( reclen >= 0 ) {

         curr = data + pos;
         pos += reclen;
      } else {
         char error[256];
         sprintf(error,"Record size is negative, record @ offset %06x",unsigned(pos-4));
         throw CException(error,__FILE__,__LINE__);
      }

      process_item(rectype,reclen,curr);
   }

#if DEBUG > 1
   if (logFile)
      fclose(logFile);
#endif

   return 1;
}

void CExcelSheet::encode(unsigned sheetStreamPosition) {
   m_recordsBuffer.reset();
   CExcelRecordsBuffer& records = m_recordsBuffer;

   // BOF record - the beginning of the sheet
   records.append(BOF,16,"\x0\x6\x10\x0\xBB\xD\xCC\x7\x49\x0\x0\x0\x6\x0\x0\x0");

   // INDEX record. Actual indices to DBCELL records must be filled in later
   CExcelIndex *indexRecord = new CExcelIndex(&rows);
   unsigned indexRecordPosition = records.bytes();
   records.append(INDEX,indexRecord->bytes(),indexRecord->data());

   records.append(CALCMODE,2,"\x1\x0");
   records.append(CALCCOUNT,2,"\x64\x0");
   records.append(REFMODE,2,"\x1\x0");
   records.append(ITERATION,2,"\x0\x0");
   records.append(DELTA,8,"\xFC\xA9\xF1\xD2\x4D\x62\x50\x3F");
   records.append(SAVERECALC,2,"\x1\x0");
   records.append(PRINTHEADERS,2,"\x0\x0");
   records.append(PRINTGRIDLINES,2,"\x0\x0");
   records.append(GRIDSET,2,"\x1\x0");
   records.append(GUTS,8,"\x0\x0\x0\x0\x0\x0\x0\x0");
   records.append(DEFAULTROWHEIGHT,4,"\x0\x0\xFF\x0");
   records.append(WSBOOL,2,"\xC1\x04");
   records.append(HEADER,0,"");
   records.append(FOOTER,0,"");
   records.append(HCENTER,2,"\x0\x0");
   records.append(VCENTER,2,"\x0\x0");
   records.append(SETUP,34,"\x00\x00\xff\x00\x01\x00\x01\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x3f\x00\x00\x00\x00\x00\x00\xe0\x3f\x4e\x4a");

   unsigned defColWidthPosition = records.bytes() + sheetStreamPosition;
   records.append(DEFCOLWIDTH,2,"\x8\x0");

   unsigned rowCount = rows.size();

   // BIFF8:12 BIFF7:8 ???
   char *ptr = new char[15];
   memset(ptr,0,14);
   CExcelRecord::setint(ptr,0,0);                   // First defined row
   CExcelRecord::setint(ptr,4,rowCount);            // Last defined row + 1
   CExcelRecord::setshort(ptr,8,0);                 // First defined column
   unsigned colCount = 0;
   if (rowCount)
      colCount = rows[0].size();
   CExcelRecord::setshort(ptr,10,colCount);         // Last defined column + 1

   records.append(DIMENSIONS,14,ptr);
   delete ptr;

   m_columns.saveRecords(records);

   if (!m_parentBook)
      throw CException("The sheet '"+m_sheetName+"' doesn't have parent book.",__FILE__,__LINE__);

   CExcelSST& sst = m_parentBook->sharedStringTable;

   indexRecord->saveRecords(records,sst,sheetStreamPosition,defColWidthPosition);

   ptr = new char[19];
   memset(ptr,0,18);
   ptr[0] = char(0xB6);
   ptr[1] = char(0x06);
   ptr[6] = char(0x40);
   records.append(WINDOW2,18,ptr);
   delete ptr;

   ptr = new char[16];
   memset(ptr,0,15);
   ptr[0] = char(0x3);
   ptr[7] = char(0x1);
   records.append(SELECTION,15,ptr);
   delete ptr;

   // Copy updated index record buffer to the records ..
   memcpy(records.data()+indexRecordPosition+4,indexRecord->data(),indexRecord->bytes());

   delete indexRecord;

   records.append(MSEOF,0,"");
}

static char *trimRight(char *s) {
   int l = strlen(s);
   for (; l >= 0; l--) {
      if ((unsigned char)s[l] > 32) break;
   }
   s[l+1] = 0;
   return s;
}

int CExcelSheet::readCSVFile(std::string fileName) {
   CBuffer buffer;
   buffer.loadFromFile(fileName.c_str());
   unsigned fileLength = buffer.bytes();

   unsigned row = 0;
   unsigned sourceLine = 0;
   unsigned firstRowColumns = 0;
   int percentage = -1;
   char *p = buffer.data();
   char *lineEnd = strchr(buffer.data(),'\n');
   if (lineEnd) *lineEnd = 0;
   while (p) {
      unsigned col = 0;
      bool finished = false;

      unsigned offset = p - buffer.data();
      int perc = offset * 100 / fileLength;

      if (percentage != perc) {
         percentage = perc;
         if (m_parentBook)
            m_parentBook->do_callback("Reading CSV file",perc);
      }

      if (*p == '\r') p++;
      char *ptr = p;

      do {
         bool quotesInside = false;
         if (*ptr == '"') {
            ptr++;
            p = strchr(ptr,'"');
            while (p && *(p+1) == '"') {
               quotesInside = true;
               p = strchr(p+2,'"');
            }
            while (!p && lineEnd) {
               *lineEnd = '\n';
               lineEnd = strchr(lineEnd+1,'\n');
               if (lineEnd) *lineEnd = 0;
               sourceLine++;
               p = strchr(ptr,'"');
               while (p && *(p+1) == '"') {
                  quotesInside = true;
                  p = strchr(p+2,'"');
               }
            }
            if (!p)
               throw CException("Error in " + fileName + "(" + int2string(sourceLine) + "). Quote is missing.");
            *p = 0;
            p++;
            if (*p != ',')
               finished = true;
         } else {
            p = strchr(ptr,',');
         }

         if (!p || *p == 0) {
            finished = true;
         } else {
            *p = 0;
         }

         trimRight(ptr);

         if (quotesInside) {
            std::string s(ptr);
            replaceAll(s,"\"\"","\"");
            setValue(row,col,s.c_str(),0L);
         } else {
            setValue(row,col,ptr,0L);
         }

         ptr = p + 1;
         col++;
      } while (!finished);

      if (row == 0)
         firstRowColumns = col;
      else if (col > 1) { 
         if (col > firstRowColumns)
            throw CException("Error in " + fileName + "(" + int2string(sourceLine) + "). Number of columns("+int2string(col)+") greater than it is in the first row.");
         while (col < firstRowColumns) {
            setValue(row,col,"",0L);
            col++;
         }
      }

      row++;

      if (!lineEnd)
         break;
      p = lineEnd+1;

      lineEnd = strchr(lineEnd+1,'\n');
      if (lineEnd) *lineEnd = 0;

      sourceLine++;
   }

   if (m_parentBook)
      m_parentBook->do_callback("Completed",100);

   return true;
}

int CExcelSheet::writeCSVFile(std::string fileName) {
   FILE *outputFile = fopen(fileName.c_str(),"w+t");
   if (!outputFile)
      throw CException("Can't open file "+fileName + "for writing.");

   int rc = writeCSVFile(outputFile);

   fclose(outputFile);

   return rc;
}

int CExcelSheet::writeCSVFile(FILE *outputFile) {
   unsigned rowCount = rows.size();
   if (!rowCount) return false;

   unsigned colCount = rows[0].size();
   if (!colCount) return false;

   for (unsigned r = 0; r < rowCount; r++) {
      CExcelRow& arow = rows[r];
      fprintf(outputFile,"\"%s\"",arow[0].asString().c_str()); 
      for (unsigned c = 1; c < colCount; c++) {
         fprintf(outputFile,",\"%s\"",arow[c].asString().c_str()); 
      }
      fprintf(outputFile,"\n");
   }

   return true;
}

void CExcelSheet::format(const CExcelCellFormat *format,unsigned row1,unsigned col1,unsigned row2,unsigned col2) { 
   if (!rows.size()) return;
   if (!rows[0].size()) return;

   if (row2 >= rows.size()) row2 = rows.size() - 1;
   if (col2 > rows[0].size()) col2 = rows[0].size() - 1;

   for (unsigned r = row1; r <= row2; r++) {
      CExcelRow& arow = rows[r];
      for (unsigned c = col1; c <= col2; c++)
         arow[c].format(format); 
   }
}

void CExcelSheet::updatePositions() {
   unsigned cnt = rows.size();
   if (cnt < 2) return;
   unsigned position = rows[m_lastComputedPosition].m_position + rows[m_lastComputedPosition].m_height;
   for (unsigned i = m_lastComputedPosition + 1; i < cnt; i++) {
      CExcelRow& row = rows[m_lastComputedPosition];
      row.m_position = position;
      position += row.m_height;
   }
   m_lastComputedPosition = cnt - 1;
}

void CExcelSheets::clear() {
   for (iterator itor = begin(); itor != end(); itor++) 
      delete *itor;
   vector<CExcelSheet *>::clear();
}
