
// 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 "imageconv.hpp"
#include "attr16.hpp"

#include <vector>

namespace Ep128ImgConv {

  void ImageConv_Attr16::initializePalettes()
  {
    for (int i = 0; i < 256; i++)
      convertEPColorToYUV(i, paletteY[i], paletteU[i], paletteV[i]);
    for (int c0 = 0; c0 < 256; c0++) {
      for (int c1 = 0; c1 < 256; c1++) {
        errorTable[(c0 << 8) | c1] =
            Ep128ImgConv::calculateYUVErrorSqr(
                paletteY[c0], paletteU[c0], paletteV[c0],
                paletteY[c1], paletteU[c1], paletteV[c1],
                colorErrorScale);
      }
    }
  }

  void ImageConv_Attr16::setFixBias(int n)
  {
    fixBiasValue = n & 31;
    for (int yc = 0; yc < height; yc++) {
      for (int i = 8; i < 16; i++)
        palette[yc][i] = (unsigned char) ((fixBiasValue << 3) | (i & 7));
    }
  }

  void ImageConv_Attr16::randomizePalette(int yc, int seedValue)
  {
    int     tmp = 0;
    setRandomSeed(tmp, uint32_t(seedValue));
    for (int i = 0; i < 8; i++) {
      unsigned char c = (unsigned char) (getRandomNumber(tmp) & 0xFF);
      if (!fixedColors[i])
        palette[yc][i] = c;
    }
  }

  double ImageConv_Attr16::calculateError(
      const size_t *colorCntTable, const unsigned char *palette_,
      int colorChanged, unsigned char *colorIndexCache, double *errorCache,
      double maxError)
  {
    double  totalError = 0.0;
    for (size_t c = 0; c < 256 && totalError < (maxError * 1.000001); c++) {
      if (colorCntTable[c] < 1)
        continue;
      const double  *tbl = &(errorTable[c << 8]);
      if (colorChanged >= 0) {
        if (int(colorIndexCache[c]) != colorChanged) {
          double  err = tbl[palette_[colorChanged]]
                        * double(long(colorCntTable[c]));
          double  minErr = errorCache[c];
          if (err < minErr) {
            minErr = err;
            colorIndexCache[c] = (unsigned char) colorChanged;
            errorCache[c] = err;
          }
          totalError += minErr;
          continue;
        }
      }
      double  minErr = tbl[palette_[0]];
      int     ci = 0;
      for (int i = 1; i < 16; i += 3) {
        double  err = tbl[palette_[i]];
        if (err < minErr) {
          minErr = err;
          ci = i;
        }
        err = tbl[palette_[i + 1]];
        if (err < minErr) {
          minErr = err;
          ci = i + 1;
        }
        err = tbl[palette_[i + 2]];
        if (err < minErr) {
          minErr = err;
          ci = i + 2;
        }
      }
      minErr *= double(long(colorCntTable[c]));
      if (colorIndexCache) {
        colorIndexCache[c] = (unsigned char) ci;
        errorCache[c] = minErr;
      }
      totalError += minErr;
    }
    return totalError;
  }

