import type { Dissected, Field, Segment } from "@hamradio/packet";
import { FieldType } from "@hamradio/packet";

import { DataType, DataTypeNames, type IAddress, type IFrame, type Payload } from "./frame.types";
import decodeCapabilitiesPayload from "./payload.capabilities";
import decodeItemPayload from "./payload.item";
import decodeMessagePayload from "./payload.message";
import decodeMicEPayload from "./payload.mice";
import decodeObjectPayload from "./payload.object";
import decodePositionPayload from "./payload.position";
import decodeQueryPayload from "./payload.query";
import decodeRawGPSPayload from "./payload.rawgps";
import decodeStatusPayload from "./payload.status";
import decodeTelemetryPayload from "./payload.telemetry";
import { decodeThirdPartyPayload, decodeUserDefinedPayload } from "./payload.thirdparty";
import decodeWeatherPayload from "./payload.weather";

export class Address implements IAddress {
  call: string;
  ssid: string = "";
  isRepeated: boolean = false;

  constructor(call: string, ssid: string | number = "", isRepeated: boolean = false) {
    this.call = call;
    if (typeof ssid === "number") {
      this.ssid = ssid.toString();
    } else if (typeof ssid === "string") {
      this.ssid = ssid;
    } else {
      throw new Error("SSID must be a string or number");
    }
    if (typeof isRepeated !== "boolean") {
      throw new Error("isRepeated must be a boolean");
    }
    this.isRepeated = isRepeated || false;
  }

  public toString(): string {
    return `${this.call}${this.ssid ? "-" + this.ssid : ""}${this.isRepeated ? "*" : ""}`;
  }

  public static fromString(addr: string): Address {
    const isRepeated = addr.endsWith("*");

    const baseAddr = isRepeated ? addr.slice(0, -1) : addr;
    const parts = baseAddr.split("-");
    const call = parts[0];
    const ssid = parts.length > 1 ? parts[1] : "";

    return new Address(call, ssid, isRepeated);
  }

  public static parse(addr: string): Address {
    return Address.fromString(addr);
  }
}

export class Frame implements IFrame {
  source: Address;
  destination: Address;
  path: Address[];
  payload: string;
  private _routingSection?: Segment;

  constructor(source: Address, destination: Address, path: Address[], payload: string, routingSection?: Segment) {
    this.source = source;
    this.destination = destination;
    this.path = path;
    this.payload = payload;
    this._routingSection = routingSection;
  }

  /**
   * Get the data type identifier (first character of payload)
   */
  getDataTypeIdentifier(): string {
    return this.payload.charAt(0);
  }

  /**
   * Get or build routing section from cached data
   */
  private getRoutingSection(): Segment | undefined {
    return this._routingSection;
  }

