import { reverseBuffer, asHexString } from "./util";
import { decrypt, decryptJoin, decryptFOpts } from "./crypto";
import { recalculateMIC } from "./mic";

enum MType {
  JOIN_REQUEST = 0,
  JOIN_ACCEPT = 1,
  UNCONFIRMED_DATA_UP = 2,
  UNCONFIRMED_DATA_DOWN = 3,
  CONFIRMED_DATA_UP = 4,
  CONFIRMED_DATA_DOWN = 5,
  REJOIN_REQUEST = 6,
}

const MTYPE_DESCRIPTIONS: { [key: number]: string } = {
  [MType.JOIN_REQUEST]: "Join Request",
  [MType.JOIN_ACCEPT]: "Join Accept",
  [MType.UNCONFIRMED_DATA_UP]: "Unconfirmed Data Up",
  [MType.UNCONFIRMED_DATA_DOWN]: "Unconfirmed Data Down",
  [MType.CONFIRMED_DATA_UP]: "Confirmed Data Up",
  [MType.CONFIRMED_DATA_DOWN]: "Confirmed Data Down",
  [MType.REJOIN_REQUEST]: "Rejoin Request",
};

const DESCRIPTIONS_MTYPE: { [description: string]: MType } = Object.keys(MTYPE_DESCRIPTIONS).reduce((acc, key) => {
  const mTypeKey = (key as unknown) as MType; // Cast the key to MType
  const description = MTYPE_DESCRIPTIONS[mTypeKey];
  acc[description] = mTypeKey;
  return acc;
}, {} as { [description: string]: MType });

type Range = {
  start: number;
  end: number;
};

type PacketStructures = {
  [key: string]: {
    [key: string]: Range;
  };
};

const PACKET_STRUCTURES: PacketStructures = {
  JOIN_REQUEST: {
    AppEUI: { start: 1, end: 9 },
    DevEUI: { start: 9, end: 17 },
    DevNonce: { start: 17, end: 19 },
  },
  JOIN_ACCEPT: {
    AppNonce: { start: 1, end: 4 },
    NetID: { start: 4, end: 7 },
    DevAddr: { start: 7, end: 11 },
    DLSettings: { start: 11, end: 12 },
    RxDelay: { start: 12, end: 13 },
  },
  REJOIN_TYPE_1: {
    NetID: { start: 2, end: 5 },
    DevEUI: { start: 5, end: 13 },
    RJCount0: { start: 13, end: 15 },
  },
  REJOIN_TYPE_2: {
    JoinEUI: { start: 2, end: 10 },
    DevEUI: { start: 10, end: 18 },
    RJCount1: { start: 13, end: 15 },
  },
};

enum LorawanVersion {
  V1_0 = "1.0",
  V1_1 = "1.1",
}

enum Masks {
  FCTRL_ADR = 0x80,
  FCTRL_ADRACKREQ = 0x40,
  FCTRL_ACK = 0x20,
  FCTRL_FPENDING = 0x10,
  DLSETTINGS_RXONEDROFFSET_MASK = 0x70,
  DLSETTINGS_RXONEDROFFSET_POS = 4,
  DLSETTINGS_RXTWODATARATE_MASK = 0x0f,
  DLSETTINGS_RXTWODATARATE_POS = 0,
  DLSETTINGS_OPTNEG_MASK = 0x80,
  DLSETTINGS_OPTNEG_POS = 7,
  RXDELAY_DEL_MASK = 0x0f,
  RXDELAY_DEL_POS = 0,
}

export interface UserFields {
  CFList?: Buffer;
  RxDelay?: Buffer | number;
  DLSettings?: Buffer | number;
  NetID?: Buffer;
  AppNonce?: Buffer;
  DevNonce?: Buffer;
  DevEUI?: Buffer;
  AppEUI?: Buffer;
  FPort?: number;
  FOpts?: string | Buffer;
  FCnt?: number | Buffer;
  MType?: string | number;
  DevAddr?: Buffer;
  payload?: string | Buffer;
  FCtrl?: {
    ADR?: boolean;
    ADRACKReq?: boolean;
    ACK?: boolean;
    FPending?: boolean;
  };
  JoinReqType?: Buffer | number;
}

function extractBytesFromBuffer(buffer: Buffer, start: number, end: number): Buffer {
  return reverseBuffer(buffer.slice(start, end));
}

function extractStructuredBytesFromBuffer(buffer: Buffer, name: string): { [key: string]: Buffer } {
  const structure = PACKET_STRUCTURES[name];
  const ret: { [key: string]: Buffer } = {};
  for (const key in structure) {
    if (structure.hasOwnProperty(key)) {
      ret[key] = extractBytesFromBuffer(buffer, structure[key].start, structure[key].end);
    }
  }
  return ret;
}