  double ImageConv_Attr16::calculateAttrCellError(int xc, int yc,
                                                  int c0, int c1)
  {
    xc = xc & (~(int(7)));
    float   inBufY[8];
    float   inBufU[8];
    float   inBufV[8];
    for (int i = 0; i < 8; i++) {
      inBufY[i] = inputImage.y(xc + i, yc) + ditherErrorImage.y(xc + i, yc);
      inBufU[i] = inputImage.u(xc + i, yc) + ditherErrorImage.u(xc + i, yc);
      inBufV[i] = inputImage.v(xc + i, yc) + ditherErrorImage.v(xc + i, yc);
      limitYUVColor(inBufY[i], inBufU[i], inBufV[i]);
    }
    float   tmpPaletteY[5];
    float   tmpPaletteU[5];
    float   tmpPaletteV[5];
    for (int i = 0; i < 5; i++) {
      float   f = float(i > 0 ? (i > 1 ? (i > 2 ? (i > 3 ? 3 : 1) : 2) : 4) : 0)
                  * 0.25f;
      tmpPaletteY[i] = (paletteY[c0] * (1.0f - f)) + (paletteY[c1] * f);
      tmpPaletteU[i] = (paletteU[c0] * (1.0f - f)) + (paletteU[c1] * f);
      tmpPaletteV[i] = (paletteV[c0] * (1.0f - f)) + (paletteV[c1] * f);
    }
    double  totalError = 0.0;
    // 8x1
    for (int i = 0; i < 8; i++) {
      double  minErr = 1000000000.0;
      float   y = inBufY[i];
      float   u = inBufU[i];
      float   v = inBufV[i];
      for (int j = 0; j < 2; j++) {
        double  err = Ep128ImgConv::calculateYUVErrorSqr(
                          tmpPaletteY[j], tmpPaletteU[j], tmpPaletteV[j],
                          y, u, v, colorErrorScale);
        if (err < minErr)
          minErr = err;
      }
      totalError += minErr;
    }
    if (ditherType == 0)
      return totalError;
    for (int i = 0; i < 4; i++) {
      // downsample to 4x1
      inBufY[i] = (inBufY[i << 1] + inBufY[(i << 1) + 1]) * 0.5f;
      inBufU[i] = (inBufU[i << 1] + inBufU[(i << 1) + 1]) * 0.5f;
      inBufV[i] = (inBufV[i << 1] + inBufV[(i << 1) + 1]) * 0.5f;
      double  minErr = 1000000000.0;
      float   y = inBufY[i];
      float   u = inBufU[i];
      float   v = inBufV[i];
      for (int j = 0; j < 3; j++) {
        double  err = Ep128ImgConv::calculateYUVErrorSqr(
                          tmpPaletteY[j], tmpPaletteU[j], tmpPaletteV[j],
                          y, u, v, colorErrorScale);
        if (err < minErr)
          minErr = err;
      }
      totalError += (minErr * 3.0);
    }
    for (int i = 0; i < 2; i++) {
      // downsample to 2x1
      double  minErr = 1000000000.0;
      float   y = (inBufY[i << 1] + inBufY[(i << 1) + 1]) * 0.5f;
      float   u = (inBufU[i << 1] + inBufU[(i << 1) + 1]) * 0.5f;
      float   v = (inBufV[i << 1] + inBufV[(i << 1) + 1]) * 0.5f;
      for (int j = 0; j < 5; j++) {
        double  err = Ep128ImgConv::calculateYUVErrorSqr(
                          tmpPaletteY[j], tmpPaletteU[j], tmpPaletteV[j],
                          y, u, v, colorErrorScale);
        if (err < minErr)
          minErr = err;
      }
      totalError += (minErr * 2.0);
    }
    return totalError;
  }

  double ImageConv_Attr16::calculateLineError2(int yc, double maxError)
  {
    double  totalError = 0.0;
    for (int xc = 0; xc < width; xc += 8) {
      totalError += calculateAttrCellError(xc, yc,
                                           attr0[yc][xc >> 3],
                                           attr1[yc][xc >> 3]);
      if (totalError > (maxError * 1.000001))
        break;
    }
    return totalError;
  }

  double ImageConv_Attr16::calculateTotalError2()
  {
    double  totalError = 0.0;
    for (int yc = 0; yc < height; yc++)
      totalError += calculateLineError2(yc);
    return totalError;
  }

  double ImageConv_Attr16::calculateLineError16(
      int yc, int colorChanged, unsigned char *colorIndexCache,
      double *errorCache, double maxError)
  {
    double  totalError = 0.0;
    const unsigned char *palettePtr = palette[yc];
    for (int xc = 0; xc < width && totalError < (maxError * 1.00001); xc++) {
      const double  *tbl = &(errorTable[int(convertedImage[yc][xc]) << 8]);
      if (colorChanged >= 0) {
        if (int(colorIndexCache[xc]) != colorChanged) {
          double  err = tbl[palettePtr[colorChanged]];
          double  minErr = errorCache[xc];
          if (err < minErr) {
            minErr = err;
            colorIndexCache[xc] = (unsigned char) colorChanged;
            errorCache[xc] = err;
          }
          totalError += minErr;
          continue;
        }
      }
      double  minErr = tbl[palettePtr[0]];
      int     ci = 0;
      for (int i = 1; i < 16; i += 3) {
        double  err = tbl[palettePtr[i]];
        if (err < minErr) {
          minErr = err;
          ci = i;
        }
        err = tbl[palettePtr[i + 1]];
        if (err < minErr) {
          minErr = err;
          ci = i + 1;
        }
        err = tbl[palettePtr[i + 2]];
        if (err < minErr) {
          minErr = err;
          ci = i + 2;
        }
      }
      if (colorIndexCache) {
        colorIndexCache[xc] = (unsigned char) ci;
        errorCache[xc] = minErr;
      }
      totalError += minErr;
    }
    return totalError;
  }

  double ImageConv_Attr16::calculateTotalError16(double maxError)
  {
    double  totalError = 0.0;
    for (int yc = 0; yc < height && totalError < (maxError * 1.000001); yc++)
      totalError += calculateLineError16(yc);
    return totalError;
  }

