/*
   This file is part of the paintmyblog.com web client
   Copyright 2007 Karl Henrik Falck <f@lck.nu>
*/

var version = 1;

var Actions = Class.create();
Actions.prototype = {
  DRAW_LINE: 1,
  DRAW_DOT: 2,
  CHANGE_TOOL: 3,
  CHANGE_WIDTH: 5,
  CHANGE_SWATCH: 4,
  DRAW_COORDS: 6,
  SET_PAINTER: 7,
  
  initialize: function() {
    this.version = version;
    this.actions = [];
    this.playidx = 0;
    this.painter = null;
    this.skip = false;
    this.coder = new Coder();
  },

  execute: function() {
    if (!this.hasMore()) return;
    var a = this.actions[this.playidx++];
    if (a[0] == this.SET_PAINTER) {
      this.painter = a[1];
      this.skip = !fastForward && this.painter == mypainter;
    }
    else if (!this.skip)
      this.painter.execute(a);
  },

  hasMore: function() {
    return this.playidx < this.actions.length;
  },

  count: function() {
    return this.actions.length - this.playidx;
  },

  serialize: function(painter) {
    this.actions.push([this.SET_PAINTER, painter]);
    var actions = painter.actions;
    painter.setDirty();
    painter.playidx = 0;
    this.version = version;
    var len = actions.length;
    var str = "";
    for (var i = 0; i < len; ++i) {
      var a = actions.shift();
      this.actions.push(a);
      str += this.serializeAction(a);
    }
    return str;
  },

  deserialize: function(painter, str, initial) {
    if (!initial || this.painter != painter) {
      if (initial)
	this.pushCoords(this.actions);
      this.actions.push([this.SET_PAINTER, painter]);
      this.painter = painter;
      this.tool_val = null;
      this.width_val = null;
      this.swatch_val = null;
    }
    if (painter.protocolVersion != undefined)
      this.version = painter.protocolVersion;
    else
      this.version = 0;
    if (!fastForward) {
      var actions = [];
      this.deserializeActions(actions, str);
      for (var i = 0; i < painter.actions.length; ++i)
	this.actions.push(painter.actions[i]);
      painter.play(actions);
      this.playidx += actions.length;
    }
    else this.deserializeActions(this.actions, str, initial);
    painter.protocolVersion = this.version;
  },

  deserializeActions: function(actions, str, initial) {
    this.coder.setDecodeString(str);
    while (this.coder.hasMore()) {
      var type = this.coder.peekType();
      if (type == COORD_DATA)
	this.deserializeCoordData(actions, initial);
      else if (type != UNKNOWN_DATA)
	this.deserializeGeneric(actions, initial);
      else {
	debugMessage("unknown type " + this.version);
	return;
      }
    }
  },
  
  serializeAction: function(a) {
    if (a[0] == this.DRAW_LINE)
      return this.serializeDrawLine(a);
    else if (a[0] == this.DRAW_DOT)
      return this.serializeDrawDot(a);
    else
      return this.serializeGenericAction(a);
  },

  serializeDrawLine: function(a) {
    return this.coder.encodeCoordData(a[1], a[2], false) +
    this.coder.encodeCoordData(a[3], a[4], a[6] ? true : false, a[5]);
    /* version 0
    return encodeCoordData(a[1], a[2], false) +
    encodeCoordData(a[3], a[4], a[6] ? true : false) +
    encode12(a[5] ? a[5] : 0);
    */
  },

  serializeDrawDot: function(a) {
    return this.coder.encodeCoordData(a[1], a[2], true);
  },

  serializeGenericAction: function(a) {
    var type = 0;
    var val = 0;
    if (a[0] == this.CHANGE_TOOL) {
      type = TOOL_DATA;
      val = a[1];
    }
    else if (a[0] == this.CHANGE_WIDTH) {
      type = WIDTH_DATA;
      val = parseInt(7 * (a[1] - 0.5) / 1.5);
    }
    else if (a[0] == this.CHANGE_SWATCH) {
      type = SWATCH_DATA;
      val = version;
      var val2 = a[1];
    }
    if (val2 == undefined)
      return this.coder.encodeChange(type, val);
    else
      return this.coder.encodeChange(type, val) + this.coder.encode6(val2);
  },

  deserializeCoordData: function(actions, initial) {
    var ary = this.coder.decodeCoordData();
    if (!ary[2]) { // line
      var ary2 = this.coder.decodeCoordData();
      if (this.version == 0) var time = this.coder.decode12();
      else var time = ary2[3];
      if (initial) {
	var simplify = true;
	if (!this.coords) {
	  this.coords = [this.DRAW_COORDS, simplify, ary[0], ary[1]];
	  if (!simplify) this.coords.push(ary2[0], ary2[1]);
	}
	else {
	  this.coords.push(ary[0], ary[1]);
	  if (!simplify) this.coords.push(ary2[0], ary2[1]);
	}
	if (ary2[2]) {
	  if (!simplify) this.coords.push(ary[0], ary[1]);
	  this.coords.push(ary2[0], ary2[1]);
	  if (simplify) this.coords.push(null);
	}
      }
      else
	actions.push([this.DRAW_LINE, ary[0], ary[1], ary2[0], ary2[1], time, ary2[2]]);
    }
    else { // dot
      actions.push([this.DRAW_DOT, ary[0], ary[1]]);
    }
  },

  pushCoords: function(actions) {  
    if (this.coords) {
      if (this.coords[this.coords.length - 1] == null)
	this.coords.pop();
      actions.push(this.coords);
      this.coords = null;
    }
  },

  deserializeGeneric: function(actions, initial) {
    var type = this.coder.peekType();
    var val = this.coder.decode3();
    if (type == TOOL_DATA &&
	(!initial || this.tool_val != val)) {
      this.pushCoords(actions);
      actions.push([this.CHANGE_TOOL, val]);
    }
    else if (type == WIDTH_DATA &&
	     (!initial || this.width_val != val)) {
      this.pushCoords(actions);
      val = 0.5 + val / 7 * 1.5;
      actions.push([this.CHANGE_WIDTH, val]);
    }
    else if (type == SWATCH_DATA) {
      this.version = val;
      this.pushCoords(actions);
      var val2 = this.coder.decode6();
      if (!initial || this.swatch_val != val2)
	actions.push([this.CHANGE_SWATCH, val2]);
    }
  }
}
  