class LoraPacket {
  static fromWire(buffer: Buffer): LoraPacket {
    const payload = new LoraPacket();
    payload._initfromWire(buffer);
    return payload;
  }

  static fromFields(
    fields: UserFields,
    AppSKey?: Buffer,
    NwkSKey?: Buffer,
    AppKey?: Buffer,
    FCntMSBytes?: Buffer,
    ConfFCntDownTxDrTxCh?: Buffer
  ): LoraPacket {
    if (!FCntMSBytes) FCntMSBytes = Buffer.alloc(2, 0);
    const payload = new LoraPacket();

    payload._initFromFields(fields);
    if (payload.isDataMessage()) {
      // to encrypt, need NwkSKey if port=0, else AppSKey

      const port = payload.getFPort();

      if (port !== null && ((port === 0 && NwkSKey?.length === 16) || (port > 0 && AppSKey?.length === 16))) {
        // crypto is reversible (just XORs FRMPayload), so we can
        //  just do "decrypt" on the plaintext to get ciphertext

        let ciphertext: Buffer;
        if (port === 0 && NwkSKey?.length === 16 && AppSKey?.length === 16 && AppKey?.length === 16) {
          ciphertext = decrypt(payload, undefined, AppSKey, FCntMSBytes);
        } else {
          ciphertext = decrypt(payload, AppSKey, NwkSKey, FCntMSBytes);
        }

        // overwrite payload with ciphertext
        payload.FRMPayload = ciphertext;
        // recalculate buffers to be ready for MIC calc'n
        payload._mergeGroupFields();
        if (NwkSKey?.length === 16) {
          recalculateMIC(payload, NwkSKey, AppKey, FCntMSBytes, ConfFCntDownTxDrTxCh);
          payload._mergeGroupFields();
        }
      }
    } else if (payload._getMType() === MType.JOIN_REQUEST) {
      if (AppKey?.length === 16) {
        recalculateMIC(payload, NwkSKey, AppKey, FCntMSBytes);
        payload._mergeGroupFields();
      }
    } else if (payload._getMType() === MType.JOIN_ACCEPT) {
      if (AppKey?.length === 16) {
        recalculateMIC(payload, NwkSKey, AppKey, FCntMSBytes);
        payload._mergeGroupFields();
        const ciphertext = decryptJoin(payload, AppKey);
        // overwrite payload with ciphertext
        if (payload.MACPayloadWithMIC) ciphertext.copy(payload.MACPayloadWithMIC);
      }
    }

    return payload;
  }

  private assignFromStructuredBuffer(buffer: Buffer, structure: string) {
    const fields = extractStructuredBytesFromBuffer(buffer, structure);
    Object.assign(this, fields);
  }

  private _initfromWire(contents: Buffer): void {
    const incoming = Buffer.from(contents);

    this.PHYPayload = incoming;

    this.MHDR = incoming.slice(0, 1);
    this.MACPayload = incoming.slice(1, incoming.length - 4);
    this.MACPayloadWithMIC = incoming.slice(1, incoming.length);
    this.MIC = incoming.slice(incoming.length - 4);

    const mtype = this._getMType();

    if (mtype == MType.JOIN_REQUEST) {
      if (incoming.length < 5 + 18) {
        throw new Error("contents too short for a Join Request");
      }
      this.assignFromStructuredBuffer(incoming, "JOIN_REQUEST");
    } else if (mtype == MType.JOIN_ACCEPT) {
      if (incoming.length < 5 + 12) {
        throw new Error("contents too short for a Join Accept");
      }
      this.assignFromStructuredBuffer(incoming, "JOIN_ACCEPT");
      this.JoinReqType = Buffer.from([0xff]);

      if (incoming.length == 13 + 16 + 4) {
        this.CFList = incoming.slice(13, 13 + 16);
      } else {
        this.CFList = Buffer.alloc(0);
      }
    } else if (mtype == MType.REJOIN_REQUEST) {
      this.RejoinType = incoming.slice(1, 1 + 1);
      if (this.RejoinType[0] === 0 || this.RejoinType[0] === 2) {
        if (incoming.length < 5 + 14) {
          throw new Error("contents too short for a Rejoin Request (Type 0/2)");
        }
        this.assignFromStructuredBuffer(incoming, "REJOIN_TYPE_1");
      } else if (this.RejoinType[0] === 1) {
        if (incoming.length < 5 + 19) {
          throw new Error("contents too short for a Rejoin Request (Type 1)");
        }
        this.assignFromStructuredBuffer(incoming, "REJOIN_TYPE_2");
      }
    } else if (this.isDataMessage()) {
      this.DevAddr = reverseBuffer(incoming.slice(1, 5));
      this.FCtrl = reverseBuffer(incoming.slice(5, 6));
      this.FCnt = reverseBuffer(incoming.slice(6, 8));

      const FCtrl = this.FCtrl.readInt8(0);
      const FOptsLen = FCtrl & 0x0f;
      this.FOpts = incoming.slice(8, 8 + FOptsLen);
      const FHDR_length = 7 + FOptsLen;
      this.FHDR = incoming.slice(1, 1 + FHDR_length);

      if (FHDR_length == this.MACPayload.length) {
        this.FPort = Buffer.alloc(0);
        this.FRMPayload = Buffer.alloc(0);
      } else {
        this.FPort = incoming.slice(FHDR_length + 1, FHDR_length + 2);
        this.FRMPayload = incoming.slice(FHDR_length + 2, incoming.length - 4);
      }
    }
  }

