
// epimgconv: Enterprise 128 image converter utility
// Copyright (C) 2008-2016 Istvan Varga <istvanv@users.sourceforge.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.
//
// 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// The Enterprise 128 program files generated by this utility are not covered
// by the GNU General Public License, and can be used, modified, and
// distributed without any restrictions.

#include "epimgconv.hpp"
#include "img_disp.hpp"
#include "display.hpp"
#include "gldisp.hpp"
#include "memory.hpp"
#include "nick.hpp"

#include <GL/gl.h>
#include <GL/glext.h>

void Ep128ImgConvGUI_Nick::drawLine(const uint8_t *buf, size_t nBytes)
{
  display.drawLine(buf, nBytes);
}

void Ep128ImgConvGUI_Nick::vsyncStateChange(bool newState,
                                            unsigned int currentSlot_)
{
  display.vsyncStateChange(newState, currentSlot_);
}

static const uint8_t vblankLPTData[20] = {
  0xFD, 0x80, 0x3F, 0x00,  0xFE, 0x00, 0x06, 0x3F,  0xFF, 0x00, 0x3F, 0x20,
  0xFD, 0x00, 0x3F, 0x00,  0xF3, 0x02, 0x06, 0x3F
};

static const uint8_t vblankLPTDataI[20] = {
  0xFD, 0x80, 0x3F, 0x00,  0xFE, 0x00, 0x20, 0x3F,  0xFF, 0x00, 0x3F, 0x38,
  0xFC, 0x00, 0x3F, 0x00,  0xF3, 0x02, 0x06, 0x3F
};

void Ep128ImgConvGUI_Nick::createLPT(
    int width, int height, unsigned char mode, bool isInterlaced,
    unsigned char borderColor_, unsigned char fixBias_,
    uint16_t *lineLPBAddrTable)
{
  std::memset(videoRAMPtr, 0x00, 65536);
  width = (width > 0 ? (width < 46 ? width : 46) : 0);
  height = (height > 0 ? (height < 288 ? height : 288) : 0);
  uint16_t  lpbAddr = 0x0000;
  uint16_t  ld1Addr = 0x2800;
  uint16_t  ld2Addr = 0x9400;
  for (int i = 0; i < (int(isInterlaced) + 1); i++) {
    // display area
    for (int j = 0; j < height; j++) {
      if (lineLPBAddrTable)
        lineLPBAddrTable[(i == 0 ? j : (j + height))] = lpbAddr;
      videoRAMPtr[lpbAddr] = 0xFF;
      videoRAMPtr[lpbAddr + 1] = mode & 0x6E;
      videoRAMPtr[lpbAddr + 2] = uint8_t((63 - width) >> 1);
      videoRAMPtr[lpbAddr + 3] = uint8_t((63 + width) >> 1);
      videoRAMPtr[lpbAddr + 4] = uint8_t(ld1Addr & 0xFF);
      videoRAMPtr[lpbAddr + 5] = uint8_t(ld1Addr >> 8);
      videoRAMPtr[lpbAddr + 6] = uint8_t(ld2Addr & 0xFF);
      videoRAMPtr[lpbAddr + 7] = uint8_t(ld2Addr >> 8);
      ld1Addr = ld1Addr + uint16_t(width);
      if ((mode & 0x0E) == 0x02)                // PIXEL
        ld1Addr = ld1Addr + uint16_t(width);
      else if ((mode & 0x0E) == 0x04)           // ATTRIBUTE
        ld2Addr = ld2Addr + uint16_t(width);
      lpbAddr = lpbAddr + 16;
    }
    // borders and VBLANK
    if (height < 288) {
      // bottom border
      videoRAMPtr[lpbAddr] = uint8_t(256 - (((288 - height) + 1) >> 1));
      videoRAMPtr[lpbAddr + 1] = 0x02;
      videoRAMPtr[lpbAddr + 2] = 63;
      videoRAMPtr[lpbAddr + 3] = 0;
      lpbAddr = lpbAddr + 16;
    }
    for (int j = 0; j < 20; j++) {
      if (i == 1 || !isInterlaced)
        videoRAMPtr[lpbAddr] = vblankLPTData[j];
      else
        videoRAMPtr[lpbAddr] = vblankLPTDataI[j];
      lpbAddr++;
      if ((j & 3) == 3)
        lpbAddr = lpbAddr + 12;
    }
    // top border
    videoRAMPtr[lpbAddr] = uint8_t(256 - (((288 - height) + 4) >> 1));
    videoRAMPtr[lpbAddr + 1] = 0x02 | uint8_t(i == 1 || !isInterlaced);
    videoRAMPtr[lpbAddr + 2] = 63;
    videoRAMPtr[lpbAddr + 3] = 0;
    lpbAddr = lpbAddr + 16;
  }
  writePort(0x80, fixBias_ & 0x1F);
  writePort(0x81, borderColor_);
  writePort(0x82, 0x00);
  writePort(0x83, 0x00);
  writePort(0x83, 0x40);
  writePort(0x83, 0xC0);
}

