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

import type { Extras, ITelemetry, Payload } from "./frame.types";
import { base91ToNumber, feetToMeters, knotsToKmh, milesToMeters } from "./parser";

/**
 * Decodes structured extras from an APRS comment string, extracting known tokens
 * for altitude, range, PHG, DFS, course/speed, and embedded telemetry, and
 * returns an object with the extracted values and a cleaned comment string with
 * the tokens removed.
 *
 * If withStructure is true, also returns an array of fields representing the
 * structure of the extras for use in structured packet parsing.
 *
 * @param comment The APRS comment string to decode.
 * @param withStructure Whether to include structured fields in the result.
 * @returns An object containing the decoded extras and the cleaned comment string.
 */
export const decodeCommentExtras = (comment: string, withStructure: boolean = false): Extras => {
  if (!comment || comment.length === 0) return { comment };

  const extras: Partial<Extras> = {};
  const fields: Field[] = [];
  const beforeFields: Field[] = [];
  let altitudeOffset: number | undefined = undefined;
  let altitudeFields: Field[] = [];
  let commentOffset: number = 0;
  let commentBefore: string | undefined = undefined;

  // eslint-disable-next-line no-useless-assignment
  let match: RegExpMatchArray | null = null;

  // Process successive 7-byte data extensions at the start of the comment.
  comment = comment.trimStart();
  let ext = comment;
  while (ext.length >= 7) {
    // We first process the altitude marker, because it may appear anywhere
    // in the comment and we want to extract it and its value before
    // processing other tokens that may be present.
    //
    // /A=NNNNNN -> altitude in feet (6 digits)
    // /A=-NNNNN -> altitude in feet with leading minus for negative altitudes (5 digits)
    const altMatch = ext.match(/\/A=(-\d{5}|\d{6})/);
    if (altitudeOffset === undefined && altMatch) {
      const altitude = feetToMeters(parseInt(altMatch[1], 10)); // feet to meters
      if (isNaN(altitude)) {
        break; // Invalid altitude format, stop parsing extras
      }
      extras.altitude = altitude;

      // Keep track of where the altitude token appears in the comment for structure purposes.
      altitudeOffset = comment.indexOf(altMatch[0]);

      if (withStructure) {
        altitudeFields = [
          {
            type: FieldType.STRING,
            name: "altitude marker",
            data: new TextEncoder().encode("/A=").buffer,
            value: "/A=",
            length: 3
          },
          {
            type: FieldType.STRING,
            name: "altitude",
            data: new TextEncoder().encode(altMatch[1]).buffer,
            value: altitude.toFixed(1) + "m",
            length: 6
          }
        ];
      }

      if (altitudeOffset > 0) {
        // Reset the comment with the altitude marker removed.
        commentBefore = comment.substring(0, altitudeOffset);
        comment = comment.substring(altitudeOffset + altMatch[0].length);
        ext = commentBefore; // Continue processing extensions in the part of the comment before the altitude marker
        commentOffset = 0; // Reset
        continue;
      }

      // remove altitude token from ext and advance ext for further parsing
      commentOffset += altMatch[0].length;
      ext = ext.replace(altMatch[0], "").trimStart();

      continue;
    }

    // RNGrrrr -> pre-calculated range in miles (4 digits)
    if ((match = ext.match(/^RNG(\d{4})/))) {
      const r = match[1];
      extras.range = milesToMeters(parseInt(r, 10)) / 1000.0; // Convert to kilometers
      if (withStructure) {
        (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
          {
            type: FieldType.STRING,
            name: "range marker",
            value: "RNG",
            length: 3
          },
          {
            type: FieldType.STRING,
            name: "range (rrrr)",
            length: 4,
            value: extras.range.toFixed(1) + "km"
          }
        );
      }

      // remove range token from ext and advance ext for further parsing
      if (commentBefore !== undefined && commentBefore.length > 0) {
        commentBefore = commentBefore.substring(7);
        ext = commentBefore;
      } else {
        commentOffset += 7;
        ext = ext.substring(7);
      }

      continue;
    }

    // PHGphgd
    //if (!extras.phg && ext.startsWith("PHG")) {
    if (!extras.phg && (match = ext.match(/^PHG([0-9 ])([0-9 ])([0-9 ])([0-9 ])/))) {
      // PHGphgd: p = power (0-9 or space), h = height (0-9 or space), g = gain (0-9 or space), d = directivity (0-9 or space)
      const p = match[1];
      const h = match[2];
      const g = match[3];
      const d = match[4];
      const pNum = parseInt(p, 10);
      const powerWatts = Number.isNaN(pNum) ? undefined : pNum * pNum;
      const hIndex = h.charCodeAt(0) - 48;
      const heightFeet = 10 * Math.pow(2, hIndex);
      const heightMeters = feetToMeters(heightFeet);
      const gNum = parseInt(g, 10);
      const gainDbi = Number.isNaN(gNum) ? undefined : gNum;
      const dNum = parseInt(d, 10);
      let directivity: number | "omni" | "unknown" | undefined;
      if (Number.isNaN(dNum)) {
        directivity = undefined;
      } else if (dNum === 0) {
        directivity = "omni";
      } else if (dNum >= 1 && dNum <= 8) {
        directivity = dNum * 45;
      } else if (dNum === 9) {
        directivity = "unknown";
      }

      extras.phg = {
        power: powerWatts,
        height: heightMeters,
        gain: gainDbi,
        directivity
      };

      if (withStructure) {
        (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
          { type: FieldType.STRING, name: "PHG marker", length: 3, value: "PHG" },
          {
            type: FieldType.STRING,
            name: "power (p)",
            length: 1,
            value: powerWatts !== undefined ? powerWatts.toString() + "W" : undefined
          },
          {
            type: FieldType.STRING,
            name: "height (h)",
            length: 1,
            value: heightMeters !== undefined ? heightMeters.toString() + "m" : undefined
          },
          {
            type: FieldType.STRING,
            name: "gain (g)",
            length: 1,
            value: gainDbi !== undefined ? gainDbi.toString() + "dBi" : undefined
          },
          {
            type: FieldType.STRING,
            name: "directivity (d)",
            length: 1,
            value:
              directivity !== undefined
                ? typeof directivity === "number"
                  ? directivity.toString() + "°"
                  : directivity
                : undefined
          }
        );
      }

      // remove PHG token from ext and advance ext for further parsing
      if (commentBefore !== undefined && commentBefore.length > 0) {
        commentBefore = commentBefore.substring(7);
      } else {
        commentOffset += 7;
      }
      ext = ext.substring(7).trimStart();

      continue;
    }

    // DFSshgd
    if (ext.startsWith("DFS")) {
      // DFSshgd: s = strength (0-9), h = height (0-9), g = gain (0-9), d = directivity (0-9)
      const s = ext.charAt(3);
      const h = ext.charAt(4);
      const g = ext.charAt(5);
      const d = ext.charAt(6);

      const sNum = parseInt(s, 10);
      const hNum = parseInt(h, 10);
      const gNum = parseInt(g, 10);
      const dNum = parseInt(d, 10);

      // Strength: s = 0-9, direct value
      const strength = Number.isNaN(sNum) ? undefined : sNum;

      // Height: h = 0-9, height = 10 * 2^h feet (spec: h is exponent)
      const heightFeet = Number.isNaN(hNum) ? undefined : 10 * Math.pow(2, hNum);
      const heightMeters = heightFeet !== undefined ? feetToMeters(heightFeet) : undefined;

      // Gain: g = 0-9, gain in dB
      const gainDbi = Number.isNaN(gNum) ? undefined : gNum;

      // Directivity: d = 0-9, 0 = omni, 1-8 = d*45°, 9 = unknown
      let directivity: number | "omni" | "unknown" | undefined;
      if (Number.isNaN(dNum)) {
        directivity = undefined;
      } else if (dNum === 0) {
        directivity = "omni";
      } else if (dNum >= 1 && dNum <= 8) {
        directivity = dNum * 45;
      } else if (dNum === 9) {
        directivity = "unknown";
      }

      extras.dfs = {
        strength,
        height: heightMeters,
        gain: gainDbi,
        directivity
      };

      if (withStructure) {
        (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
          { type: FieldType.STRING, name: "DFS marker", length: 3, value: "DFS" },
          {
            type: FieldType.STRING,
            name: "strength (s)",
            length: 1,
            value: strength !== undefined ? strength.toString() : undefined
          },
          {
            type: FieldType.STRING,
            name: "height (h)",
            length: 1,
            value: heightMeters !== undefined ? heightMeters.toString() + "m" : undefined
          },
          {
            type: FieldType.STRING,
            name: "gain (g)",
            length: 1,
            value: gainDbi !== undefined ? gainDbi.toString() + "dBi" : undefined
          },
          {
            type: FieldType.STRING,
            name: "directivity (d)",
            length: 1,
            value:
              directivity !== undefined
                ? typeof directivity === "number"
                  ? directivity.toString() + "°"
                  : directivity
                : undefined
          }
        );
      }

      // remove DFS token from ext and advance ext for further parsing
      if (commentBefore !== undefined && commentBefore.length > 0) {
        commentBefore = commentBefore.substring(7);
      } else {
        commentOffset += 7;
      }
      ext = ext.substring(7).trimStart();

      continue;
    }

    // Course/Speed DDD/SSS (7 bytes: 3 digits / 3 digits)
    if (extras.cse === undefined && /^\d{3}\/\d{3}/.test(ext)) {
      const courseStr = ext.substring(0, 3);
      const speedStr = ext.substring(4, 7);
      extras.cse = parseInt(courseStr, 10);
      extras.spd = knotsToKmh(parseInt(speedStr, 10));

      if (withStructure) {
        (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
          { type: FieldType.STRING, name: "course", length: 3, value: extras.cse.toString() + "°" },
          { type: FieldType.CHAR, name: "marker", length: 1, value: "/" },
          { type: FieldType.STRING, name: "speed", length: 3, value: extras.spd.toString() + " km/h" }
        );
      }

      // remove course/speed token from comment and advance ext for further parsing
      ext = ext.substring(7).trimStart();

      // If there is an 8-byte DF/NRQ following (leading '/'), parse that too
      if (ext.length >= 8 && ext.charAt(0) === "/") {
        const dfExt = ext.substring(0, 8); // e.g. /270/729
        const m = dfExt.match(/\/(\d{3})\/(\d{3})/);
        if (m) {
          const dfBearing = parseInt(m[1], 10);
          const dfStrength = parseInt(m[2], 10);
          if (extras.dfs === undefined) {
            extras.dfs = {};
          }
          extras.dfs.bearing = dfBearing;
          extras.dfs.strength = dfStrength;

          if (withStructure) {
            (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
              { type: FieldType.STRING, name: "DF marker", length: 1, value: "/" },
              { type: FieldType.STRING, name: "bearing", length: 3, value: dfBearing.toString() + "°" },
              { type: FieldType.CHAR, name: "separator", length: 1, value: "/" },
              { type: FieldType.STRING, name: "strength", length: 3, value: dfStrength.toString() }
            );
          }

          // remove DF token from ext and advance ext for further parsing
          if (commentBefore !== undefined && commentBefore.length > 0) {
            commentBefore = commentBefore.substring(8);
          } else {
            commentOffset += 8;
          }
          ext = ext.substring(8).trimStart();

          continue;
        }
      }
      continue;
    }

    // No recognized 7+-byte extension at start
    break;
  }

  // Parse embedded telemetry in comment. Look for |ss11|, |ss1122|, |ss112233|, |ss1122334455|, or |ss1122334455!"| patterns (where ss is sequence and each pair of digits is an analog channel in base91, and optional last pair is digital channel in base91).
  if ((match = comment.match(/\|([^|]+)\|/))) {
    try {
      const telemetry = decodeTelemetry(match[1]);
      extras.telemetry = telemetry;
      comment = comment.replace(match[0], "").trim();
      if (withStructure) {
        fields.push(
          {
            type: FieldType.CHAR,
            name: "telemetry start",
            length: 1,
            value: "|"
          },
          {
            type: FieldType.STRING,
            name: "sequence",
            length: 2,
            value: telemetry.sequence.toString()
          },
          ...telemetry.analog.map((a, i) => ({
            type: FieldType.STRING,
            name: `analog${i + 1}`,
            length: 2,
            value: a.toString()
          })),
          ...(telemetry.digital !== undefined
            ? [
                {
                  type: FieldType.STRING,
                  name: "digital",
                  length: 2,
                  value: telemetry.digital.toString()
                }
              ]
            : []),
          {
            type: FieldType.CHAR,
            name: "telemetry end",
            length: 1,
            value: "|"
          }
        );
      }
    } catch {
      // Invalid telemetry format, ignore
    }
  }

  // Export comment with extras fields removed, if any were parsed.
  if (commentOffset > 0 && commentBefore !== undefined && commentBefore.length > 0) {
    extras.comment = commentBefore.substring(commentOffset) + comment;
  } else if (commentBefore !== undefined && commentBefore.length > 0) {
    extras.comment = commentBefore + comment;
  } else {
    extras.comment = comment.substring(commentOffset);
  }

  if (withStructure) {
    const commentBeforeFields: Field[] = commentBefore
      ? [
          {
            type: FieldType.STRING,
            name: "comment",
            length: commentBefore.length
          }
        ]
      : [];

    const commentFields: Field[] = comment
      ? [
          {
            type: FieldType.STRING,
            name: "comment",
            length: comment.length
          }
        ]
      : [];

    // Insert the altitude fields at the correct position in the comment section based on where the altitude token was located in the original comment. If there was no altitude token, put all fields at the start of the comment section.
    extras.fields = [...beforeFields, ...commentBeforeFields, ...altitudeFields, ...fields, ...commentFields];
  }

  return extras as Extras;
};

