

#include "trop.h"

/*
See Alexander Reshetov's "Morphological Antialiasing" paper on Intel Labs site.

Basically, this antialiasing algorithm is based on the following ideas:

 - Suppose that our image is just made up of flat colors. Then, a simple
antialiasing
   approach is that of assuming that the 'actual' line separating two distinct
colors
   is the polyline that passes through the midpoint of each edge of its original
jaggy
   counterpart.
   As pixels around the border are cut through by the polyline, the area of the
pixel
   that is filled of a certain color is its weight in the output filtered pixel.

 - The above method can be applied on each single uniform piece of a scanline,
considering
   the lines originated by the vertical extensions of its left and right edges.

 - Of these lines, only those which lie completely on pixels adjacent to the
edge are
   considered - so that the antialiasing effect is kept only around the
contours.

This algorithm would yield a good result at what may be considered 50% softness.
Implementing
a generalized softness simply requires that the line slopes used above are
modified
accordingly (divide by 2 * softFactor).
*/

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

namespace {
template <typename PIX>
class PixelSelector {
public:
  typedef PIX pixel_type;

private:
  int m_thresh;

public:
  PixelSelector(int thresh) : m_thresh(thresh) {}

  bool areEqual(const PIX &a, const PIX &b) const {
    return std::max({abs((int)a.r - b.r), abs((int)a.g - b.g),
                     abs((int)a.b - b.b), abs((int)a.m - b.m)}) < m_thresh;
  }
};

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

template <>
class PixelSelector<TPixelCM32> {
public:
  typedef TPixelCM32 pixel_type;

private:
  int m_thresh;

public:
  PixelSelector(int thresh) : m_thresh(thresh) {}

  bool areEqual(const TPixelCM32 &a, const TPixelCM32 &b) const {
    return (a.getInk() == b.getInk()) &&
           (abs(a.getTone() - b.getTone()) < m_thresh);
  }
};

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

template <typename PIX>
inline void weightPix(PIX *out, const PIX *a, const PIX *b, double weightA,
                      double weightB) {
  out->r = a->r * weightA + b->r * weightB;
  out->g = a->g * weightA + b->g * weightB;
  out->b = a->b * weightA + b->b * weightB;
  out->m = a->m * weightA + b->m * weightB;
}

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

template <>
inline void weightPix<TPixelCM32>(TPixelCM32 *out, const TPixelCM32 *a,
                                  const TPixelCM32 *b, double weightA,
                                  double weightB) {
  *out =
      TPixelCM32(out->isPurePaint() ? b->getInk() : a->getInk(), a->getPaint(),
                 a->getTone() * weightA + b->getTone() * weightB);
}

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

// Returns 0 if pixels to connect are on the 00-11 diagonal, 1 on the 01-10 one.
template <typename PIX, typename SELECTOR>
inline bool checkNeighbourHood(int x, int y, PIX *pix, int lx, int ly, int dx,
                               int dy, const SELECTOR &sel) {
  int count1 = 0, count2 = 0;
  int dx2 = 2 * dx, dy2 = 2 * dy;
  if (y > 1) {
    // Lower edge
    count1 += (int)sel.areEqual(*(pix - dx), *(pix - dy2)) +
              (int)sel.areEqual(*(pix - dx), *(pix - dy2 - dx));
    count2 += (int)sel.areEqual(*pix, *(pix - dy2)) +
              (int)sel.areEqual(*pix, *(pix - dy2 - dx));
  }
  if (y < ly - 1) {
    // Upper edge
    count1 += (int)sel.areEqual(*(pix - dx), *(pix + dy)) +
              (int)sel.areEqual(*(pix - dx), *(pix + dy - dx));
    count2 += (int)sel.areEqual(*pix, *(pix + dy)) +
              (int)sel.areEqual(*pix, *(pix + dy - dx));
  }
  if (x > 1) {
    // Left edge
    count1 += (int)sel.areEqual(*(pix - dx), *(pix - dx2)) +
              (int)sel.areEqual(*(pix - dx), *(pix - dx2 - dy));
    count2 += (int)sel.areEqual(*pix, *(pix - dx2)) +
              (int)sel.areEqual(*pix, *(pix - dx2 - dy));
  }
  if (x < lx - 1) {
    // Left edge
    count1 += (int)sel.areEqual(*(pix - dx), *(pix + dx)) +
              (int)sel.areEqual(*(pix - dx), *(pix + dx - dy));
    count2 += (int)sel.areEqual(*pix, *(pix + dx)) +
              (int)sel.areEqual(*pix, *(pix + dx - dy));
  }

  // Connect by minority: if there are more pixels like those on the 00-11
  // diagonal, connect the other,
  // and vice versa.
  return count1 > count2;
}
}

//========================================================================================