  private _initFromFields(userFields: UserFields): void {
    if (typeof userFields.MType !== "undefined") {
      let MTypeNo;
      if (typeof userFields.MType === "number") {
        MTypeNo = userFields.MType;
      } else if (typeof userFields.MType == "string") {
        const mhdr_idx = DESCRIPTIONS_MTYPE[userFields.MType];
        if (mhdr_idx >= 0) {
          MTypeNo = mhdr_idx;
        } else {
          throw new Error("MType is unknown");
        }
      } else {
        throw new Error("MType is required in a suitable format");
      }

      if (MTypeNo == MType.JOIN_REQUEST) {
        this._initialiseJoinRequestPacketFromFields(userFields);
      } else if (MTypeNo == MType.JOIN_ACCEPT) {
        this._initialiseJoinAcceptPacketFromFields(userFields);
      } else {
        this._initialiseDataPacketFromFields(userFields);
      }
    } else {
      if (userFields.DevAddr && typeof userFields.payload !== "undefined") {
        this._initialiseDataPacketFromFields(userFields);
      } else if (userFields.AppEUI && userFields.DevEUI && userFields.DevNonce) {
        this._initialiseJoinRequestPacketFromFields(userFields);
      } else if (userFields.AppNonce && userFields.NetID && userFields.DevAddr) {
        this._initialiseJoinAcceptPacketFromFields(userFields);
      } else {
        throw new Error("No plausible packet");
      }
    }
  }

  private _mergeGroupFields(): void {
    if (this.MHDR && this.MIC) {
      if (this._getMType() === MType.JOIN_REQUEST && this.AppEUI && this.DevEUI && this.DevNonce) {
        this.MACPayload = Buffer.concat([
          reverseBuffer(this.AppEUI),
          reverseBuffer(this.DevEUI),
          reverseBuffer(this.DevNonce),
        ]);
        this.PHYPayload = Buffer.concat([this.MHDR, this.MACPayload, this.MIC]);
        this.MACPayloadWithMIC = this.PHYPayload.slice(this.MHDR.length, this.PHYPayload.length);
      } else if (
        this._getMType() === MType.JOIN_ACCEPT &&
        this.AppNonce &&
        this.NetID &&
        this.DevAddr &&
        this.DLSettings &&
        this.RxDelay &&
        this.CFList
      ) {
        this.MACPayload = Buffer.concat([
          reverseBuffer(this.AppNonce),
          reverseBuffer(this.NetID),
          reverseBuffer(this.DevAddr),
          this.DLSettings,
          this.RxDelay,
          this.CFList,
        ]);
        this.PHYPayload = Buffer.concat([this.MHDR, this.MACPayload, this.MIC]);
        this.MACPayloadWithMIC = this.PHYPayload.slice(this.MHDR.length, this.PHYPayload.length);
      } else if (this.FCtrl && this.DevAddr && this.FPort && this.FCnt && this.FRMPayload && this.FOpts) {
        this.FHDR = Buffer.concat([reverseBuffer(this.DevAddr), this.FCtrl, reverseBuffer(this.FCnt), this.FOpts]);
        this.MACPayload = Buffer.concat([this.FHDR, this.FPort, this.FRMPayload]);
        this.PHYPayload = Buffer.concat([this.MHDR, this.MACPayload, this.MIC]);
        this.MACPayloadWithMIC = this.PHYPayload.slice(this.MHDR.length, this.PHYPayload.length);
      }
    }
  }

