/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * Copyright 2009 Pierre Ossman for Cendio AB
 * Copyright (C) 2011 D. R. Commander.  All Rights Reserved.
 * Copyright (C) 2011 Brian P. Hinz
 *
 * This 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 software 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 software; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */

//
// PixelFormat
//

package com.tigervnc.rfb;

import java.awt.color.*;
import java.awt.image.*;
import java.nio.*;
import java.util.*;

import com.tigervnc.rdr.*;

public class PixelFormat {

  public PixelFormat(int b, int d, boolean e, boolean t,
                     int rm, int gm, int bm, int rs, int gs, int bs)
  {
    bpp = b; depth = d; trueColour = t; bigEndian = e;
    redMax = rm; greenMax = gm; blueMax = bm;
    redShift = rs; greenShift = gs; blueShift = bs;
    converters = new HashMap<Integer, ColorConvertOp>();
    assert(isSane());

    updateState();
  }

  public PixelFormat()
  {
    this(8, 8, false, true, 7, 7, 3, 0, 3, 6);
    updateState();
  }

  public boolean equal(PixelFormat other)
  {
    if (bpp != other.bpp || depth != other.depth)
      return false;

    if (redMax != other.redMax)
      return false;
    if (greenMax != other.greenMax)
      return false;
    if (blueMax != other.blueMax)
      return false;

    // Endianness requires more care to determine compatibility
    if (bigEndian == other.bigEndian || bpp == 8) {
      if (redShift != other.redShift)
        return false;
      if (greenShift != other.greenShift)
        return false;
      if (blueShift != other.blueShift)
        return false;
    } else {
      // Has to be the same byte for each channel
      if (redShift/8 != (3 - other.redShift/8))
        return false;
      if (greenShift/8 != (3 - other.greenShift/8))
        return false;
      if (blueShift/8 != (3 - other.blueShift/8))
        return false;

      // And the same bit offset within the byte
      if (redShift%8 != other.redShift%8)
        return false;
      if (greenShift%8 != other.greenShift%8)
        return false;
      if (blueShift%8 != other.blueShift%8)
        return false;

      // And not cross a byte boundary
      if (redShift/8 != (redShift + redBits - 1)/8)
        return false;
      if (greenShift/8 != (greenShift + greenBits - 1)/8)
        return false;
      if (blueShift/8 != (blueShift + blueBits - 1)/8)
        return false;
    }

    return true;
  }

  public void read(InStream is) {
    bpp = is.readU8();
    depth = is.readU8();
    bigEndian = is.readU8()!=0;
    trueColour = is.readU8()!=0;
    redMax = is.readU16();
    greenMax = is.readU16();
    blueMax = is.readU16();
    redShift = is.readU8();
    greenShift = is.readU8();
    blueShift = is.readU8();
    is.skip(3);

    // We have no real support for colour maps. If the client
    // wants one, then we force a 8-bit true colour format and
    // pretend it's a colour map.
    if (!trueColour) {
      redMax = 7;
      greenMax = 7;
      blueMax = 3;
      redShift = 0;
      greenShift = 3;
      blueShift = 6;
    }

    if (!isSane())
      throw new Exception("invalid pixel format: "+print());

    updateState();
  }

  public void write(OutStream os) {
    os.writeU8(bpp);
    os.writeU8(depth);
    os.writeU8(bigEndian?1:0);
    os.writeU8(trueColour?1:0);
    os.writeU16(redMax);
    os.writeU16(greenMax);
    os.writeU16(blueMax);
    os.writeU8(redShift);
    os.writeU8(greenShift);
    os.writeU8(blueShift);
    os.pad(3);
  }

  public final boolean isBigEndian() {
    return bigEndian;
  }

  public final boolean isLittleEndian() {
    return ! bigEndian;
  }

  public final boolean is888() {
    if(!trueColour)
      return false;
    if(bpp != 32)
      return false;
    if(depth != 24)
      return false;
    if(redMax != 255)
      return false;
    if(greenMax != 255)
      return false;
    if(blueMax != 255)
      return false;

    return true;
  }

