import { NativeModules, Platform, NativeEventEmitter} from 'react-native';
import {decode, encode} from 'base-64'

const LINKING_ERROR =
  `The package '@waku/react-native' doesn't seem to be linked. Make sure: \n\n` +
  Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
  '- You rebuilt the app after installing the package\n' +
  '- You are not using Expo managed workflow\n';

const ReactNative = NativeModules.ReactNative
  ? NativeModules.ReactNative
  : new Proxy(
      {},
      {
        get() {
          throw new Error(LINKING_ERROR);
        },
      }
    );

export class WakuMessage {
      payload: Uint8Array = new Uint8Array();
      contentTopic: String | null = "";
      version: Number | null = 0;
      timestamp: Number | null = null;
    
      toJSON(){
        const b64encoded = encode(String.fromCharCode(...this.payload));
        return {
          contentTopic: this.contentTopic,
          version: this.version,
          timestamp: this.timestamp,
          payload: b64encoded
        }
      }
    }

var eventEmitter = new NativeEventEmitter(NativeModules.ReactNative);

/**
 * Execute function each time a message is received
 * @param cb callback to be eecuted
 */
export function onMessage(cb: (arg0:any) => void) {
  eventEmitter.addListener("message", event => {
    let signal = JSON.parse(event.signal);
    let msg = signal.event.wakuMessage;
    signal.event.wakuMessage = new WakuMessage();
    signal.event.wakuMessage.timestamp = msg.timestamp;
    signal.event.wakuMessage.version = msg.version || 0;
    signal.event.wakuMessage.contentTopic = msg.contentTopic;
    signal.event.wakuMessage.payload = new Uint8Array(decode(msg.payload ?? []).split("").map((c:any) => c.charCodeAt(0)));
    cb(signal.event);
  })
}

export class Config {
  host: String | null = null
  port: Number | null = null
  advertiseAddr: String | null = null
  nodeKey: String | null = null
  keepAliveInterval: Number | null = null
  relay: Boolean | null = null
  filter: Boolean | null = null
  minPeersToPublish: Number | null = null
}

/**
 * Instantiates a Waku node.
 * @param config options used to initialize a go-waku node
 */
export function newNode(config: Config | null): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {    
    let response = JSON.parse(await ReactNative.newNode(config ? JSON.stringify(config) : ""));
    if(response.error){
      reject(response.error);
    } else {
      resolve();
    }
  });
}

/**
 * Start a Waku node mounting all the protocols that were enabled during the Waku node instantiation.
 */
export function start(): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.start());
    if(response.error){
      reject(response.error);
    } else {
      resolve();
    }
  });
}

/**
 * Stops a Waku node.
 */
export function stop(): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.stop());
    if(response.error){
      reject(response.error);
    } else {
      resolve();
    }
  });
}

/**
 * Is the node started?
 * @returns `true` if the node is started, `false` otherwise
 */
export function isStarted(): Promise<Boolean> {
  return new Promise<Boolean>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.isStarted());
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Get the peer ID of the waku node.
 * @returns Base58 encoded peer ID
 */
export function peerID(): Promise<string> {
  return new Promise<string>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.peerID());
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Publish a message using Waku Relay.
 * @param msg WakuMessage to publish. The message version is overwritten to `0`
 * @param pubsubTopic pubsub topic on which to publish the message. If not specified, it will use the default pubsub topic
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 * @returns string containing the message id
 */
