var fs     = require("fs"),
    stream = require("stream"),
    zlib   = require("zlib"),
    HEADER = new Buffer("89504e470d0a1a0a", "hex")

function ImageData(width, height, channels, data, trailer) {
  this.width    = width;
  this.height   = height;
  this.channels = channels;
  this.data     = data;
  this.trailer  = trailer;
}

ImageData.prototype.getPixel = function(x, y) {
  x = x|0;
  y = y|0;

  if(x < 0 || y < 0 || x >= this.width || y >= this.height)
    return 0;

  var index = (y * this.width + x) * this.channels,
      r, g, b, a;

  switch(this.channels) {
    case 1:
      r = g = b = this.data[index];
      a = 255;
      break;

    case 2:
      r = g = b = this.data[index    ];
      a         = this.data[index + 1];
      break;

    case 3:
      r = this.data[index    ];
      g = this.data[index + 1];
      b = this.data[index + 2];
      a = 255;
      break;

    case 4:
      r = this.data[index    ];
      g = this.data[index + 1];
      b = this.data[index + 2];
      a = this.data[index + 3];
      break;
  }

  return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;
}

function paeth(a, b, c) {
  var p  = a + b - c,
      pa = Math.abs(p - a),
      pb = Math.abs(p - b),
      pc = Math.abs(p - c)

  if((pa <= pb) && (pa <= pc))
    return a

  if(pb <= pc)
    return b

  return c
}