template <typename PIX>
inline void filterLine(PIX *inLPix, PIX *inUPix, PIX *outLPix, PIX *outUPix,
                       int ll, int inDl, int outLDl, int outUDl, double hStart,
                       double slope, bool filterLower) {
  assert(hStart >= 0.0 && slope > 0.0);

  double h0   = hStart, h1, area;
  double base = hStart / slope;

  int i, end = std::min(tfloor(base), ll);
  if (filterLower) {
    // Filter lower line
    for (i = 0; i < end;
         ++i, h0 = h1, inLPix += inDl, inUPix += inDl, outLPix += outLDl) {
      h1   = h0 - slope;
      area = 0.5 * (h0 + h1);
      weightPix(outLPix, outLPix, inUPix, 1.0 - area, area);
    }

    if (i < ll) {
      double remnant = base - end;
      area           = 0.5 * remnant * h0;
      weightPix(outLPix, outLPix, inUPix, 1.0 - area, area);
    }
  } else {
    // Filter upper line
    for (i = 0; i < end;
         ++i, h0 = h1, inLPix += inDl, inUPix += inDl, outUPix += outUDl) {
      h1   = h0 - slope;
      area = 0.5 * (h0 + h1);
      weightPix(outUPix, outUPix, inLPix, 1.0 - area, area);
    }

    if (i < ll) {
      double remnant = base - end;
      area           = 0.5 * remnant * h0;
      weightPix(outUPix, outUPix, inLPix, 1.0 - area, area);
    }
  }
}

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

template <typename PIX, typename SELECTOR>
inline bool checkLength(int lLine, int y, int ly, int dy, PIX *pixL1,
                        PIX *pixU1, PIX *pixL2, PIX *pixU2, bool uniteU,
                        bool do1Line, const SELECTOR &sel) {
  // 1-length edges must be processed (as primary edges) only if explicitly
  // required,
  // and only when its associated secondary edge is of the same length.

  return (lLine > 1) ||
         (do1Line && ((uniteU && (y > 1 &&
                                  !(sel.areEqual(*pixL1, *(pixL1 - dy)) &&
                                    sel.areEqual(*pixL2, *(pixL2 - dy))))) ||
                      (y < ly - 1 &&
                       !(sel.areEqual(*pixU1, *(pixU1 + dy)) &&
                         sel.areEqual(*pixU2, *(pixU2 + dy))))));
}

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