  double ImageConv_Attr16::optimizeLinePalette(int yc, int optimizeLevel)
  {
    bool    colorUsed[256];
    int     nColors = 0;
    float   minY = 1.0f;
    float   maxY = 0.0f;
    float   minU = 1.0f;
    float   maxU = -1.0f;
    float   minV = 1.0f;
    float   maxV = -1.0f;
    for (int i = 0; i < 256; i++)
      colorUsed[i] = false;
    for (int xc = 0; xc < width; xc++) {
      int     c = convertedImage[yc][xc];
      if (!colorUsed[c]) {
        colorUsed[c] = true;
        nColors++;
      }
      float   y = paletteY[c];
      float   u = paletteU[c];
      float   v = paletteV[c];
      minY = (y < minY ? y : minY);
      maxY = (y > maxY ? y : maxY);
      minU = (u < minU ? u : minU);
      maxU = (u > maxU ? u : maxU);
      minV = (v < minV ? v : minV);
      maxV = (v > maxV ? v : maxV);
    }
    minY -= 0.0001f;
    maxY += 0.0001f;
    minU -= 0.0001f;
    maxU += 0.0001f;
    minV -= 0.0001f;
    maxV += 0.0001f;
    int     maxColors = 8;
    for (int i = 0; i < 16; i++) {
      if (i < 8) {
        if (!fixedColors[i])
          continue;
        maxColors--;
      }
      int     c = palette[yc][i];
      if (colorUsed[c]) {
        colorUsed[c] = false;
        nColors--;
      }
    }
    if (nColors <= maxColors) {
      // if lossless conversion is possible:
      nColors = 0;
      for (int i = 0; i < 256; i++) {
        if (colorUsed[i]) {
          while (fixedColors[nColors])
            nColors++;
          palette[yc][nColors] = (unsigned char) i;
          nColors++;
        }
      }
      for ( ; nColors < 8; nColors++) {
        if (!fixedColors[nColors])
          palette[yc][nColors] = 0x00;
      }
      return 0.0;
    }
    for (int i = 0; i < 256; i++) {
      float   y = paletteY[i];
      float   u = paletteU[i];
      float   v = paletteV[i];
      colorUsed[i] = (y >= minY && y <= maxY && u >= minU && u <= maxU &&
                      v >= minV && v <= maxV);
    }
    for (int i = 8; i < 16; i++)
      colorUsed[palette[yc][i]] = false;
    std::vector< unsigned char >  colorIndexCache(size_t(width), 0x00);
    std::vector< double >   errorCache(size_t(width), 1000000000.0);
    double  bestError = 1000000000.0;
    int     bestPalette[8];
    for (int l = 0; l < optimizeLevel; l++) {
      randomizePalette(yc, l + 1);
      double  minErr = calculateLineError16(yc);
      bool    doneFlag = true;
      do {
        doneFlag = true;
        for (int i = 0; i < 8; i++) {
          int     bestColor = palette[yc][i];
          if (!fixedColors[i]) {
            (void) calculateLineError16(yc, -1, &(colorIndexCache.front()),
                                        &(errorCache.front()));
            for (int c = 0; c < 256; c++) {
              if (!colorUsed[c])
                continue;
              palette[yc][i] = (unsigned char) c;
              double  err = calculateLineError16(yc, i,
                                                 &(colorIndexCache.front()),
                                                 &(errorCache.front()), minErr);
              if (err < (minErr * 0.999999)) {
                bestColor = c;
                doneFlag = false;
                minErr = err;
              }
            }
          }
          palette[yc][i] = (unsigned char) bestColor;
          colorUsed[bestColor] = false;
        }
        for (int i = 0; i < 8; i++)
          colorUsed[palette[yc][i]] = true;
      } while (!doneFlag);
      if (minErr < bestError) {
        for (int i = 0; i < 8; i++)
          bestPalette[i] = palette[yc][i];
        bestError = minErr;
      }
    }
    for (int i = 0; i < 8; i++)
      palette[yc][i] = (unsigned char) bestPalette[i];
    sortLinePalette(yc);
    return bestError;
  }