  private _initialiseDataPacketFromFields(userFields: UserFields): void {
    if (userFields.DevAddr && userFields.DevAddr.length == 4) {
      this.DevAddr = Buffer.from(userFields.DevAddr);
    } else {
      throw new Error("DevAddr is required in a suitable format");
    }

    if (typeof userFields.payload === "string") {
      this.FRMPayload = Buffer.from(userFields.payload);
    } else if (userFields.payload instanceof Buffer) {
      this.FRMPayload = Buffer.from(userFields.payload);
    }

    if (typeof userFields.MType !== "undefined") {
      if (typeof userFields.MType === "number") {
        this.MHDR = Buffer.alloc(1);
        this.MHDR.writeUInt8(userFields.MType << 5, 0);
      } else if (typeof userFields.MType === "string") {
        const mhdr_idx = DESCRIPTIONS_MTYPE[userFields.MType];
        if (mhdr_idx >= 0) {
          this.MHDR = Buffer.alloc(1);
          this.MHDR.writeUInt8(mhdr_idx << 5, 0);
        } else {
          throw new Error("MType is unknown");
        }
      } else {
        throw new Error("MType is required in a suitable format");
      }
    }

    if (userFields.FCnt) {
      if (userFields.FCnt instanceof Buffer && userFields.FCnt.length == 2) {
        this.FCnt = Buffer.from(userFields.FCnt);
      } else if (typeof userFields.FCnt === "number") {
        this.FCnt = Buffer.alloc(2);
        this.FCnt.writeUInt16BE(userFields.FCnt, 0);
      } else {
        throw new Error("FCnt is required in a suitable format");
      }
    }

    if (typeof userFields.FOpts !== "undefined") {
      if (typeof userFields.FOpts === "string") {
        this.FOpts = Buffer.from(userFields.FOpts, "hex");
      } else if (userFields.FOpts instanceof Buffer) {
        this.FOpts = Buffer.from(userFields.FOpts);
      } else {
        throw new Error("FOpts is required in a suitable format");
      }
      if (15 < this.FOpts.length) {
        throw new Error("Too many options for piggybacking");
      }
    } else {
      this.FOpts = Buffer.from("", "hex");
    }

    let fctrl = 0;
    if (userFields.FCtrl?.ADR) {
      fctrl |= Masks.FCTRL_ADR;
    }
    if (userFields.FCtrl?.ADRACKReq) {
      fctrl |= Masks.FCTRL_ADRACKREQ;
    }
    if (userFields.FCtrl?.ACK) {
      fctrl |= Masks.FCTRL_ACK;
    }
    if (userFields.FCtrl?.FPending) {
      fctrl |= Masks.FCTRL_FPENDING;
    }

    fctrl |= this.FOpts.length & 0x0f;
    this.FCtrl = Buffer.alloc(1);
    this.FCtrl.writeUInt8(fctrl, 0);

    if (!isNaN(userFields.FPort) && userFields.FPort >= 0 && userFields.FPort <= 255) {
      this.FPort = Buffer.alloc(1);
      this.FPort.writeUInt8(userFields.FPort, 0);
    }

    if (!this?.MHDR) {
      this.MHDR = Buffer.alloc(1);
      this.MHDR.writeUInt8(MType.UNCONFIRMED_DATA_UP << 5, 0);
    }

    if (this?.FPort == null) {
      if (this?.FRMPayload && this.FRMPayload.length > 0) {
        this.FPort = Buffer.from("01", "hex");
      } else {
        this.FPort = Buffer.alloc(0);
      }
    }

    if (!this?.FPort == null) {
      this.FPort = Buffer.from("01", "hex");
    }

    if (!this.FCnt) {
      this.FCnt = Buffer.from("0000", "hex");
    }

    if (!this.MIC) {
      this.MIC = Buffer.from("EEEEEEEE", "hex");
    }

    this._mergeGroupFields();
  }

  private _initialiseJoinRequestPacketFromFields(userFields: UserFields): void {
    if (userFields.AppEUI && userFields.AppEUI.length == 8) {
      this.AppEUI = Buffer.from(userFields.AppEUI);
    } else {
      throw new Error("AppEUI is required in a suitable format");
    }

    if (userFields.DevEUI && userFields.DevEUI.length == 8) {
      this.DevEUI = Buffer.from(userFields.DevEUI);
    } else {
      throw new Error("DevEUI is required in a suitable format");
    }

    if (userFields.DevNonce && userFields.DevNonce.length == 2) {
      this.DevNonce = Buffer.from(userFields.DevNonce);
    } else {
      throw new Error("DevNonce is required in a suitable format");
    }

    if (userFields.FCnt) {
      if (userFields.FCnt instanceof Buffer && userFields.FCnt.length == 2) {
        this.FCnt = Buffer.from(userFields.FCnt);
      } else if (typeof userFields.FCnt === "number") {
        this.FCnt = Buffer.alloc(2);
        this.FCnt.writeUInt16BE(userFields.FCnt, 0);
      } else {
        throw new Error("FCnt is required in a suitable format");
      }
    }
    this.MHDR = Buffer.alloc(1);
    this.MHDR.writeUInt8(MType.JOIN_REQUEST << 5, 0);

    if (!this.MIC) {
      this.MIC = Buffer.from("EEEEEEEE", "hex");
    }

    this._mergeGroupFields();
  }