template <typename PIX, typename SELECTOR>
void processLine(int r, int lx, int ly, PIX *inLRow, PIX *inURow, PIX *outLRow,
                 PIX *outURow, int inDx, int inDy, int outLDx, int outUDx,
                 bool do1Line, double hStart, double slope,
                 const SELECTOR &sel) {
  // Using a 'horizontal' notation here - but the same applies in vertical too
  ++r;

  // As long as we don't reach row end, process uninterrupted separation lines
  // between colors

  PIX *inLL = inLRow, *inLR, *inUL = inURow, *inUR;
  PIX *inLL_1, *inUL_1, *inLR_1, *inUR_1;
  PIX *inLEnd = inLRow + lx * inDx;
  int x, lLine;
  bool uniteLL, uniteUL, uniteLR, uniteUR;

  // Special case: a line at row start has different weights
  if (!sel.areEqual(*inLL, *inUL)) {
    // Look for line ends
    for (inLR = inLL + inDx, inUR = inUL + inDx;
         inLR != inLEnd && sel.areEqual(*inLL, *inLR) &&
         sel.areEqual(*inUL, *inUR);
         inLR += inDx, inUR += inDx)
      ;

    if (inLR != inLEnd) {
      // Found a line to process
      lLine = (inLR - inLL) / inDx;

      inLR_1 = inLR - inDx, inUR_1 = inUR - inDx;
      x = (inLR_1 - inLRow) / inDx;

      uniteUR = sel.areEqual(*inUR_1, *inLR);
      uniteLR = sel.areEqual(*inLR_1, *inUR);
      if (uniteUR || uniteLR) {
        if (uniteUR && uniteLR)
          // Ambiguous case. Check neighborhood to find out which one must be
          // actually united.
          uniteUR =
              !checkNeighbourHood(x + 1, r, inUR, lx, ly, inDx, inDy, sel);

        if (checkLength(lLine, r, ly, inDy, inLR_1, inUR_1, inLR, inUR, uniteUR,
                        do1Line, sel))
          filterLine(inLR_1, inUR_1, outLRow + x * outLDx, outURow + x * outUDx,
                     lLine, -inDx, -outLDx, -outUDx, hStart,
                     slope / (lLine << 1), uniteUR);
      }
    }

    // Update lefts
    inLL = inLR, inUL = inUR;
  }

  // Search for a line start
  for (; inLL != inLEnd && sel.areEqual(*inLL, *inUL);
       inLL += inDx, inUL += inDx)
    ;

  while (inLL != inLEnd) {
    // Look for line ends
    for (inLR = inLL + inDx, inUR = inUL + inDx;
         inLR != inLEnd && sel.areEqual(*inLL, *inLR) &&
         sel.areEqual(*inUL, *inUR);
         inLR += inDx, inUR += inDx)
      ;

    if (inLR == inLEnd) break;  // Dealt with later

    // Found a line to process
    lLine = (inLR - inLL) / inDx;

    // First, filter left to right
    inLL_1 = inLL - inDx, inUL_1 = inUL - inDx;
    x = (inLL - inLRow) / inDx;

    uniteUL = sel.areEqual(*inUL, *inLL_1);
    uniteLL = sel.areEqual(*inLL, *inUL_1);
    if (uniteUL || uniteLL) {
      if (uniteUL && uniteLL)
        uniteUL = checkNeighbourHood(x, r, inUL, lx, ly, inDx, inDy, sel);

      if (checkLength(lLine, r, ly, inDy, inLL_1, inUL_1, inLL, inUL, uniteUL,
                      do1Line, sel))
        filterLine(inLL, inUL, outLRow + x * outLDx, outURow + x * outUDx,
                   lLine, inDx, outLDx, outUDx, hStart, slope / lLine, uniteUL);
    }

    // Then, filter right to left
    inLR_1 = inLR - inDx, inUR_1 = inUR - inDx;
    x = (inLR_1 - inLRow) / inDx;

    uniteUR = sel.areEqual(*inUR_1, *inLR);
    uniteLR = sel.areEqual(*inLR_1, *inUR);
    if (uniteUR || uniteLR) {
      if (uniteUR && uniteLR)
        uniteUR = !checkNeighbourHood(x + 1, r, inUR, lx, ly, inDx, inDy, sel);

      if (checkLength(lLine, r, ly, inDy, inLR_1, inUR_1, inLR, inUR, uniteUR,
                      do1Line, sel))
        filterLine(inLR_1, inUR_1, outLRow + x * outLDx, outURow + x * outUDx,
                   lLine, -inDx, -outLDx, -outUDx, hStart, slope / lLine,
                   uniteUR);
    }

    // Update lefts - search for a new line start
    inLL = inLR, inUL = inUR;
    for (; inLL != inLEnd && sel.areEqual(*inLL, *inUL);
         inLL += inDx, inUL += inDx)
      ;
  }

  // Special case: filter the last line in the row
  if (inLL != inLEnd) {
    // Found a line to process
    lLine = (inLR - inLL) / inDx;

    inLL_1 = inLL - inDx, inUL_1 = inUL - inDx;
    x = (inLL - inLRow) / inDx;

    uniteUL = sel.areEqual(*inUL, *inLL_1);
    uniteLL = sel.areEqual(*inLL, *inUL_1);
    if (uniteUL || uniteLL) {
      if (uniteUL && uniteLL)
        uniteUL = checkNeighbourHood(x, r, inUL, lx, ly, inDx, inDy, sel);

      if (checkLength(lLine, r, ly, inDy, inLL_1, inUL_1, inLL, inUL, uniteUL,
                      do1Line, sel))
        filterLine(inLL, inUL, outLRow + x * outLDx, outURow + x * outUDx,
                   lLine, inDx, outLDx, outUDx, hStart, slope / (lLine << 1),
                   uniteUL);
    }
  }
}

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

template <typename PIX>
void makeAntialias(const TRasterPT<PIX> &src, TRasterPT<PIX> &dst,
                   int threshold, int softness) {
  dst->copy(src);
  if (softness == 0) return;

  double slope  = (50.0 / softness);
  double hStart = 0.5;  // fixed for now

  src->lock();
  dst->lock();

  PixelSelector<PIX> sel(threshold);

  // First, filter by rows
  int x, y, lx = src->getLx(), ly = src->getLy(), lx_1 = lx - 1, ly_1 = ly - 1;
  for (y = 0; y < ly_1; ++y) {
    processLine(y, lx, ly, src->pixels(y), src->pixels(y + 1), dst->pixels(y),
                dst->pixels(y + 1), 1, src->getWrap(), 1, 1, true, hStart,
                slope, sel);
  }

  // Then, go by columns
  for (x = 0; x < lx_1; ++x) {
    processLine(x, ly, lx, src->pixels(0) + x, src->pixels(0) + x + 1,
                dst->pixels(0) + x, dst->pixels(0) + x + 1, src->getWrap(), 1,
                dst->getWrap(), dst->getWrap(), false, hStart, slope, sel);
  }

  dst->unlock();
  src->unlock();
}

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

void TRop::antialias(const TRasterP &src, const TRasterP &dst, int threshold,
                     int softness) {
  assert(src->getSize() == dst->getSize());

  TRaster32P src32(src), dst32(dst);
  if (src32 && dst32) {
    makeAntialias<TPixel32>(src32, dst32, threshold, softness);
    return;
  }

  TRaster64P src64(src), dst64(dst);
  if (src64 && dst64) {
    makeAntialias<TPixel64>(src64, dst64, threshold << 8, softness);
    return;
  }

  TRasterCM32P srcCM(src), dstCM(dst);
  if (srcCM && dstCM) {
    makeAntialias<TPixelCM32>(srcCM, dstCM, threshold, softness);
    return;
  }

  assert(!"Source and destination rasters must be of the same type!");
}
