chromium/mojo/public/js/bindings_lite.js

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

/** @const {number} */
mojo.internal.kArrayHeaderSize = 8;

/** @const {number} */
mojo.internal.kStructHeaderSize = 8;

/** @const {number} */
mojo.internal.kUnionHeaderSize = 8;

/** @const {number} */
mojo.internal.kUnionDataSize = 16;

/** @const {number} */
mojo.internal.kMessageV0HeaderSize = 24;

/** @const {number} */
mojo.internal.kMessageV1HeaderSize = 32;

/** @const {number} */
mojo.internal.kMessageV2HeaderSize = 48;

/** @const {number} */
mojo.internal.kMapDataSize = 24;

/** @const {number} */
mojo.internal.kEncodedInvalidHandleValue = 0xffffffff;

/** @const {number} */
mojo.internal.kMessageFlagExpectsResponse = 1 << 0;

/** @const {number} */
mojo.internal.kMessageFlagIsResponse = 1 << 1;

/** @const {number} */
mojo.internal.kInterfaceNamespaceBit = 0x80000000;

/** @const {boolean} */
mojo.internal.kHostLittleEndian = (function() {
  const wordBytes = new Uint8Array(new Uint16Array([1]).buffer);
  return !!wordBytes[0];
})();

/**
 * @param {*} x
 * @return {boolean}
 */
mojo.internal.isNullOrUndefined = function(x) {
  return x === null || x === undefined;
};

/**
 * @param {*} x
 * @return {boolean}
 */
mojo.internal.isNullableValueKindField = function(x) {
  return typeof x.nullableValueKindProperties !== 'undefined';
}

/**
 * @param {number} size
 * @param {number} alignment
 * @return {number}
 */
mojo.internal.align = function(size, alignment) {
  return size + (alignment - (size % alignment)) % alignment;
};

/**
 * @param {!DataView} dataView
 * @param {number} byteOffset
 * @param {number|bigint} value
 */
mojo.internal.setInt64 = function(dataView, byteOffset, value) {
  if (mojo.internal.kHostLittleEndian) {
    dataView.setUint32(
        byteOffset, Number(BigInt(value) & BigInt(0xffffffff)),
        mojo.internal.kHostLittleEndian);
    dataView.setInt32(
        byteOffset + 4,
        Number((BigInt(value) >> BigInt(32)) & BigInt(0xffffffff)),
        mojo.internal.kHostLittleEndian);
  } else {
    dataView.setInt32(
        byteOffset, Number((BigInt(value) >> BigInt(32)) & BigInt(0xffffffff)),
        mojo.internal.kHostLittleEndian);
    dataView.setUint32(
        byteOffset + 4, Number(BigInt(value) & BigInt(0xffffffff)),
        mojo.internal.kHostLittleEndian);
  }
};

/**
 * @param {!DataView} dataView
 * @param {number} byteOffset
 * @param {number|bigint} value
 */
mojo.internal.setUint64 = function(dataView, byteOffset, value) {
  if (mojo.internal.kHostLittleEndian) {
    dataView.setUint32(
        byteOffset, Number(BigInt(value) & BigInt(0xffffffff)),
        mojo.internal.kHostLittleEndian);
    dataView.setUint32(
        byteOffset + 4,
        Number((BigInt(value) >> BigInt(32)) & BigInt(0xffffffff)),
        mojo.internal.kHostLittleEndian);
  } else {
    dataView.setUint32(
        byteOffset, Number((BigInt(value) >> BigInt(32)) & BigInt(0xffffffff)),
        mojo.internal.kHostLittleEndian);
    dataView.setUint32(
        byteOffset + 4, Number(BigInt(value) & BigInt(0xffffffff)),
        mojo.internal.kHostLittleEndian);
  }
};

/**
 * @param {!DataView} dataView
 * @param {number} byteOffset
 * @return {bigint}
 */
mojo.internal.getInt64 = function(dataView, byteOffset) {
  let low, high;
  if (mojo.internal.kHostLittleEndian) {
    low = dataView.getUint32(byteOffset, mojo.internal.kHostLittleEndian);
    high = dataView.getInt32(byteOffset + 4, mojo.internal.kHostLittleEndian);
  } else {
    low = dataView.getUint32(byteOffset + 4, mojo.internal.kHostLittleEndian);
    high = dataView.getInt32(byteOffset, mojo.internal.kHostLittleEndian);
  }
  return (BigInt(high) << BigInt(32)) | BigInt(low);
};

/**
 * @param {!DataView} dataView
 * @param {number} byteOffset
 * @return {bigint}
 */
mojo.internal.getUint64 = function(dataView, byteOffset) {
  let low, high;
  if (mojo.internal.kHostLittleEndian) {
    low = dataView.getUint32(byteOffset, mojo.internal.kHostLittleEndian);
    high = dataView.getUint32(byteOffset + 4, mojo.internal.kHostLittleEndian);
  } else {
    low = dataView.getUint32(byteOffset + 4, mojo.internal.kHostLittleEndian);
    high = dataView.getUint32(byteOffset, mojo.internal.kHostLittleEndian);
  }
  return (BigInt(high) << BigInt(32)) | BigInt(low);
};

/**
 * @typedef {{
 *   size: number,
 *   numInterfaceIds: (number|undefined),
 * }}
 */
mojo.internal.MessageDimensions;

/**
 * This computes the total amount of buffer space required to hold a struct
 * value and all its fields, including indirect objects like arrays, structs,
 * and nullable unions.
 *
 * @param {!mojo.internal.StructSpec} structSpec
 * @param {!Object} value
 * @return {!mojo.internal.MessageDimensions}
 */
mojo.internal.computeStructDimensions = function(structSpec, value) {
  let size = structSpec.packedSize;
  let numInterfaceIds = 0;
  for (const field of structSpec.fields) {
    let fieldValue = value[field.name];
    if (mojo.internal.isNullOrUndefined(fieldValue)) {
      fieldValue = field.defaultValue;
    }
    if (fieldValue === null) {
      continue;
    }

    if (field.type.$.computeDimensions) {
      const fieldDimensions =
          field.type.$.computeDimensions(fieldValue, field.nullable);
      size += mojo.internal.align(fieldDimensions.size, 8);
      numInterfaceIds += fieldDimensions.numInterfaceIds;
    } else if (field.type.$.hasInterfaceId) {
      numInterfaceIds++;
    }
  }
  return {size, numInterfaceIds};
};

/**
 * @param {!mojo.internal.UnionSpec} unionSpec
 * @param {!Object} value
 * @return {!mojo.internal.MessageDimensions}
 */
