content: /*
   * HalfNES by Andrew Hoffman
   * Licensed under the GNU GPL Version 3. See LICENSE file
   */
  package com.grapeshot.halfnes.video;
  
  import java.awt.image.*;
  import java.util.*;
  
  import java.util.zip.CRC32;
  
  /**
   *
   * @author Andrew
   */
  public class NTSCRenderer extends Renderer {
  
      private final static List<Integer> lines;
      static {
          lines = new ArrayList<>();
          for (int line = 0; line < 240; ++line) {
              lines.add(line);
          }
      }
  
      //hm, if I downsampled these perfectly to 4Fsc i could get rid of matrix decode
      //and the sine tables altogether...
      private final static int[][] colorphases = { //int for alignment reasons
          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},//0x00
          {0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1},//0x01
          {0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0},//0x02
          {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0},//0x03
          {0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0},//0x04
          {0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0},//0x05
          {0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0},//0x06
          {1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0},//0x07
          {1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1},//0x08
          {1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1},//0x09
          {1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1},//0x0A
          {1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1},//0x0B
          {1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1},//0x0C
          {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},//0x0D
          {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},//0x0E
          {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};//0x0F
      //i would like to replace these tables with logic but it's a tricky shape
      //for a Karnaugh map
      private final static float[][][] lumas = genlumas();
      private final static int[][] coloremph = {
          {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
          {1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1},//X
          {0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0},//Y
          {1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1},//XY
          {1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1},//Z
          {1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1},//XZ
          {1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1},//YZ
          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}};//XYZ
      //private final static float sync = -0.359f;
      private int frames = 0;
      private final float[] i_filter = new float[12], q_filter = new float[12];
      private final static int[] colortbl = genColorCorrectTbl();
  
      public NTSCRenderer() {
          frame_width = 704 * 3;
          init_images();
          int hue = -512;
          double col_adjust = 1.2 / .707;
          for (int j = 0; j < 12; ++j) {
              float angle = (float) (Math.PI * ((hue + (j << 8)) / (12 * 128.0) - 33.0 / 180));
              i_filter[j] = (float) (-col_adjust * Math.cos(angle));
              q_filter[j] = (float) (col_adjust * Math.sin(angle));
          }
      }
  
      public static int[] genColorCorrectTbl() {
          int[] corr = new int[256];
          //float gamma = 1.2;
          float brightness = 20;
          float contrast = 1;
          for (int i = 0; i < 256; ++i) {
              float br = (i * contrast - (128 * contrast) + 128 + brightness) / 255.f;
              corr[i] = clamp((int) (255 * Math.pow(br, 1.3)));
              //convert tv gamma image (~2.2-2.5) to computer gamma (~1.8)
          }
          return corr;
      }
  
      public static float[][][] genlumas() {
          float[][] lumas = {
              {-0.117f, 0.000f, 0.308f, 0.715f}, //low phase
              //0x00    0x10    0x20    0x30
              {0.397f, 0.681f, 1.0f, 1.0f} //high phase
          };
          float[][][] premultlumas = new float[lumas.length][lumas[0].length][2];
          for (int i = 0; i < lumas.length; ++i) {
              for (int j = 0; j < lumas[i].length; ++j) {
                  premultlumas[i][j][0] = lumas[i][j];
                  premultlumas[i][j][1] = lumas[i][j] * 0.735f;
              }
          }
          return premultlumas;
      }
  
      public final float[] ntsc_encode(final int[] nescolors, final int offset, final int scanline, final int bgcolor) {
          //part one of the process. creates a 2728 pxl array of floats representing
          //ntsc version of scanline passed to it. Meant to be called 240x a frame
  
          //todo:
          //-make this encode an entire frame at a time
          //-reduce # of array lookups (precalc. what is necessary)
          int i, col = bgcolor & 0xf, lum = (bgcolor >> 4) & 3, emphasis = (bgcolor >> 6);
          //luminance portion of nes color is bits 4-6, chrominance part is bits 1-3
          //they are both used as the index into various tables
          //the chroma generator chops between 2 different voltages from luma table 
          //at a constant rate but shifted phase.
  
          //sync and front porch are not actually used by decoder so not implemented here
          //dot 0-200:sync
          //dot 200-232:black
          //dot 232-352:colorburst
          //dot 352-400:black  
          //dot 520-2568:picture
          //dot 400-520 and 2568-2656: background color
          //dot 2656-2720:black
          //but then i'm going to chop off before dot 240 and after 2656 b/c it's not used
          //so after this comment, add 240 to any num. in this for dot #
          final float[] sample = new float[2728 - 240];
          for (i = 400 - 240; i < 520 - 240; ++i) { //bg color at beginning
              final int phase = (i + offset) % 12;
              final int hue = colorphases[col][phase];
              sample[i] = lumas[hue][lum][coloremph[emphasis][phase]];
          }
          for (i = 2568 - 240; i < 2656 - 240; ++i) { //bg color at end of line
              final int phase = (i + offset) % 12;
              final int hue = colorphases[col][phase];
              sample[i] = lumas[hue][lum][coloremph[emphasis][phase]];
          }
          for (i = 520 - 240; i < 2568 - 240; ++i) { //picture
              if ((i & 7) == 0) {
                  col = nescolors[(((i - (520 - 240)) >> 3))];
                  if ((col & 0xf) > 0xd) {
                      col = 0x0f;
                  }
                  lum = (col >> 4) & 3;
                  emphasis = (col >> 6);
                  col &= 0xf;
              }
              final int phase = (i + offset) % 12;
              final int hue = colorphases[col][phase];
              sample[i] = lumas[hue][lum][coloremph[emphasis][phase]];
          }
          sample[2728 - 241] = offset; //hack to not have to deal with a tuple
          return sample;
      }
      public final static float chroma_filterfreq = 3579000.f, pixel_rate = 42950000.f;
      private final static int[] cbstphase = {240 - 240, 0, 250 - 240, 0, 248 - 240, 0, 246 - 240, 0, 244 - 240, 0, 242 - 240, 0};
      //starting point for color burst (depends on offset of previous line, even values not used in a progressive signal)
  
      public final int[] ntsc_decode(final float[] ntsc, final int offset) {
          final float[] chroma = new float[2656 - 240];
          final float[] luma = new float[2656 - 240];
          final float[] eye = new float[2656 - 240];
          final float[] queue = new float[2656 - 240];
          final int[] line = new int[frame_w];
  
          //decodes one scan line of ntsc video and outputs as rgb packed in int
          //uses the cheap TV method, which is filtering the chroma from the luma w/o
          //combing or buffering previous lines
          box_filter(ntsc, luma, chroma, 12);
  
          for (int cbst = cbstphase[offset], j = 0; cbst < 2656 - 240 - 50; ++cbst, ++j, j %= 12) {
              //matrix decode the color difference signals;
              eye[cbst] = i_filter[j] * chroma[cbst + 12];
              queue[cbst] = q_filter[j] * chroma[cbst + 12]; //comment out for teal and orange filter
          }
  
          lowpass_filter(eye, 0.06f);
          lowpass_filter(queue, 0.05f);
  
          for (int i = 0, x = 492 - 240; i < frame_w; ++i, ++x) {
              line[i] = compose_col(
                      ((luma[x] <= 0) ? 0 : colortbl[clamp((int) (iqm[0][0] * luma[x] + iqm[0][1] * eye[x] + iqm[0][2] * queue[x]))]),
                      ((luma[x] <= 0) ? 0 : colortbl[clamp((int) (iqm[1][0] * luma[x] + iqm[1][1] * eye[x] + iqm[1][2] * queue[x]))]),
                      ((luma[x] <= 0) ? 0 : colortbl[clamp((int) (iqm[2][0] * luma[x] + iqm[2][1] * eye[x] + iqm[2][2] * queue[x]))]));
          }
          return line;
      }
  
      public static void box_filter(final float[] in, final float[] lpout, final float[] hpout, final int order) {
          float accum = 0;
          for (int i = 12; i < 2656 - 240; ++i) {
              accum += in[i] - in[i - order];
              lpout[i] = accum / order;
              hpout[i] = in[i] - lpout[i];
          }
      }
  
      public static void lowpass_filter(final float[] arr, final float order) {
          float b = 0;
          for (int i = 0; i < 2656 - 240; ++i) {
              arr[i] -= b;
              b += arr[i] * order;
              arr[i] = b;
          }
      }
  
      private static int compose_col(int r, int g, int b) {
          return (r << 16) | (g << 8) | (b) | 0xff000000;
      }
      private final static int[][] iqm = {{255, -244, 158}, {255, 69, -165}, {255, 282, 434}};
  
      public static int clamp(final int a) {
          return (a != (a & 0xff)) ? ((a < 0) ? 0 : 255) : a;
      }
      public final static int frame_w = 704 * 3;
      int[] frame = new int[frame_w * 240];
  //    Kernel kernel = new Kernel(3, 3,
  //            new float[]{-.0625f, .125f, -.0625f,
  //                .125f, .75f, .125f,
  //                -.0625f, .125f, -.0625f});
  //    BufferedImageOp op = new ConvolveOp(kernel);
  
      @Override
      public BufferedImage render(final int[] nespixels, final int[] bgcolors, final boolean dotcrawl) {
          // multithreaded filter
          lines.parallelStream().forEach(line -> cacheRender(nespixels, line, bgcolors, dotcrawl));
  
          BufferedImage i = getBufferedImage(frame);
          ++frames;
          //i = op.filter(i, null); //sharpen
          return i;
      }
      //ConcurrentHashMap cache = new ConcurrentHashMap<Long, int[]>(600);
      Map<Long, int[]> cache = Collections.synchronizedMap(new WeakHashMap<Long, int[]>(600));
      //weak hash map allows things in it to be garbage collected
  
      private void cacheRender(final int[] nespixels, final int line, final int[] bgcolors, final boolean dotcrawl) {
  
          //first of all, increment scanline numbers and get the offset for this line.
          int offset = ((frames & 1) == 0 && dotcrawl) ? 0 : 6;
          offset = (4 * line + offset) % 12; //3 line dot crawl
          final int[] inpixels = new int[256];
          System.arraycopy(nespixels, line << 8, inpixels, 0, 256);
          final long crc = crc32(inpixels, offset, bgcolors[line]);
  //        //you'd think crc32 would have too many collisions but i haven't seen a one
          int[] outpixels;
          outpixels = (int[]) cache.get(crc);
          if (outpixels == null) { //not in cache
              //could do with hints from the PPU here: if the entire screen is
              //scrolling horizontally, the cache will be useless.
              outpixels = ntsc_decode(ntsc_encode(inpixels, offset, line, bgcolors[line]), offset);
             cache.put(crc, outpixels);
          }
  
          System.arraycopy(outpixels, 0, frame, line * frame_w, frame_w);
      }
  
      public static long crc32(int[] array, int offset, int bgcolor) {
          CRC32 c = new CRC32();
          for (int i : array) {
              c.update(i);
          }
          //it's not immediately obvious, but this ONLY sets the CRC based on the
          //blue channel of the output. Still works well though.
          //You get some interesting compressiony effects if you only take the CRC
          //of every 20th pixel to see if your line is the same.
          //especially in sidescrolling things.
          c.update(offset);
          c.update(bgcolor);
          return c.getValue();
      }
  }
  