export const attachExtras = (payload: Payload, extras: Extras): void => {
  if ("position" in payload && payload.position) {
    if (extras.altitude !== undefined) {
      payload.position.altitude = extras.altitude;
    }
    if (extras.range !== undefined) {
      payload.position.range = extras.range;
    }
    if (extras.phg !== undefined) {
      payload.position.phg = extras.phg;
    }
    if (extras.dfs !== undefined) {
      payload.position.dfs = extras.dfs;
    }
    if (extras.cse !== undefined && payload.position.course === undefined) {
      payload.position.course = extras.cse;
    }
    if (extras.spd !== undefined && payload.position.speed === undefined) {
      payload.position.speed = extras.spd;
    }
  }
  if ("altitude" in payload && payload.altitude === undefined && extras.altitude !== undefined) {
    payload.altitude = extras.altitude;
  }
  if ("range" in payload && payload.range === undefined && extras.range !== undefined) {
    payload.range = extras.range;
  }
  if ("phg" in payload && payload.phg === undefined && extras.phg !== undefined) {
    payload.phg = extras.phg;
  }
  if ("dfs" in payload && payload.dfs === undefined && extras.dfs !== undefined) {
    payload.dfs = extras.dfs;
  }
  if ("course" in payload && payload.course === undefined && extras.cse !== undefined) {
    payload.course = extras.cse;
  }
  if ("speed" in payload && payload.speed === undefined && extras.spd !== undefined) {
    payload.speed = extras.spd;
  }
};

