/* BlinkenLib
 * version 0.1.1 date 2006-10-17
 * Copyright (C) 2004-2006: Stefan Schuermans <1stein@schuermans.info>
 * Copyleft: GNU public license - http://www.gnu.org/copyleft/gpl.html
 * a blinkenarea.org project
 */

package org.blinkenarea.BlinkenLib;

import java.awt.*;
import org.blinkenarea.BlinkenLib.*;

public class BlinkenFrame
{

  private static byte [] BlinkenProtoBlpMagic = {(byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF};
  private static byte [] BlinkenProtoEblpMagic = {(byte)0xFE, (byte)0xED, (byte)0xBE, (byte)0xEF};
  private static byte [] BlinkenProtoMcufMagic = {(byte)0x23, (byte)0x54, (byte)0x26, (byte)0x66};

  private int height;
  private int width;
  private int channels;
  private int maxval;
  private int duration;
  private byte[][] data;

  public BlinkenFrame( int height, int width, int channels, int maxval, int duration )
  {
    if( height < BlinkenConstants.BlinkenHeightMin ) height = BlinkenConstants.BlinkenHeightMin;
    if( height > BlinkenConstants.BlinkenHeightMax ) height = BlinkenConstants.BlinkenHeightMax;
    if( width < BlinkenConstants.BlinkenWidthMin ) width = BlinkenConstants.BlinkenWidthMin;
    if( width > BlinkenConstants.BlinkenWidthMax ) width = BlinkenConstants.BlinkenWidthMax;
    if( channels < BlinkenConstants.BlinkenChannelsMin ) channels = BlinkenConstants.BlinkenChannelsMin;
    if( channels > BlinkenConstants.BlinkenChannelsMax ) channels = BlinkenConstants.BlinkenChannelsMax;
    if( maxval < BlinkenConstants.BlinkenMaxvalMin ) maxval = BlinkenConstants.BlinkenMaxvalMin;
    if( maxval > BlinkenConstants.BlinkenMaxvalMax ) maxval = BlinkenConstants.BlinkenMaxvalMax;
    if( duration < BlinkenConstants.BlinkenDurationMin ) duration = BlinkenConstants.BlinkenDurationMin;
    if( duration > BlinkenConstants.BlinkenDurationMax ) duration = BlinkenConstants.BlinkenDurationMax;

    this.height = height;
    this.width = width;
    this.channels = channels;
    this.maxval = maxval;
    this.duration = duration;
    data = new byte[height][width * channels];
  }

  public BlinkenFrame( BlinkenFrame frame )
  {
    int y, x, c, i;
    height = frame.height;
    width = frame.width;
    channels = frame.channels;
    maxval = frame.maxval;
    duration = frame.duration;
    data = new byte[height][width * channels];
    for( y = 0; y < height; y++ )
      for( x = 0, i = 0; x < width; x++ )
        for( c = 0; c < channels; c++, i++ )
          data[y][i] = frame.data[y][i];
  }

  public void clear( )
  {
    int x, y, c, i;
    for( y = 0; y < height; y++ )
      for( x = 0, i = 0; x < width; x++ )
        for( c = 0; c < channels; c++, i++ )
          data[y][i] = 0;
  }

  public int getHeight( )
  {
    return height;
  }

  public int getWidth( )
  {
    return width;
  }

  public int getChannels( )
  {
    return channels;
  }

  public int getMaxval( )
  {
    return maxval;
  }

  public int getDuration( )
  {
    return duration;
  }

  public void setDuration( int duration )
  {
    if( duration < BlinkenConstants.BlinkenDurationMin ) duration = BlinkenConstants.BlinkenDurationMin;
    if( duration > BlinkenConstants.BlinkenDurationMax ) duration = BlinkenConstants.BlinkenDurationMax;
    this.duration = duration;
  }

  public byte getPixel( int y, int x, int c )
  {
    if( y < 0 || y >= height ||
        x < 0 || x >= width ||
	c < 0 || c >= channels )
      return 0;
    return data[y][x * channels + c];
  }

  public void setPixel( int y, int x, int c, byte val )
  {
    if( y < 0 || y >= height ||
        x < 0 || x >= width ||
	c < 0 || c >= channels )
      return;
    if( val > maxval )
      val = (byte)maxval;
    data[y][x * channels + c] = val;
  }

  public Color getColor( int y, int x )
  {
    if( y < 0 || y >= height ||
        x < 0 || x >= width ||
        channels < 1 )
      return new Color( 0, 0, 0 );
    if( channels == 1 )
      return new Color( (((int)data[y][x * 1 + 0] & 0xFF) * 255 + maxval / 2) / maxval,
                        (((int)data[y][x * 1 + 0] & 0xFF) * 255 + maxval / 2) / maxval,
                        (((int)data[y][x * 1 + 0] & 0xFF) * 255 + maxval / 2) / maxval );
    if( channels == 2 )
      return new Color( (((int)data[y][x * 2 + 0] & 0xFF) * 255 + maxval / 2) / maxval,
                        (((int)data[y][x * 2 + 1] & 0xFF) * 255 + maxval / 2) / maxval, 0 );
    int i = x * channels;
    return new Color( (((int)data[y][i + 0] & 0xFF) * 255 + maxval / 2) / maxval,
                      (((int)data[y][i + 1] & 0xFF) * 255 + maxval / 2) / maxval,
                      (((int)data[y][i + 2] & 0xFF) * 255 + maxval / 2) / maxval );
  }

  public void setColor( int y, int x, Color color )
  {
    int alpha, alpha_, c;
    if( y < 0 || y >= height ||
        x < 0 || x >= width )
      return;
    int i = x * channels;
    alpha = color.getAlpha( );
    alpha_ = 255 - alpha;
    if( channels >= 1 )
      data[y][i + 0] = (byte)(( ((color.getRed( ) * maxval + 127) / 255) * alpha
                              + ((int)data[y][i + 0] & 0xFF) * alpha_
                              ) / 255);
    if( channels >= 2 )
      data[y][i + 1] = (byte)(( ((color.getGreen( ) * maxval + 127) / 255) * alpha
                              + ((int)data[y][i + 1] & 0xFF) * alpha_
                              ) / 255);
    if( channels >= 3 )
      data[y][i + 2] = (byte)(( ((color.getBlue( ) * maxval + 127) / 255) * alpha
                              + ((int)data[y][i + 2] & 0xFF) * alpha_
                              ) / 255);
    for( c = 3; c < channels; c++ )
      data[y][i + c] = (byte)(( 0
                              + ((int)data[y][i + c] & 0xFF) * alpha_
                              ) / 255);
  }

  public void resize( int height, int width, int channels, int maxval )
  {
    byte[][] new_data;
    int y, x, c, i;
    int emptyY, emptyX, skipY, skipX, rangeY, rangeX, val, div;

    if( height < BlinkenConstants.BlinkenHeightMin ) height = BlinkenConstants.BlinkenHeightMin;
    if( height > BlinkenConstants.BlinkenHeightMax ) height = BlinkenConstants.BlinkenHeightMax;
    if( width < BlinkenConstants.BlinkenWidthMin ) width = BlinkenConstants.BlinkenWidthMin;
    if( width > BlinkenConstants.BlinkenWidthMax ) width = BlinkenConstants.BlinkenWidthMax;
    if( channels < BlinkenConstants.BlinkenChannelsMin ) channels = BlinkenConstants.BlinkenChannelsMin;
    if( channels > BlinkenConstants.BlinkenChannelsMax ) channels = BlinkenConstants.BlinkenChannelsMax;
    if( maxval < BlinkenConstants.BlinkenMaxvalMin ) maxval = BlinkenConstants.BlinkenMaxvalMin;
    if( maxval > BlinkenConstants.BlinkenMaxvalMax ) maxval = BlinkenConstants.BlinkenMaxvalMax;

    if( height == this.height &&
        width == this.width &&
        channels == this.channels &&
        maxval == this.maxval )
      return;

    //allocate new data array
    new_data = new byte[height][width * channels];
    for( y = 0; y < height; y++ )
      for( x = 0, i = 0; x < width; x++ )
        for( c = 0; c < channels; c++, i++ )
          new_data[y][i] = 0;

    //get number of pixels to skip / to leave empty in X and Y direction
    if( height > this.height )
    {
      emptyY = (height - this.height) / 2; 
      skipY = 0;
      rangeY = this.height;
    }
    else
    {
      emptyY = 0;
      skipY = (this.height - height) / 2;
      rangeY = height;
    }
    if( width > this.width )
    {
      emptyX = (width - this.width) / 2; 
      skipX = 0;
      rangeX = this.width;
    }
    else
    {
      emptyX = 0;
      skipX = (this.width - width) / 2;
      rangeX = width;
    }

    //resize frame with help of calculated parameters
    for( y = 0; y < rangeY; y++ )
    {
      for( x = 0; x < rangeX; x++ )
      {
        int srcI = (skipX + x) * this.channels;
        int destI = (emptyX + x) * channels;
        if( channels >= this.channels ) //add channels: copy last channel into new channels
	{
          for( c = 0; c < this.channels; c++ )
            new_data[emptyY + y][destI + c] = (byte)((((int)data[skipY + y][srcI + c] & 0xFF) * maxval + this.maxval / 2) / this.maxval);
	  for( ; c < channels; c++ )
	    new_data[emptyY + y][destI + c] = data[emptyY + y][srcI + this.channels - 1];
	}
	else //remove channels: merge leftover channels with last kept channel
	{
          val = 0;
          for( c = 0; c < channels - 1; c++ )
            new_data[emptyY + y][destI + c] = (byte)((((int)data[skipY + y][srcI + c] & 0xFF) * maxval + this.maxval / 2) / this.maxval);
          for( c = channels - 1; c < this.channels; c++ )
            val += (int)data[skipY + y][srcI + c] & 0xFF;
          div = this.maxval * (this.channels - channels + 1);
          new_data[emptyY + y][destI + channels - 1] = (byte)((val * maxval + div / 2) / div);
	}
      }
    }

    this.height = height;
    this.width = width;
    this.channels = channels;
    this.maxval = maxval;
    data = new_data;
  }

  public void scale( int height, int width )
  {
    byte[][] new_data;
    double scaleHor, scaleVer, ox, oy, ox1, oy1, val;
    int c, nx, nx_c, ny, x, x_c, y, oxi, oxi_c, oyi, ox1i, ox1i_c, oy1i;

    if( height < BlinkenConstants.BlinkenHeightMin ) height = BlinkenConstants.BlinkenHeightMin;
    if( height > BlinkenConstants.BlinkenHeightMax ) height = BlinkenConstants.BlinkenHeightMax;
    if( width < BlinkenConstants.BlinkenWidthMin ) width = BlinkenConstants.BlinkenWidthMin;
    if( width > BlinkenConstants.BlinkenWidthMax ) width = BlinkenConstants.BlinkenWidthMax;

    if( height == this.height &&
        width == this.width )
      return;

    scaleHor = (double)width / (double)this.width;
    scaleVer = (double)height / (double)this.height;

    //allocate new data array
    new_data = new byte[height][width * channels];

    //scale every channel
    for( c = 0; c < channels; c++ )
    {
      for( ny = 0; ny < height; ny++ )
      {
        for( nx = 0, nx_c = c; nx < width; nx++, nx_c += channels )
        {
          oy = (double)ny / scaleVer; //sub-pixel exact range in old picture
          ox = (double)nx / scaleHor;
          oy1 = (double)(ny + 1) / scaleVer - 0.000001;
          ox1 = (double)(nx + 1) / scaleHor - 0.000001;
          if( oy < 0 || ox < 0 || oy1 >= this.height || ox1 >= this.width) //out of old picture
            new_data[ny][nx_c] = 0;
          else
          {
            oyi = (int)oy;
            oxi = (int)ox;
            oxi_c = oxi * channels + c;
            oy1i = (int)oy1;
            ox1i = (int)ox1;
            ox1i_c = ox1i * channels + c;
            if( oyi == oy1i )
            {
              if( oxi == ox1i) //one source pixel
              {
                val = (double)((int)data[oyi][oxi_c] & 0xFF);
              }
              else //one line of source pixels
              {
                val = (double)((int)data[oyi][oxi_c] & 0xFF) * (1 - ox + oxi)
                    + (double)((int)data[oyi][ox1i_c] & 0xFF) * (ox1 - ox1i);
                for( x_c = oxi_c + channels; x_c < ox1i_c; x_c += channels )
                  val += (double)((int)data[oyi][x_c] & 0xFF);
                val /= ox1 - ox;
              }
            }
            else //one column of source pixels
            {
              if( oxi == ox1i )
              {
                val = (double)((int)data[oyi][oxi_c] & 0xFF) * (1 - oy + oyi)
                    + (double)((int)data[oy1i][oxi_c] & 0xFF) * (oy1 - oy1i);
                for( y = oyi + 1; y < oy1i; y++ )
                  val += (double)((int)data[y][oxi_c] & 0xFF);
                val /= oy1 - oy;
              }
              else //rectangle of source pixels
              {
                val = (double)((int)data[oyi][oxi_c] & 0xFF) * (1 - oy + oyi) * (1 - ox + oxi)
                    + (double)((int)data[oyi][ox1i_c] & 0xFF) * (1 - oy + oyi) * (ox1 - ox1i)
                    + (double)((int)data[oy1i][oxi_c] & 0xFF) * (oy1 - oy1i) * (1 - ox + oxi)
                    + (double)((int)data[oy1i][ox1i_c] & 0xFF) * (oy1 - oy1i) * (ox1 - ox1i);
                for( y = oyi + 1; y < oy1i; y++ )
                {
                  val += (double)((int)data[y][oxi_c] & 0xFF) * (1 - ox + oxi)
                       + (double)((int)data[y][ox1i_c] & 0xFF) * (ox1 - ox1i);
                }
                for( x_c = oxi_c + channels; x_c < ox1i_c; x_c += channels )
                {
                  val += (double)((int)data[oyi][x_c] & 0xFF) * (1 - oy + oyi)
                       + (double)((int)data[oy1i][x_c] & 0xFF) * (oy1 - oy1i);
                }
                for( y = oyi + 1; y < oy1i; y++ )
                  for( x_c = oxi_c + channels; x_c < ox1i_c; x_c += channels )
                    val += (double)((int)data[y][x_c] & 0xFF);
                val /= (oy1 - oy) * (ox1 - ox);
              }
            }
            new_data[ny][nx_c] = (byte)(int)(val + 0.5);
          }
        } //for( nx ...
      } //for( ny ...
    } //for( c ...

    this.height = height;
    this.width = width;
    data = new_data;
  }

  public String toString( )
  {
    String str = "";
    int h, w, c, i, val;
    for( h = 0; h < height; h++ )
    {
      for( w = 0, i = 0; w < width; w++ )
      {
        val = 0;
        for( val = 0, c = 0; c < channels; c++, i++ )
          val += ((int)data[h][i] & 0xFF);
        val = val * 7 / maxval / channels;
        str = str + " -+*%#&@".substring( val, val + 1); 
      }
      str = str + "\n";
    }
    return str + duration + " ms\n";
  }

  public byte [] toNetwork( int/*BlinkenProto*/ proto )
  {
    byte [] netData;

    if( proto == BlinkenConstants.BlinkenProtoNone ) {
      return new byte [0];
    }

    if( proto == BlinkenConstants.BlinkenProtoBlp ) {
      netData = new byte[12 + height * width]; //allocate buffer for frame
      netData[0] = BlinkenProtoBlpMagic[0]; //magic
      netData[1] = BlinkenProtoBlpMagic[1];
      netData[2] = BlinkenProtoBlpMagic[2];
      netData[3] = BlinkenProtoBlpMagic[3];
      netData[4] = 0; //frameNo
      netData[5] = 0;
      netData[6] = 0;
      netData[7] = 0;
      netData[8] = (byte)(width >> 8 & 0xFF); //width
      netData[9] = (byte)(width & 0xFF);
      netData[10] = (byte)(height >> 8 & 0xFF); //height
      netData[11] = (byte)(height & 0xFF);
      for( int i = 12, y = 0; y < height; y++ )
      {
        for( int x = 0, j = 0; x < width; x++, i++ )
        {
          int val = 0;
          for( int c = 0; c < channels; c++, j++ )
            val += (int)data[y][j] & 0xFF;
          netData[i] = (byte)(val >= channels * maxval / 2 ? 0x01 : 0x00);
        }
      }
      return netData;
    }

    if( proto == BlinkenConstants.BlinkenProtoEblp ) {
      netData = new byte[12 + height * width]; //allocate buffer for frame
      netData[0] = BlinkenProtoEblpMagic[0]; //magic
      netData[1] = BlinkenProtoEblpMagic[1];
      netData[2] = BlinkenProtoEblpMagic[2];
      netData[3] = BlinkenProtoEblpMagic[3];
      netData[4] = 0; //frameNo
      netData[5] = 0;
      netData[6] = 0;
      netData[7] = 0;
      netData[8] = (byte)(width >> 8 & 0xFF); //width
      netData[9] = (byte)(width & 0xFF);
      netData[10] = (byte)(height >> 8 & 0xFF); //height
      netData[11] = (byte)(height & 0xFF);
      for( int i = 12, y = 0; y < height; y++ )
      {
        for( int x = 0, j = 0; x < width; x++, i++ )
        {
          int val = 0;
          for( int c = 0; c < channels; c++, j++ )
            val += (int)data[y][j] & 0xFF;
          val /= channels;
          netData[i] = (byte)(maxval == 255 ? val
                                            : (val * 255 + maxval / 2) / maxval);
        }
      }
      return netData;
    }

    if( proto == BlinkenConstants.BlinkenProtoMcuf ) {
      netData = new byte[12 + height * width * channels]; //allocate buffer for frame
      netData[0] = BlinkenProtoMcufMagic[0]; //magic
      netData[1] = BlinkenProtoMcufMagic[1];
      netData[2] = BlinkenProtoMcufMagic[2];
      netData[3] = BlinkenProtoMcufMagic[3];
      netData[4] = (byte)(height >> 8 & 0xFF); //height
      netData[5] = (byte)(height & 0xFF);
      netData[6] = (byte)(width >> 8 & 0xFF); //width
      netData[7] = (byte)(width & 0xFF);
      netData[8] = (byte)(channels >> 8 & 0xFF); //channels
      netData[9] = (byte)(channels & 0xFF);
      netData[10] = (byte)(maxval >> 8 & 0xFF); //maxval
      netData[11] = (byte)(maxval & 0xFF);
      for( int i = 12, y = 0; y < height; y++ )
        for( int x = 0, j = 0; x < width; x++ )
          for( int c = 0; c < channels; c++, i++, j++ )
            netData[i] = data[y][j];
      return netData;
    }

    return new byte [0];
  }

  public int/*BlinkenProto*/ fromNetwork( byte [] netData )
  {
    if( netData.length >= 12 && netData[0] == BlinkenProtoBlpMagic[0] &&
                                netData[1] == BlinkenProtoBlpMagic[1] &&
                                netData[2] == BlinkenProtoBlpMagic[2] &&
                                netData[3] == BlinkenProtoBlpMagic[3] )
    {
      int netWidth = ((int)netData[8] & 0xFF) << 8 | ((int)netData[9] & 0xFF); //get header data
      int netHeight = ((int)netData[10] & 0xFF) << 8 | ((int)netData[11] & 0xFF);
      if( netData.length < 12 + netHeight * netWidth ) //check length of data
        return BlinkenConstants.BlinkenProtoNone;
      if( netHeight < BlinkenConstants.BlinkenHeightMin || netHeight > BlinkenConstants.BlinkenHeightMax || //check header data
          netWidth < BlinkenConstants.BlinkenWidthMin || netWidth > BlinkenConstants.BlinkenWidthMax )
        return BlinkenConstants.BlinkenProtoNone;
      height = netHeight; //set frame to header data
      width = netWidth;
      channels = 1;
      maxval = 1;
      duration = BlinkenConstants.BlinkenDurationMin;
      data = new byte[height][width * channels];
      for( int i = 12, y = 0; y < height; y++ ) //put data into frame
        for( int x = 0; x < width; x++, i++ )
          data[y][x] = (byte)(netData[i] != 0 ? 0x01 : 0x00);
      return BlinkenConstants.BlinkenProtoBlp;
    }

    if( netData.length >= 12 && netData[0] == BlinkenProtoEblpMagic[0] &&
                                netData[1] == BlinkenProtoEblpMagic[1] &&
                                netData[2] == BlinkenProtoEblpMagic[2] &&
                                netData[3] == BlinkenProtoEblpMagic[3] )
    {
      int netWidth = ((int)netData[8] & 0xFF) << 8 | ((int)netData[9] & 0xFF); //get header data
      int netHeight = ((int)netData[10] & 0xFF) << 8 | ((int)netData[11] & 0xFF);
      if( netData.length < 12 + netHeight * netWidth ) //check length of data
        return BlinkenConstants.BlinkenProtoNone;
      if( netHeight < BlinkenConstants.BlinkenHeightMin || netHeight > BlinkenConstants.BlinkenHeightMax || //check header data
          netWidth < BlinkenConstants.BlinkenWidthMin || netWidth > BlinkenConstants.BlinkenWidthMax )
        return BlinkenConstants.BlinkenProtoNone;
      height = netHeight; //set frame to header data
      width = netWidth;
      channels = 1;
      maxval = 255;
      duration = BlinkenConstants.BlinkenDurationMin;
      data = new byte[height][width * channels];
      for( int i = 12, y = 0; y < height; y++ ) //put data into frame
        for( int x = 0; x < width; x++, i++ )
          data[y][x] = netData[i];
      return BlinkenConstants.BlinkenProtoEblp;
    }

    if( netData.length >= 12 && netData[0] == BlinkenProtoMcufMagic[0] &&
                                netData[1] == BlinkenProtoMcufMagic[1] &&
                                netData[2] == BlinkenProtoMcufMagic[2] &&
                                netData[3] == BlinkenProtoMcufMagic[3] )
    {
      int netHeight = ((int)netData[4] & 0xFF) << 8 | ((int)netData[5] & 0xFF); //get header data
      int netWidth = ((int)netData[6] & 0xFF) << 8 | ((int)netData[7] & 0xFF);
      int netChannels = ((int)netData[8] & 0xFF) << 8 | ((int)netData[9] & 0xFF);
      int netMaxval = ((int)netData[10] & 0xFF) << 8 | ((int)netData[11] & 0xFF);
      if( netData.length < 12 + netHeight * netWidth * netChannels ) //check length of data
        return BlinkenConstants.BlinkenProtoNone;
      if( netHeight < BlinkenConstants.BlinkenHeightMin || netHeight > BlinkenConstants.BlinkenHeightMax || //check header data
          netWidth < BlinkenConstants.BlinkenWidthMin || netWidth > BlinkenConstants.BlinkenWidthMax ||
          netChannels < BlinkenConstants.BlinkenChannelsMin || netChannels > BlinkenConstants.BlinkenChannelsMax ||
          netMaxval < BlinkenConstants.BlinkenMaxvalMin || netMaxval > BlinkenConstants.BlinkenMaxvalMax )
        return BlinkenConstants.BlinkenProtoNone;
      height = netHeight; //set frame to header data
      width = netWidth;
      channels = netChannels;
      maxval = netMaxval;
      duration = BlinkenConstants.BlinkenDurationMin;
      data = new byte[height][width * channels];
      for( int i = 12, y = 0; y < height; y++ ) //put data into frame
        for( int x = 0, j = 0; x < width; x++ )
          for( int c = 0; c < channels; c++, i++, j++ )
            data[y][j] = netData[i];
      return BlinkenConstants.BlinkenProtoMcuf;
    }

    return BlinkenConstants.BlinkenProtoNone;
  }

} //public class BlinkenFrame