  double ImageConv_Attr16::optimizeLineAttributes(int yc, bool usePalette,
                                                  int optimizeLevel)
  {
    double  totalError = 0.0;
    for (int xc = 0; xc < width; xc += 8) {
      double  minErr = 1000000000.0;
      int     bestColor0 = 0;
      int     bestColor1 = 0;
      if (usePalette) {
        for (int c0 = 0; c0 < 15; c0++) {
          for (int c1 = c0 + 1; c1 < 16; c1++) {
            double  err = calculateAttrCellError(
                              xc, yc, palette[yc][c0], palette[yc][c1]);
            if (err < minErr) {
              bestColor0 = palette[yc][c0];
              bestColor1 = palette[yc][c1];
              minErr = err;
            }
          }
        }
      }
      else {
        double  bestError = 1000000000.0;
        for (int l = 0; l < optimizeLevel; l++) {
          int     tmp = 0;
          setRandomSeed(tmp, uint32_t(l + 10000));
          int     c0 = getRandomNumber(tmp) & 0xFF;
          int     c1 = getRandomNumber(tmp) & 0xFF;
          bool    doneFlag = false;
          do {
            doneFlag = true;
            for (int i = 0; i < 256; i++) {
              double  err = calculateAttrCellError(xc, yc, i, c1);
              if (err < (minErr * 0.999999)) {
                c0 = i;
                minErr = err;
                doneFlag = false;
              }
            }
            for (int i = 0; i < 256; i++) {
              double  err = calculateAttrCellError(xc, yc, c0, i);
              if (err < (minErr * 0.999999)) {
                c1 = i;
                minErr = err;
                doneFlag = false;
              }
            }
          } while (!doneFlag);
          if (minErr < bestError) {
            bestColor0 = c0;
            bestColor1 = c1;
            bestError = minErr;
          }
        }
        minErr = bestError;
      }
      totalError += minErr;
      attr0[yc][xc >> 3] = bestColor0;
      attr1[yc][xc >> 3] = bestColor1;
    }
    return totalError;
  }

  double ImageConv_Attr16::optimizeImagePalette(int optimizeLevel,
                                                bool optimizeFixBias)
  {
    std::vector< size_t > colorCntTable(256, 0);
    for (int yc = 0; yc < height; yc++) {
      for (int xc = 0; xc < width; xc++) {
        int     tmp = convertedImage[yc][xc] & 0xFF;
        colorCntTable[tmp] = colorCntTable[tmp] + 1;
      }
    }
    if (optimizeFixBias)
      optimizeLevel = optimizeLevel * 32;
    std::vector< unsigned char >  colorIndexCache(256, 0x00);
    std::vector< double >   errorCache(256, 1000000000.0);
    double  bestError = 1000000000.0;
    int     bestPalette[8];
    int     bestFixBias = fixBiasValue;
    int     progressCnt = optimizeLevel * 14;
    int     progressMax = optimizeLevel * 20;
    for (int l = 0; l < optimizeLevel; l++) {
      if (!setProgressPercentage(progressCnt * 100 / progressMax))
        return -1.0;
      progressCnt += 5;
      if (optimizeFixBias)
        setFixBias(l & 0x1F);
      randomizePalette(0, l + 1);
      setFixedPalette();
      double  minErr = calculateError(&(colorCntTable.front()), palette[0]);
      bool    doneFlag = true;
      do {
        doneFlag = true;
        for (int i = 0; i < 8; i++) {
          int     bestColor = palette[0][i];
          if (!fixedColors[i]) {
            (void) calculateError(&(colorCntTable.front()), palette[0],
                                  -1, &(colorIndexCache.front()),
                                  &(errorCache.front()));
            for (int c = 0; c < 256; c++) {
              palette[0][i] = (unsigned char) c;
              setFixedPalette();
              double  err =
                  calculateError(&(colorCntTable.front()), palette[0],
                                 i, &(colorIndexCache.front()),
                                 &(errorCache.front()), minErr);
              if (err < (minErr * 0.999999)) {
                bestColor = c;
                doneFlag = false;
                minErr = err;
              }
            }
          }
          palette[0][i] = (unsigned char) bestColor;
        }
      } while (!doneFlag);
      if (minErr < bestError) {
        for (int i = 0; i < 8; i++)
          bestPalette[i] = palette[0][i];
        bestFixBias = fixBiasValue;
        bestError = minErr;
      }
    }
    setFixBias(bestFixBias);
    for (int i = 0; i < 8; i++)
      palette[0][i] = (unsigned char) bestPalette[i];
    sortLinePalette(0);
    setFixedPalette();
    return bestError;
  }