/**
 * Decodes a Base91 Telemetry extension string (delimited by '|') into its components.
 *
 * @param ext The string between the '|' delimiters (e.g. 'ss11', 'ss112233', 'ss1122334455!"')
 * @returns An object with sequence, analog (array), and optional digital (number)
 */
export const decodeTelemetry = (ext: string): ITelemetry => {
  if (!ext || ext.length < 4) throw new Error("Telemetry extension too short");
  // Must be even length, at least 4 (2 for seq, 2 for ch1)
  if (ext.length % 2 !== 0) throw new Error("Telemetry extension must have even length");

  // Sequence counter is always first 2 chars
  const sequence = base91ToNumber(ext.slice(0, 2));
  const analog: number[] = [];
  let i = 2;
  // If there are more than 12 chars, last pair is digital
  let digital: number | undefined = undefined;
  const analogPairs = Math.min(Math.floor((ext.length - 2) / 2), 5);
  for (let j = 0; j < analogPairs; j++, i += 2) {
    analog.push(base91ToNumber(ext.slice(i, i + 2)));
  }
  // If there are 2 chars left after 5 analogs, it's digital
  if (ext.length === 14) {
    digital = base91ToNumber(ext.slice(12, 14));
  }
  return {
    sequence,
    analog,
    digital
  };
};