Ep128ImgConvGUI_Nick::Ep128ImgConvGUI_Nick(Ep128Emu::VideoDisplay& display_,
                                           Ep128::Memory& m_)
  : Ep128::Nick(m_),
    display(display_),
    videoRAMPtr((uint8_t *) 0)
{
  videoRAMPtr = const_cast< uint8_t * >(m_.getVideoMemory());
  this->reset();
}

Ep128ImgConvGUI_Nick::~Ep128ImgConvGUI_Nick()
{
}

void Ep128ImgConvGUI_Nick::reset()
{
  createLPT(0, 0, 0x02, false, 0x00, 0x00);
}

static uint8_t tvcColorToEPColor(uint8_t c)
{
  c = (c | (c >> 1)) & 0x55;
  c = ((c & 0x50) >> 3) | ((c & 0x04) >> 2) | ((c & 0x01) << 2);
  if (c & 0x08)
    c = (c & 0x07) | ((c & 0x07) << 3) | ((c & 0x03) << 6);
  return c;
}

void Ep128ImgConvGUI_Nick::loadImage(const Ep128ImgConv::ImageData& imgData,
                                     bool isTVC)
{
  uint16_t  lineLPBAddrTable[576];
  int     width = imgData[8];
  int     height = int(imgData[6]) | (int(imgData[7]) << 8);
  int     xOffs = 0;
  int     yOffs = 0;
  int     lineBytes = width;
  if (width > 46) {
    xOffs = (width - 46) >> 1;
    width = 46;
  }
  if (height > 288) {
    yOffs = (height - 288) >> 1;
    height = 288;
  }
  uint8_t mode = imgData[imgData.getVideoModeTableOffset()] & 0x6E;
  uint8_t interlaceFlags = imgData[5] & 0x9F;
  createLPT(width, height, mode, bool(interlaceFlags & 0x10),
            (!isTVC ? imgData[9] : tvcColorToEPColor(imgData[9])),
            (!isTVC ? imgData[imgData.getFixBiasTableOffset()] : 0x00),
            &(lineLPBAddrTable[0]));
  int     nPaletteColors = 0;
  if (mode == 0x02 || mode == 0x0E)
    nPaletteColors = 2;
  else if (mode == 0x22 || mode == 0x2E)
    nPaletteColors = 4;
  else if ((mode == 0x04 || mode == 0x42 || mode == 0x4E) && !isTVC)
    nPaletteColors = 8;
  for (int i = 0; i < (int((interlaceFlags & 0x10) >> 4) + 1); i++) {
    for (int j = 0; j < height; j++) {
      long      palettePos = long(imgData.getPaletteDataOffset(0));
      if (nPaletteColors > 0 && imgData[4] == 0x01) {
        palettePos = long(imgData.getPaletteDataOffset(i)
                          + size_t((j + yOffs) * nPaletteColors));
      }
      uint16_t  lpbAddr = lineLPBAddrTable[(i == 0 ? j : (j + height))];
      for (int k = 0; k < nPaletteColors; k++) {
        uint8_t c = imgData[palettePos + k];
        if (isTVC)
          c = tvcColorToEPColor(c);
        videoRAMPtr[lpbAddr + 8 + k] = c;
      }
      if (isTVC && (mode & 0x60) == 0x40) {
        for (int k = 0; k < 8; k++) {
          uint8_t c = uint8_t(k | (k << 3) | ((k & 3) << 6));
          videoRAMPtr[lpbAddr + 8 + k] = c;
        }
      }
      uint16_t  ld1Addr = uint16_t(videoRAMPtr[lpbAddr + 4])
                          | (uint16_t(videoRAMPtr[lpbAddr + 5]) << 8);
      int       dataOffs = (j + yOffs) * lineBytes + xOffs;
      if ((mode & 0x0E) == 0x02) {              // PIXEL
        long    pixelPos = long(imgData.getVideoDataOffset(i)
                                + (size_t(dataOffs) << 1));
        if (isTVC && mode == 0x42) {
          for (int k = 0; k < (width << 1); k++) {
            unsigned char c = imgData[pixelPos + k];
            c = ((c & 0x0F) << 4) | ((c & 0x30) >> 2) | ((~c & 0xC0) >> 6);
            videoRAMPtr[ld1Addr + k] = c;
          }
        }
        else {
          for (int k = 0; k < (width << 1); k++)
            videoRAMPtr[ld1Addr + k] = imgData[pixelPos + k];
        }
      }
      else if ((mode & 0x0E) == 0x04) {         // ATTRIBUTE
        uint16_t  ld2Addr = uint16_t(videoRAMPtr[lpbAddr + 6])
                            | (uint16_t(videoRAMPtr[lpbAddr + 7]) << 8);
        long    attrPos = long(imgData.getAttributeDataOffset(i)
                               + size_t(dataOffs));
        long    pixelPos = long(imgData.getVideoDataOffset(i)
                                + size_t(dataOffs));
        for (int k = 0; k < width; k++) {
          videoRAMPtr[ld1Addr + k] = imgData[attrPos + k];
          videoRAMPtr[ld2Addr + k] = imgData[pixelPos + k];
        }
      }
      else if ((mode & 0x0E) == 0x0E) {         // LPIXEL
        long    pixelPos = long(imgData.getVideoDataOffset(i)
                                + size_t(dataOffs));
        for (int k = 0; k < width; k++)
          videoRAMPtr[ld1Addr + k] = imgData[pixelPos + k];
      }
    }
  }
  // set display parameters depending on the machine (Enterprise or TVC)
  Ep128Emu::VideoDisplay::DisplayParameters dp;
  dp.displayQuality = (isTVC ? 4 : 3);
  dp.indexToRGBFunc = &Ep128::Nick::convertPixelToRGB;
  if (isTVC) {
    dp.blueGamma = 0.724542f;
    dp.pixelAspectRatio = float(768.0 / 872.0);
  }
  if (!(interlaceFlags & 0x10)) {
    dp.lineShade = 0.8f;
    dp.blendScale = 1.0f;
    dp.motionBlur = 0.1f;
  }
  else {
    dp.lineShade = 0.5f;
    dp.blendScale = 1.2f;
    dp.motionBlur = 0.3f;
  }
  display.setDisplayParameters(dp);
}

