/*------------------------------------------------------------------------------
 src/lib/djWidget/imageWidget.cxx
 
 Copyright (c) 2004-2006 Darby Johnston.
 All rights reserved.
 Use is subject to license terms.
------------------------------------------------------------------------------*/

//#define DJ_DBG
#include "djImageWidget.h"
#include <djOgl.h>
#include <FL/Fl.H>
#include <FL/fl_draw.H>

namespace dj {

ImageWidget::ImageWidget() :
  Fl_Gl_Window(0, 0, 0, 0),
  _image(0),
  _viewZoom(0)
{
  mode(FL_RGB | FL_DOUBLE);
  set_visible_focus();
  end();
}

void ImageWidget::set(const Image * in)
{
  if (in == _image) return;
  _image = in;
  //redraw();
}

void ImageWidget::viewPosition(const Vec2i & in)
{
  if (in == _viewPosition) return;
  _viewPosition = in;
  //redraw();
}

void ImageWidget::viewZoom(int in, const Vec2i & focus)
{
  if (in == _viewZoom) return;
  
  DJ_DBG_FNC("dj::ImageWidget::viewZoom");
  DJ_DBG_PRN("in = " << in);
  DJ_DBG_PRN("focus = " << focus);
  
  const int tmp = clamp(in, -imageWidgetZoomMax, imageWidgetZoomMax);
  
  const double scale =
    ! isZero(zoomToFloat(_viewZoom)) ?
    (zoomToFloat(tmp) / zoomToFloat(_viewZoom)) :
    0.0;

  _viewPosition = Vec2i(
    focus.x() + dj::floor((_viewPosition.x() - focus.x()) * scale),
    focus.y() + dj::floor((_viewPosition.y() - focus.y()) * scale)
  );
  _viewZoom = tmp;
  
  //redraw();
}

void ImageWidget::viewPositionZoom(const Vec2i & position, int zoom)
{
  if (position == _viewPosition && zoom == _viewZoom) return;
  _viewPosition = position;
  _viewZoom = clamp(zoom, -imageWidgetZoomMax, imageWidgetZoomMax);
  //redraw();
}

void ImageWidget::viewBg(const Color & in)
{
  if (in == _viewBg) return;
  _viewBg = in;
  //redraw();
}

void ImageWidget::viewLeft(int inc)
{
  viewPosition(_viewPosition + Vec2i(inc, 0));
  redraw();
}

void ImageWidget::viewRight(int inc)
{
  viewPosition(_viewPosition + Vec2i(-inc, 0));
  redraw();
}

void ImageWidget::viewUp(int inc)
{
  viewPosition(_viewPosition + Vec2i(0, -inc));
  redraw();
}

void ImageWidget::viewDown(int inc)
{
  viewPosition(_viewPosition + Vec2i(0, inc));
  redraw();
}

void ImageWidget::viewCenter()
{
  viewPosition(Vec2i(w(), h()) / 2 - imageScreenSize() / 2);
  redraw();
}

void ImageWidget::viewZoomIn(const Vec2i & in)
{
  viewZoom(_viewZoom + 1, in);
  redraw();
}

void ImageWidget::viewZoomIn()
{
  viewZoomIn(Vec2i(w() / 2, h() / 2));
}

void ImageWidget::viewZoomOut(const Vec2i & in)
{
  viewZoom(_viewZoom - 1, in);
  redraw();
}
  
void ImageWidget::viewZoomOut()
{
  viewZoomOut(Vec2i(w() / 2, h() / 2));
}

void ImageWidget::viewZoomReset()
{
  viewZoom(0, Vec2i(w() / 2, h() / 2));
  redraw();
}

void ImageWidget::viewReset()
{
  viewPositionZoom(Vec2i(), 0);
  redraw();
}

void ImageWidget::viewFit()
{
  if (! _image) return;
  
  DJ_DBG_FNC("dj::ImageWidget::viewFit");

  const int proxyScale = imageProxyScale(_image->proxy());
  Vec2i size = _image->size() * proxyScale;
  DJ_DBG_PRN("size = " << Vec2i(w(), h()));
  DJ_DBG_PRN("image = " << size);
  
  int zoom = 0;
  Vec2i tmp = size;
  if (w() < tmp.x() * 1.5) {
    while (w() < tmp.x() && tmp.x() > 0) {
      --zoom;
      tmp = tmp * zoomToFloat(zoom);
    }
  } else if (w() > tmp.x() * 1.5) {
    while (w() > tmp.x() && tmp.x() > 0) {
      ++zoom;
      tmp = tmp * zoomToFloat(zoom);
    }
  }
  
  const Vec2i position = Vec2i(w(), h()) / 2 - size * zoomToFloat(zoom) / 2;
  DJ_DBG_PRN("position = " << position);
  DJ_DBG_PRN("zoom = " << zoom);

  viewPositionZoom(position, zoom);
  redraw();
}

void ImageWidget::resize(int x, int y, int w, int h)
{
  Fl_Gl_Window::resize(x, y, w, h);
  LayoutItem::resize(x, y, w, h);
}

int ImageWidget::visible() const { return Fl_Gl_Window::visible(); }

void ImageWidget::draw()
{
  if (! visible()) return;
  
  DJ_DBG_FNC("dj::ImageWidget::draw");
  DJ_DBG_PRN("size = " << Vec2i(w(), h()));
  DJ_DBG_PRN("view position = " << _viewPosition);
  DJ_DBG_PRN("view zoom = " << _viewZoom);

  // Setup.
  //
  if (! valid()) {
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    
    glViewport(0, 0, w(), h());
    oglOrtho(w(), h());
  }
  
  Color colorBg(PIXEL_RGB, PIXEL_F32);
  convert(_viewBg, &colorBg);
  glClearColor(colorBg.f32(0), colorBg.f32(1), colorBg.f32(2), 0.0);
  
  const Box2i imageBox = intersect(imageScreenBox(), Box2i(w(), h()));
  DJ_DBG_PRN("image box = " << imageBox);
  if (! _image || ! areaIsValid(imageBox)) {
    glClear(GL_COLOR_BUFFER_BIT);
    return;
  }
  
  const int proxy = max(_image->proxy(), 0);
  int zoom = _viewZoom + proxy;
  const double zoomFloat = zoomToFloat(zoom);
  const double zoomInvFloat = 1.0 / zoomFloat;
  DJ_DBG_PRN("image = " << *_image);
  DJ_DBG_PRN("proxy = " << proxy);
  DJ_DBG_PRN("zoom float = " << zoomFloat);
  
  // Clear background.
  //
  glEnable(GL_SCISSOR_TEST);
  glScissor(0, 0, w(), imageBox.y());
  //glClearColor(1, 0, 0, 0);
  glClear(GL_COLOR_BUFFER_BIT);
  glScissor(0, imageBox.y(), imageBox.x(), imageBox.height());
  //glClearColor(1, 1, 0, 0);
  glClear(GL_COLOR_BUFFER_BIT);
  glScissor(
    0, imageBox.p1().y() + 1,
    w(), h() - (imageBox.p1().y() + 1)
  );
  //glClearColor(0, 1, 1, 0);
  glClear(GL_COLOR_BUFFER_BIT);
  glScissor(
    imageBox.p1().x() + 1, imageBox.y(),
    w() - (imageBox.p1().x() + 1), imageBox.height()
  );
  //glClearColor(0, 0, 1, 0);
  glClear(GL_COLOR_BUFFER_BIT);
  glDisable(GL_SCISSOR_TEST);
  
  // XXX OpenGL implementations don't seem to like pixel zoom too much.
  //
  glColor3f(1, 1, 1);
  glRasterPos2i(0, 0);
  //if (1) {
  if (
    0 == zoom && 0 == proxy &&
    (
      PIXEL_L == _image->format() ||
      PIXEL_LA == _image->format() ||
      PIXEL_RGB == _image->format() ||
      PIXEL_RGBA == _image->format()
    )
  ) {
    DJ_DBG_PRN("opengl render");
    
    DJ_DBG_OGL(glPixelZoom(zoomFloat, zoomFloat));
    DJ_DBG_OGL(glBitmap(0, 0, 0, 0, _viewPosition.x(), _viewPosition.y(), 0));
    DJ_DBG_OGL(glDrawPixels(
      _image->size().x(), _image->size().y(),
      oglFormat(_image->format()), oglType(_image->type()),
      _image->data()
    ));

  } else {
     DJ_DBG_PRN("software render");
     
    // Temporary buffers.
    //
    if (imageBox.size() != _framebuffer.size())
      _framebuffer.set(imageBox.size(), Pixel(PIXEL_RGBA, PIXEL_U8));

    // XXX Sloppy math. This will probably crash some day.
    //
    const int scanlineWidth = min(
      ceil(imageBox.width() * (zoom >= 0 ? zoomInvFloat : 1) + 1),
      _image->width()
    );
    DJ_DBG_PRN("scanline width = " << scanlineWidth);
    
    if (scanlineWidth != _scanline.width())
      _scanline.set(Vec2i(scanlineWidth, 1), Pixel(PIXEL_RGBA, PIXEL_U8));

    // Horizontal position in image.
    //
    const int imageX = clamp(
      floor((imageBox.x() - _viewPosition.x()) * zoomInvFloat),
      0, _image->width() - 1
    );
  
    // Work.
    //
    glPixelZoom(1, 1);
    int tmpY = -1;
    for (int y = 0; y < imageBox.height(); ++y) {

      // Vertical position in image.
      //
      const int imageY = clamp(
        floor((imageBox.y() - _viewPosition.y() + y) * zoomInvFloat),
        0, _image->height() - 1
      );

      // Process scanline.
      //
      if (imageY != tmpY) {
        tmpY = y;

        // Copy scanline to buffer.
        //
        pixelCopy(
          _image->data(imageX, imageY), _scanline.data(),
          scanlineWidth, _image->pixel(), _scanline.pixel(),
          zoom < 0 ? imageProxyScale(-zoom) : 1
        );
        
        const int offset =
          -((_viewPosition.x() - imageBox.x()) % ceil(zoomFloat));
        const int repeat = imageProxyScale(zoom);
        //DJ_DBG_PRN("offset = " << offset);
        //DJ_DBG_PRN("repeat = " << repeat);
        const uint32_t * inP =
          reinterpret_cast<const uint32_t *>(_scanline.data());
        uint32_t * outP =
          reinterpret_cast<uint32_t *>(_framebuffer.data(0, y));
        uint32_t * const end = outP + imageBox.width();
        uint32_t tmp;
        int chunk = repeat - offset;
        while (outP < end) {
          tmp = *inP++;
          uint32_t * endP = min(outP + chunk, end);
          while (outP < endP)
            *outP++ = tmp;
          chunk = repeat;
        }

      } else {

        // Duplicate previous scanline.
        //
        memCopy(
          _framebuffer.data(0, tmpY), _framebuffer.data(0, y),
          imageBox.width() * pixelSize(_framebuffer.pixel())
        );
      }
    }

    // Draw.
    //
    glBitmap(0, 0, 0, 0, imageBox.x(), imageBox.y(), 0);
    glDrawPixels(
      imageBox.width(), imageBox.height(), GL_RGBA, GL_UNSIGNED_BYTE,
      _framebuffer.data()
    );
  }
  
  //glColor3f(1, 1, 0);
  //glBegin(GL_LINE_LOOP);
  //oglPoints(imageBox);
  //glEnd();
}

const Vec2i ImageWidget::imageScreenSize() const
{
  return
    _image ?
    (
      _image->size() *
      imageProxyScale(_image->proxy()) *
      zoomToFloat(_viewZoom)
    ) :
    Vec2i();
}

const Box2i ImageWidget::imageScreenBox() const
{
  return Box2i(_viewPosition, imageScreenSize());
}

double ImageWidget::zoomToFloat(int in)
{
  return in >= 0 ? pow(2, in) : (1.0 / pow(2, -in));
}

ImageViewWidget::ImageViewWidget() :
  _inside(false)
{}

void ImageViewWidget::viewZoomIn(const Vec2i & in)
{
  ImageWidget::viewZoomIn(in);
}

void ImageViewWidget::viewZoomIn()
{
  viewZoomIn(_inside ? _mouse : Vec2i(w() / 2, h() / 2));
}
  
void ImageViewWidget::viewZoomOut(const Vec2i & in)
{
  ImageWidget::viewZoomOut(in);
}

void ImageViewWidget::viewZoomOut()
{
  viewZoomOut(_inside ? _mouse : Vec2i(w() / 2, h() / 2));
}

void ImageViewWidget::viewZoomReset()
{
  viewZoom(0, _inside ? _mouse : Vec2i(w() / 2, h() / 2));
  redraw();
}

int ImageViewWidget::handle(int in)
{
  const Vec2i mouse = Vec2i(Fl::event_x(), h() - 1 - Fl::event_y());
  switch (in) {
  
    case FL_PUSH:
      take_focus();
      _mouse = _mouseTd = mouse;
      _viewPositionTmp = viewPosition();
      return 1;
    
    case FL_MOVE:
      _mouse = mouse;
      return 1;
    
    case FL_DRAG:
      _mouse = mouse;
      viewPosition(_viewPositionTmp + (_mouse - _mouseTd));
      redraw();
      return 1;
    
    case FL_SHORTCUT:
      switch (Fl::event_key()) {
      
        case FL_Left:
        case FL_KP + 4: viewLeft(); return 1;
        case FL_Right:
        case FL_KP + 6: viewRight(); return 1;
        case FL_Up:
        case FL_KP + 8: viewUp(); return 1;
        case FL_Down:
        case FL_KP + 2: viewDown(); return 1;
        
        case '=':
        case '+': viewZoomIn(); return 1;
        case '-': viewZoomOut(); return 1;
        case '0': viewZoomReset(); return 1;
        
        case FL_End: viewCenter(); return 1;
        case FL_Home: viewReset(); return 1;
        case FL_Insert: viewFit(); return 1;
      }
      break;
      
    case FL_ENTER: _inside = true; return 1;
    case FL_LEAVE: _inside = false; return 1;
    
    case FL_FOCUS:
    case FL_UNFOCUS: return 1;
  }
  
  return ImageWidget::handle(in);
}

}