export function relayPublish(msg: WakuMessage, pubsubTopic: String = "", timeoutMs: Number = 0): Promise<string> {
  return new Promise<string>(async (resolve, reject) => {
    let messageJSON = JSON.stringify(msg)
    let response = JSON.parse(await ReactNative.relayPublish(messageJSON, pubsubTopic, timeoutMs));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Optionally sign, encrypt using asymmetric encryption and publish a message using Waku Relay. 
 * @param msg WakuMessage to publish. The message version is overwritten to `1`
 * @param publicKey hex encoded public key to be used for encryption.
 * @param optionalSigningKey hex encoded private key to be used to sign the message.
 * @param pubsubTopic pubsub topic on which to publish the message. If not specified, it will use the default pubsub topic.
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 * @returns string containing the message id
 */
export function relayPublishEncodeAsymmetric(msg: WakuMessage, publicKey: String, optionalSigningKey: String = "", pubsubTopic: String = "", timeoutMs: Number = 0): Promise<string> {
  return new Promise<string>(async (resolve, reject) => {
    let messageJSON = JSON.stringify(msg)
    let response = JSON.parse(await ReactNative.relayPublishEncodeAsymmetric(messageJSON, pubsubTopic, publicKey, optionalSigningKey, timeoutMs));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Optionally sign, encrypt using symmetric encryption and publish a message using Waku Relay.
 * @param msg WakuMessage to publish. The message version is overwritten to `1`
 * @param symmetricKey  32 byte hex encoded secret key to be used for encryption
 * @param optionalSigningKey hex encoded private key to be used to sign the message
 * @param pubsubTopic pubsub topic on which to publish the message. If not specified, it will use the default pubsub topic
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 * @returns string containing the message id
 */
export function relayPublishEncodeSymmetric(msg: WakuMessage, symmetricKey: String, optionalSigningKey: String = "", pubsubTopic: String = "", timeoutMs: Number = 0): Promise<string> {
  return new Promise<string>(async (resolve, reject) => {
    let messageJSON = JSON.stringify(msg)
    let response = JSON.parse(await ReactNative.relayPublishEncodeAsymmetric(messageJSON, pubsubTopic, symmetricKey, optionalSigningKey, timeoutMs));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Subscribe to a Waku Relay pubsub topic to receive messages.
 * @param topic Pubsub topic to subscribe to. 
 */
export function relaySubscribe(topic: String = ""): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.relaySubscribe(topic));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Returns the default pubsub topic used for exchanging waku messages defined in [RFC 10](https://rfc.vac.dev/spec/10/).
 * @returns the default pubsub topic `/waku/2/default-waku/proto`
 */
export function defaultPubsubTopic(): Promise<String> {
  return ReactNative.defaultPubsubTopic();
}

/**
 * Get the multiaddresses the Waku node is listening to.
 * @returns an array of multiaddresses
 */
export function listenAddresses(): Promise<Array<String>> {
  return new Promise<Array<string>>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.listenAddresses());
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Add a node multiaddress and protocol to the waku node's peerstore.
 * @param multiAddress multiaddress (with peer id) to reach the peer being added
 * @param protocol protocol we expect the peer to support
 * @returns  peer ID as a base58 `string` of the peer that was added
 */
export function addPeer(multiAddress: String, protocol: String): Promise<String> {
  return new Promise<string>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.addPeer(multiAddress, protocol));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Dial peer using a multiaddress.
 * @param multiAddress multiaddress to reach the peer being dialed
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 */
export function connect(multiAddress: String, timeoutMs: Number = 0): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.connect(multiAddress, timeoutMs));
    if(response.error){
      reject(response.error);
    } else {
      resolve();
    }
  });
}

/**
 * Dial peer using its peer ID.
 * @param peerID  Peer ID to dial. The peer must be already known.  It must have been added before with `addPeer` or previously dialed with `connect`
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 */
export function connectPeerID(peerID: String, timeoutMs: Number = 0): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.connectPeerID(peerID, timeoutMs));
    if(response.error){
      reject(response.error);
    } else {
      resolve();
    }
  });
}

/**
 * Disconnect a peer using its peerID
 * @param peerID Peer ID to disconnect.
 */
export function disconnect(peerID: String): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.disconnect(peerID));
    if(response.error){
      reject(response.error);
    } else {
      resolve();
    }
  });
}

/**
 * Get number of connected peers.
 * @returns number of connected peers
 */
export function peerCnt(): Promise<Number> {
  return new Promise<Number>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.peerCnt());
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

export class DecodedPayload {
  payload: Uint8Array = new Uint8Array();
  padding: Uint8Array = new Uint8Array();
  pubkey: String | null = "";
  signature: String | null = "";

  toJSON(){
    const b64payload = encode(String.fromCharCode(...this.payload));
    const b64padding = encode(String.fromCharCode(...this.padding));
    return {
      payload: b64payload,
      padding: b64padding,
      pubkey: this.pubkey,
      signature: this.signature,
    }
  }
}

/**
 * Decrypt a message using a symmetric key
 * @param msg WakuMessage to decode. The message version is expected to be 1
 * @param symmetricKey 32 byte symmetric key hex encoded
 * @returns DecodedPayload
 */
export function decodeSymmetric(msg: WakuMessage, symmetricKey: String): Promise<DecodedPayload> {
  return new Promise<DecodedPayload>(async (resolve, reject) => {
    let messageJSON = JSON.stringify(msg);
    let response = JSON.parse(await ReactNative.decodeSymmetric(messageJSON, symmetricKey));
    if(response.error){
      reject(response.error);
    } else {
      let decodedPayload = new DecodedPayload();
      decodedPayload.payload = new Uint8Array(atob(response.result.payload).split("").map(c => c.charCodeAt(0)));
      decodedPayload.padding = new Uint8Array(atob(response.result.padding).split("").map(c => c.charCodeAt(0)));
      decodedPayload.pubkey = response.result.pubkey;
      decodedPayload.signature = response.result.signature;
      resolve(decodedPayload);
    }
  });
}