exports.parseStream = function(stream, callback) {
  var inflate           = zlib.createInflate(),
      state             = 0,
      off               = 0,
      buf               = new Buffer(13),
      waiting           = 2,
      b                 = -1,
      p                 = 0,
      pngPaletteEntries = 0,
      pngAlphaEntries   = 0,
      chunkLength, pngWidth, pngHeight, pngBitDepth, pngDepthMult,
      pngColorType, pngPixels, pngSamplesPerPixel, pngBytesPerPixel,
      pngBytesPerScanline, pngSamples, currentScanline, priorScanline,
      scanlineFilter, pngTrailer, pngPalette, pngAlpha, idChannels;

  function error(err) {
    /* FIXME: stream.destroy no longer exists in node 0.10. I can't actually
     * find what the right way to say "hey stream, I am no longer going to read
     * from you ever" or "hey stream, I am never going to write to you ever
     * again", so I'm really not sure what the right way to handle this is.
     *
     * I would appreciate pull requests from somebody who actually understands
     * how node streams are supposed to function. */
    if(stream.destroy)
      stream.destroy()

    if(inflate.destroy)
      inflate.destroy()

    return callback(err)
  }

  function end() {
    if(!--waiting)
      return callback(
        undefined,
        new ImageData(pngWidth, pngHeight, idChannels, pngPixels, pngTrailer)
      )
  }

  stream.on("error", error)
  inflate.on("error", error)

  stream.on("end", function() {
    stream.destroy()

    if(!pngPixels)
      return error(new Error("Corrupt PNG?"))

    if(!pngTrailer)
      return error(new Error("Corrupt PNG?"))

    return end()
  })

  inflate.on("end", function() {
    if(inflate.destroy)
      inflate.destroy()

    if(p !== pngPixels.length)
      return error(new Error("Too little pixel data! (Corrupt PNG?)"))

    return end()
  })

  stream.on("data", function(data) {
    /* If an error occurred, bail. */
    if(!stream.readable)
      return

    var len = data.length,
        i   = 0,
        tmp, j;

    while(i !== len)
      switch(state) {
        case 0: /* PNG header */
          if(data[i++] !== HEADER[off++])
            return error(new Error("Invalid PNG header."))

          if(off === HEADER.length) {
            state = 1
            off   = 0
          }
          break

        case 1: /* PNG chunk length and type */
          if(len - i < 8 - off) {
            data.copy(buf, off, i)
            off += len - i
            i    = len
          }

          else {
            data.copy(buf, off, i, i + 8 - off)

            i          += 8 - off
            off         = 0
            chunkLength = buf.readUInt32BE(0)

            switch(buf.toString("ascii", 4, 8)) {
              case "IHDR":
                state = 2
                break

              case "PLTE":
                /* The PNG spec states that PLTE is only required for type 3.
                 * It may appear in other types, but is only useful if the
                 * display does not support true color. Since we're just a data
                 * storage format, we don't have to worry about it. */
                if(pngColorType !== 3)
                  state = 7

                else {
                  if(chunkLength % 3 !== 0)
                    return error(new Error("Invalid PLTE size."))

                  pngPaletteEntries = chunkLength / 3
                  pngPalette        = new Buffer(chunkLength)
                  state             = 3
                }
                break

              case "tRNS":
                if(pngColorType !== 3)
                  return error(new Error("tRNS for non-paletted images not yet supported."));

                /* We only support tRNS on paletted images right now. Those
                 * images may either have 1 or 3 channels, but in either case
                 * we add one for transparency. */
                idChannels       ++;

                pngAlphaEntries = chunkLength;
                pngAlpha        = new Buffer(chunkLength);
                state           = 4;
                break

              case "IDAT":
                /* Allocate the PNG if we havn't yet. (We wait to do it until
                 * here since tRNS may change idChannels, so we can't be sure of
                 * the size needed until we hit IDAT. With all that, might as
                 * well wait until we're actually going to start filling the
                 * buffer in case of errors...) */
                if(!pngPixels)
                  pngPixels = new Buffer(pngWidth * pngHeight * idChannels);

                state = 5
                break

              case "IEND":
                state = 6
                break

              default:
                state = 7
                break
            }
          }
          break

        case 2: /* IHDR */
          if(chunkLength !== 13)
            return error(new Error("Invalid IHDR chunk."))

          else if(len - i < chunkLength - off) {
            data.copy(buf, off, i)
            off += len - i
            i    = len
          }

          else {
            data.copy(buf, off, i, i + chunkLength - off)

            if(buf.readUInt8(10) !== 0)
              return error(new Error("Unsupported compression method."))

            if(buf.readUInt8(11) !== 0)
              return error(new Error("Unsupported filter method."))

            if(buf.readUInt8(12) !== 0)
              return error(new Error("Unsupported interlace method."))

            i           += chunkLength - off
            state        = 8
            off          = 0
            pngWidth     = buf.readUInt32BE(0)
            pngHeight    = buf.readUInt32BE(4)
            pngBitDepth  = buf.readUInt8(8)
            pngDepthMult = 255 / ((1 << pngBitDepth) - 1)
            pngColorType = buf.readUInt8(9)

            switch(pngColorType) {
              case 0:
                pngSamplesPerPixel = 1;
                pngBytesPerPixel   = Math.ceil(pngBitDepth * 0.125);
                idChannels         = 1;
                break

              case 2:
                pngSamplesPerPixel = 3;
                pngBytesPerPixel   = Math.ceil(pngBitDepth * 0.375);
                idChannels         = 3;
                break;

              case 3:
                pngSamplesPerPixel = 1;
                pngBytesPerPixel   = 1;
                idChannels         = 3;
                break

              case 4:
                pngSamplesPerPixel = 2;
                pngBytesPerPixel   = Math.ceil(pngBitDepth * 0.250);
                idChannels         = 2;
                break

              case 6:
                pngSamplesPerPixel = 4;
                pngBytesPerPixel   = Math.ceil(pngBitDepth * 0.5);
                idChannels         = 4;
                break;

              default:
                return error(
                  new Error("Unsupported color type: " + pngColorType)
                );
            }

            pngBytesPerScanline = Math.ceil(
              pngWidth * pngBitDepth * pngSamplesPerPixel / 8
            )
            pngSamples          = new Buffer(pngSamplesPerPixel)
            currentScanline     = new Buffer(pngBytesPerScanline)
            priorScanline       = new Buffer(pngBytesPerScanline)
            currentScanline.fill(0)
          }
          break

        case 3: /* PLTE */
          if(len - i < chunkLength - off) {
            data.copy(pngPalette, off, i)
            off += len - i
            i    = len
          }

          else {
            data.copy(pngPalette, off, i, i + chunkLength - off)
            i    += chunkLength - off
            state = 8
            off   = 0

            /* If each entry in the color palette is grayscale, set the channel
             * count to 1. */
            idChannels = 1;
            for(j = pngPaletteEntries; j--; )
              if(pngPalette[j * 3 + 0] !== pngPalette[j * 3 + 1] ||
                 pngPalette[j * 3 + 0] !== pngPalette[j * 3 + 2]) {
                idChannels = 3;
                break;
              }
          }
          break

        case 4: /* tRNS */
          if(len - i < chunkLength - off) {
            data.copy(pngAlpha, off, i)
            off += len - i
            i    = len
          }

          else {
            data.copy(pngAlpha, off, i, i + chunkLength - off)
            i    += chunkLength - off
            state = 8
            off   = 0
          }
          break

        case 5: /* IDAT */
          /* If the amount available is less than the amount remaining, then
           * feed as much as we can to the inflator. */
          if(len - i < chunkLength - off) {
            /* FIXME: Do I need to be smart and check the return value? */
            inflate.write(data.slice(i))
            off += len - i
            i    = len
          }

          /* Otherwise, write the last bit of the data to the inflator, and
           * finish processing the chunk. */
          else {
            /* FIXME: Do I need to be smart and check the return value? */
            inflate.write(data.slice(i, i + chunkLength - off))
            i    += chunkLength - off
            state = 8
            off   = 0
          }
          break

        case 6: /* IEND */
          if(chunkLength !== 0)
            return error(new Error("Invalid IEND chunk."))

          else if(len - i < 4 - off) {
            off += len - i
            i    = len
          }

          else {
            pngTrailer = new Buffer(0)
            i         += 4 - off
            state      = 9
            off        = 0

            inflate.end()
          }
          break

        case 7: /* unrecognized chunk */
          if(len - i < chunkLength - off) {
            off += len - i
            i    = len
          }

          else {
            i    += chunkLength - off
            state = 8
            off   = 0
          }
          break

        case 8: /* chunk crc */
          /* FIXME: CRC is blatantly ignored */
          if(len - i < 4 - off) {
            off += len - i
            i    = len
          }

          else {
            i    += 4 - off
            state = 1
            off   = 0
          }
          break

        case 9: /* trailing data */
          /* FIXME: It is inefficient to create a trailer buffer of length zero
           * and keep reallocating it every time we want to add more data. */
          tmp = new Buffer(off + len - i)
          pngTrailer.copy(tmp)
          data.copy(tmp, off, i, len)
          pngTrailer = tmp
          off       += len - i
          i          = len
          break
      }
  })

  inflate.on("data", function(data) {
    /* If an error occurred, bail. */
    if(!inflate.readable)
      return

    var len = data.length,
        i, tmp, x, j, k

    for(i = 0; i !== len; ++i) {
      if(b === -1) {
        scanlineFilter  = data[i]
        tmp             = currentScanline
        currentScanline = priorScanline
        priorScanline   = tmp
      }

      else
        switch(scanlineFilter) {
          case 0:
            currentScanline[b] = data[i]
            break

          case 1:
            currentScanline[b] =
              b < pngBytesPerPixel ?
                data[i] :
                (data[i] + currentScanline[b - pngBytesPerPixel]) & 255
            break

          case 2:
            currentScanline[b] = (data[i] + priorScanline[b]) & 255
            break

          case 3:
            currentScanline[b] = (data[i] + ((
              b < pngBytesPerPixel ?
                priorScanline[b] :
                currentScanline[b - pngBytesPerPixel] + priorScanline[b]
            ) >>> 1)) & 255
            break

          case 4:
            currentScanline[b] = (data[i] + (
              b < pngBytesPerPixel ?
                priorScanline[b] :
                paeth(
                  currentScanline[b - pngBytesPerPixel],
                  priorScanline[b],
                  priorScanline[b - pngBytesPerPixel]
                )
            )) & 255
            break

          default:
            return error(
              new Error("Unsupported scanline filter: " + scanlineFilter)
            )
        }

      if(++b === pngBytesPerScanline) {
        /* One scanline too many? */
        if(p === pngPixels.length)
          return error(new Error("Too much pixel data! (Corrupt PNG?)"))

        /* We have now read a complete scanline, so unfilter it and write it
         * into the pixel array. */
        for(j = 0, x = 0; x !== pngWidth; ++x) {
          /* Read all of the samples into the sample buffer. */
          for(k = 0; k !== pngSamplesPerPixel; ++j, ++k)
            switch(pngBitDepth) {
              case 1:
                pngSamples[k] =
                  (currentScanline[(j >>> 3)] >> (7 - (j & 7))) & 1
                break

              case 2:
                pngSamples[k] =
                  (currentScanline[(j >>> 2)] >> ((3 - (j & 3)) << 1)) & 3
                break

              case 4:
                pngSamples[k] =
                  (currentScanline[(j >>> 1)] >> ((1 - (j & 1)) << 2)) & 15
                break

              case 8:
                pngSamples[k] = currentScanline[j]
                break

              default:
                return error(new Error("Unsupported bit depth: " + pngBitDepth))
            }

          /* Write the pixel based off of the samples so collected. */
          switch(pngColorType) {
            case 0:
              pngPixels[p++] = pngSamples[0] * pngDepthMult;
              break;

            case 2:
              pngPixels[p++] = pngSamples[0] * pngDepthMult;
              pngPixels[p++] = pngSamples[1] * pngDepthMult;
              pngPixels[p++] = pngSamples[2] * pngDepthMult;
              break;

            case 3:
              if(pngSamples[0] >= pngPaletteEntries)
                return error(new Error("Invalid palette index."));

              switch(idChannels) {
                case 1:
                  pngPixels[p++] = pngPalette[pngSamples[0] * 3];
                  break;

                case 2:
                  pngPixels[p++] = pngPalette[pngSamples[0] * 3];
                  pngPixels[p++] =
                    pngSamples[0] < pngAlphaEntries ?
                      pngAlpha[pngSamples[0]] :
                      255;
                  break;

                case 3:
                  pngPixels[p++] = pngPalette[pngSamples[0] * 3 + 0];
                  pngPixels[p++] = pngPalette[pngSamples[0] * 3 + 1];
                  pngPixels[p++] = pngPalette[pngSamples[0] * 3 + 2];
                  break;

                case 4:
                  pngPixels[p++] = pngPalette[pngSamples[0] * 3 + 0];
                  pngPixels[p++] = pngPalette[pngSamples[0] * 3 + 1];
                  pngPixels[p++] = pngPalette[pngSamples[0] * 3 + 2];
                  pngPixels[p++] =
                    pngSamples[0] < pngAlphaEntries ?
                      pngAlpha[pngSamples[0]] :
                      255;
                  break;
              }
              break;

            case 4:
              pngPixels[p++] = pngSamples[0] * pngDepthMult;
              pngPixels[p++] = pngSamples[1] * pngDepthMult;
              break;

            case 6:
              pngPixels[p++] = pngSamples[0] * pngDepthMult;
              pngPixels[p++] = pngSamples[1] * pngDepthMult;
              pngPixels[p++] = pngSamples[2] * pngDepthMult;
              pngPixels[p++] = pngSamples[3] * pngDepthMult;
              break;
          }
        }

        b = -1;
      }
    }
  })
}

exports.parseFile = function(pathname, callback) {
  return exports.parseStream(fs.createReadStream(pathname), callback)
}

exports.parseBuffer = function(buf, callback) {
  /* Create a mock stream. */
  var s = new stream.Stream()

  /* Set up the destroy functionality. */
  s.readable = true
  s.destroy  = function() { s.readable = false }

  /* Set up the PNG parsing hooks. */
  exports.parseStream(s, callback)

  /* Send the data down the stream. */
  s.emit("data", buf)

  /* If no errors occurred in the data, close the stream. */
  if(s.readable)
    s.emit("end")
}

/* FIXME: This is deprecated. Remove it in 2.0. */
exports.parse = exports.parseBuffer