  private _initialiseJoinAcceptPacketFromFields(userFields: UserFields): void {
    if (userFields.AppNonce && userFields.AppNonce.length == 3) {
      this.AppNonce = Buffer.from(userFields.AppNonce);
    } else {
      throw new Error("AppNonce is required in a suitable format");
    }

    if (userFields.NetID && userFields.NetID.length == 3) {
      this.NetID = Buffer.from(userFields.NetID);
    } else {
      throw new Error("NetID is required in a suitable format");
    }

    if (userFields.DevAddr && userFields.DevAddr.length == 4) {
      this.DevAddr = Buffer.from(userFields.DevAddr);
    } else {
      throw new Error("DevAddr is required in a suitable format");
    }

    if (userFields.DLSettings) {
      if (userFields.DLSettings instanceof Buffer && userFields.DLSettings.length == 1) {
        this.DLSettings = Buffer.from(userFields.DLSettings);
      } else if (typeof userFields.DLSettings === "number") {
        this.DLSettings = Buffer.alloc(1);
        this.DLSettings.writeUInt8(userFields.DLSettings, 0);
      } else {
        throw new Error("DLSettings is required in a suitable format");
      }
    }

    if (userFields.RxDelay) {
      if (userFields.RxDelay instanceof Buffer && userFields.RxDelay.length == 1) {
        this.RxDelay = Buffer.from(userFields.RxDelay);
      } else if (typeof userFields.RxDelay == "number") {
        this.RxDelay = Buffer.alloc(1);
        this.RxDelay.writeUInt8(userFields.RxDelay, 0);
      } else {
        throw new Error("RxDelay is required in a suitable format");
      }
    }

    if (userFields.CFList) {
      if (userFields.CFList instanceof Buffer && (userFields.CFList.length == 0 || userFields.CFList.length == 16)) {
        this.CFList = Buffer.from(userFields.CFList);
      } else {
        throw new Error("CFList is required in a suitable format");
      }
    }

    if (!userFields.JoinReqType) {
      this.JoinReqType = Buffer.from("ff", "hex");
    } else {
      if (userFields.JoinReqType instanceof Buffer && userFields.JoinReqType.length == 1) {
        this.JoinReqType = Buffer.from(userFields.JoinReqType);
      } else if (typeof userFields.JoinReqType === "number") {
        this.JoinReqType = Buffer.alloc(1);
        this.JoinReqType.writeUInt8(userFields.JoinReqType, 0);
      } else {
        throw new Error("JoinReqType is required in a suitable format");
      }
    }

    if (userFields.AppEUI && userFields.AppEUI.length == 8) {
      this.AppEUI = Buffer.from(userFields.AppEUI);
    } else if (this.getDLSettingsOptNeg()) {
      throw new Error("AppEUI/JoinEUI is required in a suitable format");
    }

    if (userFields.DevNonce && userFields.DevNonce.length == 2) {
      this.DevNonce = Buffer.from(userFields.DevNonce);
    } else if (this.getDLSettingsOptNeg()) {
      throw new Error("DevNonce is required in a suitable format");
    }

    if (!this.DLSettings) {
      this.DLSettings = Buffer.from("00", "hex");
    }
    if (!this.RxDelay) {
      this.RxDelay = Buffer.from("00", "hex");
    }
    if (!this.CFList) {
      this.CFList = Buffer.from("", "hex");
    }
    this.MHDR = Buffer.alloc(1);
    this.MHDR.writeUInt8(MType.JOIN_ACCEPT << 5, 0);

    if (!this.MIC) {
      this.MIC = Buffer.from("EEEEEEEE", "hex");
    }

    this._mergeGroupFields();
  }

  private _getMType(): number {
    if (this.MHDR) return (this.MHDR.readUInt8(0) & 0xff) >> 5;
    return -1;
  }

  public isDataMessage(): boolean {
    const mtype = this._getMType();
    return mtype >= MType.UNCONFIRMED_DATA_UP && mtype <= MType.CONFIRMED_DATA_DOWN;
  }

  public isConfirmed(): boolean {
    const mtype = this._getMType();
    return mtype === MType.CONFIRMED_DATA_DOWN || mtype === MType.CONFIRMED_DATA_UP;
  }

  /**
   * Provide MType as a string
   */
  public getMType(): string {
    return MTYPE_DESCRIPTIONS[this._getMType()] || "Proprietary";
  }

  /**
   * Provide Direction as a string
   */
  public getDir(): string | null {
    const mType = this._getMType();
    if (mType > 5) return null;
    if (mType % 2 == 0) return "up";
    return "down";
  }

  /**
   * Provide FPort as a number
   */
  public getFPort(): number | null {
    if (this.FPort && this.FPort.length) return this.FPort.readUInt8(0);
    return null;
  }

  /**
   * Provide FCnt as a number
   */
  public getFCnt(): number | null {
    if (this.FCnt) return this.FCnt.readUInt16BE(0);
    return null;
  }