/**
 * Decrypt a message using a secp256k1 private key 
 * @param msg WakuMessage to decode. The message version is expected to be 1
 * @param privateKey secp256k1 private key hex encoded
 * @returns DecodedPayload
 */
export function decodeAsymmetric(msg: WakuMessage, privateKey: String): Promise<DecodedPayload> {
  return new Promise<DecodedPayload>(async (resolve, reject) => {
    let messageJSON = JSON.stringify(msg);
    let response = JSON.parse(await ReactNative.decodeSymmetric(messageJSON, privateKey));
    if(response.error){
      reject(response.error);
    } else {
      let decodedPayload = new DecodedPayload();
      decodedPayload.payload = new Uint8Array(atob(response.result.payload).split("").map(c => c.charCodeAt(0)));
      decodedPayload.padding = new Uint8Array(atob(response.result.padding).split("").map(c => c.charCodeAt(0)));
      decodedPayload.pubkey = response.result.pubkey;
      decodedPayload.signature = response.result.signature;
      resolve(decodedPayload);
    }
  });
}

/**
 * Determine if there are enough peers to publish a message on a given pubsub topic.
 * @param pubsubTopic Pubsub topic to verify. If not specified, it will verify the default pubsub topic
 * @returns boolean indicates whether there are enough peers
 */
export function relayEnoughPeers(pubsubTopic: String = ""): Promise<Boolean> {
  return new Promise<Boolean>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.relayEnoughPeers(pubsubTopic));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Closes the pubsub subscription to a pubsub topic. No more messages will be received from this pubsub topic.
 * @param pubsubTopic 
 */
export function relayUnsubscribe(pubsubTopic: String = ""): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.relayUnsubscribe(pubsubTopic));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Publish a message using Waku Lightpush.
 * @param msg WakuMessage to publish. The message version is overwritten to `0`
 * @param pubsubTopic pubsub topic on which to publish the message. If not specified, it uses the default pubsub topic. 
 * @param peerID Peer ID supporting the lightpush protocol.  The peer must be already known.  It must have been added before with `addPeer` or previously dialed with `connect`
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 * @returns the message ID
 */
