// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

part of bindings;

const int kAlignment = 8;
const int kArrayHeaderSize = 8;
const int kStructHeaderSize = 8;
const int kMessageHeaderSize = 16;
const int kMessageWithRequestIDHeaderSize = 24;
const int kMapStructPayloadSize = 16;
const int kStructHeaderNumBytesOffset = 0;
const int kStructHeaderNumFieldsOffset = 4;
const int kEncodedInvalidHandleValue = 0xffffffff;
const String kErrorUnsigned = "Passing negative value to unsigned encoder";


int align(int size) => size + (kAlignment - (size % kAlignment)) % kAlignment;
bool isAligned(offset) => (offset >= 0) && ((offset % kAlignment) == 0);


Uint8List utf8OfString(String s) =>
  (new Uint8List.fromList((const Utf8Encoder()).convert(s)));


String stringOfUtf8(Uint8List bytes) =>
    (const Utf8Decoder()).convert(bytes.toList());


// Given an argument that is either a Type or an instance:
// Invoke the static method "decode" of the Type, or the instance method
// "decode" of the instance, on the MojoDecoder.
Object _callDecode(Object typeOrInstance, MojoDecoder decoder) {
  if (typeOrInstance is Type) {
    return reflectClass(typeOrInstance).invoke(#decode, [decoder]).reflectee;
  } else {
    return typeOrInstance.decode(decoder);
  }
}


// Given an argument that is either a Type or an instance:
// Invoke the static method "encode" of the Type, or the instance method
// "encode" of the instance, on the MojoEncoder and value to be encoded.
void _callEncode(Object typeOrInstance, MojoEncoder encoder, Object val) {
  if (typeOrInstance is Type) {
    reflectClass(typeOrInstance).invoke(#encode, [encoder, val]);
  } else {
    typeOrInstance.encode(encoder, val);
  }
}


// Given an argument that is either a Type or an instance:
// Invoke the static getter "encodedSize" of the Type, or the instance getter
// "encodedSize" of the instance, and return the result.
int getEncodedSize(Object typeOrInstance) {
  if (typeOrInstance is Type) {
    return reflectClass(typeOrInstance).getField(#encodedSize).reflectee;
  } else {
    return typeOrInstance.encodedSize;
  }
}


class MojoDecoder {
  ByteData buffer;
  List<int> handles;
  int base;
  int next;

  MojoDecoder(this.buffer, this.handles, this.base) {
    next = base;
  }

  void skip(int offset) {
    next += offset;
  }

  int readInt8() {
    int result = buffer.getInt8(next);
    next += 1;
    return result;
  }

  int readUint8() {
    int result = buffer.getUint8(next);
    next += 1;
    return result;
  }

  int readInt16() {
    int result = buffer.getInt16(next, Endianness.LITTLE_ENDIAN);
    next += 2;
    return result;
  }

  int readUint16() {
    int result = buffer.getUint16(next, Endianness.LITTLE_ENDIAN);
    next += 2;
    return result;
  }

  int readInt32() {
    int result = buffer.getInt32(next, Endianness.LITTLE_ENDIAN);
    next += 4;
    return result;
  }

  int readUint32() {
    int result = buffer.getUint32(next, Endianness.LITTLE_ENDIAN);
    next += 4;
    return result;
  }

  int readInt64() {
    int result = buffer.getInt64(next, Endianness.LITTLE_ENDIAN);
    next += 8;
    return result;
  }

  int readUint64() {
    int result = buffer.getUint64(next, Endianness.LITTLE_ENDIAN);
    next += 8;
    return result;
  }

  double readFloat() {
    double result = buffer.getFloat32(next,Endianness.LITTLE_ENDIAN);
    next += 4;
    return result;
  }

  double readDouble() {
    double result = buffer.getFloat64(next, Endianness.LITTLE_ENDIAN);
    next += 8;
    return result;
  }

  int decodePointer() {
    int offsetPointer = next;
    int offset = readUint64();
    if (offset == 0) {
      return 0;
    }
    return offsetPointer + offset;
  }

  MojoDecoder decodeAndCreateDecoder(int offset) {
    return new MojoDecoder(buffer, handles, offset);
  }

  int decodeHandle() {
    return handles[readUint32()];
  }

  String decodeString() {
    int numBytes = readUint32();
    int numElements = readUint32();
    int base = next;
    next += numElements;
    return stringOfUtf8(buffer.buffer.asUint8List(base, numElements));
  }

  List decodeArray(Object type) {
    int numBytes = readUint32();
    int numElements = readUint32();
    if (type == PackedBool) {
      int b;
      List<bool> result = new List<bool>(numElements);
      for (int i = 0; i < numElements; i++) {
        if ((i % 8) == 0) {
          b = readUint8();
        }
        result[i] = ((b & (1 << (i % 8)) != 0) ? true : false);
      }
      return result;
    } else {
      List result = new List(numElements);
      for (int i = 0; i < numElements; i++) {
        result[i] = _callDecode(type, this);
      }
      return result;
    }
  }

  Object decodeStruct(Object t) {
    return _callDecode(t, this);
  }

  Object decodeStructPointer(Object t) {
    int pointer = decodePointer();
    if (pointer == 0) {
      return null;
    }
    return _callDecode(t, decodeAndCreateDecoder(pointer));
  }

  List decodeArrayPointer(Object type) {
    int pointer = decodePointer();
    if (pointer == 0) {
      return null;
    }
    return decodeAndCreateDecoder(pointer).decodeArray(type);
  }

  String decodeStringPointer() {
    int pointer = decodePointer();
    if (pointer == 0) {
      return null;
    }
    return decodeAndCreateDecoder(pointer).decodeString();
  }

  Map decodeMap(Object keyType, Object valType) {
    skip(4);  // number of bytes.
    skip(4);  // number of fields.
    List keys = decodeArrayPointer(keyType);
    List values = decodeArrayPointer(valType);
    return new Map.fromIterables(keys, values);
  }

  Map decodeMapPointer(Object keyType, Object valType) {
    int pointer = this.decodePointer();
    if (pointer == 0) {
      return null;
    }
    MojoDecoder decoder = decodeAndCreateDecoder(pointer);
    return decoder.decodeMap(keyType, valType);
  }
}


class MojoEncoder {
  ByteData buffer;
  List<int> handles;
  int base;
  int next;
  int extent;

  MojoEncoder(this.buffer, this.handles, this.base, this.extent) {
    next = base;
  }

  void skip(int offset) {
    next += offset;
  }

  void writeInt8(int val) {
    buffer.setInt8(next, val);
    next += 1;
  }

  void writeUint8(int val) {
    if (val < 0) {
      throw new ArgumentError("$kErrorUnsigned: $val");
    }
    buffer.setUint8(next, val);
    next += 1;
  }

  void writeInt16(int val) {
    buffer.setInt16(next, val, Endianness.LITTLE_ENDIAN);
    next += 2;
  }

  void writeUint16(int val) {
    if (val < 0) {
      throw new ArgumentError("$kErrorUnsigned: $val");
    }
    buffer.setUint16(next, val, Endianness.LITTLE_ENDIAN);
    next += 2;
  }

  void writeInt32(int val) {
    buffer.setInt32(next, val, Endianness.LITTLE_ENDIAN);
    next += 4;
  }

  void writeUint32(int val) {
    if (val < 0) {
      throw new ArgumentError("$kErrorUnsigned: $val");
    }
    buffer.setUint32(next, val, Endianness.LITTLE_ENDIAN);
    next += 4;
  }

  void writeInt64(int val) {
    buffer.setInt64(next, val, Endianness.LITTLE_ENDIAN);
    next += 8;
  }

  void writeUint64(int val) {
    if (val < 0) {
      throw new ArgumentError("$kErrorUnsigned: $val");
    }
    buffer.setUint64(next, val, Endianness.LITTLE_ENDIAN);
    next += 8;
  }

  void writeFloat(double val) {
    buffer.setFloat32(next, val, Endianness.LITTLE_ENDIAN);
    next += 4;
  }

  void writeDouble(double val) {
    buffer.setFloat64(next, val, Endianness.LITTLE_ENDIAN);
    next += 8;
  }

  void encodePointer(int pointer) {
    if (pointer == null) {
      writeUint64(0);
      return;
    }
    int offset = pointer - next;
    writeUint64(offset);
  }

  void grow(int new_size) {
    Uint8List new_buffer = new Uint8List(new_size);
    new_buffer.setRange(0, next, buffer.buffer.asUint8List());
    buffer = new_buffer.buffer.asByteData();
  }

  int alloc(int size_request) {
    int pointer = extent;
    extent += size_request;
    if (extent > buffer.lengthInBytes) {
      int new_size = buffer.lengthInBytes + size_request;
      new_size += new_size ~/ 2;
      grow(new_size);
    }
    return pointer;
  }

  MojoEncoder createAndEncodeEncoder(int size) {
    int pointer = alloc(align(size));
    encodePointer(pointer);
    return new MojoEncoder(buffer, handles, pointer, extent);
  }

  void encodeHandle(int handle) {
    handles.add(handle);
    writeUint32(handles.length - 1);
  }

  void encodeString(String val) {
    Uint8List utf8string = utf8OfString(val);
    int numElements = utf8string.lengthInBytes;
    int numBytes = kArrayHeaderSize + numElements;
    writeUint32(numBytes);
    writeUint32(numElements);
    buffer.buffer.asUint8List().setRange(next, next + numElements, utf8string);
    next += numElements;
  }

  void encodeArray(Object t, List val, [int numElements, int encodedSize]) {
    if (numElements == null) {
      numElements = val.length;
    }
    if (encodedSize == null) {
      encodedSize = kArrayHeaderSize + (getEncodedSize(t) * numElements);
    }

    writeUint32(encodedSize);
    writeUint32(numElements);

    if (t == PackedBool) {
      int b = 0;
      for (int i = 0; i < numElements; i++) {
        if (val[i]) {
          b |= (1 << (i % 8));
        }
        if (((i % 8) == 7) || (i == (numElements - 1))) {
          Uint8.encode(this, b);
        }
      }
    } else {
      for (int i = 0; i < numElements; i++) {
        _callEncode(t, this, val[i]);
      }
    }
  }

  void encodeStruct(Object t, Object val) {
    _callEncode(t, this, val);
  }

  void encodeStructPointer(Object t, Object val) {
    if (val == null) {
      encodePointer(val);
      return;
    }
    MojoEncoder encoder = createAndEncodeEncoder(getEncodedSize(t));
    _callEncode(t, encoder, val);
    extent = encoder.extent;
    buffer = encoder.buffer;
  }

  void encodeArrayPointer(Object t, List val) {
    if (val == null) {
      encodePointer(val);
      return;
    }
    int numElements = val.length;
    int encodedSize = kArrayHeaderSize + ((t == PackedBool) ?
        (numElements / 8).ceil() : (getEncodedSize(t) * numElements));
    MojoEncoder encoder = createAndEncodeEncoder(encodedSize);
    encoder.encodeArray(t, val, numElements, encodedSize);
    extent = encoder.extent;
    buffer = encoder.buffer;
  }

  void encodeStringPointer(String val) {
    if (val == null) {
      encodePointer(val);
      return;
    }
    int encodedSize = kArrayHeaderSize + utf8OfString(val).lengthInBytes;
    MojoEncoder encoder = createAndEncodeEncoder(encodedSize);
    encoder.encodeString(val);
    extent = encoder.extent;
    buffer = encoder.buffer;
  }

  void encodeMap(Object keyType, Object valType, Map val) {
    List keys = val.keys;
    List vals = val.values;
    writeUint32(kStructHeaderSize + kMapStructPayloadSize);
    writeUint32(2);
    encodeArrayPointer(keyType, keys);
    encodeArrayPointer(valType, vals);
  }

  void encodeMapPointer(Object keyTYpe, Object valType, Map val) {
    if (val == null) {
      encodePointer(val);
      return;
    }
    int encodedSize = kStructHeaderSize + kMapStructPayloadSize;
    MojoEncoder encoder = createAndEncodeEncoder(encodedSize);
    encoder.encodeMap(keyType, valType, val);
    extent = encoder.extent;
    buffer = encoder.buffer;
  }
}


const int kMessageNameOffset = kStructHeaderSize;
const int kMessageFlagsOffset = kMessageNameOffset + 4;
const int kMessageRequestIDOffset = kMessageFlagsOffset + 4;
const int kMessageExpectsResponse = 1 << 0;
const int kMessageIsResponse = 1 << 1;

class Message {
  ByteData buffer;
  List<int> handles;

  Message(this.buffer, this.handles);

  int getHeaderNumBytes() => buffer.getUint32(kStructHeaderNumBytesOffset);
  int getHeaderNumFields() => buffer.getUint32(kStructHeaderNumFieldsOffset);
  int getName() => buffer.getUint32(kMessageNameOffset);
  int getFlags() => buffer.getUint32(kMessageFlagsOffset);
  bool isResponse() => (getFlags() & kMessageIsResponse) != 0;
  bool expectsResponse() => (getFlags() & kMessageExpectsResponse) != 0;

  void setRequestID(int id) {
    buffer.setUint64(kMessageRequestIDOffset, id);
  }
}


class MessageBuilder {
  MojoEncoder encoder;
  List<int> handles;

  MessageBuilder(int name, int payloadSize) {
    int numBytes = kMessageHeaderSize + payloadSize;
    var buffer = new ByteData(numBytes);
    handles = [];

    encoder = new MojoEncoder(buffer, handles, 0, kMessageHeaderSize);
    encoder.writeUint32(kMessageHeaderSize);
    encoder.writeUint32(2);  // num_fields;
    encoder.writeUint32(name);
    encoder.writeUint32(0);  // flags.
  }

  MojoEncoder createEncoder(int size) {
    encoder = new MojoEncoder(encoder.buffer,
                              handles,
                              encoder.next,
                              encoder.next + size);
    return encoder;
  }

  void encodeStruct(Object t, Object val) {
    encoder = createEncoder(getEncodedSize(t));
    _callEncode(t, encoder, val);
  }

  ByteData _trimBuffer() {
    return new ByteData.view(encoder.buffer.buffer, 0, encoder.extent);
  }

  Message finish() {
    Message message = new Message(_trimBuffer(), handles);
    encoder = null;
    handles = null;
    return message;
  }
}


class MessageWithRequestIDBuilder extends MessageBuilder {
  MessageWithRequestIDBuilder(
      int name, int payloadSize, int flags, int requestID) {
    int numBytes = kMessageWithRequestIDHeaderSize + payloadSize;
    buffer = new ByteData(numBytes);
    handles = [];
    base = 0;

    encoder = createEncoder(0, kMessageWithRequestIDHeaderSize);
    encoder.writeUint32(kMessageWithRequestIDHeaderSize);
    encoder.writeUint32(3);  // num_fields.
    encoder.writeUint32(name);
    encoder.writeUint32(flags);
    encoder.writeUint64(requestID);
    base = encoder.next;
    buffer = encoder.buffer;
  }
}


class MessageReader {
  MojoDecoder decoder;
  int payloadSize;
  int name;
  int flags;
  int requestID;

  MessageReader(Message message) {
    decoder = new MojoDecoder(message.buffer, message.handles, 0);

    int messageHeaderSize = decoder.readUint32();
    payloadSize = message.buffer.lengthInBytes - messageHeaderSize;

    int num_fields = decoder.readUint32();
    name = decoder.readUint32();
    flags = decoder.readUint32();

    if (num_fields >= 3) {
      requestID = decoder.readUint64();
    }
    decoder.skip(messageHeaderSize - decoder.next);
  }

  Object decodeStruct(Object t) => _callDecode(t, decoder);
}


abstract class MojoType<T> {
  static const int encodedSize = 0;
  static T decode(MojoDecoder decoder) { return null }
  static void encode(MojoEncoder encoder, T val) {}
}


class PackedBool {}


class Int8 implements MojoType<int> {
  static const int encodedSize = 1;
  static int decode(MojoDecoder decoder) => decoder.readInt8();
  static void encode(MojoEncoder encoder, int val) {
    encoder.writeInt8(val);
  }
}


class Uint8 implements MojoType<int> {
  static const int encodedSize = 1;
  static int decode(MojoDecoder decoder) => decoder.readUint8();
  static void encode(MojoEncoder encoder, int val) {
    encoder.writeUint8(val);
  }
}


class Int16 implements MojoType<int> {
  static const int encodedSize = 2;
  static int decode(MojoDecoder decoder) => decoder.readInt16();
  static void encode(MojoEncoder encoder, int val) {
    encoder.writeInt16(val);
  }
}


class Uint16 implements MojoType<int> {
  static const int encodedSize = 2;
  static int decode(MojoDecoder decoder) => decoder.readUint16();
  static void encode(MojoEncoder encoder, int val) {
    encoder.writeUint16(val);
  }
}


class Int32 implements MojoType<int> {
  static const int encodedSize = 4;
  static int decode(MojoDecoder decoder) => decoder.readInt32();
  static void encode(MojoEncoder encoder, int val) {
    encoder.writeInt32(val);
  }
}


class Uint32 implements MojoType<int> {
  static const int encodedSize = 4;
  static int decode(MojoDecoder decoder) => decoder.readUint32();
  static void encode(MojoEncoder encoder, int val) {
    encoder.writeUint32(val);
  }
}


class Int64 implements MojoType<int> {
  static const int encodedSize = 8;
  static int decode(MojoDecoder decoder) => decoder.readInt64();
  static void encode(MojoEncoder encoder, int val) {
    encoder.writeInt64(val);
  }
}


class Uint64 implements MojoType<int> {
  static const int encodedSize = 8;
  static int decode(MojoDecoder decoder) => decoder.readUint64();
  static void encode(MojoEncoder encoder, int val) {
    encoder.writeUint64(val);
  }
}


class MojoString implements MojoType<String> {
  static const int encodedSize = 8;
  static String decode(MojoDecoder decoder) => decoder.decodeStringPointer();
  static void encode(MojoEncoder encoder, String val) {
    encoder.encodeStringPointer(val);
  }
}


class NullableMojoString implements MojoType<String> {
  static const int encodedSize = MojoString.encodedSize;
  static var decode = MojoString.decode;
  static var encode = MojoString.encode;
}


class Float implements MojoType<double> {
  static const int encodedSize = 4;
  static double decode(MojoDecoder decoder) => decoder.readFloat();
  static void encode(MojoEncoder encoder, double val) {
    encoder.writeFloat(val);
  }
}


class Double implements MojoType<double> {
  static const int encodedSize = 8;
  static double decode(MojoDecoder decoder) => decoder.readDouble();
  static void encode(MojoEncoder encoder, double val) {
    encoder.writeDouble(val);
  }
}


class PointerTo {
  Object val;

  PointerTo(this.val);

  int encodedSize = 8;
  Object decode(MojoDecoder decoder) {
    int pointer = decoder.decodePointer();
    if (pointer == 0) {
      return null;
    }
    return _callDecode(val, decoder.decodeAndCreateDecoder(pointer));
  }
  void encode(MojoEncoder encoder, Object val) {
    if (val == null) {
      encoder.encodePointer(val);
      return;
    }
    MojoEncoder obj_encoder =
        encoder.createAndEncodeEncoder(getEncodedSize(this.val));
    _callEncode(this.val, obj_encoder, val);
  }
}


class NullablePointerTo extends PointerTo {
  static const int encodedSize = PointerTo.encodedSize;
}


class ArrayOf {
  Object val;
  int length;

  ArrayOf(this.val, [this.length = 0]);

  int dimensions() => [length].addAll((val is ArrayOf) ? val.dimensions() : []);

  int encodedSize = 8;
  List decode(MojoDecoder decoder) => decoder.decodeArrayPointer(val);
  void encode(MojoEncoder encoder, List val) {
    encoder.encodeArrayPointer(this.val, val);
  }
}


class NullableArrayOf extends ArrayOf {
  static const int encodedSize = ArrayOf.encodedSize;
}


class Handle implements MojoType<int> {
  static const int encodedSize = 4;
  static int decode(MojoDecoder decoder) => decoder.decodeHandle();
  static void encode(MojoEncoder encoder, int val) {
    encoder.encodeHandle(val);
  }
}


class NullableHandle implements MojoType<int> {
  static const int encodedSize = Handle.encodedSize;
  static const decode = Handle.decode;
  static const encode = Handle.encode;
}