mojo.internal.computeUnionDimensions = function(unionSpec, nullable, value) {
  // Unions are normally inlined since they're always a fixed width of 16
  // bytes, but nullable union-typed fields require indirection. Hence this
  // unique special case where a union field requires additional storage
  // beyond the struct's own packed field data only when it's nullable.
  let size = nullable ? mojo.internal.kUnionDataSize : 0;
  let numInterfaceIds = 0;

  const keys = Object.keys(value);
  if (keys.length !== 1) {
    throw new Error(
        `Value for ${unionSpec.name} must be an Object with a ` +
        'single property named one of: ' +
        Object.keys(unionSpec.fields).join(','));
  }

  const tag = keys[0];
  const field = unionSpec.fields[tag];
  const fieldValue = value[tag];
  if (!mojo.internal.isNullOrUndefined(fieldValue)) {
    // Nested unions are always encoded with indirection, which we induce by
    // claiming the field is nullable even if it's not.
    if (field['type'].$.computeDimensions) {
      const nullable = !!field['type'].$.unionSpec || field['nullable'];
      const fieldDimensions =
          field['type'].$.computeDimensions(fieldValue, nullable);
      size += mojo.internal.align(fieldDimensions.size, 8);
      numInterfaceIds += fieldDimensions.numInterfaceIds;
    } else if (field['type'].$.hasInterfaceId) {
      numInterfaceIds++;
    }
  }

  return {size, numInterfaceIds};
};

/**
 * @param {!mojo.internal.ArraySpec} arraySpec
 * @param {!Array|!Uint8Array} value
 * @return {number}
 */
mojo.internal.computeInlineArraySize = function(arraySpec, value) {
  if (arraySpec.elementType === mojo.internal.Bool) {
    return mojo.internal.kArrayHeaderSize +
        mojo.internal.computeHasValueBitfieldSize(arraySpec, value.length) +
        ((value.length + 7) >> 3);
  } else {
    return mojo.internal.kArrayHeaderSize +
        mojo.internal.computeHasValueBitfieldSize(arraySpec, value.length) +
        value.length *
        arraySpec.elementType.$.arrayElementSize(!!arraySpec.elementNullable);
  }
};

/**
 * @param {!mojo.internal.ArraySpec} arraySpec
 * @param {number} length
 * @return {number} the number of bytes needed for the an array's has-value
 *   bitfield. If the arraySpec does not require a has-value bitfield, this
 *   method will return 0.
 */
mojo.internal.computeHasValueBitfieldSize = function(arraySpec, length) {
    const isNullableValueType = !!arraySpec.elementNullable &&
        !!arraySpec.elementType.$.isValueType;
    if (!isNullableValueType) {
      return 0;
    }
    const element_type_bytes =
        arraySpec.elementType.$.arrayElementSize(/* nullable= */ true);
    const element_type_bits = element_type_bytes * 8;
    const needed_bits = length + element_type_bits - 1;
    // >> 0 to force integer arithmetic.
    return  ((needed_bits/element_type_bits) >> 0)  * element_type_bytes;
}

/**
 * @param {!mojo.internal.ArraySpec} arraySpec
 * @param {!Array|!Uint8Array} value
 * @return {number}
 */
mojo.internal.computeTotalArraySize = function(arraySpec, value) {
  const inlineSize = mojo.internal.computeInlineArraySize(arraySpec, value);
  if (!arraySpec.elementType.$.computeDimensions)
    return inlineSize;

  let totalSize = inlineSize;
  for (let elementValue of value) {
    if (!mojo.internal.isNullOrUndefined(elementValue)) {
      totalSize += mojo.internal.align(
          arraySpec.elementType.$
              .computeDimensions(elementValue, !!arraySpec.elementNullable)
              .size,
          8);
    }
  }

  return totalSize;
};

/** Owns an outgoing message buffer and facilitates serialization. */
mojo.internal.Message = class {
  /**
   * @param {?mojo.internal.interfaceSupport.Endpoint} sender
   * @param {number} interfaceId
   * @param {number} flags
   * @param {number} ordinal
   * @param {number} requestId
   * @param {!mojo.internal.StructSpec} paramStructSpec
   * @param {!Object} value
   * @public
   */
  constructor(
      sender, interfaceId, flags, ordinal, requestId, paramStructSpec, value) {
    const dimensions =
        mojo.internal.computeStructDimensions(paramStructSpec, value);

    let headerSize, version;
    if (dimensions.numInterfaceIds > 0) {
      headerSize = mojo.internal.kMessageV2HeaderSize;
      version = 2;
    } else if (
        (flags &
         (mojo.internal.kMessageFlagExpectsResponse |
          mojo.internal.kMessageFlagIsResponse)) == 0) {
      headerSize = mojo.internal.kMessageV0HeaderSize;
      version = 0;
    } else {
      headerSize = mojo.internal.kMessageV1HeaderSize;
      version = 1;
    }

    const headerWithPayloadSize = headerSize + dimensions.size;
    const interfaceIdsSize = dimensions.numInterfaceIds > 0 ?
        mojo.internal.kArrayHeaderSize + dimensions.numInterfaceIds * 4 :
        0;
    const paddedInterfaceIdsSize = mojo.internal.align(interfaceIdsSize, 8);
    const totalMessageSize = headerWithPayloadSize + paddedInterfaceIdsSize;

    /** @public {!ArrayBuffer} */
    this.buffer = new ArrayBuffer(totalMessageSize);

    /** @public {!Array<MojoHandle>} */
    this.handles = [];

    const header = new DataView(this.buffer);
    header.setUint32(0, headerSize, mojo.internal.kHostLittleEndian);
    header.setUint32(4, version, mojo.internal.kHostLittleEndian);
    header.setUint32(8, interfaceId, mojo.internal.kHostLittleEndian);
    header.setUint32(12, ordinal, mojo.internal.kHostLittleEndian);
    header.setUint32(16, flags, mojo.internal.kHostLittleEndian);
    header.setUint32(20, 0);  // Padding
    if (version >= 1) {
      mojo.internal.setUint64(header, 24, requestId);
      if (version >= 2) {
        mojo.internal.setUint64(header, 32, BigInt(16));
        mojo.internal.setUint64(header, 40, BigInt(headerWithPayloadSize - 40));
        header.setUint32(
            headerWithPayloadSize, interfaceIdsSize,
            mojo.internal.kHostLittleEndian);
        header.setUint32(
            headerWithPayloadSize + 4, dimensions.numInterfaceIds || 0,
            mojo.internal.kHostLittleEndian);
      }
    }

    /** @private {number} */
    this.nextInterfaceIdIndex_ = 0;

    /** @private {?Uint32Array} */
    this.interfaceIds_ = null;

    if (dimensions.numInterfaceIds) {
      this.interfaceIds_ = new Uint32Array(
          this.buffer, headerWithPayloadSize + mojo.internal.kArrayHeaderSize,
          dimensions.numInterfaceIds);
    }

    /** @private {number} */
    this.nextAllocationOffset_ = headerSize;

    const paramStructData = this.allocate(paramStructSpec.packedSize);
    const encoder =
        new mojo.internal.Encoder(this, paramStructData, {endpoint: sender});
    encoder.encodeStructInline(paramStructSpec, value);
  }

  /**
   * @param {number} numBytes
   * @return {!DataView} A view into the allocated message bytes.
   */
  allocate(numBytes) {
    const alignedSize = mojo.internal.align(numBytes, 8);
    const view =
        new DataView(this.buffer, this.nextAllocationOffset_, alignedSize);
    this.nextAllocationOffset_ += alignedSize;
    return view;
  }
};

