/** 
 *  Yudit Unicode Editor Source File
 *
 *  GNU Copyright (C) 1997-2023  Gaspar Sinai <gaspar@yudit.org>  
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License, version 2,
 *  dated June 1991. See file COPYYING for details.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
/**
 * @author: Gaspar Sinai <gaspar@yudit.org>
 * @version: 2020-04-12
 * This is the abstract widget toolkit
 */

#include "stoolkit/SExcept.h"
#include "stoolkit/SEvent.h"
#include "stoolkit/SString.h"
#include "stoolkit/SUtil.h"
#include "stoolkit/SStringVector.h"
#include "stoolkit/SBinHashtable.h"
#include "swindow/SAwt.h"
#include "swindow/SGEngine.h"
#include "swindow/SWindow.h"
#include "swindow/SRedrawEvent.h"

#include "swindow/sosx/SOSX.h"
#include "swindow/sosx/SEventOSX.h"
#include "swindow/sosx/SExtern.h"


#include <sys/types.h>

#include <time.h>

#include <stdio.h>

typedef SBinHashtable<SOSXWindow*> SWindowHashtable;
typedef SBinHashtable<SWindowListener*> SListenerHashtable;

// FIXME not used.
static SWindowHashtable windowHashtable;
static SListenerHashtable listenerHashtable;

static bool winOK = true;

static bool bitmapCacheOn=true;

static SEventOSX * seventHandlerOSX = 0; 

SOSXImpl::SOSXImpl()
{
  if (!seventHandlerOSX) {
    seventHandlerOSX = new SEventOSX();
    SEventHandler::setImpl (seventHandlerOSX);
  }
}

SOSXImpl::~SOSXImpl()
{
}

bool
SOSXImpl::isOK()
{
  return winOK;
}

SEncoder impEncoder;
void
SOSXImpl::setEncoding(const SString& str)
{
  SEncoder enc = SEncoder (str);
  if (!enc.isOK())
  {
    fprintf (stderr, "SOSX clipboard encoder `%*.*s' unknown\n", 
      SSARGS(str));
  }
  else
  {
    encoder = enc;
    impEncoder = enc;
  }
}

SWindow*
SOSXImpl::getWindow (SWindowListener* l, const SString& name)
{
  char* cname = name.cString();
  void* w = cocoaCreateView(cname);
  delete[] cname;
  CHECK_NEW (w);
  
  SOSXWindow* sw = new SOSXWindow (name, this, w);
  CHECK_NEW (sw);

  // TODO:
  // FIXME FIRST
  listenerHashtable.put (w, l);
  sw->listener = l;
  cocoaSetPosition(sw->cocoaWindow, sw->getPositionX(), sw->getPositionY());
  cocoaSetSize(sw->cocoaWindow, sw->getWidth(), sw->getHeight());
  cocoaSetCPPWindow (w, sw);
  return sw;
}

SOSXWindow::SOSXWindow(const SString& n, SOSXImpl* i, void* _id) 
  : background ("white"), pen (SColor(0), SColor(0xffffffff)), name(n)
{
  modalFlag = false;
  xpos = SD_WIN_X;
  ypos = SD_WIN_Y;
  xwidth = SD_WIN_W;
  yheight = SD_WIN_H;

  isCacheOn = true;

  currentFocusWindow = 0;
  shown = false;
  engine = 0;
  imname = "";
  impl = i;
  id = (long) _id;
  cocoaWindow = _id;
  modalID = 0;
  parentID = 0;
  windowHashtable.put (id, this);
}

SOSXWindow::~SOSXWindow()
{
  if (engine) delete engine;
  windowHashtable.remove (id);
  listenerHashtable.remove (id);
}

void
SOSXWindow::show ()
{
  cocoaShow(cocoaWindow);
  shown = true;
}

bool
SOSXWindow::isVisible ()
{
  return shown;
}

void
SOSXWindow::hide ()
{
  cocoaHide(cocoaWindow);
  shown = false;
}

/**
 * This requests a redraw, efficiently after all events got processed.
 * @param clear is true if the window needs to be cleared before calling redraw.
 * @param x is the x origin of the event
 * @param y is the y origin of the event
 * @param width is the width of the event
 * @param height is the height of the event
 */