// ----------------------------------------------------------------------------

Ep128ImgConvGUI_Display::Ep128ImgConvGUI_Display(Ep128ImgConvGUI& gui_,
                                                 int xx, int yy, int ww, int hh,
                                                 const char *lbl,
                                                 bool isDoubleBuffered)
  : Ep128Emu::OpenGLDisplay(xx, yy, ww, hh, lbl, isDoubleBuffered),
    gui(gui_)
{
}

Ep128ImgConvGUI_Display::~Ep128ImgConvGUI_Display()
{
}

bool Ep128ImgConvGUI_Display::checkEvents()
{
  if (gui.previewEnabled && !gui.busyFlag) {
    double  t = gui.emulationTimer.getRealTime();
    gui.emulationTimer.reset();
    t = (t > 0.0 ? (t < 0.1 ? t : 0.1) : 0.0);
    int     nickSlots = int(t * 889846.02 + 0.5);
    while (nickSlots--)
      gui.nick->runOneSlot();
    return Ep128Emu::OpenGLDisplay::checkEvents();
  }
  if (!textureID) {
    // initialize texture on first call
    Ep128Emu::OpenGLDisplay::checkEvents();
  }
  if (gui.emulationTimer.getRealTime() >= 1.0) {
    gui.emulationTimer.reset();
    return true;
  }
  return false;
}