/**
 * Additional context to aid in encoding and decoding of message data.
 *
 * @typedef {{
 *   endpoint: ?mojo.internal.interfaceSupport.Endpoint,
 * }}
 */
mojo.internal.MessageContext;

/**
 * Helps encode outgoing messages. Encoders may be created recursively to encode
 * parial message fragments indexed by indirect message offsets, as with encoded
 * arrays and nested structs.
 */
mojo.internal.Encoder = class {
  /**
   * @param {!mojo.internal.Message} message
   * @param {!DataView} data
   * @param {?mojo.internal.MessageContext=} context
   * @public
   */
  constructor(message, data, context = null) {
    /** @const {?mojo.internal.MessageContext} */
    this.context_ = context;

    /** @private {!mojo.internal.Message} */
    this.message_ = message;

    /** @private {!DataView} */
    this.data_ = data;
  }

  encodeBool(byteOffset, bitOffset, value) {
    const oldValue = this.data_.getUint8(byteOffset);
    if (value)
      this.data_.setUint8(byteOffset, oldValue | (1 << bitOffset));
    else
      this.data_.setUint8(byteOffset, oldValue & ~(1 << bitOffset));
  }

  encodeInt8(offset, value) {
    this.data_.setInt8(offset, value);
  }

  encodeUint8(offset, value) {
    this.data_.setUint8(offset, value);
  }

  encodeInt16(offset, value) {
    this.data_.setInt16(offset, value, mojo.internal.kHostLittleEndian);
  }

  encodeUint16(offset, value) {
    this.data_.setUint16(offset, value, mojo.internal.kHostLittleEndian);
  }

  encodeInt32(offset, value) {
    this.data_.setInt32(offset, value, mojo.internal.kHostLittleEndian);
  }

  encodeUint32(offset, value) {
    this.data_.setUint32(offset, value, mojo.internal.kHostLittleEndian);
  }

  encodeInt64(offset, value) {
    mojo.internal.setInt64(this.data_, offset, value);
  }

  encodeUint64(offset, value) {
    mojo.internal.setUint64(this.data_, offset, value);
  }

  encodeFloat(offset, value) {
    this.data_.setFloat32(offset, value, mojo.internal.kHostLittleEndian);
  }

  encodeDouble(offset, value) {
    this.data_.setFloat64(offset, value, mojo.internal.kHostLittleEndian);
  }

  encodeHandle(offset, value) {
    this.encodeUint32(offset, this.message_.handles.length);
    this.message_.handles.push(value);
  }

  encodeAssociatedEndpoint(offset, endpoint) {
    console.assert(
        endpoint.isPendingAssociation, 'expected unbound associated endpoint');
    const sender = this.context_.endpoint;
    const id = sender.associatePeerOfOutgoingEndpoint(endpoint);
    const index = this.message_.nextInterfaceIdIndex_++;
    this.encodeUint32(offset, index);
    this.message_.interfaceIds_[index] = id;
  }

  encodeString(offset, value) {
    if (typeof value !== 'string')
      throw new Error('Unxpected non-string value for string field.');
    this.encodeArray(
        {elementType: mojo.internal.Uint8}, offset,
        mojo.internal.Encoder.stringToUtf8Bytes(value));
  }

  encodeOffset(offset, absoluteOffset) {
    this.encodeUint64(offset, absoluteOffset - this.data_.byteOffset - offset);
  }

  /**
   * @param {!mojo.internal.ArraySpec} arraySpec
   * @param {number} offset
   * @param {!Array|!Uint8Array} value
   */
  encodeArray(arraySpec, offset, value) {
    const arraySize = mojo.internal.computeInlineArraySize(arraySpec, value);
    const arrayData = this.message_.allocate(arraySize);
    const arrayEncoder =
        new mojo.internal.Encoder(this.message_, arrayData, this.context_);
    this.encodeOffset(offset, arrayData.byteOffset);

    arrayEncoder.encodeUint32(0, arraySize);
    arrayEncoder.encodeUint32(4, value.length);
    this.maybeEncodeHasValueBitfield(arraySpec, arrayEncoder, 8, value);

    let byteOffset = 8 +
        mojo.internal.computeHasValueBitfieldSize(arraySpec, value.length);
    if (arraySpec.elementType === mojo.internal.Bool) {
      let bitOffset = 0;
      for (const e of value) {
        arrayEncoder.encodeBool(byteOffset, bitOffset, e);
        bitOffset++;
        if (bitOffset == 8) {
          bitOffset = 0;
          byteOffset++;
        }
      }
    } else {
      for (const e of value) {
        if (e === null) {
          if (!arraySpec.elementNullable) {
            throw new Error(
                'Trying to send a null element in an array of ' +
                'non-nullable elements');
          }
          arraySpec.elementType.$.encodeNull(arrayEncoder, byteOffset);
        } else {
          arraySpec.elementType.$.encode(
              e, arrayEncoder, byteOffset, 0, !!arraySpec.elementNullable);
        }
        byteOffset += arraySpec.elementType.$.arrayElementSize(
            !!arraySpec.elementNullable);
      }
    }
  }

  /**
   * Optionally writes a has-value bitfield to the encoder if necessary. If the
   * arraySpec does not require a has-value bitfield, this method call is
   * noop.
   * @param {!mojo.internal.ArraySpec} arraySpec
   * @param {mojo.internal.Encoder} arrayEncoder
   * @param {number} startOffset
   * @param {!Array|!Uint8Array} value
   */
  maybeEncodeHasValueBitfield(arraySpec, arrayEncoder, startOffset, value) {
    if (!arraySpec.elementNullable ||
        !arraySpec.elementType.$.isValueType) {
      return;
    }

    let bitOffset = 0;
    let byteOffset = startOffset;
    for (const e of value) {
      if (e === null || e === undefined) {
        arrayEncoder.encodeBool(byteOffset, bitOffset, false);
      } else {
        arrayEncoder.encodeBool(byteOffset, bitOffset, true);
      }
      bitOffset++;
      if (bitOffset == 8) {
        bitOffset = 0;
        byteOffset++;
      }
    }
  }

  /**
   * @param {!mojo.internal.MapSpec} mapSpec
   * @param {number} offset
   * @param {!Map|!Object} value
   */
  encodeMap(mapSpec, offset, value) {
    let keys, values;
    if (value.constructor.name == 'Map') {
      keys = Array.from(value.keys());
      values = Array.from(value.values());
    } else {
      keys = Object.keys(value);
      values = keys.map(k => value[k]);
    }

    const mapData = this.message_.allocate(mojo.internal.kMapDataSize);
    const mapEncoder =
        new mojo.internal.Encoder(this.message_, mapData, this.context_);
    this.encodeOffset(offset, mapData.byteOffset);

    mapEncoder.encodeUint32(0, mojo.internal.kMapDataSize);
    mapEncoder.encodeUint32(4, 0);
    mapEncoder.encodeArray({elementType: mapSpec.keyType}, 8, keys);
    mapEncoder.encodeArray(
        {
          elementType: mapSpec.valueType,
          elementNullable: mapSpec.valueNullable,
        },
        16, values);
  }

  /**
   * @param {!mojo.internal.StructSpec} structSpec
   * @param {number} offset
   * @param {!Object} value
   */
  encodeStruct(structSpec, offset, value) {
    const structData = this.message_.allocate(structSpec.packedSize);
    const structEncoder =
        new mojo.internal.Encoder(this.message_, structData, this.context_);
    this.encodeOffset(offset, structData.byteOffset);
    structEncoder.encodeStructInline(structSpec, value);
  }

  /**
   * @param {!mojo.internal.StructSpec} structSpec
   * @param {!Object} value
   */
  encodeStructInline(structSpec, value) {
    const versions = structSpec.versions;
    this.encodeUint32(0, structSpec.packedSize);
    this.encodeUint32(4, versions[versions.length - 1].version);
    for (const field of structSpec.fields) {
      const byteOffset = mojo.internal.kStructHeaderSize + field.packedOffset;

      const encodeStructField = (field_value) => {
        field.type.$.encode(field_value, this, byteOffset,
                            field.packedBitOffset, field.nullable);
      };

      // Encode a single optional numeric field into a flag field
      // or a value field.
      if (value && mojo.internal.isNullableValueKindField(field)) {
        const props = field.nullableValueKindProperties;
        const hasValue =
          !mojo.internal.isNullOrUndefined(value[props.originalFieldName]);
        if (props.isPrimary) {
          encodeStructField(hasValue);
        } else if (hasValue) {
          encodeStructField(value[props.originalFieldName]);
        } else {
          // Use `defaultValue` to cover the enum case.
          encodeStructField(field.defaultValue);
        }
        continue;
      }

      if (value && !mojo.internal.isNullOrUndefined(value[field.name])) {
        encodeStructField(value[field.name]);
        continue;
      }

      if (field.defaultValue !== null) {
        encodeStructField(field.defaultValue);
        continue;
      }

      if (field.nullable) {
        field.type.$.encodeNull(this, byteOffset);
        continue;
      }

      throw new Error(
        structSpec.name + ' missing value for non-nullable ' +
        'field "' + field.name + '"');
    }
  }

  /**
   * @param {!mojo.internal.UnionSpec} unionSpec
   * @param {number} offset
   * @param {!Object} value
   */
  encodeUnionAsPointer(unionSpec, offset, value) {
    const unionData = this.message_.allocate(mojo.internal.kUnionDataSize);
    const unionEncoder =
        new mojo.internal.Encoder(this.message_, unionData, this.context_);
    this.encodeOffset(offset, unionData.byteOffset);
    unionEncoder.encodeUnion(unionSpec, /*offset=*/0, value);
  }

  /**
   * @param {!mojo.internal.UnionSpec} unionSpec
   * @param {number} offset
   * @param {!Object} value
   */
  encodeUnion(unionSpec, offset, value) {
    const keys = Object.keys(value);
    if (keys.length !== 1) {
      throw new Error(
          `Value for ${unionSpec.name} must be an Object with a ` +
          'single property named one of: ' +
          Object.keys(unionSpec.fields).join(','));
    }

    const tag = keys[0];
    const field = unionSpec.fields[tag];
    this.encodeUint32(offset, mojo.internal.kUnionDataSize);
    this.encodeUint32(offset + 4, field['ordinal']);
    const fieldByteOffset = offset + mojo.internal.kUnionHeaderSize;
    if (typeof field['type'].$.unionSpec !== 'undefined') {
      // Unions are encoded as pointers when inside unions.
      this.encodeUnionAsPointer(field['type'].$.unionSpec,
                                fieldByteOffset,
                                value[tag]);
      return;
    }
    field['type'].$.encode(
        value[tag], this, fieldByteOffset, 0, field['nullable']);
  }

  /**
   * @param {string} value
   * @return {!Uint8Array}
   */
  static stringToUtf8Bytes(value) {
    if (!mojo.internal.Encoder.textEncoder)
      mojo.internal.Encoder.textEncoder = new TextEncoder('utf-8');
    return mojo.internal.Encoder.textEncoder.encode(value);
  }
};