  /**
   * Provide FCtrl.ACK as a flag
   */
  public getFCtrlACK(): boolean | null {
    if (!this.FCtrl) return null;
    return !!(this.FCtrl.readUInt8(0) & Masks.FCTRL_ACK);
  }

  /**
   * Provide FCtrl.ADR as a flag
   */
  public getFCtrlADR(): boolean | null {
    if (!this.FCtrl) return null;
    return !!(this.FCtrl.readUInt8(0) & Masks.FCTRL_ADR);
  }

  /**
   * Provide FCtrl.ADRACKReq as a flag
   */
  public getFCtrlADRACKReq(): boolean | null {
    if (!this.FCtrl) return null;
    return !!(this.FCtrl.readUInt8(0) & Masks.FCTRL_ADRACKREQ);
  }

  /**
   * Provide FCtrl.FPending as a flag
   */
  public getFCtrlFPending(): boolean | null {
    if (!this.FCtrl) return null;
    return !!(this.FCtrl.readUInt8(0) & Masks.FCTRL_FPENDING);
  }

  /**
   * Provide DLSettings.RX1DRoffset as integer
   */
  public getDLSettingsRxOneDRoffset(): number | null {
    if (!this.DLSettings) return null;
    return (this.DLSettings.readUInt8(0) & Masks.DLSETTINGS_RXONEDROFFSET_MASK) >> Masks.DLSETTINGS_RXONEDROFFSET_POS;
  }

  /**
   * Provide DLSettings.RX2DataRate as integer
   */
  public getDLSettingsRxTwoDataRate(): number | null {
    if (!this.DLSettings) return null;
    return (this.DLSettings.readUInt8(0) & Masks.DLSETTINGS_RXTWODATARATE_MASK) >> Masks.DLSETTINGS_RXTWODATARATE_POS;
  }

  /**
   * Provide DLSettings.OptNeg as boolean
   */
  public getDLSettingsOptNeg(): boolean | null {
    if (!this.DLSettings) return null;
    return (this.DLSettings.readUInt8(0) & Masks.DLSETTINGS_OPTNEG_MASK) >> Masks.DLSETTINGS_OPTNEG_POS === 1;
  }

  /**
   * Provide RxDelay.Del as integer
   */
  public getRxDelayDel(): number | null {
    if (!this.RxDelay) return null;
    return (this.RxDelay.readUInt8(0) & Masks.RXDELAY_DEL_MASK) >> Masks.RXDELAY_DEL_POS;
  }

  /**
   * Provide CFList.FreqChFour as buffer
   */
  public getCFListFreqChFour(): Buffer {
    if (this.CFList && this.CFList.length === 16) {
      return reverseBuffer(this.CFList.slice(0, 0 + 3));
    } else {
      return Buffer.alloc(0);
    }
  }

  /**
   * Provide CFList.FreqChFive as buffer
   */
  public getCFListFreqChFive(): Buffer {
    if (this.CFList && this.CFList.length === 16) {
      return reverseBuffer(this.CFList.slice(3, 3 + 3));
    } else {
      return Buffer.alloc(0);
    }
  }

  /**
   * Provide CFList.FreqChSix as buffer
   */
  public getCFListFreqChSix(): Buffer {
    if (this.CFList && this.CFList.length === 16) {
      return reverseBuffer(this.CFList.slice(6, 6 + 3));
    } else {
      return Buffer.alloc(0);
    }
  }

  /**
   * Provide CFList.FreqChSeven as buffer
   */
  public getCFListFreqChSeven(): Buffer {
    if (this.CFList && this.CFList.length === 16) {
      return reverseBuffer(this.CFList.slice(9, 9 + 3));
    } else {
      return Buffer.alloc(0);
    }
  }

  /**
   * Provide CFList.FreqChEight as buffer
   */
  public getCFListFreqChEight(): Buffer {
    if (this.CFList && this.CFList.length === 16) {
      return reverseBuffer(this.CFList.slice(12, 12 + 3));
    } else {
      return Buffer.alloc(0);
    }
  }

  public getBuffers() {
    return this;
  }

  public decryptFOpts(
    NwkSEncKey: Buffer,
    NwkSKey?: Buffer,
    FCntMSBytes?: Buffer,
    ConfFCntDownTxDrTxCh?: Buffer
  ): Buffer {
    return this.encryptFOpts(NwkSEncKey, NwkSKey, FCntMSBytes, ConfFCntDownTxDrTxCh);
  }