  void ImageConv_Attr16::sortLinePalette(int yc)
  {
    // sort palette colors by bit-reversed color value
    for (int i = 0; i < 8; i++) {
      unsigned char tmp = palette[yc][i];
      tmp = ((tmp & 0x55) << 1) | ((tmp & 0xAA) >> 1);
      tmp = ((tmp & 0x33) << 2) | ((tmp & 0xCC) >> 2);
      tmp = ((tmp & 0x0F) << 4) | ((tmp & 0xF0) >> 4);
      palette[yc][i] = tmp;
    }
    for (int i = 0; i < 7; i++) {
      for (int j = i + 1; j < 8; j++) {
        if (fixedColors[i] || fixedColors[j] ||
            palette[yc][i] <= palette[yc][j]) {
          continue;
        }
        unsigned char tmp = palette[yc][i];
        palette[yc][i] = palette[yc][j];
        palette[yc][j] = tmp;
      }
    }
    for (int i = 0; i < 8; i++) {
      unsigned char tmp = palette[yc][i];
      tmp = ((tmp & 0x55) << 1) | ((tmp & 0xAA) >> 1);
      tmp = ((tmp & 0x33) << 2) | ((tmp & 0xCC) >> 2);
      tmp = ((tmp & 0x0F) << 4) | ((tmp & 0xF0) >> 4);
      palette[yc][i] = tmp;
    }
  }

  void ImageConv_Attr16::setFixedPalette()
  {
    for (int yc = 1; yc < height; yc++) {
      for (int i = 0; i < 8; i++)
        palette[yc][i] = palette[0][i];
    }
  }

  void ImageConv_Attr16::ditherLine(long yc, bool updateError)
  {
    if (yc < 0L || yc >= long(height))
      return;
    for (int xc = 0; xc < width; xc++) {
      if ((yc & 1L) != 0L)
        xc = (width - 1) - xc;
      float   y0 = inputImage.y(xc, yc);
      float   u0 = inputImage.u(xc, yc);
      float   v0 = inputImage.v(xc, yc);
      float   y = y0 + ditherErrorImage.y(xc, yc);
      float   u = u0 + ditherErrorImage.u(xc, yc);
      float   v = v0 + ditherErrorImage.v(xc, yc);
      limitYUVColor(y, u, v);
      int     c0 = attr0[yc][xc >> 3];
      double  err0 = Ep128ImgConv::calculateYUVErrorSqr(
                         paletteY[c0], paletteU[c0], paletteV[c0],
                         y, u, v, colorErrorScale);
      int     c1 = attr1[yc][xc >> 3];
      double  err1 = Ep128ImgConv::calculateYUVErrorSqr(
                         paletteY[c1], paletteU[c1], paletteV[c1],
                         y, u, v, colorErrorScale);
      int     c = (err1 < err0 ? 1 : 0);
      convertedImage[yc][xc] = (unsigned char) c;
      if (updateError) {
        limitYUVColorToRGB(y, u, v);
        float   yErr = (y0 + ((y - y0) * float(ditherDiffusion)))
                       - paletteY[(c == 0 ? c0 : c1)];
        float   uErr = (u0 + ((u - u0) * float(ditherDiffusion)))
                       - paletteU[(c == 0 ? c0 : c1)];
        float   vErr = (v0 + ((v - v0) * float(ditherDiffusion)))
                       - paletteV[(c == 0 ? c0 : c1)];
        switch (ditherType) {
        case 1:                                 // Floyd-Steinberg
          for (int j = 0; j < 2; j++) {
            long    yc_ = yc + j;
            if (yc_ >= long(height))
              break;
            for (int i = (j > 0 ? -1 : 1); i < 2; i++) {
              long    xc_ = xc + ((yc & 1L) == 0L ? i : (-i));
              if (xc_ < 0L || xc_ >= long(width))
                continue;
              float   errMult =
                  float(j > 0 ? (i < 0 ? 3 : (i > 0 ? 1 : 5)) : 7) / 16.0f;
              ditherErrorImage.y(xc_, yc_) += (yErr * errMult);
              ditherErrorImage.u(xc_, yc_) += (uErr * errMult);
              ditherErrorImage.v(xc_, yc_) += (vErr * errMult);
            }
          }
          break;
        case 2:                                 // Stucki
          for (int j = 0; j < 3; j++) {
            long    yc_ = yc + j;
            if (yc_ >= long(height))
              break;
            for (int i = (j > 0 ? -2 : 1); i < 3; i++) {
              long    xc_ = xc + ((yc & 1L) == 0L ? i : (-i));
              if (xc_ < 0L || xc_ >= long(width))
                continue;
              float   errMult = float(16 >> (j + (i >= 0 ? i : (-i)))) / 42.0f;
              ditherErrorImage.y(xc_, yc_) += (yErr * errMult);
              ditherErrorImage.u(xc_, yc_) += (uErr * errMult);
              ditherErrorImage.v(xc_, yc_) += (vErr * errMult);
            }
          }
          break;
        case 3:                                 // Jarvis
          for (int j = 0; j < 3; j++) {
            long    yc_ = yc + j;
            if (yc_ >= long(height))
              break;
            for (int i = (j > 0 ? -2 : 1); i < 3; i++) {
              long    xc_ = xc + ((yc & 1L) == 0L ? i : (-i));
              if (xc_ < 0L || xc_ >= long(width))
                continue;
              float   errMult = (4.5f - float(j + (i >= 0 ? i : (-i)))) / 24.0f;
              ditherErrorImage.y(xc_, yc_) += (yErr * errMult);
              ditherErrorImage.u(xc_, yc_) += (uErr * errMult);
              ditherErrorImage.v(xc_, yc_) += (vErr * errMult);
            }
          }
          break;
        default:                                // no dithering
          break;
        }
      }
      if ((yc & 1L) != 0L)
        xc = (width - 1) - xc;
    }
  }