void
SOSXWindow::redraw (bool clear, int _x, int _y, unsigned int _width, unsigned int _height)
{
  if (_x+(int)_width  < 0 || _y + (int) _height < 0)
  {
    return;
  }
  if (_x  > (int)getWidth() || _y > (int)getHeight())
  {
    return;
  }
  // FIXME:
  cocoaNeedsDisplay (cocoaWindow);
}

/**
 * Reparent the window. 
 * TODO: move it to x y
 * @param p is the parent window
 */
void
SOSXWindow::setParent (SWindow* p, int x, int y)
{
  cocoaSetParent (cocoaWindow, ((SOSXWindow*)p)->cocoaWindow);
  parentID = ((SOSXWindow *)p)->id;
  cocoaSetPosition(cocoaWindow, x, y);
}

void
SOSXWindow::resize (unsigned int _width, unsigned int _height)
{
  if (getWidth() == _width && getHeight() == _height) return;
  cocoaSetSize(cocoaWindow, _width, _height);
}

void
SOSXWindow::resized (unsigned int _width, unsigned int _height) {
  setSize(_width, _height);
  if (!parentID)
  {
     SWindowListener* l = listenerHashtable.get (cocoaWindow);
     //if (getWidth() == 600 && getHeight() == 400)
     l->resized(this, getPositionX(), getPositionY(), getWidth(), getHeight());
//
//     fprintf (stderr, "Parent resized %d %d %u %u\n",
//        getPositionX(), getPositionY(), getWidth(), getHeight());
  } else {
  }
}

void
SOSXWindow::move (int _x, int _y)
{
    // Window manager might have moved the window.
  if (getPositionX() == _x && getPositionY() == _y && parentID != 0) return;
  cocoaSetPosition(cocoaWindow, _x, _y);
}

void
SOSXWindow::moved (int _x, int _y) {
  setPosition(_x, _y);
  if (!parentID)
  {
     SWindowListener* l = listenerHashtable.get (cocoaWindow);
     //if (getWidth() == 600 && getHeight() == 400)
     l->resized(this, getPositionX(), getPositionY(), getWidth(), getHeight());
 //    fprintf (stderr, "Parent moved %d %d %u %u\n",
  //      getPositionX(), getPositionY(), getWidth(), getHeight());
  }
}


void
SOSXWindow::setTitle (const SString& title)
{
  name = title;
  char* cname = name.cString();
  cocoaSetTitle(cocoaWindow, cname);
  delete[] cname;
}

void
SOSXWindow::setBackground(const SColor &color)
{
//  SColor color (1.0,1.0,0.48, 1.0);
  
/*
  fprintf (stderr, "SOSXWindow::setBackground %lu color=%u,%u,%u,%u\n", 
    cocoaWindow,
    color.red, color.green, color.blue, color.alpha);
*/
  background = color;
  cocoaSetBackground (cocoaWindow, color.red, color.green, color.blue, color.alpha);
  pen = SPen(pen.getForeground(), background, pen.getLineWidth());
}

/**
 * Fill a solid rectangle
 * @param x is the upper left corner
 * @param y is the upper top corner
 * @param width is the width of the region to fill
 * @param height is the height of the region to fill
 */
void
SOSXWindow::bitfill (const SColor& bg, int _x, int _y, 
 unsigned int _width, unsigned int _height)
{
  cocoaBitfill (cocoaWindow, bg, _x, _y, _width, _height);
}

/**
 * Draw a solid line.
 * @param x is the starting x point
 * @param y is the starting y point
 * @param x is the ending non-exclusive  x point
 * @param y is the ending non-exclusive  y point
 */
void
SOSXWindow::bitline (const SColor& fg, int _x, int _y, int _tox, int _toy)
{
  // TODO
  cocoaBitline (cocoaWindow, fg, _x, _y, _tox, _toy);
}

/**
 * Draw a solid point.
 * @param x is the x point
 * @param y is the y point
 */
void
SOSXWindow::bitpoint (const SColor& clr, int _x, int _y)
{
  // TODO
  cocoaBitpoint (cocoaWindow, clr, _x, _y);
}