  public int pixelFromRGB(int red, int green, int blue, ColorModel cm)
  {
    if (trueColour) {
      int r = (red   * redMax     + 32767) / 65535;
      int g = (green * greenMax   + 32767) / 65535;
      int b = (blue  * blueMax    + 32767) / 65535;

      return (r << redShift) | (g << greenShift) | (b << blueShift);
    } else if (cm != null) {
      // Try to find the closest pixel by Cartesian distance
      int colours = 1 << depth;
      int diff = 256 * 256 * 4;
      int col = 0;
      for (int i=0; i<colours; i++) {
        int r, g, b;
        r = cm.getRed(i);
        g = cm.getGreen(i);
        b = cm.getBlue(i);
        int rd = (r-red) >> 8;
        int gd = (g-green) >> 8;
        int bd = (b-blue) >> 8;
        int d = rd*rd + gd*gd + bd*bd;
        if (d < diff) {
          col = i;
          diff = d;
        }
      }
      return col;
    }
    // XXX just return 0 for colour map?
    return 0;
  }

  public void bufferFromRGB(ByteBuffer dst, ByteBuffer src, int pixels)
  {
    bufferFromRGB(dst, src, pixels, pixels, 1);
  }

  public void bufferFromRGB(ByteBuffer dst, ByteBuffer src,
                            int w, int stride, int h)
  {
    if (is888()) {
      // Optimised common case
      int r, g, b, x;

      if (bigEndian) {
        r = dst.position() + (24 - redShift)/8;
        g = dst.position() + (24 - greenShift)/8;
        b = dst.position() + (24 - blueShift)/8;
        x = dst.position() + (24 - (48 - redShift - greenShift - blueShift))/8;
      } else {
        r = dst.position() + redShift/8;
        g = dst.position() + greenShift/8;
        b = dst.position() + blueShift/8;
        x = dst.position() + (48 - redShift - greenShift - blueShift)/8;
      }

      int dstPad = (stride - w) * 4;
      while (h-- > 0) {
        int w_ = w;
        while (w_-- > 0) {
          dst.put(r, src.get());
          dst.put(g, src.get());
          dst.put(b, src.get());
          dst.put(x, (byte)0);
          r += 4;
          g += 4;
          b += 4;
          x += 4;
        }
        r += dstPad;
        g += dstPad;
        b += dstPad;
        x += dstPad;
      }
    } else {
      // Generic code
      int dstPad = (stride - w) * bpp/8;
      while (h-- > 0) {
        int w_ = w;
        while (w_-- > 0) {
          int p;
          int r, g, b;

          r = src.get();
          g = src.get();
          b = src.get();

          p = pixelFromRGB(r, g, b, model);

          bufferFromPixel(dst, p);
          dst.position(dst.position() + bpp/8);
        }
        dst.position(dst.position() + dstPad);
      }
    }
  }

  public void rgbFromBuffer(ByteBuffer dst, ByteBuffer src, int pixels)
  {
    rgbFromBuffer(dst, src, pixels, pixels, 1);
  }

  public void rgbFromBuffer(ByteBuffer dst, ByteBuffer src,
                            int w, int stride, int h)
  {
    if (is888()) {
      // Optimised common case
      int r, g, b;

      if (bigEndian) {
        r = src.position() + (24 - redShift)/8;
        g = src.position() + (24 - greenShift)/8;
        b = src.position() + (24 - blueShift)/8;
      } else {
        r = src.position() + redShift/8;
        g = src.position() + greenShift/8;
        b = src.position() + blueShift/8;
      }

      int srcPad = (stride - w) * 4;
      while (h-- > 0) {
        int w_ = w;
        while (w_-- > 0) {
          dst.put(src.get(r));
          dst.put(src.get(g));
          dst.put(src.get(b));
          r += 4;
          g += 4;
          b += 4;
        }
        r += srcPad;
        g += srcPad;
        b += srcPad;
      }
    } else {
      // Generic code
      int srcPad = (stride - w) * bpp/8;
      while (h-- > 0) {
        int w_ = w;
        while (w_-- > 0) {
          int p;
          byte r, g, b;

          p = pixelFromBuffer(src.duplicate());

          r = (byte)getColorModel().getRed(p);
          g = (byte)getColorModel().getGreen(p);
          b = (byte)getColorModel().getBlue(p);

          dst.put(r);
          dst.put(g);
          dst.put(b);
          src.position(src.position() + bpp/8);
        }
        src.reset().position(src.position() + srcPad).mark();
      }
    }
  }