  void ImageConv_Attr16::preDitherImage()
  {
    ditherErrorImage.clear();
    unsigned char tmpPalette[256];
    for (int i = 0; i < 256; i++)
      tmpPalette[i] = (unsigned char) i;
    for (int yc = 0; yc < height; yc++) {
      Ep128ImgConv::ditherLine(convertedImage, inputImage, ditherErrorImage, yc,
                               ditherType, ditherDiffusion, colorErrorScale,
                               &(tmpPalette[0]), 256,
                               paletteY, paletteU, paletteV);
    }
  }

  double ImageConv_Attr16::optimizeLinePalette_fast(int yc)
  {
    double  tmpPaletteY[16];
    double  tmpPaletteU[16];
    double  tmpPaletteV[16];
    int     colorWeights[16];
    for (int i = 0; i < 16; i++) {
      int     c = palette[yc][i];
      tmpPaletteY[i] = paletteY[c];
      tmpPaletteU[i] = paletteU[c];
      tmpPaletteV[i] = paletteV[c];
      colorWeights[i] = 0;
    }
    int     k = 2;
    while ((k >> 1) < width)
      k = k << 1;
    for (int xc = 0; k > 1; xc += k) {
      if (xc >= width) {
        k = k >> 1;
        xc = k >> 1;
        continue;
      }
      double  bestError = 1000000000.0;
      int     bestColor = 0;
      int     c = convertedImage[yc][xc];
      float   y = paletteY[c];
      float   u = paletteU[c];
      float   v = paletteV[c];
      for (int i = 0; i < 16; i++) {
        double  err = 0.0;
        if (i < 8 && !fixedColors[i]) {
          double  tmp1 = double(colorWeights[i]);
          double  tmp2 = 1.0 / double(colorWeights[i] + 1);
          double  tmpY = ((tmpPaletteY[i] * tmp1) + y) * tmp2;
          double  tmpU = ((tmpPaletteU[i] * tmp1) + u) * tmp2;
          double  tmpV = ((tmpPaletteV[i] * tmp1) + v) * tmp2;
          err = Ep128ImgConv::calculateYUVErrorSqr(tmpY, tmpU, tmpV, y, u, v,
                                                   colorErrorScale);
          err += (colorWeights[i] > 0 ? 0.000001 : 0.00001);
        }
        else {
          err = Ep128ImgConv::calculateYUVErrorSqr(tmpPaletteY[i],
                                                   tmpPaletteU[i],
                                                   tmpPaletteV[i],
                                                   y, u, v, colorErrorScale);
        }
        if (err < bestError) {
          bestError = err;
          bestColor = i;
        }
      }
      colorWeights[bestColor] = colorWeights[bestColor] + 1;
      if (bestColor < 8 && !fixedColors[bestColor]) {
        double  tmp1 = double(colorWeights[bestColor] - 1);
        double  tmp2 = 1.0 / double(colorWeights[bestColor]);
        tmpPaletteY[bestColor] = ((tmpPaletteY[bestColor] * tmp1) + y) * tmp2;
        tmpPaletteU[bestColor] = ((tmpPaletteU[bestColor] * tmp1) + u) * tmp2;
        tmpPaletteV[bestColor] = ((tmpPaletteV[bestColor] * tmp1) + v) * tmp2;
      }
    }
    for (int i = 0; i < 8; i++) {
      if (!fixedColors[i]) {
        double  y = tmpPaletteY[i];
        double  u = tmpPaletteU[i];
        double  v = tmpPaletteV[i];
        double  bestError = 1000000000.0;
        int     bestColor = 0;
        for (int c = 0; c < 256; c++) {
          double  err =
              Ep128ImgConv::calculateYUVErrorSqr(
                  paletteY[c], paletteU[c], paletteV[c],
                  y, u, v, colorErrorScale);
          if (err < bestError) {
            bestError = err;
            bestColor = c;
          }
        }
        palette[yc][i] = (unsigned char) bestColor;
      }
    }
    sortLinePalette(yc);
    return calculateLineError16(yc);
  }

