/**
     * Decodes a single frame of animation. Does not colour the frame with the palette afterwards.
     *
     * @param framenum The number of the frame to decode.
     * @param lastbytes Frame data of the last frame decoded.
     * @return Raw decoded frame data.
     */
    private ByteBuffer decodeFrame(int framenum, ByteBuffer lastbytes) {

      int offset = wsaoffsets[framenum];
      int sourcelength = wsaoffsets[framenum + 1] - offset;

      // Source frame data (is at frame offset + palette size)
      ByteBuffer sourcebytes =
          com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
              sourcelength);
      try {
        inputchannel.read(sourcebytes);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      sourcebytes.rewind();

      // Intermediate and final frame data
      int framesize = width() * height();
      ByteBuffer intbytes =
          com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(framesize);
      ByteBuffer framebytes =
          com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(framesize);

      // First decompress from Format80, then decode as Format40
      CodecUtility.decodeFormat80(sourcebytes, intbytes);
      CodecUtility.decodeFormat40(intbytes, framebytes, lastbytes);

      return framebytes;
    }
  /** {@inheritDoc} */
  @Override
  public void write(GatheringByteChannel outputchannel) {

    try {
      int numimages = numImages();

      // Encode each image
      ByteBuffer[] images = new ByteBuffer[numimages];
      for (int i = 0; i < images.length; i++) {
        ByteBuffer image =
            com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
                shpimages[i].capacity());
        CodecUtility.encodeFormat80(shpimages[i], image);
        images[i] = image;
      }

      // Construct image offset headers for each image
      ByteBuffer[] offsets = new ByteBuffer[numimages + 2];
      int offsettotal =
          ShpFileHeaderCNC.HEADER_SIZE + (ShpImageOffsetCNC.OFFSET_SIZE * offsets.length);
      for (int i = 0; i < numImages(); i++) {
        offsets[i] = new ShpImageOffsetCNC(offsettotal, FORMAT80, 0, (byte) 0).toByteBuffer();
        offsettotal += images[i].limit();
      }

      // The 2 special image offsets at the end of the offset array
      offsets[numimages] = new ShpImageOffsetCNC(offsettotal, (byte) 0, 0, (byte) 0).toByteBuffer();
      offsets[numimages + 1] = new ShpImageOffsetCNC(0, (byte) 0, 0, (byte) 0).toByteBuffer();

      // Build header
      ByteBuffer header = shpfileheader.toByteBuffer();

      // Write file
      try {
        outputchannel.write(header);
        outputchannel.write(offsets);
        outputchannel.write(images);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    } finally {
      try {
        outputchannel.close();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }
  /**
   * Constructor, creates a new shp file with the given name and file data.
   *
   * @param name The name of this file.
   * @param bytechannel Data of the file.
   */
  public ShpFileCNC(String name, ReadableByteChannel bytechannel) {

    super(name);

    try {
      // Construct file header
      ByteBuffer headerbytes =
          com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
              ShpFileHeaderCNC.HEADER_SIZE);
      try {
        bytechannel.read(headerbytes);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      headerbytes.rewind();
      shpfileheader = new ShpFileHeaderCNC(headerbytes);

      // numImages() + 2 for the 0 offset and EOF pointer
      ShpImageOffsetCNC[] offsets = new ShpImageOffsetCNC[numImages() + 2];
      for (int i = 0; i < offsets.length; i++) {
        ByteBuffer offsetbytes =
            com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
                ShpImageOffsetCNC.OFFSET_SIZE);
        try {
          bytechannel.read(offsetbytes);
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
        offsets[i] = new ShpImageOffsetCNC((ByteBuffer) offsetbytes.rewind());
      }

      // Decompresses the raw SHP data into palette-index data
      shpimages = new ByteBuffer[numImages()];

      // Decompress every frame
      for (int i = 0; i < numImages(); i++) {
        ShpImageOffsetCNC imageoffset = offsets[i];

        // Format conversion buffers
        ByteBuffer sourcebytes =
            com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
                offsets[i + 1].offset - imageoffset.offset);
        try {
          bytechannel.read(sourcebytes);
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
        sourcebytes.rewind();
        ByteBuffer destbytes =
            com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
                width() * height());

        switch (imageoffset.offsetformat) {

            // Format80 image
          case FORMAT80:
            CodecUtility.decodeFormat80(sourcebytes, destbytes);
            break;

            // Format40 image
          case FORMAT40:
            int refoffset = imageoffset.refoff;
            int j;
            for (j = 0; j < numImages(); j++) {
              if (refoffset == offsets[j].offset) {
                break;
              }
            }
            CodecUtility.decodeFormat40(sourcebytes, destbytes, shpimages[j]);
            break;

            // Format20 image
          case FORMAT20:
            CodecUtility.decodeFormat20(sourcebytes, shpimages[i - 1], destbytes);
            break;
        }

        // Add the decompressed image to the image array
        shpimages[i] = destbytes;
      }
    } finally {
      try {
        bytechannel.close();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }
  /** {@inheritDoc} */
  @Override
  public void write(GatheringByteChannel outputchannel) {

    int numimages = numImages();

    // Build header
    ByteBuffer header = wsaheader.toByteBuffer();

    // Build palette
    ByteBuffer palette = wsapalette.toByteBuffer();

    // Encode each frame, construct matching offsets
    ByteBuffer[] frames = new ByteBuffer[isLooping() ? numimages + 1 : numimages];
    ByteBuffer lastbytes =
        com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
            width() * height());

    ByteBuffer frameoffsets =
        com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
            (numimages + 2) * 4);
    int offsettotal = WsaFileHeaderCNC.HEADER_SIZE + ((numimages + 2) * 4);

    for (int i = 0; i < frames.length; i++) {
      ByteBuffer framebytes = wsaframes[i];
      ByteBuffer frameint =
          com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
              (int) (framebytes.capacity() * 1.5));
      ByteBuffer frame =
          com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(
              (int) (framebytes.capacity() * 1.5));

      // First encode in Format40, then Format80
      CodecUtility.encodeFormat40(framebytes, frameint, lastbytes);
      CodecUtility.encodeFormat80(frameint, frame);

      frames[i] = frame;
      lastbytes = framebytes;

      frameoffsets.putInt(offsettotal);
      offsettotal += frame.limit();
    }

    // Last offset for EOF
    frameoffsets.putInt(offsettotal);
    frameoffsets.rewind();

    // Write file to disk
    try {
      outputchannel.write(header);
      outputchannel.write(frameoffsets);
      outputchannel.write(palette);
      outputchannel.write(frames);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    // Generate high-res colour lookup table
    if (!srcnohires) {

      // Figure-out the appropriate file name
      String lookupname =
          filename.contains(".")
              ? filename.substring(0, filename.lastIndexOf('.')) + ".pal"
              : filename + ".pal";

      // Write the index of the closest interpolated palette colour
      // TODO: Perform proper colour interpolation
      ByteBuffer lookup =
          com.mikeduvall.redhorizon.util.ByteBufferFactory.createLittleEndianByteBuffer(256);
      for (int i = 0; i < 256; i++) {
        lookup.put((byte) i);
      }
      lookup.rewind();

      try (FileChannel lookupfile = FileChannel.open(Paths.get(lookupname), WRITE)) {
        for (int i = 0; i < 256; i++) {
          lookupfile.write(lookup);
        }
      }
      // TODO: Should be able to soften the auto-close without needing this
      catch (IOException ex) {
        throw new RuntimeException(ex);
      }
    }
  }