void
SOSXWindow::bitpoints (const SColor& clr, const int* _x, const int* _y, 
         unsigned int _size)
{
  // TODO
  cocoaBitpoints (cocoaWindow, clr, _x, _y, _size);
}

/**
 * After this size things wont be cached.
 */
void
SOSXWindow::setPixmapCacheSize(unsigned int _size)
{
    // TODO
  //cacheSize = _size;
}
 
/**
 * turn on/off the cache and clear it
 */
void
SOSXWindow::setPixmapCacheOn (bool _on)
{
  // FIXME
  bitmapCacheOn=_on;
}

/**
 * This one can return false if it fails.
 */
void
SOSXWindow::putImage (int _x, int _y, const SImage& im)
{
  void * cocoaImage = cocoaCreateImage (cocoaWindow, im);
  cocoaDrawImage (cocoaWindow, _x, _y, cocoaImage);
  return;
}

#define SS_SHADING_COLORS (SD_OVERSAMPLE * SD_OVERSAMPLE +1)
#define SS_DOUBLE_SCAN 1

/**
 * Drawing routines inherited from SCanvas
 */
bool
SOSXWindow::beginImage (double _x, double _y, const SString& _id, const SColor& background)
{
  if (engine ==0) engine = new SGEngine();
  SString key;
  for (unsigned int i=0; i<_id.size(); i++) {
     unsigned char c = _id[i];
     key.append (char(0x30 + (c & 0xf)));
     c = c >> 4;
     key.append (char(0x30 + (c & 0xf)));
  }
  char* cstr = key.cString();
  //fprintf(stderr, "SOSXWindow BeginImage %s\n", cstr);
  const SS_Matrix2D& m = engine->getCurrentMatrix();
  // path caching, only fill will be called.
  bool ret = cocoaBeginImage (cocoaWindow, m.toX(_x, _y), m.toY(_x,_y), cstr);
  delete[] cstr;
  return ret;
}

void
SOSXWindow::newpath ()
{
  cocoaNewPath(cocoaWindow);
}

void
SOSXWindow::endImage ()
{
  cocoaEndImage(cocoaWindow);
}

/**
 * FIXME: fill and return the resulting image for better caching.
 */
void
SOSXWindow::fill (const SPen& _pen)
{
  if (engine ==0) engine = new SGEngine();
  if (pen != _pen)
  {
     pen = _pen;
  }
  cocoaFill (cocoaWindow, pen);
}

void
SOSXWindow::stroke (const SPen& _pen) {
   if (engine ==0) engine = new SGEngine();
   if (engine ==0) engine = new SGEngine();
   pen = _pen;
   cocoaStroke (cocoaWindow, pen);
}

/**
 * Move to a new point
 * This will clear the path and push 3 element-pairs 
 * one is the bounding low, second is bounding high 
 * third is the new coord.
 */
void
SOSXWindow::moveto (double x, double y)
{
   if (engine ==0) return;
   //engine->moveto (x, y);
   const SS_Matrix2D& m = engine->getCurrentMatrix();
   cocoaMoveTo (cocoaWindow, m.toX(x, y), m.toY(x,y));
}

/**
 * The lowest level function to add a new element
 */
void
SOSXWindow::lineto (double x, double y)
{
  if (engine ==0) return;
  const SS_Matrix2D& m = engine->getCurrentMatrix();
  //engine->lineto (x, y);
  cocoaLineTo (cocoaWindow, m.toX(x, y), m.toY(x,y));
}

/** 
 *  Draw a cubic beizer curve
 */
void
SOSXWindow::curveto (double _x0, double _y0, double _x1, 
  double _y1, double _x2, double _y2)
{
  if (engine ==0) return;
  //engine->curveto (_x0, _y0, _x1, _y1, _x2, _y2);
    const SS_Matrix2D& m = engine->getCurrentMatrix();
    cocoaCurveTo (cocoaWindow, 
        m.toX(_x0, _y0), m.toY(_x0, _y0),
        m.toX(_x1, _y1), m.toY(_x1, _y1),
        m.toX(_x2, _y2), m.toY(_x2, _y2));
}

void
SOSXWindow::closepath()
{
  if (engine ==0) return;
  cocoaClosePath (cocoaWindow); 
  //engine->closepath();
}
/**
 * TODO: not implemented 
 */