  public encryptFOpts(
    NwkSEncKey: Buffer,
    SNwkSIntKey?: Buffer,
    FCntMSBytes?: Buffer,
    ConfFCntDownTxDrTxCh?: Buffer
  ): Buffer {
    if (!this.FOpts) return Buffer.alloc(0);
    if (!NwkSEncKey || NwkSEncKey?.length !== 16) throw new Error("NwkSEncKey must be 16 bytes");
    this.FOpts = decryptFOpts(this, NwkSEncKey, FCntMSBytes);
    this._mergeGroupFields();
    if (SNwkSIntKey?.length === 16) {
      recalculateMIC(this, SNwkSIntKey, undefined, FCntMSBytes, ConfFCntDownTxDrTxCh);
      this._mergeGroupFields();
    }
    return this.FOpts;
  }

  public getPHYPayload(): Buffer | void {
    return this.PHYPayload;
  }

  public isJoinRequestMessage() {
    return this._getMType() == MType.JOIN_REQUEST;
  }

  public isRejoinRequestMessage() {
    return this._getMType() == MType.REJOIN_REQUEST;
  }

  // deprecated (bogus capitalisation)
  public isReJoinRequestMessage() {
    return this._getMType() == MType.REJOIN_REQUEST;
  }
  public isJoinAcceptMessage() {
    return this._getMType() == MType.JOIN_ACCEPT;
  }