var base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

var UNKNOWN_DATA = 0;
var COORD_DATA = 1;
var TOOL_DATA = 2;
var WIDTH_DATA = 3;
var SWATCH_DATA = 4;
var RESERVED_DATA = 5;

var Coder = Class.create();
Coder.prototype = {
  initialize: function() { },

  setDecodeString: function(str) {
    this.str = str;
    this.pos = 0;
    this.len = str.length;
  },

  hasMore: function() {
    return this.pos < this.len;
  },

  nextChar: function() {
    return this.str.charAt(this.pos++);
  },

  peekChar: function() {
    return this.str.charAt(this.pos);
  },

  nextValue: function() {
    return base64.indexOf(this.str.charAt(this.pos++));
  },

  peekType: function() {
    var c = base64.indexOf(this.peekChar());
    var cd = c & 0x20;
    if (cd == 0) return COORD_DATA;
    var t = (c & 0x18) >> 3;
    if (t < 3) return TOOL_DATA + t;
    return UNKNOWN_DATA;
  },

  encodeChange: function(type, value) {
    if (type < TOOL_DATA) type = TOOL_DATA;
    else if (type > RESERVED_DATA) type = RESERVED_DATA;
    if (value < 0) value = 0;
    else if (value > 7) value = 7;
    var c = 0x20 | ((type - TOOL_DATA) << 3) | value;
    return base64.charAt(c);
  },

  decode3: function() {
    var v = this.nextValue();
    return v & 0x7;
  },

  encodeCoordData: function(x, y, close, time) {
    var c4 = x & 0x3F;
    var c3 = y & 0x3F;
    var za = (x & 0x3C0) >> 6;
    var zb = (y & 0x3C0) >> 2;
    var z = za | zb;
    var c2 = z & 0x3F;
    var c1 = (z & 0xFC0) >> 6;
    if (close) c1 |= 0x10;
    time = time ? Math.round(time / 10) : null;
    var timed = !!time;
    if (timed) c1 |= 0x8;
    if (!timed)
      return base64.charAt(c1) + base64.charAt(c2) + base64.charAt(c3) + base64.charAt(c4);
    else
      return base64.charAt(c1) + base64.charAt(c2) + base64.charAt(c3) + base64.charAt(c4) + this.encodeVar11(time);
  },

  decodeCoordData: function() {
    var c1 = this.nextValue();
    var c2 = this.nextValue();
    var c3 = this.nextValue();
    var c4 = this.nextValue();
    var z = (c1 << 6) + c2;
    var za = (z & 0xF) << 6;
    var zb = (z & 0xF0) << 2;
    var x = c4 | za;
    var y = c3 | zb;
    var close = (c1 & 0x10) != 0;
    var timed = (c1 & 0x8) != 0;
    if (timed) {
      var time = this.decodeVar11();
      if (actions.version == 0) actions.version = 1;
      time *= 10;
    }
    else
      var time = null;
    return [x, y, close, time];
  },

  encode12: function(val) {
    if (val >= 4096) val = 4095;
    else if (val < 0) val = 0;
    var c1 = val & 0x3F;
    var c2 = (val & 0xFC0) >> 6;
    return base64.charAt(c1) + base64.charAt(c2);
  },

  decode12: function() {
    var c1 = this.nextValue();
    var c2 = this.nextValue();
    return (c2 << 6) | c1;
  },

  encodeVar11: function(val) {
    if (val > 2047) val = 2047;
    else if (val < 0) val = 0;
    var two = val > 30;
    var c1 = val & 0x1F;
    if (!two) return base64.charAt(c1);
    c1 |= 0x20;
    var c2 = (val & 0x7E0) >> 5;
    return base64.charAt(c1) + base64.charAt(c2);
  },

  decodeVar11: function() {
    var c1 = this.nextValue();
    var two = (c1 & 0x20) != 0;
    if (!two) return c1;
    c1 &= 0x1F;
    var c2 = this.nextValue();
    return (c2 << 5) | c1;
  },

  encode6: function(val) {
    if (val >= 64) val = 63;
    else if (val < 0) val = 0;
    return base64.charAt(val);
  },

  decode6: function() {
    return this.nextValue();
  }
}