void
SOSXWindow::rotate (double angle)
{
  if (engine ==0) engine = new SGEngine();
  engine->rotate (angle);
}

void
SOSXWindow::scale (double x, double y)
{
  if (engine ==0) engine = new SGEngine();
  engine->scale (x, y);
}

void
SOSXWindow::translate (double x, double y)
{
  if (engine ==0) engine = new SGEngine();
  engine->translate (x, y);
}

void
SOSXWindow::pushmatrix()
{
  if (engine ==0) engine = new SGEngine();
  engine->pushmatrix();
}

void
SOSXWindow::popmatrix()
{
  if (engine ==0) engine = new SGEngine();
  engine->popmatrix();
}

/**
 * Clear a region (set it to the background)
 * This should work with a clipped region.
 * @param x is the upper left corner
 * @param y is the upper top corner
 * @param width is the width of the region to clear
 * @param height is the height of the region to clear
 */
void
SOSXWindow::clear (int _x, int _y, unsigned int _width, unsigned int _height)
{
  bitfill (background, _x, _y, _width, _height);
}

/**
 * Copy an area on the window to another area.
 * overlap is ok.
 * @param x is the upper left corner
 * @param y is the upper top corner
 * @param width is the width of the region to copy
 * @param height is the height of the region to copy
 * @param tox is the destination left corner
 * @param toy is the destination top corner
 */
void
SOSXWindow::copy (int _x, int _y, unsigned int _width, unsigned int _height, 
  int _tox, int _toy)
{
  // FIXME
}

/**
 * Assign a rectangualr clip area. Everything outside this area will be clipped.
 */
void
SOSXWindow::setClippingArea (int _x, int _y, unsigned int _width, unsigned int _height)
{
  cocoaSetClippingRectangle (cocoaWindow, _x, _y, _width, _height);
}

/**
 *  clear the clipping area.
 */
void
SOSXWindow::removeClippingArea ()
{
  cocoaRemoveClippingRectangle (cocoaWindow);
}

/**
 */
void
SOSXWindow::wait ()
{
    cocoaWait (cocoaWindow);
}

/* isPrimary unused - all true */
SString
SOSXWindow::getClipUTF8(bool isPrimary)
{
   return SString(cocoaGetClipboard ());
}

/* isPrimary unused - all true */
void
SOSXWindow::putClipUTF8(const SString& utf8, bool isPrimary)
{
  name = utf8;
  char* cname = name.cString();
  cocoaPutClipboard(cocoaWindow, cname);
  delete[] cname;
}

/*------ drag and drop --------*/
void
SOSXWindow::setDroppable (const SStringVector& targets) {
// FIXME
  char** list = new char*[targets.size()+1];
// droppable[0] is text/uri-list
// droppable[1] is text/plain
  int count=0;
  for (int i=0; i<targets.size(); i++) {
     if (targets[i] == "text/uri-list") {
        list[count++] = targets[i].cString();
        continue;
     }
     if (targets[i] == "text/plain") {
        list[count++] = targets[i].cString();
        continue;
     }
     fprintf (stderr, "SOSXWindow:setDroppable mime type %*.*s not supported\n",  SSARGS(targets[i]));
  }
  list[count] = 0;
  if (count > 0) {
    cocoaRegisterDroppable (cocoaWindow, list);
  }
  count = 0;
  while (list[count]) {
     delete[] list[count];
     list[count++] = 0;
  }
  delete[] list;
}

void
SOSXWindow::setModal (SWindow* _parent, bool decorated)
{
   /* CHECKME */
   modalFlag = true;
   cocoaSetModal (cocoaWindow, ((SOSXWindow*)_parent)->cocoaWindow, decorated);
}

/**
 * put this window in the middle
 */
void
SOSXWindow::center (SWindow* _window)
{
  cocoaCenter (cocoaWindow, 
     _window==0 ? 0 : ((SOSXWindow*)_window)->cocoaWindow);
}

void
SOSXWindow::getKeyboardFocus ()
{
    cocoaGetKeyboardFocus (cocoaWindow);
}