  public toString(): string {
    let msg = "";

    if (this.isJoinRequestMessage()) {
      msg += "          Message Type = Join Request" + "\n";
      msg += "            PHYPayload = " + asHexString(this.PHYPayload).toUpperCase() + "\n";
      msg += "\n";
      msg += "          ( PHYPayload = MHDR[1] | MACPayload[..] | MIC[4] )\n";
      msg += "                  MHDR = " + asHexString(this.MHDR) + "\n";
      msg += "            MACPayload = " + asHexString(this.MACPayload) + "\n";
      msg += "                   MIC = " + asHexString(this.MIC) + "\n";
      msg += "\n";
      msg += "          ( MACPayload = AppEUI[8] | DevEUI[8] | DevNonce[2] )\n";
      msg += "                AppEUI = " + asHexString(this.AppEUI) + "\n";
      msg += "                DevEUI = " + asHexString(this.DevEUI) + "\n";
      msg += "              DevNonce = " + asHexString(this.DevNonce) + "\n";
    } else if (this.isJoinAcceptMessage()) {
      msg += "          Message Type = Join Accept" + "\n";
      msg += "            PHYPayload = " + asHexString(this.PHYPayload).toUpperCase() + "\n";
      msg += "\n";
      msg += "          ( PHYPayload = MHDR[1] | MACPayload[..] | MIC[4] )\n";
      msg += "                  MHDR = " + asHexString(this.MHDR) + "\n";
      msg += "            MACPayload = " + asHexString(this.MACPayload) + "\n";
      msg += "                   MIC = " + asHexString(this.MIC) + "\n";
      msg += "\n";
      msg +=
        "          ( MACPayload = AppNonce[3] | NetID[3] | DevAddr[4] | DLSettings[1] | RxDelay[1] | CFList[0|15] )\n";
      msg += "              AppNonce = " + asHexString(this.AppNonce) + "\n";
      msg += "                 NetID = " + asHexString(this.NetID) + "\n";
      msg += "               DevAddr = " + asHexString(this.DevAddr) + "\n";
      msg += "            DLSettings = " + asHexString(this.DLSettings) + "\n";
      msg += "               RxDelay = " + asHexString(this.RxDelay) + "\n";
      msg += "                CFList = " + asHexString(this.CFList) + "\n";
      msg += "\n";
      msg += "DLSettings.RX1DRoffset = " + this.getDLSettingsRxOneDRoffset() + "\n";
      msg += "DLSettings.RX2DataRate = " + this.getDLSettingsRxTwoDataRate() + "\n";
      msg += "           RxDelay.Del = " + this.getRxDelayDel() + "\n";
      msg += "\n";
      if (this.CFList.length === 16) {
        msg += "              ( CFList = FreqCh4[3] | FreqCh5[3] | FreqCh6[3] | FreqCh7[3] | FreqCh8[3] )\n";
        msg += "               FreqCh4 = " + asHexString(this.getCFListFreqChFour()) + "\n";
        msg += "               FreqCh5 = " + asHexString(this.getCFListFreqChFive()) + "\n";
        msg += "               FreqCh6 = " + asHexString(this.getCFListFreqChSix()) + "\n";
        msg += "               FreqCh7 = " + asHexString(this.getCFListFreqChSeven()) + "\n";
        msg += "               FreqCh8 = " + asHexString(this.getCFListFreqChEight()) + "\n";
      }
    } else if (this.isRejoinRequestMessage()) {
      msg += "          Message Type = ReJoin Request" + "\n";
      msg += "            PHYPayload = " + asHexString(this.PHYPayload).toUpperCase() + "\n";
      msg += "\n";
      msg += "          ( PHYPayload = MHDR[1] | MACPayload[..] | MIC[4] )\n";
      msg += "                  MHDR = " + asHexString(this.MHDR) + "\n";
      msg += "            MACPayload = " + asHexString(this.MACPayload) + "\n";
      msg += "                   MIC = " + asHexString(this.MIC) + "\n";
      msg += "\n";

      if (this.RejoinType[0] === 0 || this.RejoinType[0] === 2) {
        msg += "          ( MACPayload = RejoinType[1] | NetID[3] | DevEUI[8] | RJCount0[2] )\n";
        msg += "            RejoinType = " + asHexString(this.RejoinType) + "\n";
        msg += "                 NetID = " + asHexString(this.NetID) + "\n";
        msg += "                DevEUI = " + asHexString(this.DevEUI) + "\n";
        msg += "              RJCount0 = " + asHexString(this.RJCount0) + "\n";
      } else if (this.RejoinType[0] === 1) {
        msg += "          ( MACPayload = RejoinType[1] | JoinEUI[8] | DevEUI[8] | RJCount0[2] )\n";
        msg += "            RejoinType = " + asHexString(this.RejoinType) + "\n";
        msg += "               JoinEUI = " + asHexString(this.JoinEUI) + "\n";
        msg += "                DevEUI = " + asHexString(this.DevEUI) + "\n";
        msg += "              RJCount0 = " + asHexString(this.RJCount0) + "\n";
      }
    } else if (this.isDataMessage()) {
      msg += "Message Type = Data" + "\n";
      msg += "            PHYPayload = " + asHexString(this.PHYPayload).toUpperCase() + "\n";
      msg += "\n";
      msg += "          ( PHYPayload = MHDR[1] | MACPayload[..] | MIC[4] )\n";
      msg += "                  MHDR = " + asHexString(this.MHDR) + "\n";
      msg += "            MACPayload = " + asHexString(this.MACPayload) + "\n";
      msg += "                   MIC = " + asHexString(this.MIC) + "\n";
      msg += "\n";
      msg += "          ( MACPayload = FHDR | FPort | FRMPayload )\n";
      msg += "                  FHDR = " + asHexString(this.FHDR) + "\n";
      msg += "                 FPort = " + asHexString(this.FPort) + "\n";
      msg += "            FRMPayload = " + asHexString(this.FRMPayload) + "\n";
      msg += "\n";
      msg += "                ( FHDR = DevAddr[4] | FCtrl[1] | FCnt[2] | FOpts[0..15] )\n";
      msg += "               DevAddr = " + asHexString(this.DevAddr) + " (Big Endian)\n";
      msg += "                 FCtrl = " + asHexString(this.FCtrl) + "\n"; //TODO as binary?
      msg += "                  FCnt = " + asHexString(this.FCnt) + " (Big Endian)\n";
      msg += "                 FOpts = " + asHexString(this.FOpts) + "\n";
      msg += "\n";
      msg += "          Message Type = " + this.getMType() + "\n";
      msg += "             Direction = " + this.getDir() + "\n";
      msg += "                  FCnt = " + this.getFCnt() + "\n";
      msg += "             FCtrl.ACK = " + this.getFCtrlACK() + "\n";
      msg += "             FCtrl.ADR = " + this.getFCtrlADR() + "\n";
      if (this._getMType() == MType.CONFIRMED_DATA_DOWN || this._getMType() == MType.UNCONFIRMED_DATA_DOWN) {
        msg += "        FCtrl.FPending = " + this.getFCtrlFPending() + "\n";
      } else {
        msg += "       FCtrl.ADRACKReq = " + this.getFCtrlADRACKReq() + "\n";
      }
    }
    return msg;
  }

  get JoinEUI(): Buffer {
    return this.AppEUI;
  }

  set JoinEUI(v: Buffer) {
    this.AppEUI = v;
  }

  get JoinNonce(): Buffer {
    return this.AppNonce;
  }

  set JoinNonce(v: Buffer) {
    this.AppNonce = v;
  }

  PHYPayload?: Buffer;
  MHDR?: Buffer;
  MACPayload?: Buffer;
  MACPayloadWithMIC?: Buffer;
  AppEUI?: Buffer;
  DevEUI?: Buffer;
  DevNonce?: Buffer;
  MIC?: Buffer;
  AppNonce?: Buffer;
  NetID?: Buffer;
  DevAddr?: Buffer;
  DLSettings?: Buffer;
  RxDelay?: Buffer;
  CFList?: Buffer;
  FCtrl?: Buffer;
  FOpts?: Buffer;
  FCnt?: Buffer;
  FHDR?: Buffer;
  FPort?: Buffer;
  FRMPayload?: Buffer;
  JoinReqType?: Buffer;
  RejoinType?: Buffer;
  RJCount0?: Buffer;
  RJCount1?: Buffer;
}

export default LoraPacket;
export { LorawanVersion };