  public void rgbFromPixels(byte[] dst, int dstPtr, int[] src, int srcPtr, int pixels, ColorModel cm)
  {
    int p;
    byte r, g, b;

    for (int i=0; i < pixels; i++) {
      p = src[i];

      dst[dstPtr++] = (byte)cm.getRed(p);
      dst[dstPtr++] = (byte)cm.getGreen(p);
      dst[dstPtr++] = (byte)cm.getBlue(p);
    }
  }

  public int pixelFromBuffer(ByteBuffer buffer)
  {
    int p;

    p = 0xff000000;

    if (!bigEndian) {
      switch (bpp) {
      case 32:
        p |= buffer.get() << 24;
        p |= buffer.get() << 16;
      case 16:
        p |= buffer.get() << 8;
      case 8:
        p |= buffer.get();
      }
    } else {
      p |= buffer.get(0);
      if (bpp >= 16) {
        p |= buffer.get(1) << 8;
        if (bpp == 32) {
          p |= buffer.get(2) << 16;
          p |= buffer.get(3) << 24;
        }
      }
    }

    return p;
  }

  public String print() {
    StringBuffer s = new StringBuffer();
    s.append("depth "+depth+" ("+bpp+"bpp)");
    if (bpp != 8) {
      if (bigEndian)
        s.append(" big-endian");
      else
        s.append(" little-endian");
    }

    if (!trueColour) {
      s.append(" colour-map");
      return s.toString();
    }

    if (blueShift == 0 && greenShift > blueShift && redShift > greenShift &&
        blueMax  == (1 << greenShift) - 1 &&
        greenMax == (1 << (redShift-greenShift)) - 1 &&
        redMax   == (1 << (depth-redShift)) - 1)
    {
      s.append(" rgb"+(depth-redShift)+(redShift-greenShift)+greenShift);
      return s.toString();
    }

    if (redShift == 0 && greenShift > redShift && blueShift > greenShift &&
        redMax   == (1 << greenShift) - 1 &&
        greenMax == (1 << (blueShift-greenShift)) - 1 &&
        blueMax  == (1 << (depth-blueShift)) - 1)
    {
      s.append(" bgr"+(depth-blueShift)+(blueShift-greenShift)+greenShift);
      return s.toString();
    }

    s.append(" rgb max "+redMax+","+greenMax+","+blueMax+" shift "+redShift+
             ","+greenShift+","+blueShift);
    return s.toString();
  }

  private static int bits(int value)
  {
    int bits;

    bits = 16;

    if ((value & 0xff00) == 0) {
      bits -= 8;
      value <<= 8;
    }
    if ((value & 0xf000) == 0) {
      bits -= 4;
      value <<= 4;
    }
    if ((value & 0xc000) == 0) {
      bits -= 2;
      value <<= 2;
    }
    if ((value & 0x8000) == 0) {
      bits -= 1;
      value <<= 1;
    }

    return bits;
  }

  private void updateState()
  {
    int endianTest = 1;

    redBits = bits(redMax);
    greenBits = bits(greenMax);
    blueBits = bits(blueMax);

    maxBits = redBits;
    if (greenBits > maxBits)
      maxBits = greenBits;
    if (blueBits > maxBits)
      maxBits = blueBits;

    minBits = redBits;
    if (greenBits < minBits)
      minBits = greenBits;
    if (blueBits < minBits)
      minBits = blueBits;

    if ((((char)endianTest) == 0) != bigEndian)
      endianMismatch = true;
    else
      endianMismatch = false;

    model = getColorModel(this);
  }

  private boolean isSane()
  {
    int totalBits;

    if ((bpp != 8) && (bpp != 16) && (bpp != 32))
      return false;
    if (depth > bpp)
      return false;

    if (!trueColour && (depth != 8))
      return false;

    if ((redMax & (redMax + 1)) != 0)
      return false;
    if ((greenMax & (greenMax + 1)) != 0)
      return false;
    if ((blueMax & (blueMax + 1)) != 0)
      return false;

    /*
     * We don't allow individual channels > 8 bits in order to keep our
     * conversions simple.
     */
    if (redMax >= (1 << 8))
      return false;
    if (greenMax >= (1 << 8))
      return false;
    if (blueMax >= (1 << 8))
      return false;

    totalBits = bits(redMax) + bits(greenMax) + bits(blueMax);
    if (totalBits > bpp)
      return false;

    if (((redMax << redShift) & (greenMax << greenShift)) != 0)
      return false;
    if (((redMax << redShift) & (blueMax << blueShift)) != 0)
      return false;
    if (((greenMax << greenShift) & (blueMax << blueShift)) != 0)
      return false;

    return true;
  }