void
SOSXWindow::setMinimumSize (unsigned int _width, unsigned int _height)
{
    cocoaSetMinimumSize (cocoaWindow, _width, _height);
}

/**
 * add and remove keyboard accelerator
 */
void
SOSXWindow::addAccelerator (const SAccelerator& a, SAcceleratorListener* l)
{
    cocoaAddAccelerator (cocoaWindow, a, l);
}

void
SOSXWindow::removeAccelerator (const SAccelerator& a, SAcceleratorListener* l)
{
    cocoaRemoveAccelerator (cocoaWindow, a, l);
}

/**
 * Start a native input method.
 * @param name is the name of the input method:
 *  like "kinput2"
 * @param properties provide some attributes to the input method.
 */
bool
SOSXWindow::startInputMethod (const SString& name, const SProperties& prop, SPreEditor* preEditor )
{
  if (name == "x-none" || name == "x-ascii" || name == "x-utf-8")
  {
    if (imname == name) return true;
    stopInputMethod();
    imname = name;
    return true;
  }
  if (name != "_JAPANESE_CONVERSION") {
    fprintf (stderr, "Use Kinput on mac instead of %*.*s\n", SSARGS(name));
    return false;
  }
  // TODO
  imname = name;
  cocoaStartMacKeyboard(cocoaWindow, preEditor);
  return true;
}

void
SOSXWindow::stopInputMethod ()
{
  // TODO
  /* stop previous one */
  if (imname.size())
  {
    cocoaStopMacKeyboard(cocoaWindow);
  }
  imname = "";
}

/**
 * Change properties of the input method on the fly.
 * @param prop contains properties like:
 * InputStyle: root over-the-spot off-the-spot
 */