  void ImageConv_Attr16::pixelStoreCallback(void *userData, int xc, int yc,
                                            float y, float u, float v)
  {
    ImageConv_Attr16&  this_ =
        *(reinterpret_cast<ImageConv_Attr16 *>(userData));
    xc = xc >> 1;
    yc = yc >> 1;
    if (xc < 0 || xc >= this_.width || yc < 0 || yc >= this_.height)
      return;
    limitYUVColor(y, u, v);
    this_.inputImage.y(xc, yc) += (y * 0.25f);
    this_.inputImage.u(xc, yc) += (u * 0.25f);
    this_.inputImage.v(xc, yc) += (v * 0.25f);
  }

  void ImageConv_Attr16::pixelStoreCallbackI(void *userData, int xc, int yc,
                                             float y, float u, float v)
  {
    ImageConv_Attr16&  this_ =
        *(reinterpret_cast<ImageConv_Attr16 *>(userData));
    xc = xc >> 1;
    if (xc < 0 || xc >= this_.width || yc < 0 || yc >= this_.height)
      return;
    limitYUVColor(y, u, v);
    this_.inputImage.y(xc, yc) += (y * 0.5f);
    this_.inputImage.u(xc, yc) += (u * 0.5f);
    this_.inputImage.v(xc, yc) += (v * 0.5f);
  }

  ImageConv_Attr16::ImageConv_Attr16()
    : ImageConverter(),
      width(1),
      height(1),
      colorErrorScale(0.5f),
      fixBiasValue(0x00),
      inputImage(1, 1),
      ditherErrorImage(1, 1),
      convertedImage(1, 1),
      palette(16, 1),
      attr0(1, 1),
      attr1(1, 1),
      conversionQuality(3),
      borderColor(0x00),
      ditherType(1),
      ditherDiffusion(0.95f),
      errorTable((double *) 0)
  {
    for (int i = 0; i < 8; i++)
      fixedColors[i] = false;
    errorTable = new double[256 * 256];
    initializePalettes();
  }

  ImageConv_Attr16::~ImageConv_Attr16()
  {
    delete[] errorTable;
  }