  public void bufferFromPixel(ByteBuffer buffer, int p)
  {
    if (bigEndian) {
      switch (bpp) {
        case 32:
          buffer.put((byte)((p >> 24) & 0xff));
          buffer.put((byte)((p >> 16) & 0xff));
          break;
        case 16:
          buffer.put((byte)((p >> 8) & 0xff));
          break;
        case 8:
          buffer.put((byte)((p >> 0) & 0xff));
          break;
      }
    } else {
      buffer.put(0, (byte)((p >> 0) & 0xff));
      if (bpp >= 16) {
        buffer.put(1, (byte)((p >> 8) & 0xff));
        if (bpp == 32) {
          buffer.put(2, (byte)((p >> 16) & 0xff));
          buffer.put(3, (byte)((p >> 24) & 0xff));
        }
      }
    }
  }

  public ColorModel getColorModel()
  {
    return model;
  }

  public static ColorModel getColorModel(PixelFormat pf) {
    if (!(pf.bpp == 32) && !(pf.bpp == 16) && !(pf.bpp == 8))
      throw new Exception("Internal error: bpp must be 8, 16, or 32 in PixelBuffer ("+pf.bpp+")");
    ColorModel cm;
    switch (pf.depth) {
    case  3:
      // Fall-through to depth 8
    case  6:
      // Fall-through to depth 8
    case  8:
      int rmask = pf.redMax << pf.redShift;
      int gmask = pf.greenMax << pf.greenShift;
      int bmask = pf.blueMax << pf.blueShift;
      cm = new DirectColorModel(8, rmask, gmask, bmask);
      break;
    case 16:
      cm = new DirectColorModel(32, 0xF800, 0x07C0, 0x003E);
      break;
    case 24:
      cm = new DirectColorModel(32, (0xff << 16), (0xff << 8), 0xff);
      break;
    case 32:
      cm = new DirectColorModel(32, (0xff << pf.redShift),
        (0xff << pf.greenShift), (0xff << pf.blueShift));
      break;
    default:
      throw new Exception("Unsupported color depth ("+pf.depth+")");
    }
    assert(cm != null);
    return cm;
  }

  public ColorConvertOp getColorConvertOp(ColorSpace src)
  {
    // The overhead associated with initializing ColorConvertOps is
    // enough to justify maintaining a static lookup table.
    if (converters.containsKey(src.getType()))
      return converters.get(src.getType());
    ColorSpace dst = model.getColorSpace();
    converters.put(src.getType(), new ColorConvertOp(src, dst, null));
    return converters.get(src.getType());
  }

  public ByteOrder getByteOrder()
  {
    if (isBigEndian())
      return ByteOrder.BIG_ENDIAN;
    else
      return ByteOrder.LITTLE_ENDIAN;
  }

  public Raster rasterFromBuffer(Rect r, ByteBuffer buf)
  {
    Buffer dst;
    DataBuffer db = null;

    SampleModel sm =
      model.createCompatibleSampleModel(r.width(), r.height());
    switch (sm.getTransferType()) {
    case DataBuffer.TYPE_INT:
      dst = IntBuffer.allocate(r.area()).put(buf.asIntBuffer());
      db = new DataBufferInt(((IntBuffer)dst).array(), r.area());
      break;
    case DataBuffer.TYPE_BYTE:
      db = new DataBufferByte(buf.array(), r.area());
      break;
    case DataBuffer.TYPE_SHORT:
      dst = ShortBuffer.allocate(r.area()).put(buf.asShortBuffer());
      db = new DataBufferShort(((ShortBuffer)dst).array(), r.area());
      break;
    }
    assert(db != null);
    return Raster.createRaster(sm, db, new java.awt.Point(0, 0));
  }

  private static HashMap<Integer, ColorConvertOp> converters;

  public int bpp;
  public int depth;
  public boolean bigEndian;
  public boolean trueColour;
  public int redMax;
  public int greenMax;
  public int blueMax;
  public int redShift;
  public int greenShift;
  public int blueShift;

  protected int redBits, greenBits, blueBits;
  protected int maxBits, minBits;
  protected boolean endianMismatch;

  private ColorModel model;
}