/** @type {TextEncoder} */
mojo.internal.Encoder.textEncoder = null;

/**
 * Helps decode incoming messages. Decoders may be created recursively to
 * decode partial message fragments indexed by indirect message offsets, as with
 * encoded arrays and nested structs.
 */
mojo.internal.Decoder = class {
  /**
   * @param {!DataView} data
   * @param {!Array<MojoHandle>} handles
   * @param {?mojo.internal.MessageContext=} context
   */
  constructor(data, handles, context = null) {
    /** @private {?mojo.internal.MessageContext} */
    this.context_ = context;

    /** @private {!DataView} */
    this.data_ = data;

    /** @private {!Array<MojoHandle>} */
    this.handles_ = handles;
  }

  decodeBool(byteOffset, bitOffset) {
    return !!(this.data_.getUint8(byteOffset) & (1 << bitOffset));
  }

  decodeInt8(offset) {
    return this.data_.getInt8(offset);
  }

  decodeUint8(offset) {
    return this.data_.getUint8(offset);
  }

  decodeInt16(offset) {
    return this.data_.getInt16(offset, mojo.internal.kHostLittleEndian);
  }

  decodeUint16(offset) {
    return this.data_.getUint16(offset, mojo.internal.kHostLittleEndian);
  }

  decodeInt32(offset) {
    return this.data_.getInt32(offset, mojo.internal.kHostLittleEndian);
  }

  decodeUint32(offset) {
    return this.data_.getUint32(offset, mojo.internal.kHostLittleEndian);
  }

  decodeInt64(offset) {
    return mojo.internal.getInt64(this.data_, offset);
  }

  decodeUint64(offset) {
    return mojo.internal.getUint64(this.data_, offset);
  }

  decodeFloat(offset) {
    return this.data_.getFloat32(offset, mojo.internal.kHostLittleEndian);
  }

  decodeDouble(offset) {
    return this.data_.getFloat64(offset, mojo.internal.kHostLittleEndian);
  }

  decodeHandle(offset) {
    const index = this.data_.getUint32(offset, mojo.internal.kHostLittleEndian);
    if (index == 0xffffffff)
      return null;
    if (index >= this.handles_.length)
      throw new Error('Decoded invalid handle index');
    return this.handles_[index];
  }

  decodeString(offset) {
    const data = this.decodeArray({elementType: mojo.internal.Uint8}, offset);
    if (!data)
      return null;

    if (!mojo.internal.Decoder.textDecoder)
      mojo.internal.Decoder.textDecoder = new TextDecoder('utf-8');
    return mojo.internal.Decoder.textDecoder.decode(
        new Uint8Array(data).buffer);
  }

  decodeOffset(offset) {
    const relativeOffset = this.decodeUint64(offset);
    if (relativeOffset == 0)
      return 0;
    if (relativeOffset > BigInt(Number.MAX_SAFE_INTEGER))
      throw new Error('Mesage offset too large');
    return this.data_.byteOffset + offset + Number(relativeOffset);
  }

  /**
   * @param {!mojo.internal.ArraySpec} arraySpec
   * @return {Array}
   */
  decodeArray(arraySpec, offset) {
    const arrayOffset = this.decodeOffset(offset);
    if (!arrayOffset)
      return null;

    const arrayDecoder = new mojo.internal.Decoder(
        new DataView(this.data_.buffer, arrayOffset), this.handles_,
        this.context_);

    const numElements = arrayDecoder.decodeUint32(4);
    if (!numElements)
      return [];

    // Nullable primitives use a bitfield to represent whether a value at a
    // certain index is set. This is not needed for non-primitive or
    // non-nullable types.
    const isNullableValueType = !!arraySpec.elementNullable &&
        arraySpec.elementType.$.isValueType;
    const elementHasValue = isNullableValueType ? [] : null;

    if (isNullableValueType) {
      let bitfieldByte = 8;
      let bitfieldBit = 0;

      for (let i = 0; i < numElements; ++i) {
        elementHasValue.push(
          arrayDecoder.decodeBool(bitfieldByte, bitfieldBit));
        bitfieldBit++;
        if (bitfieldBit === 8) {
          bitfieldBit = 0;
          bitfieldByte++;
        }
      }
    }

    let byteOffset = 8 +
        mojo.internal.computeHasValueBitfieldSize(arraySpec, numElements);
    const result = [];
    if (arraySpec.elementType === mojo.internal.Bool) {
      for (let i = 0; i < numElements; ++i)
        if (isNullableValueType && !elementHasValue[i]) {
          result.push(null);
        } else {
          result.push(arrayDecoder.decodeBool(byteOffset + (i >> 3), i % 8));
        }
    } else {
      for (let i = 0; i < numElements; ++i) {
        if (isNullableValueType && !elementHasValue[i]) {
          result.push(null);
        } else {
          const element = arraySpec.elementType.$.decode(
            arrayDecoder, byteOffset, 0, !!arraySpec.elementNullable);
          if (element === null && !arraySpec.elementNullable)
            throw new Error('Received unexpected array element');
          result.push(element);
        }
        byteOffset += arraySpec.elementType.$.arrayElementSize(
            !!arraySpec.elementNullable);
      }
    }
    return result;
  }

  /**
   * @param {!mojo.internal.MapSpec} mapSpec
   * @return {Object|Map}
   */
  decodeMap(mapSpec, offset) {
    const mapOffset = this.decodeOffset(offset);
    if (!mapOffset)
      return null;

    const mapDecoder = new mojo.internal.Decoder(
        new DataView(this.data_.buffer, mapOffset), this.handles_,
        this.context_);
    const mapStructSize = mapDecoder.decodeUint32(0);
    const mapStructVersion = mapDecoder.decodeUint32(4);
    if (mapStructSize != mojo.internal.kMapDataSize || mapStructVersion != 0)
      throw new Error('Received invalid map data');

    const keys = mapDecoder.decodeArray({elementType: mapSpec.keyType}, 8);
    const values = mapDecoder.decodeArray(
        {
          elementType: mapSpec.valueType,
          elementNullable: mapSpec.valueNullable
        },
        16);

    if (keys.length != values.length)
      throw new Error('Received invalid map data');
    if (!mapSpec.keyType.$.isValidObjectKeyType) {
      const map = new Map;
      for (let i = 0; i < keys.length; ++i)
        map.set(keys[i], values[i]);
      return map;
    }

    const map = {};
    for (let i = 0; i < keys.length; ++i)
      map[keys[i]] = values[i];
    return map;
  }

  /**
   * @param {!mojo.internal.StructSpec} structSpec
   * @return {Object}
   */
  decodeStruct(structSpec, offset) {
    const structOffset = this.decodeOffset(offset);
    if (!structOffset)
      return null;

    const decoder = new mojo.internal.Decoder(
        new DataView(this.data_.buffer, structOffset), this.handles_,
        this.context_);
    return decoder.decodeStructInline(structSpec);
  }

  /**
   * @param {!mojo.internal.StructSpec} structSpec
   * @param {number} size
   * @param {number} version
   * @return {boolean}
   */
  isStructHeaderValid(structSpec, size, version) {
    const versions = structSpec.versions;
    for (let i = versions.length - 1; i >= 0; --i) {
      const info = versions[i];
      if (version > info.version) {
        // If it's newer than the next newest version we know about, the only
        // requirement is that it's at least large enough to decode that next
        // newest version.
        return size >= info.packedSize;
      }
      if (version == info.version) {
        // If it IS the next newest version we know about, expect an exact size
        // match.
        return size == info.packedSize;
      }
    }

    // This should be effectively unreachable, because we always generate info
    // for version 0, and the `version` parameter here is guaranteed in practice
    // to be a non-negative value.
    throw new Error(
        `Impossible version ${version} for struct ${structSpec.name}`);
  }

  /**
   * @param {!mojo.internal.StructSpec} structSpec
   * @return {!Object}
   */
  decodeStructInline(structSpec) {
    const size = this.decodeUint32(0);
    const version = this.decodeUint32(4);
    if (!this.isStructHeaderValid(structSpec, size, version)) {
      throw new Error(
          `Received ${structSpec.name} of invalid size (${size}) and/or ` +
          `version (${version})`);
    }

    const decodeStructField = (structField) => {
      const byteOffset =
        mojo.internal.kStructHeaderSize + structField.packedOffset;
      const value = structField.type.$.decode(
        this, byteOffset, structField.packedBitOffset, !!structField.nullable);

      if (value === null && !structField.nullable) {
        throw new Error(
          `Received ${structSpec.name} with invalid null field ` +
          `"${structField.name}"`)
      }
      return value;
    };

    const result = {};
    for (const field of structSpec.fields) {
      // Decode an optional numeric pair into a single
      // field.
      if (mojo.internal.isNullableValueKindField(field)) {
        const props = field.nullableValueKindProperties;
        if (props.isPrimary && field.minVersion > version) {
          result[props.originalFieldName] = null;
        } else if (props.isPrimary) {
          const hasValue = decodeStructField(field);
          // If the field is null, set it here. If it isn't,
          // the value will be decoded as part of decoding
          // the non-primary field below.
          if (!hasValue) {
            result[props.originalFieldName] = null;
          }
        } else {
          // If the field hasn't been set yet, then it's not
          // null and we need to decode the value.
          if (!(props.originalFieldName in result)) {
            result[props.originalFieldName] =
              decodeStructField(field);
          }
        }
        continue;
      }

      if (field.minVersion > version) {
        result[field.name] = field.defaultValue;
        continue;
      }
      result[field.name] = decodeStructField(field);
    }

    return result;
  }

  /**
   * @param {!mojo.internal.UnionSpec} unionSpec
   * @param {number} offset
   */
  decodeUnionFromPointer(unionSpec, offset) {
    const unionOffset = this.decodeOffset(offset);
    if (!unionOffset)
      return null;

    const decoder = new mojo.internal.Decoder(
        new DataView(this.data_.buffer, unionOffset), this.handles_,
        this.context_);
    return decoder.decodeUnion(unionSpec, 0);
  }

  /**
   * @param {!mojo.internal.UnionSpec} unionSpec
   * @param {number} offset
   */
  decodeUnion(unionSpec, offset) {
    const size = this.decodeUint32(offset);
    if (size === 0)
      return null;

    const ordinal = this.decodeUint32(offset + 4);
    for (const fieldName in unionSpec.fields) {
      const field = unionSpec.fields[fieldName];
      if (field['ordinal'] === ordinal) {
        const fieldValue = (() => {
          const fieldByteOffset = offset + mojo.internal.kUnionHeaderSize;
          // Unions are encoded as pointers when inside other
          // unions.
          if (typeof field['type'].$.unionSpec !== 'undefined') {
            return this.decodeUnionFromPointer(
              field['type'].$.unionSpec, fieldByteOffset);
          }
          return field['type'].$.decode(
            this, fieldByteOffset, 0, field['nullable'])
        })();

        if (fieldValue === null && !field['nullable']) {
          throw new Error(
              `Received ${unionSpec.name} with invalid null ` +
              `field: ${field['name']}`);
        }
        const value = {};
        value[fieldName] = fieldValue;
        return value;
      }
    }
  }

  decodeInterfaceProxy(type, offset) {
    const handle = this.decodeHandle(offset);
    // TODO: support versioning
    if (!handle)
      return null;
    return new type(handle);
  }

  decodeInterfaceRequest(type, offset) {
    const handle = this.decodeHandle(offset);
    if (!handle)
      return null;
    return new type(mojo.internal.interfaceSupport.createEndpoint(handle));
  }

  decodeAssociatedEndpoint(offset) {
    if (!this.context_ || !this.context_.endpoint) {
      throw new Error('cannot deserialize associated endpoint without context');
    }
    const receivingEndpoint = this.context_.endpoint;
    const message = new DataView(this.data_.buffer);
    const interfaceIdsOffset = Number(mojo.internal.getUint64(message, 40));
    const numInterfaceIds = message.getUint32(
        interfaceIdsOffset + 44, mojo.internal.kHostLittleEndian);
    const interfaceIds = new Uint32Array(
        message.buffer,
        interfaceIdsOffset + mojo.internal.kArrayHeaderSize + 40,
        numInterfaceIds);
    const index = this.decodeUint32(offset);
    const interfaceId = interfaceIds[index];
    return new mojo.internal.interfaceSupport.Endpoint(
        receivingEndpoint.router, interfaceId);
  }
};

