import { BufferReader, BufferWriter } from '@node-dlc/bufio';
import { BitField } from '@node-dlc/common';
import * as crypto from '@node-dlc/crypto';

import { deserializeAddress } from '../deserialize/address/deserializeAddress';
import { Address } from '../domain/Address';
import { NodeFeatureFlags } from '../flags/NodeFeatureFlags';
import { MessageType } from '../MessageType';
import { serializeAddress } from '../serialize/address/serializeAddress';
import { IWireMessage } from './IWireMessage';

/**
 * This gossip message allows a node to indicate extra data associated with it,
 * in addition to its public key. To avoid trivial denial of service attacks,
 * nodes not associated with an already known channel are ignored.
 */
export class NodeAnnouncementMessage implements IWireMessage {
  public static deserialize(payload: Buffer): NodeAnnouncementMessage {
    const instance = new NodeAnnouncementMessage();
    const reader = new BufferReader(payload);
    reader.readUInt16BE(); // read off type

    instance.signature = reader.readBytes(64);

    const flen = reader.readUInt16BE();
    instance.features = BitField.fromBuffer(reader.readBytes(flen));

    instance.timestamp = reader.readUInt32BE();
    instance.nodeId = reader.readBytes(33);
    instance.rgbColor = reader.readBytes(3);
    instance.alias = reader.readBytes(32);
    instance.addresses = [];

    const addrlen = reader.readUInt16BE(); // number of bytes
    const startPos = reader.position;
    while (reader.position < startPos + addrlen) {
      const type = reader.readUInt8();
      const address = deserializeAddress(type, reader);
      instance.addresses.push(address);
    }

    return instance;
  }

  /**
   * Message hashing is after the first 66 bytes of the message
   * and excludes the type and signature. It performs a double
   * sha-256 hash of the remaining bytes.
   */
  public static hash(msg: NodeAnnouncementMessage): Buffer {
    const bytes = msg.serialize().slice(66); // type + signature
    return crypto.hash256(bytes);
  }

  /**
   * Verifies the message signature
   */
  public static verifySignatures(msg: NodeAnnouncementMessage): boolean {
    const hash = NodeAnnouncementMessage.hash(msg);
    return crypto.verifySig(hash, msg.signature, msg.nodeId);
  }

  /**
   * Type 257
   */
  public type: MessageType = MessageType.NodeAnnouncement;

  /**
   * Signature of the announcement message by the node's public key
   * returned as a 64-byte Buffer.
   */
  public signature: Buffer;

  public features: BitField<NodeFeatureFlags>;

  public timestamp: number;

  /**
   * Compressed public key of the node that is a 33-byte
   * buffer.
   */
  public nodeId: Buffer;

  /**
   * Color of the node returned as a 3-byte Buffer.
   */
  public rgbColor: Buffer;

  /**
   * Alias of the node returned as a 32-byte Buffer.
   */
  public alias: Buffer;

  /**
   * Addresses that the node allow public network connections
   * on. The type indicates how the address is encoded. Addresses
   * are in order of connectivity preference. Currently
   * supported addresses formats are IPv4, IPv6, Tor2 and Tor3
   */
  public addresses: Address[] = [];

  public serialize(): Buffer {
    const featuresBuffer = this.features.toBuffer();
    const featuresLen = featuresBuffer.length;

    // serialize addresses into buffers so we can obtain the length
    const addressBuffers = [];
    for (const address of this.addresses) {
      addressBuffers.push(serializeAddress(address));
    }

    // obtain total address length
    // eslint-disable-next-line
    const addressBytes: number = addressBuffers.map(b => b.length).reduce((sum, val) => sum + val, 0); // prettier-ignore

    const len =
      2 + // type
      64 + // signature
      2 + // flen
      featuresLen + // features length
      4 + // timestamp
      33 + // node_id
      3 + // rgb_color
      32 + // alias
      2 + // addresses
      addressBytes; // cumulative addr bytes
    const writer = new BufferWriter(Buffer.alloc(len));

    writer.writeUInt16BE(this.type);
    writer.writeBytes(this.signature);
    writer.writeUInt16BE(featuresLen);
    if (featuresLen > 0) writer.writeBytes(featuresBuffer);
    writer.writeUInt32BE(this.timestamp);
    writer.writeBytes(this.nodeId);
    writer.writeBytes(this.rgbColor);
    writer.writeBytes(this.alias);
    writer.writeUInt16BE(addressBytes);
    for (const addressBuffer of addressBuffers) {
      writer.writeBytes(addressBuffer);
    }

    return writer.toBuffer();
  }
}