export function lightpushPublish(msg: WakuMessage, pubsubTopic: String = "", peerID: String = "", timeoutMs: Number = 0): Promise<string> {
  return new Promise<string>(async (resolve, reject) => {
    let messageJSON = JSON.stringify(msg)
    let response = JSON.parse(await ReactNative.lightpushPublish(messageJSON, pubsubTopic, peerID, timeoutMs));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Optionally sign, encrypt using asymmetric encryption and publish a message using Waku Lightpush.
 * @param msg WakuMessage to publish. The message version is overwritten to `1`
 * @param publicKey hex encoded public key to be used for encryption
 * @param optionalSigningKey hex encoded private key to be used to sign the message
 * @param pubsubTopic pubsub topic on which to publish the message. If not specified, it uses the default pubsub topic. 
 * @param peerID Peer ID supporting the lightpush protocol.  The peer must be already known.  It must have been added before with `addPeer` or previously dialed with `connect`
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 * @returns the message ID
 */
export function lightpushPublishEncAsymmetric(msg: WakuMessage, publicKey: String, optionalSigningKey: String = "", pubsubTopic: String = "", peerID: String = "", timeoutMs: Number = 0): Promise<string> {
  return new Promise<string>(async (resolve, reject) => {
    let messageJSON = JSON.stringify(msg)
    let response = JSON.parse(await ReactNative.lightpushPublishEncodeAsymmetric(messageJSON, pubsubTopic, peerID, publicKey, optionalSigningKey, timeoutMs));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

/**
 * Optionally sign, encrypt using symmetric encryption and publish a message using Waku Lightpush.
 * @param msg WakuMessage to publish. The message version is overwritten to `1`
 * @param symmetricKey hex encoded secret key to be used for encryption.
 * @param optionalSigningKey hex encoded private key to be used to sign the message.
 * @param pubsubTopic pubsub topic on which to publish the message. If not specified, it uses the default pubsub topic. 
 * @param peerID Peer ID supporting the lightpush protocol.  The peer must be already known.  It must have been added before with `addPeer` or previously dialed with `connect`
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 * @returns the message ID
 */
export function lightpushPublishEncSymmetric(msg: WakuMessage, symmetricKey: String, optionalSigningKey: String = "", pubsubTopic: String = "", peerID: String = "", timeoutMs: Number = 0): Promise<string> {
  return new Promise<string>(async (resolve, reject) => {
    let messageJSON = JSON.stringify(msg)
    let response = JSON.parse(await ReactNative.lightpushPublishEncodeAsymmetric(messageJSON, pubsubTopic,  peerID, symmetricKey, optionalSigningKey, timeoutMs));
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

export class Peer {
  addrs: Array<String> = Array()
  connected: Boolean = false
  peerID: String = ""
  protocols: Array<String> = Array()

  constructor(addrs: Array<String>, connected: Boolean, peerID: String, protocols: Array<String>){
    this.addrs = addrs;
    this.connected = connected;
    this.peerID = peerID;
    this.protocols = protocols;
  }
}

/**
 * Retrieve the list of peers known by the Waku node.
 * @returns list of peers
 */
export function peers(): Promise<Array<Peer>> {
  return new Promise<Array<Peer>>(async (resolve, reject) => {
    let response = JSON.parse(await ReactNative.peers());
    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result.map((x:any) => new Peer(x.addrs, x.connected, x.peerID, x.protocols)));
    }
  })
}

export class Index {
  digest: Uint8Array = new Uint8Array();
  receiverTime: Number = 0
  senderTime: Number = 0
  pubsubTopic: String = ""
}
export class PagingOptions {
  pageSize: Number = 0
  cursor: Index | null = null
  forward: Boolean = false

  constructor(pageSize: Number = 0, forward: Boolean = false, cursor: Index | null = null){
    this.pageSize = pageSize
    this.forward = forward
    this.cursor = cursor
  }
}

export class ContentFilter {
  contentTopic: String = ""

  constructor(contentTopic: String = "") {
    this.contentTopic = contentTopic
  }
}

export class StoreQuery {
  pubsubTopic: String | null = null
  contentFilters: Array<ContentFilter> = Array()
  startTime: Number = 0
  endTime: Number = 0
  pagingOptions: PagingOptions | null = null

  constructor(pubsubTopic: String | null = null, contentFilters: Array<ContentFilter> = Array(), startTime: Number = 0, endTime: Number = 0, pagingOptions: PagingOptions | null = null) {
    this.pubsubTopic = pubsubTopic
    this.contentFilters = contentFilters
    this.startTime = startTime
    this.endTime = endTime
    this.pagingOptions = pagingOptions
  }
}

/**
 * 
 * @param query 
 * @param peerID 
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 * @returns 
 */
export function storeQuery(query: StoreQuery, peerID: String = "", timeoutMs: Number = 0): Promise<any> {
  return new Promise<string>(async (resolve, reject) => {
    let queryJSON = JSON.stringify(query)
    let response = JSON.parse(await ReactNative.storeQuery(queryJSON, peerID, timeoutMs));

    if(response.error){
      reject(response.error);
    } else {
      resolve(response.result);
    }
  });
}

export class FilterSubscription {
  pubsubTopic: String | null = null
  contentFilters: Array<ContentFilter> = Array()

  constructor(pubsubTopic: String | null = null, contentFilters: Array<ContentFilter> = Array()) {
    this.pubsubTopic = pubsubTopic
    this.contentFilters = contentFilters
  }
}

/**
 * 
 * @param filter 
 * @param peerID 
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 */
export function filterSubscribe(filter: FilterSubscription, peerID: String = "", timeoutMs: Number = 0): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let filterJSON = JSON.stringify(filter)
    let response = JSON.parse(await ReactNative.filterSubscribe(filterJSON, peerID, timeoutMs));

    if(response.error){
      reject(response.error);
    } else {
      resolve();
    }
  });
}

/**
 * 
 * @param filter 
 * @param timeoutMs Timeout value in milliseconds to execute the call. If the function takes longer than this value, the execution will be canceled and an error returned
 */
export function filterUnsubscribe(filter: FilterSubscription, timeoutMs: Number = 0): Promise<void> {
  return new Promise<void>(async (resolve, reject) => {
    let filterJSON = JSON.stringify(filter)
    let response = JSON.parse(await ReactNative.filterUnsubscribe(filterJSON, timeoutMs));

    if(response.error){
      reject(response.error);
    } else {
      resolve();
    }
  });
}