void Ep128ImgConvGUI_Display::draw()
{
  if (gui.previewEnabled && !gui.busyFlag) {
    Ep128Emu::OpenGLDisplay::draw();
    return;
  }
  if (!textureID) {
    // initialize texture on first call
    Ep128Emu::OpenGLDisplay::draw();
  }
  glViewport(0, 0, GLsizei(this->w()), GLsizei(this->h()));
  glPushMatrix();
  glOrtho(0.0, 1.0, 1.0, 0.0, 0.0, 1.0);
  GLuint  textureID_ = GLuint(textureID);
  GLint   savedTextureID = 0;
  glEnable(GL_TEXTURE_2D);
  glGetIntegerv(GL_TEXTURE_BINDING_2D, &savedTextureID);
  glBindTexture(GL_TEXTURE_2D, textureID_);
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  glPixelTransferf(GL_RED_SCALE, GLfloat(1));
  glPixelTransferf(GL_RED_BIAS, GLfloat(0));
  glPixelTransferf(GL_GREEN_SCALE, GLfloat(1));
  glPixelTransferf(GL_GREEN_BIAS, GLfloat(0));
  glPixelTransferf(GL_BLUE_SCALE, GLfloat(1));
  glPixelTransferf(GL_BLUE_BIAS, GLfloat(0));
  glPixelTransferf(GL_ALPHA_SCALE, GLfloat(1));
  glPixelTransferf(GL_ALPHA_BIAS, GLfloat(0));
  glDisable(GL_BLEND);
  for (size_t yc = 0; yc < 576; yc += 16) {
    // load texture
    for (size_t yoffs = 0; yoffs < 16; yoffs++) {
      for (size_t xc = 0; xc < 872; xc++) {
        unsigned char *p =
            &(gui.imageFileData[(((yc + yoffs) * 872) + xc) * 3]);
        uint32_t  tmp =
            uint32_t(p[0]) | (uint32_t(p[1]) << 8) | (uint32_t(p[2]) << 16)
            | 0xFF000000U;
        textureBuffer32[(yoffs * 872) + xc] = tmp;
      }
    }
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 872, 16,
                    GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, textureSpace);
    // update display
    GLfloat ycf0 = GLfloat(double(int(yc)) / 576.0);
    GLfloat ycf1 = GLfloat(double(int(yc + 16)) / 576.0);
    glBegin(GL_QUADS);
    glTexCoord2f(GLfloat(0.0), GLfloat(0.001 / 16.0));
    glVertex2f(GLfloat(0.0), ycf0);
    glTexCoord2f(GLfloat(872.0 / 1024.0), GLfloat(0.001 / 16.0));
    glVertex2f(GLfloat(1.0), ycf0);
    glTexCoord2f(GLfloat(872.0 / 1024.0), GLfloat(15.999 / 16.0));
    glVertex2f(GLfloat(1.0), ycf1);
    glTexCoord2f(GLfloat(0.0), GLfloat(15.999 / 16.0));
    glVertex2f(GLfloat(0.0), ycf1);
    glEnd();
  }
  // clean up
  glBindTexture(GL_TEXTURE_2D, GLuint(savedTextureID));
  glPopMatrix();
  glFlush();
}