void
SOSXWindow::setInputMethodProperties (const SProperties& properties)
{
   // FIXME: mac provides an excellent editor based input method.
/* TODO
  if (!isVisible()) return;
  HIMC himc = ImmGetContext((HWND)id);
  if (!himc) return;
*/

  if (properties.get ("InputStyle")==0)
  {
    fprintf (stderr, "InputStyle is not present in properties.\n");
    return;
  }

  SString s = properties["InputStyle"];

  /* ok. now I can tell you windows can not set InputStyle sorry */
   

  if (properties.get ("LineSpacing"))
  {
    SString lsp = properties["LineSpacing"];
    lsp.append ((char)0);
    int spacing;
    sscanf (lsp.array(), "%d", &spacing);
  }

  if (properties.get ("InputClientColor"))
  {
    SString col = properties["InputClientColor"];
    col.append ((char)0);
    unsigned long bg, fg;
    sscanf (col.array(), "%lu,%lu", &bg, &fg);
    SColor xbg = SColor((SS_WORD32)bg);
    SColor xfg = SColor((SS_WORD32)fg);
  }

  /* XXX: no idea how to do this... */
  if (s == "preedit-over-status-under" 
       && properties.get ("InputSpot")
       && properties.get ("InputStatusLocation")
       && properties.get ("InputStatusSize")
     )
  {
    SString spotLocation = properties["InputSpot"];
    spotLocation.append ((char)0);
    int _x, _y;
    sscanf (spotLocation.array(), "%d,%d", &_x, &_y);
/* TODO
    COMPOSITIONFORM form;
    form.dwStyle = CFS_POINT;
    form.ptCurrentPos.x = _x;
    form.ptCurrentPos.y = _y;
    form.rcArea.left = 0;
    form.rcArea.top = 0;
    form.rcArea.right =  (int) getWidth();
    form.rcArea.bottom = (int) getHeight();
    ImmSetCompositionWindow (himc, &form);
*/

    SString sl = properties["InputStatusLocation"];
    sl.append ((char)0);
    int statusX, statusY;
    sscanf (sl.array(), "%d,%d", &statusX, &statusY);

    SString ss = properties["InputStatusSize"];
    ss.append ((char)0);
    int statusWidth, statusHeight;
    sscanf (ss.array(), "%d,%d", &statusWidth, &statusHeight);

/* TODO
    POINT point;
    point.x = statusX;
    point.y = statusY;
    ImmSetStatusWindowPos(himc, &point);
*/
  }
  else if (s == "preedit-under-status-under" 
       && properties.get ("InputSpot")
       && properties.get ("InputStatusLocation")
       && properties.get ("InputStatusSize")
       && properties.get ("InputClientLocation")
       && properties.get ("InputClientSize")
       )
  {

    SString spotLocation = properties["InputSpot"];
    spotLocation.append ((char)0);
    int _x, _y;
    sscanf (spotLocation.array(), "%d,%d", &_x, &_y);

    SString sl = properties["InputStatusLocation"];
    sl.append ((char)0);
    int statusX, statusY;
    sscanf (sl.array(), "%d,%d", &statusX, &statusY);

    SString ss = properties["InputStatusSize"];
    ss.append ((char)0);
    int statusWidth, statusHeight;
    sscanf (ss.array(), "%d,%d", &statusWidth, &statusHeight);

    SString cl = properties["InputClientLocation"];
    cl.append ((char)0);
    int clientX, clientY;
    sscanf (cl.array(), "%d,%d", &clientX, &clientY);

    SString cs = properties["InputClientSize"];
    cs.append ((char)0);
    int clientWidth, clientHeight;
    sscanf (cs.array(), "%d,%d", &clientWidth, &clientHeight);

/* TODO
    POINT point;
    point.x = statusX;
    point.y = statusY;
    ImmSetStatusWindowPos(himc, &point);

    COMPOSITIONFORM form;
    form.dwStyle = CFS_RECT;
    form.ptCurrentPos.x = clientX; // starting from.
    form.ptCurrentPos.y = clientY;
    form.rcArea.left = clientX;    // next line
    form.rcArea.top = clientY;
    form.rcArea.right =  clientX +  clientWidth;
    form.rcArea.bottom =  clientY + clientHeight;
    ImmSetCompositionWindow (himc, &form);
*/
  }
  else if (s == "preedit-root-status-root")
  {
/* TODO
    COMPOSITIONFORM form;
    form.dwStyle = CFS_DEFAULT;
    form.ptCurrentPos.x = (int) getWidth();
    form.ptCurrentPos.y = (int) getHeight();
    form.rcArea.left = 0;
    form.rcArea.top = 0;
    form.rcArea.right =  (int) getWidth();
    form.rcArea.bottom = (int) getHeight();
    ImmSetCompositionWindow (himc, &form);
*/
  }
  else if (s == "preedit-over-status-over" && properties.get ("InputSpot"))
  {
    SString spotLocation = properties["InputSpot"];
    spotLocation.append ((char)0);
    int _x, _y;
    sscanf (spotLocation.array(), "%d,%d", &_x, &_y);
/*TODO
    COMPOSITIONFORM form;
    form.dwStyle = CFS_POINT;
    form.ptCurrentPos.x = _x;
    form.ptCurrentPos.y = _y;
    form.rcArea.left = 0;
    form.rcArea.top = 0;
    form.rcArea.right =  (int) getWidth();
    form.rcArea.bottom = (int) getHeight();
    ImmSetCompositionWindow (himc, &form);
*/
  }
}

/**
 * Get the current input method.
 * it returns a zero sized string if input method is not started.
 */
SString
SOSXWindow::getInputMethod ()
{
  return SString(imname);
}

unsigned long
SOSXWindow::getWindowID() const
{
  return (unsigned long) id;
}

// FIXME
void
SOSXWindow::setDoubleBuffer (bool isOn)
{
}

bool
SOSXWindow::isDoubleBufferEnabled () const
{
    // TODO: it is 
  return true;
}

void
SOSXWindow::setVisible (bool visible) {
    if (visible != shown) {
        if (!parentID) {
            cocoaCountWindows(cocoaWindow, visible?1:-1);
        }
    }
    shown = visible;
}

bool
SOSXWindow::commitInputMethod() {
    return cocoaCommitMacKeyboard (cocoaWindow);
}

// From virtual SCanvas.
bool
SOSXWindow::cacheOn(bool on)
{
  bool old = isCacheOn;
  isCacheOn = on;
  return old;
}

SS_Matrix2D
SOSXWindow::getCurrentMatrix() const
{
  if (engine!=0) return engine->getCurrentMatrix();
  return SS_Matrix2D();
}