  /**
   * Decode the APRS payload based on its data type identifier
   * Returns the decoded payload with optional structure for packet dissection
   */
  decode(withStructure?: boolean): Payload | null | { payload: Payload | null; structure: Dissected } {
    if (!this.payload) {
      if (withStructure) {
        const structure: Dissected = [];
        const routingSection = this.getRoutingSection();
        if (routingSection) {
          structure.push(routingSection);

          // Add data type identifier section
          const fieldName: string = DataTypeNames[this.getDataTypeIdentifier() as DataType] || "unknown";
          structure.push({
            name: "Data Type Identifier",
            data: new TextEncoder().encode(this.payload.charAt(0)).buffer,
            isString: true,
            fields: [{ type: FieldType.CHAR, name: "identifier", length: 1, value: fieldName }]
          });
        }
        return { payload: null, structure };
      }
      return null;
    }

    const dataType = this.getDataTypeIdentifier();
    // eslint-disable-next-line no-useless-assignment
    let decodedPayload: Payload | null = null;
    let payloadsegment: Segment[] | undefined = undefined;

    // TODO: Implement full decoding logic for each payload type
    switch (dataType) {
      case "!": // Position without timestamp, no messaging
      case "=": // Position without timestamp, with messaging
      case "/": // Position with timestamp, no messaging
      case "@": // Position with timestamp, with messaging
        ({ payload: decodedPayload, segment: payloadsegment } = decodePositionPayload(
          dataType,
          this.payload,
          withStructure
        ));
        break;

      case "`": // Mic-E
      case "'": // Mic-E old
        ({ payload: decodedPayload, segment: payloadsegment } = decodeMicEPayload(
          this.destination,
          this.payload,
          withStructure
        ));
        break;

      case ":": // Message
        ({ payload: decodedPayload, segment: payloadsegment } = decodeMessagePayload(this.payload, withStructure));
        break;

      case ";": // Object
        ({ payload: decodedPayload, segment: payloadsegment } = decodeObjectPayload(this.payload, withStructure));
        break;

      case ")": // Item
        ({ payload: decodedPayload, segment: payloadsegment } = decodeItemPayload(this.payload, withStructure));
        break;

      case ">": // Status
        ({ payload: decodedPayload, segment: payloadsegment } = decodeStatusPayload(this.payload, withStructure));
        break;

      case "?": // Query
        ({ payload: decodedPayload, segment: payloadsegment } = decodeQueryPayload(this.payload, withStructure));
        break;

      case "T": // Telemetry
        ({ payload: decodedPayload, segment: payloadsegment } = decodeTelemetryPayload(this.payload, withStructure));
        break;

      case "_": // Weather without position
        ({ payload: decodedPayload, segment: payloadsegment } = decodeWeatherPayload(this.payload, withStructure));
        break;

      case "$": // Raw GPS
        ({ payload: decodedPayload, segment: payloadsegment } = decodeRawGPSPayload(this.payload, withStructure));
        break;

      case "<": // Station capabilities
        ({ payload: decodedPayload, segment: payloadsegment } = decodeCapabilitiesPayload(this.payload, withStructure));
        break;

      case "{": // User-defined
        ({ payload: decodedPayload, segment: payloadsegment } = decodeUserDefinedPayload(this.payload, withStructure));
        break;

      case "}": // Third-party
        ({ payload: decodedPayload, segment: payloadsegment } = decodeThirdPartyPayload(this.payload, withStructure));
        break;

      default:
        decodedPayload = null;
    }

    if (withStructure) {
      const structure: Dissected = [];
      const routingSection = this.getRoutingSection();
      if (routingSection) {
        structure.push(routingSection);
      }
      // Add data type identifier section
      const fieldName: string = DataTypeNames[dataType as DataType] || "unknown";
      structure.push({
        name: "data type",
        data: new TextEncoder().encode(this.payload.charAt(0)).buffer,
        isString: true,
        fields: [{ type: FieldType.CHAR, name: "identifier", length: 1, value: fieldName }]
      });
      if (payloadsegment) {
        structure.push(...payloadsegment);
      }
      return { payload: decodedPayload, structure };
    }

    return decodedPayload;
  }

  public static fromString(data: string): Frame {
    return parseFrame(data);
  }

  public static parse(data: string): Frame {
    return parseFrame(data);
  }
}

const parseFrame = (data: string): Frame => {
  const encoder = new TextEncoder();

  const routeSepIndex = data.indexOf(":");
  if (routeSepIndex === -1) {
    throw new Error("APRS: invalid frame, no route separator found");
  }

  const route = data.slice(0, routeSepIndex);
  const payload = data.slice(routeSepIndex + 1);
  const parts = route.split(">");
  if (parts.length < 2) {
    throw new Error("APRS: invalid addresses in route");
  }

  // Parse source - track byte offset as we parse
  const sourceStr = parts[0];
  const source = Address.fromString(sourceStr);

  // Parse destination and path
  const destinationAndPath = parts[1].split(",");
  const destinationStr = destinationAndPath[0];
  const destination = Address.fromString(destinationStr);

  // Parse path
  const path: Address[] = [];
  const pathFields: Field[] = [];
  for (let i = 1; i < destinationAndPath.length; i++) {
    const pathStr = destinationAndPath[i];
    path.push(Address.fromString(pathStr));

    pathFields.push({
      type: FieldType.CHAR,
      name: `path separator ${i}`,
      length: 1
    });
    pathFields.push({
      type: FieldType.STRING,
      name: `repeater ${i}`,
      length: pathStr.length
    });
  }

  const routingSection: Segment = {
    name: "routing",
    data: encoder.encode(data.slice(0, routeSepIndex + 1)).buffer,
    isString: true,
    fields: [
      {
        type: FieldType.STRING,
        name: "source address",
        length: sourceStr.length
      },
      { type: FieldType.CHAR, name: "route separator", length: 1 },
      {
        type: FieldType.STRING,
        name: "destination address",
        length: destinationStr.length
      },
      ...pathFields,
      { type: FieldType.CHAR, name: "payload separator", length: 1 }
    ]
  };

  return new Frame(source, destination, path, payload, routingSection);
};