/** @type {TextDecoder} */
mojo.internal.Decoder.textDecoder = null;

/**
 * @typedef {{
 *   headerSize: number,
 *   headerVersion: number,
 *   interfaceId: number,
 *   ordinal: number,
 *   flags: number,
 *   requestId: number,
 * }}
 */
mojo.internal.MessageHeader;

/**
 * @param {!DataView} data
 * @return {!mojo.internal.MessageHeader}
 */
mojo.internal.deserializeMessageHeader = function(data) {
  const headerSize = data.getUint32(0, mojo.internal.kHostLittleEndian);
  const headerVersion = data.getUint32(4, mojo.internal.kHostLittleEndian);
  if ((headerVersion == 0 &&
       headerSize != mojo.internal.kMessageV0HeaderSize) ||
      (headerVersion == 1 &&
       headerSize != mojo.internal.kMessageV1HeaderSize) ||
      (headerVersion >= 2 &&
       headerSize < mojo.internal.kMessageV2HeaderSize)) {
    throw new Error('Received invalid message header');
  }
  return {
    headerSize,
    headerVersion,
    interfaceId: data.getUint32(8, mojo.internal.kHostLittleEndian),
    ordinal: data.getUint32(12, mojo.internal.kHostLittleEndian),
    flags: data.getUint32(16, mojo.internal.kHostLittleEndian),
    requestId: (headerVersion < 1) ?
        0 :
        data.getUint32(24, mojo.internal.kHostLittleEndian),
  };
};

