/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.client.rpc.impl;

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

/**
 * Base class for the client and server serialization streams. This class
 * handles the basic serialization and deserialization formatting for primitive
 * types since these are common between the client and the server. It also
 * handles Object- and String-tracking for building graph references.
 */
public abstract class AbstractSerializationStreamWriter extends
    AbstractSerializationStream implements SerializationStreamWriter {

  private static final double TWO_PWR_16_DBL = 0x10000;
  private static final double TWO_PWR_32_DBL = TWO_PWR_16_DBL * TWO_PWR_16_DBL;

  /**
   * Return a pair of doubles { low, high } that add up to the given number,
   * such that "low" is always between 0 and 2^32-1 inclusive and "high" is
   * always between -2^63 and 2^63-2^32 inclusive and is a multiple of 2^32.
   */
  public static double[] getAsDoubleArray(long value) {
    int lowBits = (int) (value & 0xffffffff);
    int highBits = (int) (value >> 32);
    return makeLongComponents(lowBits, highBits);
  }
  
  // Equivalent to getAsDoubleArray((long) highBits << 32 | lowBits);
  protected static double[] makeLongComponents(int lowBits, int highBits) {
    double high = highBits * TWO_PWR_32_DBL;
    double low = lowBits;
    if (lowBits < 0) {
      low += TWO_PWR_32_DBL;
    }
    return new double[] {low, high};
  }

  private int objectCount;

  private Map<Object, Integer> objectMap = new IdentityHashMap<Object, Integer>();

  private Map<String, Integer> stringMap = new HashMap<String, Integer>();

  private List<String> stringTable = new ArrayList<String>();

  public void prepareToWrite() {
    objectCount = 0;
    objectMap.clear();
    stringMap.clear();
    stringTable.clear();
  }

  @Override
  public abstract String toString();

  public void writeBoolean(boolean fieldValue) {
    append(fieldValue ? "1" : "0");
  }

  public void writeByte(byte fieldValue) {
    append(String.valueOf(fieldValue));
  }

  public void writeChar(char ch) {
    // just use an int, it's more foolproof
    append(String.valueOf((int) ch));
  }

  public void writeDouble(double fieldValue) {
    append(String.valueOf(fieldValue));
  }

  public void writeFloat(float fieldValue) {
    writeDouble(fieldValue);
  }

  public void writeInt(int fieldValue) {
    append(String.valueOf(fieldValue));
  }
  
  public abstract void writeLong(long value);

  public void writeObject(Object instance) throws SerializationException {
    if (instance == null) {
      // write a null string
      writeString(null);
      return;
    }

    int objIndex = getIndexForObject(instance);
    if (objIndex >= 0) {
      // We've already encoded this object, make a backref
      // Transform 0-based to negative 1-based
      writeInt(-(objIndex + 1));
      return;
    }

    saveIndexForObject(instance);

    // Serialize the type signature
    String typeSignature = getObjectTypeSignature(instance);
    if (typeSignature == null) {
      throw new SerializationException(
          "could not get type signature for " + instance.getClass());
    }
    writeString(typeSignature);
    // Now serialize the rest of the object
    serialize(instance, typeSignature);
  }

  public void writeShort(short value) {
    append(String.valueOf(value));
  }

  public void writeString(String value) {
    writeInt(addString(value));
  }

  /**
   * Add a string to the string table and return its index.
   * 
   * @param string the string to add
   * @return the index to the string
   */
  protected int addString(String string) {
    if (string == null) {
      return 0;
    }
    Integer o = stringMap.get(string);
    if (o != null) {
      return o;
    }
    stringTable.add(string);
    // index is 1-based
    int index = stringTable.size();
    stringMap.put(string, index);
    return index;
  }

  /**
   * Append a token to the underlying output buffer.
   * 
   * @param token the token to append
   */
  protected abstract void append(String token);

  /**
   * Get the index for an object that may have previously been saved via
   * {@link #saveIndexForObject(Object)}.
   * 
   * @param instance the object to save
   * @return the index associated with this object, or -1 if this object hasn't
   *         been seen before
   */
  protected int getIndexForObject(Object instance) {
    return objectMap.containsKey(instance) ? objectMap.get(instance) : -1;
  }

  /**
   * Compute and return the type signature for an object.
   * 
   * @param instance the instance to inspect
   * @return the type signature of the instance
   */
  protected abstract String getObjectTypeSignature(Object instance)
      throws SerializationException;

  /**
   * Gets the string table.
   */
  protected List<String> getStringTable() {
    return stringTable;
  }

  /**
   * Remember this object as having been seen before.
   * 
   * @param instance the object to remember
   */
  protected void saveIndexForObject(Object instance) {
    objectMap.put(instance, objectCount++);
  }

  /**
   * Serialize an object into the stream.
   * 
   * @param instance the object to serialize
   * @param typeSignature the type signature of the object
   * @throws SerializationException
   */
  protected abstract void serialize(Object instance, String typeSignature)
      throws SerializationException;
}