  bool ImageConv_Attr16::processImage(
      ImageData& imgData, const char *infileName,
      YUVImageConverter& imgConv, const ImageConvConfig& config)
  {
    width = config.width << 3;
    height = ((imgData[5] & 0x80) == 0 ? config.height : (config.height << 1));
    colorErrorScale = float(config.colorErrorScale);
    conversionQuality = config.conversionQuality;
    borderColor = config.borderColor & 0xFF;
    float   borderY = 0.0f;
    float   borderU = 0.0f;
    float   borderV = 0.0f;
    convertEPColorToYUV(borderColor, borderY, borderU, borderV);
    inputImage.setBorderColor(borderY, borderU, borderV);
    ditherType = config.ditherType;
    limitValue(ditherType, 0, 3);
    ditherDiffusion = float(config.ditherDiffusion);

    inputImage.resize(width, height);
    ditherErrorImage.resize(width, height);
    convertedImage.resize(width, height);
    palette.resize(16, height);
    attr0.resize(width >> 3, height);
    attr1.resize(width >> 3, height);
    inputImage.clear();
    ditherErrorImage.clear();
    convertedImage.clear();
    palette.clear();
    attr0.clear();
    attr1.clear();

    initializePalettes();
    setFixBias(0);
    for (int i = 0; i < 8; i++)
      fixedColors[i] = (config.paletteColors[i] >= 0);
    for (int yc = 0; yc < height; yc++) {
      randomizePalette(yc, yc + 1000);
      for (int i = 0; i < 8; i++) {
        if (fixedColors[i])
          palette[yc][i] = (unsigned char) (config.paletteColors[i] & 0xFF);
      }
    }

    if (!(imgData[5] & 0x80))
      imgConv.setPixelStoreCallback(&pixelStoreCallback, (void *) this);
    else
      imgConv.setPixelStoreCallback(&pixelStoreCallbackI, (void *) this);
    if (!imgConv.convertImageFile(infileName))
      return false;

    progressMessage("Converting image");
    setProgressPercentage(0);
    // optimize FIXBIAS (TODO: this could be improved)
    preDitherImage();
    double  bestError = 1000000000.0;
    int     bestFixBias = 0;
    if (config.fixBias < 0) {
      for (int fb = 0; fb < 32; fb++) {
        setFixBias(fb);
        double  err = 0.0;
        if (conversionQuality >= 9 && config.paletteResolution > 0) {
          for (int yc = 0; yc < height; yc++) {
            err += optimizeLinePalette_fast(yc);
            if (err > (bestError * 1.000001))
              break;
          }
        }
        else {
          err = calculateTotalError16(bestError);
        }
        if (err < bestError) {
          bestFixBias = fb;
          bestError = err;
        }
      }
    }
    else {
      bestFixBias = config.fixBias & 0x1F;
    }
    setFixBias(bestFixBias);
    ditherErrorImage.clear();
    int     optimizeLevel = 1 + ((conversionQuality - 1) >> 1);
    if (config.paletteResolution != 0) {
      // generate optimal palette independently for each line
      int     progressCnt = 0;
      int     progressMax = height;
      for (int yc = 0; yc < height; yc++) {
        if (!setProgressPercentage((progressCnt * 100) / progressMax))
          return false;
        optimizeLineAttributes(yc, false, optimizeLevel);
        ditherLine(yc, false);
        for (int xc = 0; xc < width; xc++) {
          if (convertedImage[yc][xc] == 0)
            convertedImage[yc][xc] = attr0[yc][xc >> 3];
          else
            convertedImage[yc][xc] = attr1[yc][xc >> 3];
        }
        optimizeLinePalette(yc, optimizeLevel);
        optimizeLineAttributes(yc, true, optimizeLevel);
        ditherLine(yc, true);
        for (int xc = 0; xc < width; xc += 8) {
          unsigned char&  c0 = attr0[yc][xc >> 3];
          unsigned char&  c1 = attr1[yc][xc >> 3];
          int     i = 0;
          for ( ; i < 16; i++) {
            if (palette[yc][i] == c0) {
              c0 = i;
              break;
            }
          }
          for ( ; i < 16; i++) {
            if (palette[yc][i] == c1) {
              c1 = i;
              break;
            }
          }
        }
        progressCnt++;
      }
    }
    else {
      // generate optimal palette for the whole image
      int     progressCnt = 0;
      int     progressMax = height * 10;
      for (int yc = 0; yc < height; yc++) {
        if (!setProgressPercentage((progressCnt * 100) / progressMax))
          return false;
        progressCnt += 7;
        optimizeLineAttributes(yc, false, optimizeLevel);
        ditherLine(yc, true);
        for (int xc = 0; xc < width; xc++) {
          if (convertedImage[yc][xc] == 0)
            convertedImage[yc][xc] = attr0[yc][xc >> 3];
          else
            convertedImage[yc][xc] = attr1[yc][xc >> 3];
        }
      }
      ditherErrorImage.clear();
      if (optimizeImagePalette(optimizeLevel, (config.fixBias < 0)) < -0.5)
        return false;
      progressCnt = height * 19;
      progressMax = progressCnt + height;
      for (int yc = 0; yc < height; yc++) {
        if (!setProgressPercentage((progressCnt * 100) / progressMax))
          return false;
        progressCnt++;
        optimizeLineAttributes(yc, true, optimizeLevel);
        ditherLine(yc, true);
        for (int xc = 0; xc < width; xc += 8) {
          unsigned char&  c0 = attr0[yc][xc >> 3];
          unsigned char&  c1 = attr1[yc][xc >> 3];
          int     i = 0;
          for ( ; i < 16; i++) {
            if (palette[yc][i] == c0) {
              c0 = i;
              break;
            }
          }
          for ( ; i < 16; i++) {
            if (palette[yc][i] == c1) {
              c1 = i;
              break;
            }
          }
        }
      }
    }
    imgData.setBorderColor(borderColor);
    for (int yc = 0; yc < height; yc++) {
      imgData.setFixBias(yc, fixBiasValue);
      for (int i = 0; i < 8; i++)
        imgData.setPaletteColor(yc, i, palette[yc][i]);
      for (int xc = 0; xc < width; xc += 8)
        imgData.setAttributes(xc, yc, attr0[yc][xc >> 3], attr1[yc][xc >> 3]);
      for (int xc = 0; xc < width; xc++)
        imgData.setPixel(xc, yc, convertedImage[yc][xc]);
    }
    setProgressPercentage(100);
    progressMessage("");
    char    tmpBuf[64];
    if (config.paletteResolution != 0) {
      std::sprintf(&(tmpBuf[0]), "Done; bias = %d", fixBiasValue);
    }
    else {
      std::sprintf(&(tmpBuf[0]),
                   "Done; bias = %d, palette = %d %d %d %d %d %d %d %d",
                   fixBiasValue, int(palette[0][0]), int(palette[0][1]),
                   int(palette[0][2]), int(palette[0][3]), int(palette[0][4]),
                   int(palette[0][5]), int(palette[0][6]), int(palette[0][7]));
    }
    progressMessage(&(tmpBuf[0]));
    return true;
  }

}       // namespace Ep128ImgConv