/**
 * @typedef {{
 *   encode: function(*, !mojo.internal.Encoder, number, number, boolean),
 *   encodeNull: function(!mojo.internal.Encoder, number),
 *   decode: function(!mojo.internal.Decoder, number, number, boolean):*,
 *   computeDimensions:
 *       ((function(*, boolean):!mojo.internal.MessageDimensions)|undefined),
 *   isValidObjectKeyType: boolean,
 *   hasInterfaceId: (boolean|undefined),
 *   arrayElementSize: ((function(boolean):number)|undefined),
 *   arraySpec: (!mojo.internal.ArraySpec|undefined),
 *   mapSpec: (!mojo.internal.MapSpec|undefined),
 *   structSpec: (!mojo.internal.StructSpec|undefined),
 *   isValueType: boolean
 * }}
 */
mojo.internal.MojomTypeInfo;

/**
 * @typedef {{
 *   $: !mojo.internal.MojomTypeInfo
 * }}
 */
mojo.internal.MojomType;

/**
 * @typedef {{
 *   elementType: !mojo.internal.MojomType,
 *   elementNullable: (boolean|undefined)
 * }}
 */
mojo.internal.ArraySpec;

/**
 * @typedef {{
 *   keyType: !mojo.internal.MojomType,
 *   valueType: !mojo.internal.MojomType,
 *   valueNullable: boolean
 * }}
 */
mojo.internal.MapSpec;

// Use a @record, otherwise Closure Compiler will mangle the property names and
// cause runtime errors.
/** @record */
mojo.internal.NullableValueKindProperties = class {
  constructor() {
    /** @export { boolean } */
    this.isPrimary;
    /** @export { (string|undefined) } */
    this.linkedValueFieldName;
    /** @export { string } */
    this.originalFieldName;
  }
};

/**
 * @typedef {{
 *   name: string,
 *   packedOffset: number,
 *   packedBitOffset: number,
 *   type: !mojo.internal.MojomType,
 *   defaultValue: *,
 *   nullable: boolean,
 *   minVersion: number,
 *   nullableValueKindProperties:
 *      (mojo.internal.NullableValueKindProperties|undefined),
 * }}
 */
mojo.internal.StructFieldSpec;

/**
 * @typedef {{
 *   version: number,
 *   packedSize: number,
 * }}
 */
mojo.internal.StructVersionInfo;

/**
 * @typedef {{
 *   name: string,
 *   packedSize: number,
 *   fields: !Array<!mojo.internal.StructFieldSpec>,
 *   versions: !Array<!mojo.internal.StructVersionInfo>,
 * }}
 */
mojo.internal.StructSpec;

/**
 * @typedef {{
 *   name: string,
 *   ordinal: number,
 *   nullable: boolean
 * }}
 */
mojo.internal.UnionFieldSpec;

/**
 * @typedef {{
 *   name: string,
 *   fields: !Object<string, !mojo.internal.UnionFieldSpec>
 * }}
 */
mojo.internal.UnionSpec;

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Bool = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeBool(byteOffset, bitOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      throw new Error('encoding bool null from type is not implemented');
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeBool(byteOffset, bitOffset);
    },
    // Bool has specialized serialize/deserialize logic to bit pack. However,
    // memory allocation is still a single byte.
    arrayElementSize: nullable => 1,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Int8 = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeInt8(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeInt8(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeInt8(byteOffset);
    },
    arrayElementSize: nullable => 1,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Uint8 = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeUint8(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeUint8(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeUint8(byteOffset);
    },
    arrayElementSize: nullable => 1,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Int16 = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeInt16(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeInt16(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeInt16(byteOffset);
    },
    arrayElementSize: nullable => 2,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Uint16 = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeUint16(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeUint16(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeUint16(byteOffset);
    },
    arrayElementSize: nullable => 2,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Int32 = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeInt32(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeInt32(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeInt32(byteOffset);
    },
    arrayElementSize: nullable => 4,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Uint32 = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeUint32(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeUint32(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeUint32(byteOffset);
    },
    arrayElementSize: nullable => 4,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Int64 = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeInt64(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeInt64(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeInt64(byteOffset);
    },
    arrayElementSize: nullable => 8,
    // TS Compiler does not allow Object maps to have bigint keys.
    isValidObjectKeyType: false,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Uint64 = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeUint64(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeUint64(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeUint64(byteOffset);
    },
    arrayElementSize: nullable => 8,
    // TS Compiler does not allow Object maps to have bigint keys.
    isValidObjectKeyType: false,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Float = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeFloat(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeFloat(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeFloat(byteOffset);
    },
    arrayElementSize: nullable => 4,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Double = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeDouble(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeDouble(byteOffset, 0);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeDouble(byteOffset);
    },
    arrayElementSize: nullable => 8,
    isValidObjectKeyType: true,
    isValueType: true,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Handle = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeHandle(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {
      encoder.encodeUint32(byteOffset, 0xffffffff);
    },
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeHandle(byteOffset);
    },
    arrayElementSize: nullable => 4,
    isValidObjectKeyType: false,
    isValueType: false,
  },
};

/**
 * @const {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.String = {
  $: {
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeString(byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {},
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeString(byteOffset);
    },
    computeDimensions: function(value, nullable) {
      const size = mojo.internal.computeTotalArraySize(
          {elementType: mojo.internal.Uint8},
          mojo.internal.Encoder.stringToUtf8Bytes(value));
      return {size};
    },
    arrayElementSize: nullable => 8,
    isValidObjectKeyType: true,
    isValueType: false,
  }
};

/**
 * @param {!mojo.internal.MojomType} elementType
 * @param {boolean} elementNullable
 * @return {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Array = function(elementType, elementNullable) {
  /** @type {!mojo.internal.ArraySpec} */
  const arraySpec = {
    elementType: elementType,
    elementNullable: elementNullable,
  };
  return {
    $: {
      arraySpec: arraySpec,
      encode: function(value, encoder, byteOffset, bitOffset, nullable) {
        encoder.encodeArray(arraySpec, byteOffset, value);
      },
      encodeNull: function(encoder, byteOffset) {},
      decode: function(decoder, byteOffset, bitOffset, nullable) {
        return decoder.decodeArray(arraySpec, byteOffset);
      },
      computeDimensions: function(value, nullable) {
        return {size: mojo.internal.computeTotalArraySize(arraySpec, value)};
      },
      arrayElementSize: nullable => 8,
      isValidObjectKeyType: false,
      isValueType: false,
    },
  };
};

/**
 * @param {!mojo.internal.MojomType} keyType
 * @param {!mojo.internal.MojomType} valueType
 * @param {boolean} valueNullable
 * @return {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Map = function(keyType, valueType, valueNullable) {
  /** @type {!mojo.internal.MapSpec} */
  const mapSpec = {
    keyType: keyType,
    valueType: valueType,
    valueNullable: valueNullable,
  };
  return {
    $: {
      mapSpec: mapSpec,
      encode: function(value, encoder, byteOffset, bitOffset, nullable) {
        encoder.encodeMap(mapSpec, byteOffset, value);
      },
      encodeNull: function(encoder, byteOffset) {},
      decode: function(decoder, byteOffset, bitOffset, nullable) {
        return decoder.decodeMap(mapSpec, byteOffset);
      },
      computeDimensions: function(value, nullable) {
        const keys =
            (value.constructor.name == 'Map') ? Array.from(value.keys())
                                              : Object.keys(value);
        const values =
            (value.constructor.name == 'Map') ? Array.from(value.values())
                                              : keys.map(k => value[k]);
        // Size of map is equal to kMapDataSize + 8-byte aligned array for keys
        // + (not necessarily 8-byte aligned) array for values.
        const size = mojo.internal.kMapDataSize +
            mojo.internal.align(
                mojo.internal.computeTotalArraySize(
                    {elementType: keyType}, keys),
              8) +
            mojo.internal.computeTotalArraySize(
                {
                  elementType: valueType,
                  elementNullable: valueNullable,
                },
                values);
        return {size};
      },
      arrayElementSize: nullable => 8,
      isValidObjectKeyType: false,
      isValueType: false,
    },
  };
};

/**
 * @return {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.Enum = function() {
  return {
    $: {
      encode: function(value, encoder, byteOffset, bitOffset, nullable) {
        // TODO: Do some sender-side error checking on the input value.
        encoder.encodeUint32(byteOffset, value);
      },
      encodeNull: function(encoder, byteOffset) {},
      decode: function(decoder, byteOffset, bitOffset, nullable) {
        const value = decoder.decodeInt32(byteOffset);
        // TODO: validate
        return value;
      },
      arrayElementSize: nullable => 4,
      isValidObjectKeyType: true,
      isValueType: true,
    },
  };
};

/**
 * @param {string} name
 * @param {number} packedOffset
 * @param {number} packedBitOffset
 * @param {!mojo.internal.MojomType} type
 * @param {*} defaultValue
 * @param {boolean} nullable
 * @param {number=} minVersion
 * @param {mojo.internal.NullableValueKindProperties=}
     nullableValueKindProperties
 * @return {!mojo.internal.StructFieldSpec}
 * @export
 */
mojo.internal.StructField = function(
  name, packedOffset, packedBitOffset, type, defaultValue, nullable,
  minVersion = 0, nullableValueKindProperties = undefined) {
  return {
    name: name,
    packedOffset: packedOffset,
    packedBitOffset: packedBitOffset,
    type: type,
    defaultValue: defaultValue,
    nullable: nullable,
    minVersion: minVersion,
    nullableValueKindProperties: nullableValueKindProperties,
  };
};

/**
 * @param {!Object} objectToBlessAsType
 * @param {string} name
 * @param {!Array<!mojo.internal.StructFieldSpec>} fields
 * @param {Array<!Array<number>>=} versionData
 * @export
 */
mojo.internal.Struct = function(
    objectToBlessAsType, name, fields, versionData) {
  const versions = versionData.map(v => ({version: v[0], packedSize: v[1]}));
  const packedSize = versions[versions.length - 1].packedSize;
  const structSpec = {name, packedSize, fields, versions};
  objectToBlessAsType.$ = {
    structSpec: structSpec,
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeStruct(structSpec, byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {},
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeStruct(structSpec, byteOffset);
    },
    computeDimensions: function(value, nullable) {
      return mojo.internal.computeStructDimensions(structSpec, value);
    },
    arrayElementSize: nullable => 8,
    isValidObjectKeyType: false,
  };
};

/**
 * Bridges typemapped types to mojo types. The adapter includes a function which
 * will convert a mapped type to mojo type and vice versa.
 * @export
 */
mojo.internal.TypemapAdapter = class {
  constructor(toMojoTypeFn, toMappedTypeFn) {
    this.toMojoTypeFn = toMojoTypeFn;
    this.toMappedTypeFn = toMappedTypeFn;
  }
}

/**
 * @param {!Object} objectToBlessAsType
 * @param {string} name
 * @param {!mojo.internal.TypemapAdapter} typemapAdapter
 * @param {!Array<!mojo.internal.StructFieldSpec>} fields
 * @param {Array<!Array<number>>=} versionData
 * @export
 */
mojo.internal.TypemappedStruct = function(
    objectToBlessAsType, name, typemapAdapter, fields, versionData) {
  const versions = versionData.map(v => ({version: v[0], packedSize: v[1]}));
  const packedSize = versions[versions.length - 1].packedSize;
  const structSpec = {name, packedSize, fields, versions};
  objectToBlessAsType.$ = {
    structSpec: structSpec,
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      const mojoType = typemapAdapter.toMojoTypeFn(value);
      encoder.encodeStruct(structSpec, byteOffset, mojoType);
    },
    encodeNull: function(encoder, byteOffset) {},
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      const mojoType = decoder.decodeStruct(structSpec, byteOffset);
      if (mojoType === null || mojoType === undefined) {
        return mojoType;
      }
      return typemapAdapter.toMappedTypeFn(mojoType);
    },
    computeDimensions: function(value, nullable) {
      const mojoType = typemapAdapter.toMojoTypeFn(value);
      return mojo.internal.computeStructDimensions(structSpec, mojoType);
    },
    arrayElementSize: nullable => 8,
    isValidObjectKeyType: false,
  };
};

/**
 * @param {!mojo.internal.MojomType} structMojomType
 * @return {!Function}
 * @export
 */
mojo.internal.createStructDeserializer = function(structMojomType) {
  return function(dataView) {
    if (structMojomType.$ == undefined ||
        structMojomType.$.structSpec == undefined) {
      throw new Error('Invalid struct mojom type!');
    }
    const decoder = new mojo.internal.Decoder(dataView, []);
    return decoder.decodeStructInline(structMojomType.$.structSpec);
  };
};

/**
 * @param {!Object} objectToBlessAsUnion
 * @param {string} name
 * @param {!Object} fields
 * @export
 */
mojo.internal.Union = function(objectToBlessAsUnion, name, fields) {
  /** @type {!mojo.internal.UnionSpec} */
  const unionSpec = {
    name: name,
    fields: fields,
  };
  objectToBlessAsUnion.$ = {
    unionSpec: unionSpec,
    encode: function(value, encoder, byteOffset, bitOffset, nullable) {
      encoder.encodeUnion(unionSpec, byteOffset, value);
    },
    encodeNull: function(encoder, byteOffset) {},
    decode: function(decoder, byteOffset, bitOffset, nullable) {
      return decoder.decodeUnion(unionSpec, byteOffset);
    },
    computeDimensions: function(value, nullable) {
      return mojo.internal.computeUnionDimensions(unionSpec, nullable, value);
    },
    arrayElementSize: nullable => (nullable ? 8 : 16),
    isValidObjectKeyType: false,
  };
};

/**
 * @return {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.InterfaceProxy = function(type) {
  return {
    $: {
      encode: function(value, encoder, byteOffset, bitOffset, nullable) {
        const endpoint = value.proxy.unbind();
        console.assert(endpoint, `unexpected null ${type.name}`);

        const pipe = endpoint.releasePipe();
        encoder.encodeHandle(byteOffset, pipe);
        encoder.encodeUint32(byteOffset + 4, 0);  // TODO: Support versioning
      },
      encodeNull: function(encoder, byteOffset) {
        encoder.encodeUint32(byteOffset, 0xffffffff);
      },
      decode: function(decoder, byteOffset, bitOffset, nullable) {
        return decoder.decodeInterfaceProxy(type, byteOffset);
      },
      arrayElementSize: nullable => 8,
      isValidObjectKeyType: false,
      isValueType: false,
    },
  };
};

/**
 * @return {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.InterfaceRequest = function(type) {
  return {
    $: {
      encode: function(value, encoder, byteOffset, bitOffset, nullable) {
        if (!value.handle)
          throw new Error('Unexpected null ' + type.name);

        encoder.encodeHandle(byteOffset, value.handle.releasePipe());
      },
      encodeNull: function(encoder, byteOffset) {
        encoder.encodeUint32(byteOffset, 0xffffffff);
      },
      decode: function(decoder, byteOffset, bitOffset, nullable) {
        return decoder.decodeInterfaceRequest(type, byteOffset);
      },
      arrayElementSize: nullable => 8,
      isValidObjectKeyType: false,
      isValueType: false,
    },
  };
};

/**
 * @return {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.AssociatedInterfaceProxy = function(type) {
  return {
    $: {
      type: type,
      encode: function(value, encoder, byteOffset, bitOffset, nullable) {
        console.assert(
            value.proxy.endpoint && value.proxy.endpoint.isPendingAssociation,
            `expected ${type.name} to be associated and unbound`);
        encoder.encodeAssociatedEndpoint(byteOffset, value.proxy.endpoint);
        encoder.encodeUint32(byteOffset + 4, 0);
      },
      encodeNull: function(encoder, byteOffset) {
        encoder.encodeUint32(byteOffset, 0xffffffff);
        encoder.encodeUint32(byteOffset + 4, 0);
      },
      decode: function(decoder, byteOffset, bitOffset, nullable) {
        return new type(decoder.decodeAssociatedEndpoint(byteOffset));
      },
      arrayElementSize: _ => {
        throw new Error('Arrays of associated endpoints are not yet supported');
      },
      isValidObjectKeyType: false,
      hasInterfaceId: true,
      isValueType: false,
    },
  };
};

/**
 * @return {!mojo.internal.MojomType}
 * @export
 */
mojo.internal.AssociatedInterfaceRequest = function(type) {
  return {
    $: {
      type: type,
      encode: function(value, encoder, byteOffset, bitOffset, nullable) {
        console.assert(
            value.handle && value.handle.isPendingAssociation,
            `expected ${type.name} to be associated and unbound`);

        encoder.encodeAssociatedEndpoint(byteOffset, value.handle);
      },
      encodeNull: function(encoder, byteOffset) {
        encoder.encodeUint32(byteOffset, 0xffffffff);
      },
      decode: function(decoder, byteOffset, bitOffset, nullable) {
        return new type(decoder.decodeAssociatedEndpoint(byteOffset));
      },
      arrayElementSize: _ => {
        throw new Error('Arrays of associated endpoints are not yet supported');
      },
      isValidObjectKeyType: false,
      hasInterfaceId: true,
      isValueType: false,
    },
  };
};