import type { IAddress, IFrame, Payload, ITimestamp, PositionPayload, IPosition } from "./frame.types"
import { type PacketStructure, type PacketSegment, type PacketField, FieldType } from "./parser.types"
import { Position } from "./position";
import { base91ToNumber } from "./parser"

export class Timestamp implements ITimestamp {
  day?: number;
  month?: number;
  hours: number;
  minutes: number;
  seconds?: number;
  format: 'DHM' | 'HMS' | 'MDHM';
  zulu?: boolean;

  constructor(
    hours: number,
    minutes: number,
    format: 'DHM' | 'HMS' | 'MDHM',
    options: {
      day?: number;
      month?: number;
      seconds?: number;
      zulu?: boolean;
    } = {}
  ) {
    this.hours = hours;
    this.minutes = minutes;
    this.format = format;
    this.day = options.day;
    this.month = options.month;
    this.seconds = options.seconds;
    this.zulu = options.zulu;
  }

  /**
   * Convert APRS timestamp to JavaScript Date object
   * Note: APRS timestamps don't include year, so we use current year
   * For DHM format, we find the most recent occurrence of that day
   * For HMS format, we use current date
   * For MDHM format, we use the specified month/day in current year
   */
  toDate(): Date {
    const now = new Date();

    if (this.format === 'DHM') {
      // Day-Hour-Minute format (UTC)
      // Find the most recent occurrence of this day
      const currentYear = this.zulu ? now.getUTCFullYear() : now.getFullYear();
      const currentMonth = this.zulu ? now.getUTCMonth() : now.getMonth();

      let date: Date;
      if (this.zulu) {
        date = new Date(Date.UTC(currentYear, currentMonth, this.day!, this.hours, this.minutes, 0, 0));
      } else {
        date = new Date(currentYear, currentMonth, this.day!, this.hours, this.minutes, 0, 0);
      }

      // If the date is in the future, it's from last month
      if (date > now) {
        if (this.zulu) {
          date = new Date(Date.UTC(currentYear, currentMonth - 1, this.day!, this.hours, this.minutes, 0, 0));
        } else {
          date = new Date(currentYear, currentMonth - 1, this.day!, this.hours, this.minutes, 0, 0);
        }
      }

      return date;
    } else if (this.format === 'HMS') {
      // Hour-Minute-Second format (UTC)
      // Use current date
      if (this.zulu) {
        const date = new Date();
        date.setUTCHours(this.hours, this.minutes, this.seconds || 0, 0);

        // If time is in the future, it's from yesterday
        if (date > now) {
          date.setUTCDate(date.getUTCDate() - 1);
        }

        return date;
      } else {
        const date = new Date();
        date.setHours(this.hours, this.minutes, this.seconds || 0, 0);

        if (date > now) {
          date.setDate(date.getDate() - 1);
        }

        return date;
      }
    } else {
      // MDHM format: Month-Day-Hour-Minute (local time)
      const currentYear = now.getFullYear();
      let date = new Date(currentYear, (this.month || 1) - 1, this.day!, this.hours, this.minutes, 0, 0);

      // If date is in the future, it's from last year
      if (date > now) {
        date = new Date(currentYear - 1, (this.month || 1) - 1, this.day!, this.hours, this.minutes, 0, 0);
      }

      return date;
    }
  }
}

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?: PacketSegment;

  constructor(source: Address, destination: Address, path: Address[], payload: string, routingSection?: PacketSegment) {
    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(): PacketSegment | 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: PacketStructure } {
    if (!this.payload) {
      if (withStructure) {
        const structure: PacketStructure = [];
        const routingSection = this.getRoutingSection();
        if (routingSection) {
          structure.push(routingSection);

          // Add data type identifier section
          structure.push({
            name: 'Data Type Identifier',
            data: new TextEncoder().encode(this.payload.charAt(0)),
            fields: [
              { type: FieldType.CHAR, name: 'Identifier', size: 1 },
            ],
          });
        }
        return { payload: null, structure };
      }
      return null;
    }

    const dataType = this.getDataTypeIdentifier();
    let decodedPayload: Payload | null = null;
    let payloadsegment: PacketSegment[] | 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 } = this.decodePosition(dataType, withStructure));
        break;

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

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

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

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

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

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

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

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

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

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

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

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

      default:
        decodedPayload = null;
    }

    if (withStructure) {
      const structure: PacketStructure = [];
      const routingSection = this.getRoutingSection();
      if (routingSection) {
        structure.push(routingSection);
      }
      if (payloadsegment) {
        structure.push(...payloadsegment);
      }
      return { payload: decodedPayload, structure };
    }

    return decodedPayload;
  }

  private decodePosition(dataType: string, withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    try {
      const hasTimestamp = dataType === '/' || dataType === '@';
      const messaging = dataType === '=' || dataType === '@';
      let offset = 1; // Skip data type identifier

      // Build structure as we parse
      const structure: PacketSegment[] = withStructure ? [] : [];

      let timestamp: Timestamp | undefined = undefined;

      // Parse timestamp if present (7 characters: DDHHMMz or HHMMSSh or MMDDHMMM)
      if (hasTimestamp) {
        if (this.payload.length < 8) return { payload: null };
        const timestampOffset = offset;
        const timeStr = this.payload.substring(offset, offset + 7);
        const { timestamp: parsedTimestamp, segment: timestampSegment } = this.parseTimestamp(timeStr, withStructure, timestampOffset);
        timestamp = parsedTimestamp;

        if (timestampSegment) {
          structure.push(timestampSegment);
        }

        offset += 7;
      }

      if (this.payload.length < offset + 19) return { payload: null };

      // Check if compressed format
      const positionOffset = offset;
      const isCompressed = this.isCompressedPosition(this.payload.substring(offset));

      let position: Position;
      let comment = '';

      if (isCompressed) {
        // Compressed format: /YYYYXXXX$csT
        const { position: compressed, segment: compressedSegment } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset);
        if (!compressed) return { payload: null };

        position = new Position({
          latitude: compressed.latitude,
          longitude: compressed.longitude,
          symbol: compressed.symbol,
        });

        if (compressed.altitude !== undefined) {
          position.altitude = compressed.altitude;
        }

        if (compressedSegment) {
          structure.push(compressedSegment);
        }

        offset += 13; // Compressed position is 13 chars
        comment = this.payload.substring(offset);
      } else {
        // Uncompressed format: DDMMmmH/DDDMMmmH$
        const { position: uncompressed, segment: uncompressedSegment } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset);
        if (!uncompressed) return { payload: null };

        position = new Position({
          latitude: uncompressed.latitude,
          longitude: uncompressed.longitude,
          symbol: uncompressed.symbol,
        });

        if (uncompressed.ambiguity !== undefined) {
          position.ambiguity = uncompressed.ambiguity;
        }

        if (uncompressedSegment) {
          structure.push(uncompressedSegment);
        }

        offset += 19; // Uncompressed position is 19 chars
        comment = this.payload.substring(offset);
      }

      // Parse altitude from comment if present (format: /A=NNNNNN)
      const altMatch = comment.match(/\/A=(\d{6})/);
      if (altMatch) {
        position.altitude = parseInt(altMatch[1], 10) * 0.3048; // feet to meters
      }

      if (comment) {
        position.comment = comment;

        // Emit comment section as we parse
        if (withStructure) {
          structure.push({
            name: 'comment',
            data: new TextEncoder().encode(comment),
            fields: [
              { type: FieldType.STRING, name: 'text', size: comment.length },
            ],
          });
        }
      }

      const payload: PositionPayload = {
        type: 'position',
        timestamp,
        position,
        messaging,
      };

      if (withStructure) {
        return { payload, segment: structure };
      }

      return { payload };
    } catch (e) {
      return { payload: null };
    }
  }

  private parseTimestamp(timeStr: string, withStructure: boolean = false, offset: number = 0): { timestamp: Timestamp | undefined; segment?: PacketSegment } {
    if (timeStr.length !== 7) return { timestamp: undefined };

    const timeType = timeStr.charAt(6);

    if (timeType === 'z') {
      // DHM format: Day-Hour-Minute (UTC)
      const timestamp = new Timestamp(
        parseInt(timeStr.substring(2, 4), 10),
        parseInt(timeStr.substring(4, 6), 10),
        'DHM',
        {
          day: parseInt(timeStr.substring(0, 2), 10),
          zulu: true,
        }
      );

      const segment = withStructure ? {
        name: 'timestamp',
        data: new TextEncoder().encode(timeStr),
        fields: [
          { type: FieldType.STRING, name: 'day (DD)', size: 2 },
          { type: FieldType.STRING, name: 'hour (HH)', size: 2 },
          { type: FieldType.STRING, name: 'minute (MM)', size: 2 },
          { type: FieldType.CHAR, name: 'timezone indicator', size: 1 },
        ],
      } : undefined;

      return { timestamp, segment };
    } else if (timeType === 'h') {
      // HMS format: Hour-Minute-Second (UTC)
      const timestamp = new Timestamp(
        parseInt(timeStr.substring(0, 2), 10),
        parseInt(timeStr.substring(2, 4), 10),
        'HMS',
        {
          seconds: parseInt(timeStr.substring(4, 6), 10),
          zulu: true,
        }
      );

      const segment = withStructure ? {
        name: 'timestamp',
        data: new TextEncoder().encode(timeStr),
        fields: [
          { type: FieldType.STRING, name: 'hour (HH)', size: 2 },
          { type: FieldType.STRING, name: 'minute (MM)', size: 2 },
          { type: FieldType.STRING, name: 'second (SS)', size: 2 },
          { type: FieldType.CHAR, name: 'timezone indicator', size: 1 },
        ],
      } : undefined;

      return { timestamp, segment };
    } else if (timeType === '/') {
      // MDHM format: Month-Day-Hour-Minute (local)
      const timestamp = new Timestamp(
        parseInt(timeStr.substring(4, 6), 10),
        parseInt(timeStr.substring(6, 8), 10),
        'MDHM',
        {
          month: parseInt(timeStr.substring(0, 2), 10),
          day: parseInt(timeStr.substring(2, 4), 10),
          zulu: false,
        }
      );

      const segment = withStructure ? {
        name: 'timestamp',
        data: new TextEncoder().encode(timeStr),
        fields: [
          { type: FieldType.STRING, name: 'month (MM)', size: 2 },
          { type: FieldType.STRING, name: 'day (DD)', size: 2 },
          { type: FieldType.STRING, name: 'hour (HH)', size: 2 },
          { type: FieldType.STRING, name: 'minute (MM)', size: 2 },
          { type: FieldType.CHAR, name: 'timezone indicator', size: 1 },
        ],
      } : undefined;

      return { timestamp, segment };
    }

    return { timestamp: undefined };
  }

  private isCompressedPosition(data: string): boolean {
    if (data.length < 13) return false;

    // First prefer uncompressed detection by attempting an uncompressed parse.
    // Uncompressed APRS positions do not have a fixed symbol table separator;
    // position 8 is a symbol table identifier and may vary.
    if (data.length >= 19) {
      const uncompressed = this.parseUncompressedPosition(data, false, 0);
      if (uncompressed.position) {
        return false;
      }
    }

    // For compressed format, check if the position part looks like base-91 encoded data
    // Compressed format: STYYYYXXXXcsT where ST is symbol table/code
    // Base-91 chars are in range 33-124 (! to |)
    const lat1 = data.charCodeAt(1);
    const lat2 = data.charCodeAt(2);
    const lon1 = data.charCodeAt(5);
    const lon2 = data.charCodeAt(6);

    return lat1 >= 33 && lat1 <= 124 &&
           lat2 >= 33 && lat2 <= 124 &&
           lon1 >= 33 && lon1 <= 124 &&
           lon2 >= 33 && lon2 <= 124;
  }

  private parseCompressedPosition(data: string, withStructure: boolean = false, offset: number = 0): { position: { latitude: number; longitude: number; symbol: any; altitude?: number } | null; segment?: PacketSegment } {
    if (data.length < 13) return { position: null };

    const symbolTable = data.charAt(0);
    const symbolCode = data.charAt(9);

    // Extract base-91 encoded position (4 characters each)
    const latStr = data.substring(1, 5);
    const lonStr = data.substring(5, 9);

    try {
      // Decode base-91 encoded latitude and longitude
      const latBase91 = base91ToNumber(latStr);
      const lonBase91 = base91ToNumber(lonStr);

      // Convert to degrees
      const latitude = 90 - (latBase91 / 380926);
      const longitude = -180 + (lonBase91 / 190463);

      const result: any = {
        latitude,
        longitude,
        symbol: {
          table: symbolTable,
          code: symbolCode,
        },
      };

      // Check for compressed altitude (csT format)
      const cs = data.charAt(10);
      const t = data.charCodeAt(11);

      if (cs === ' ' && t >= 33 && t <= 124) {
        // Compressed altitude: altitude = 1.002^(t-33) feet
        const altFeet = Math.pow(1.002, t - 33);
        result.altitude = altFeet * 0.3048; // Convert to meters
      }

      const section: PacketSegment | undefined = withStructure ? {
        name: 'position',
        data: new TextEncoder().encode(data.substring(0, 13)),
        fields: [
          { type: FieldType.CHAR,   size: 1, name: 'symbol table' },
          { type: FieldType.STRING, size: 4, name: 'latitude' },
          { type: FieldType.STRING, size: 4, name: 'longitude' },
          { type: FieldType.CHAR,   size: 1, name: 'symbol code' },
          { type: FieldType.CHAR,   size: 1, name: 'course/speed type' },
          { type: FieldType.CHAR,   size: 1, name: 'course/speed value' },
          { type: FieldType.CHAR,   size: 1, name: 'altitude' },
        ],
      } : undefined;

      return { position: result, segment: section };
    } catch (e) {
      return { position: null };
    }
  }

  private parseUncompressedPosition(data: string, withStructure: boolean = false, offset: number = 0): { position: { latitude: number; longitude: number; symbol: any; ambiguity?: number } | null; segment?: PacketSegment } {
    if (data.length < 19) return { position: null };

    // Format: DDMMmmH/DDDMMmmH$ where H is hemisphere, $ is symbol code
    // Positions: 0-7 (latitude), 8 (symbol table), 9-17 (longitude), 18 (symbol code)
    // Spaces may replace rightmost digits for ambiguity/privacy

    const latStr = data.substring(0, 8); // DDMMmmH (8 chars: 49 03.50 N)
    const symbolTable = data.charAt(8);
    const lonStr = data.substring(9, 18); // DDDMMmmH (9 chars: 072 01.75 W)
    const symbolCode = data.charAt(18);

    // Count and handle ambiguity (spaces in minutes part replace rightmost digits)
    let ambiguity = 0;
    const latSpaceCount = (latStr.match(/ /g) || []).length;
    const lonSpaceCount = (lonStr.match(/ /g) || []).length;

    if (latSpaceCount > 0 || lonSpaceCount > 0) {
      // Use the maximum space count (they should be the same, but be defensive)
      ambiguity = Math.max(latSpaceCount, lonSpaceCount);
    }

    // Replace spaces with zeros for parsing
    const latStrNormalized = latStr.replace(/ /g, '0');
    const lonStrNormalized = lonStr.replace(/ /g, '0');

    // Parse latitude
    const latDeg = parseInt(latStrNormalized.substring(0, 2), 10);
    const latMin = parseFloat(latStrNormalized.substring(2, 7));
    const latHem = latStrNormalized.charAt(7);

    if (isNaN(latDeg) || isNaN(latMin)) return { position: null };
    if (latHem !== 'N' && latHem !== 'S') return { position: null };

    let latitude = latDeg + (latMin / 60);
    if (latHem === 'S') latitude = -latitude;

    // Parse longitude
    const lonDeg = parseInt(lonStrNormalized.substring(0, 3), 10);
    const lonMin = parseFloat(lonStrNormalized.substring(3, 8));
    const lonHem = lonStrNormalized.charAt(8);

    if (isNaN(lonDeg) || isNaN(lonMin)) return { position: null };
    if (lonHem !== 'E' && lonHem !== 'W') return { position: null };

    let longitude = lonDeg + (lonMin / 60);
    if (lonHem === 'W') longitude = -longitude;

    const result: any = {
      latitude,
      longitude,
      symbol: {
        table: symbolTable,
        code: symbolCode,
      },
    };

    if (ambiguity > 0) {
      result.ambiguity = ambiguity;
    }

    const segment: PacketSegment | undefined = withStructure ? {
      name: 'position',
      data: new TextEncoder().encode(data.substring(0, 19)),
      fields: [
        { type: FieldType.STRING, size: 8, name: 'latitude' },
        { type: FieldType.CHAR,   size: 1, name: 'symbol table' },
        { type: FieldType.STRING, size: 9, name: 'longitude' },
        { type: FieldType.CHAR,   size: 1, name: 'symbol code' },
      ],
    } : undefined;

    return { position: result, segment };
  }

  private decodeMicE(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    try {
      // TODO: Add section emission support when withStructure is true
      // For now, Mic-E returns payload without structure

      // Mic-E encodes position in both destination address and information field
      const dest = this.destination.call;

      if (dest.length < 6) return { payload: null };
      if (this.payload.length < 9) return { payload: null }; // Need at least data type + 8 bytes

      // Decode latitude from destination address (6 characters)
      const latResult = this.decodeMicELatitude(dest);
      if (!latResult) return { payload: null };

      const { latitude, messageType, longitudeOffset, isWest, isStandard } = latResult;

      // Parse information field (skip data type identifier at position 0)
      let offset = 1;

      // Longitude: 3 bytes (degrees, minutes, hundredths)
      const lonDegRaw = this.payload.charCodeAt(offset) - 28;
      const lonMinRaw = this.payload.charCodeAt(offset + 1) - 28;
      const lonHunRaw = this.payload.charCodeAt(offset + 2) - 28;
      offset += 3;

      // Apply longitude offset and hemisphere
      let lonDeg = lonDegRaw;
      if (longitudeOffset) {
        lonDeg += 100;
      }
      if (lonDeg >= 180 && lonDeg <= 189) {
        lonDeg -= 80;
      } else if (lonDeg >= 190 && lonDeg <= 199) {
        lonDeg -= 190;
      }

      let longitude = lonDeg + (lonMinRaw / 60.0) + (lonHunRaw / 6000.0);
      if (isWest) {
        longitude = -longitude;
      }

      // Speed and course: 3 bytes
      const sp = this.payload.charCodeAt(offset) - 28;
      const dc = this.payload.charCodeAt(offset + 1) - 28;
      const se = this.payload.charCodeAt(offset + 2) - 28;
      offset += 3;

      let speed = (sp * 10) + Math.floor(dc / 10); // Speed in knots
      let course = ((dc % 10) * 100) + se; // Course in degrees

      if (course >= 400) course -= 400;
      if (speed >= 800) speed -= 800;

      // Convert speed from knots to km/h
      const speedKmh = speed * 1.852;

      // Symbol code and table
      if (this.payload.length < offset + 2) return { payload: null };
      const symbolCode = this.payload.charAt(offset);
      const symbolTable = this.payload.charAt(offset + 1);
      offset += 2;

      // Parse remaining data (altitude, comment, telemetry)
      const remaining = this.payload.substring(offset);
      let altitude: number | undefined = undefined;
      let comment = remaining;

      // Check for altitude in various formats
      // Format 1: }xyz where xyz is altitude in base-91 (obsolete)
      // Format 2: /A=NNNNNN where NNNNNN is altitude in feet
      const altMatch = remaining.match(/\/A=(\d{6})/);
      if (altMatch) {
        altitude = parseInt(altMatch[1], 10) * 0.3048; // feet to meters
      } else if (remaining.startsWith('}')) {
        // Base-91 altitude (3 characters after })
        if (remaining.length >= 4) {
          try {
            const altBase91 = remaining.substring(1, 4);
            const altFeet = base91ToNumber(altBase91) - 10000;
            altitude = altFeet * 0.3048; // feet to meters
          } catch (e) {
            // Ignore altitude parsing errors
          }
        }
      }

      const result: any = {
        type: 'position',
        position: {
          latitude,
          longitude,
          symbol: {
            table: symbolTable,
            code: symbolCode,
          },
        },
        messaging: true, // Mic-E is always messaging-capable
        micE: {
          messageType,
          isStandard,
        },
      };

      if (speed > 0) {
        result.position.speed = speedKmh;
      }

      if (course > 0 && course < 360) {
        result.position.course = course;
      }

      if (altitude !== undefined) {
        result.position.altitude = altitude;
      }

      if (comment) {
        result.position.comment = comment;
      }

      return { payload: result };
    } catch (e) {
      return { payload: null };
    }
  }

  private decodeMicELatitude(dest: string): {
    latitude: number;
    messageType: string;
    longitudeOffset: boolean;
    isWest: boolean;
    isStandard: boolean;
  } | null {
    if (dest.length < 6) return null;

    // Each destination character encodes a latitude digit and message bits
    const digits: number[] = [];
    const messageBits: number[] = [];

    for (let i = 0; i < 6; i++) {
      const code = dest.charCodeAt(i);
      let digit: number;
      let msgBit: number;

      if (code >= 48 && code <= 57) {
        // '0'-'9'
        digit = code - 48;
        msgBit = 0;
      } else if (code >= 65 && code <= 74) {
        // 'A'-'J' (A=0, B=1, ... J=9)
        digit = code - 65;
        msgBit = 1;
      } else if (code === 75) {
        // 'K' means space (used for ambiguity)
        digit = 0;
        msgBit = 1;
      } else if (code === 76) {
        // 'L' means space
        digit = 0;
        msgBit = 0;
      } else if (code >= 80 && code <= 89) {
        // 'P'-'Y' custom message types (P=0, Q=1, R=2, ... Y=9)
        digit = code - 80;
        msgBit = 1;
      } else if (code === 90) {
        // 'Z' means space
        digit = 0;
        msgBit = 1;
      } else {
        return null; // Invalid character
      }

      digits.push(digit);
      messageBits.push(msgBit);
    }

    // Decode latitude: format is DDMM.HH (degrees, minutes, hundredths)
    const latDeg = digits[0] * 10 + digits[1];
    const latMin = digits[2] * 10 + digits[3];
    const latHun = digits[4] * 10 + digits[5];

    let latitude = latDeg + (latMin / 60.0) + (latHun / 6000.0);

    // Message bits determine hemisphere and other flags
    // Bit 3 (messageBits[3]): 0 = North, 1 = South
    // Bit 4 (messageBits[4]): 0 = West, 1 = East
    // Bit 5 (messageBits[5]): 0 = longitude offset +0, 1 = longitude offset +100
    const isNorth = messageBits[3] === 0;
    const isWest = messageBits[4] === 0;
    const longitudeOffset = messageBits[5] === 1;

    if (!isNorth) {
      latitude = -latitude;
    }

    // Decode message type from bits 0, 1, 2
    const msgValue = messageBits[0] * 4 + messageBits[1] * 2 + messageBits[2];
    const messageTypes = [
      'M0: Off Duty',
      'M1: En Route',
      'M2: In Service',
      'M3: Returning',
      'M4: Committed',
      'M5: Special',
      'M6: Priority',
      'M7: Emergency',
    ];
    const messageType = messageTypes[msgValue] || 'Unknown';

    // Standard vs custom message indicator
    const isStandard = messageBits[0] === 1;

    return {
      latitude,
      messageType,
      longitudeOffset,
      isWest,
      isStandard,
    };
  }

  private decodeMessage(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // Message format: :AAAAAAAAA[ ]:message text
    // where AAAAAAAAA is a 9-character recipient field (padded with spaces)
    if (this.payload.length < 2) return { payload: null };

    let offset = 1; // skip ':' data type
    const segments: PacketSegment[] = withStructure ? [] : [];

    // Attempt to read a 9-char recipient field if present
    let recipient = '';
    if (this.payload.length >= offset + 1) {
      // Try to read up to 9 chars for recipient, but stop early if a ':' separator appears
      const look = this.payload.substring(offset, Math.min(offset + 9, this.payload.length));
      const sepIdx = look.indexOf(':');
      let raw = look;
      if (sepIdx !== -1) {
        raw = look.substring(0, sepIdx);
      } else if (look.length < 9 && this.payload.length >= offset + 9) {
        // pad to full 9 chars if possible
        raw = this.payload.substring(offset, offset + 9);
      } else if (look.length === 9) {
        raw = look;
      }

      recipient = raw.trimEnd();
      if (withStructure) {
        segments.push({
          name: 'recipient',
          data: new TextEncoder().encode(raw),
          fields: [
            { type: FieldType.STRING, name: 'to', size: 9 },
          ],
        });
      }

      // Advance offset past the raw we consumed
      offset += raw.length;
      // If there was a ':' immediately after the consumed raw, skip it as separator
      if (this.payload.charAt(offset) === ':') {
        offset += 1;
      } else if (sepIdx !== -1) {
        // Shouldn't normally happen, but ensure we advance past separator
        offset += 1;
      }
    }

    // After recipient there is typically a space and a colon separator before the text
    // Find the first ':' after the recipient (it separates the address field from the text)
    let textStart = this.payload.indexOf(':', offset);
    if (textStart === -1) {
      // No explicit separator; skip any spaces and take remainder as text
      while (this.payload.charAt(offset) === ' ' && offset < this.payload.length) offset += 1;
      textStart = offset - 1;
    }

    let text = '';
    if (textStart >= 0 && textStart + 1 <= this.payload.length) {
      text = this.payload.substring(textStart + 1);
    }

    if (withStructure) {
      // Emit text section
      segments.push({
        name: 'text',
        data: new TextEncoder().encode(text),
        fields: [
          { type: FieldType.STRING, name: 'text', size: text.length },
        ],
      });

      const payload: any = {
        type: 'message',
        to: recipient || undefined,
        text,
      };

      return { payload, segment: segments };
    }

    const payload: any = {
      type: 'message',
      to: recipient || undefined,
      text,
    };

    return { payload };
  }

  private decodeObject(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    try {
      // Object format: ;AAAAAAAAAcDDHHMMzDDMM.hhN/DDDMM.hhW$comment
      //                ^ data type
      //                 9-char name
      //                          alive (*) / killed (_)
      if (this.payload.length < 18) return { payload: null }; // 1 + 9 + 1 + 7 minimum

      let offset = 1; // Skip data type identifier ';'
      const segment: PacketSegment[] = withStructure ? [] : [];

      const rawName = this.payload.substring(offset, offset + 9);
      const name = rawName.trimEnd();
      if (withStructure) {
        segment.push({
          name: 'object name',
          data: new TextEncoder().encode(rawName),
          fields: [
            { type: FieldType.STRING, name: 'name', size: 9 },
          ],
        });
      }
      offset += 9;

      const stateChar = this.payload.charAt(offset);
      if (stateChar !== '*' && stateChar !== '_') {
        return { payload: null };
      }
      const alive = stateChar === '*';
      if (withStructure) {
        segment.push({
          name: 'object state',
          data: new TextEncoder().encode(stateChar),
          fields: [
            { type: FieldType.CHAR, name: 'State (* alive, _ killed)', size: 1 },
          ],
        });
      }
      offset += 1;

      const timeStr = this.payload.substring(offset, offset + 7);
      const { timestamp, segment: timestampSection } = this.parseTimestamp(timeStr, withStructure, offset);
      if (!timestamp) {
        return { payload: null };
      }
      if (timestampSection) {
        segment.push(timestampSection);
      }
      offset += 7;

      const positionOffset = offset;
      const isCompressed = this.isCompressedPosition(this.payload.substring(offset));

      let position: { latitude: number; longitude: number; symbol: any; ambiguity?: number; altitude?: number; comment?: string } | null = null;
      let consumed = 0;

      if (isCompressed) {
        const { position: compressed, segment: compressedSection } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset);
        if (!compressed) return { payload: null };

        position = {
          latitude: compressed.latitude,
          longitude: compressed.longitude,
          symbol: compressed.symbol,
          altitude: compressed.altitude,
        };
        consumed = 13;

        if (compressedSection) {
          segment.push(compressedSection);
        }
      } else {
        const { position: uncompressed, segment: uncompressedSection } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset);
        if (!uncompressed) return { payload: null };

        position = {
          latitude: uncompressed.latitude,
          longitude: uncompressed.longitude,
          symbol: uncompressed.symbol,
          ambiguity: uncompressed.ambiguity,
        };
        consumed = 19;

        if (uncompressedSection) {
          segment.push(uncompressedSection);
        }
      }

      offset += consumed;
      const comment = this.payload.substring(offset);
      if (comment) {
        position.comment = comment;

        if (withStructure) {
          segment.push({
            name: 'Comment',
            data: new TextEncoder().encode(comment),
            fields: [
              { type: FieldType.STRING, name: 'text', size: comment.length },
            ],
          });
        }
      }

      const payload: any = {
        type: 'object',
        name,
        timestamp,
        alive,
        position,
      };

      if (withStructure) {
        return { payload, segment };
      }

      return { payload };
    } catch (e) {
      return { payload: null };
    }
  }

  private decodeItem(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // Item format is similar to Object but name may be 3-9 chars (stored in a 9-char field)
    // Example: )NNN... where ) is data type, next 9 chars are name, then state char, then timestamp, then position
    if (this.payload.length < 12) return { payload: null }; // minimal: 1 + 3 + 1 + 7

    let offset = 1; // skip data type identifier ')'
    const segment: PacketSegment[] = withStructure ? [] : [];

    // Read 9-char name field (pad/truncate as present)
    const rawName = this.payload.substring(offset, offset + 9);
    const name = rawName.trimEnd();
    if (withStructure) {
      segment.push({
        name: 'item name',
        data: new TextEncoder().encode(rawName),
        fields: [
          { type: FieldType.STRING, name: 'name', size: 9 },
        ],
      });
    }
    offset += 9;

    // State character: '*' = alive, '_' = killed
    const stateChar = this.payload.charAt(offset);
    if (stateChar !== '*' && stateChar !== '_') {
      return { payload: null };
    }
    const alive = stateChar === '*';
    if (withStructure) {
      segment.push({
        name: 'item state',
        data: new TextEncoder().encode(stateChar),
        fields: [
          { type: FieldType.CHAR, name: 'State (* alive, _ killed)', size: 1 },
        ],
      });
    }
    offset += 1;

    // Timestamp (7 chars)
    const timeStr = this.payload.substring(offset, offset + 7);
    const { timestamp, segment: timestampSection } = this.parseTimestamp(timeStr, withStructure, offset);
    if (!timestamp) return { payload: null };
    if (timestampSection) segment.push(timestampSection);
    offset += 7;

    const positionOffset = offset;
    const isCompressed = this.isCompressedPosition(this.payload.substring(offset));

    let position: { latitude: number; longitude: number; symbol: any; ambiguity?: number; altitude?: number; comment?: string } | null = null;
    let consumed = 0;

    if (isCompressed) {
      const { position: compressed, segment: compressedSection } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset);
      if (!compressed) return { payload: null };

      position = {
        latitude: compressed.latitude,
        longitude: compressed.longitude,
        symbol: compressed.symbol,
        altitude: compressed.altitude,
      };
      consumed = 13;

      if (compressedSection) segment.push(compressedSection);
    } else {
      const { position: uncompressed, segment: uncompressedSection } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset);
      if (!uncompressed) return { payload: null };

      position = {
        latitude: uncompressed.latitude,
        longitude: uncompressed.longitude,
        symbol: uncompressed.symbol,
        ambiguity: uncompressed.ambiguity,
      };
      consumed = 19;

      if (uncompressedSection) segment.push(uncompressedSection);
    }

    offset += consumed;
    const comment = this.payload.substring(offset);
    if (comment) {
      position.comment = comment;
      if (withStructure) {
        segment.push({
          name: 'Comment',
          data: new TextEncoder().encode(comment),
          fields: [
            { type: FieldType.STRING, name: 'text', size: comment.length },
          ],
        });
      }
    }

    const payload: any = {
      type: 'item',
      name,
      alive,
      position,
    };

    if (withStructure) {
      return { payload, segment };
    }

    return { payload };
  }

  private decodeStatus(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // Status payload: optional 7-char timestamp followed by free text.
    // We'll also detect a trailing Maidenhead locator (4 or 6 chars) and expose it.
    const offsetBase = 1; // skip data type identifier '>'
    if (this.payload.length <= offsetBase) return { payload: null };

    let offset = offsetBase;
    const segments: PacketSegment[] = withStructure ? [] : [];

    // Try parse optional timestamp (7 chars)
    if (this.payload.length >= offset + 7) {
      const timeStr = this.payload.substring(offset, offset + 7);
      const { timestamp, segment: tsSegment } = this.parseTimestamp(timeStr, withStructure, offset);
      if (timestamp) {
        offset += 7;
        if (tsSegment) segments.push(tsSegment);
      }
    }

    // Remaining text is status text
    const text = this.payload.substring(offset);
    if (!text) return { payload: null };

    // Detect trailing Maidenhead locator (4 or 6 chars) at end of text separated by space
    let maidenhead: string | undefined;
    const mhMatch = text.match(/\s([A-Ra-r]{2}\d{2}(?:[A-Ra-r]{2})?)$/);
    let statusText = text;
    if (mhMatch) {
      maidenhead = mhMatch[1].toUpperCase();
      statusText = text.slice(0, mhMatch.index).trimEnd();
    }

    const payload: any = {
      type: 'status',
      timestamp: undefined,
      text: statusText,
    };

    // If timestamp was parsed, attach it
    if (segments.length > 0) {
      // The first segment may be timestamp; parseTimestamp returns the Timestamp object
      // Re-parse to obtain timestamp object (cheap) - alternate would be to capture earlier
      const timeSegment = segments.find(s => s.name === 'timestamp');
      if (timeSegment) {
        const tsStr = new TextDecoder().decode(timeSegment.data);
        const { timestamp } = this.parseTimestamp(tsStr, false, 0);
        if (timestamp) payload.timestamp = timestamp;
      }
    }

    if (maidenhead) payload.maidenhead = maidenhead;

    if (withStructure) {
      segments.push({
        name: 'status',
        data: new TextEncoder().encode(text),
        fields: [
          { type: FieldType.STRING, name: 'text', size: text.length },
        ],
      });
      return { payload, segment: segments };
    }

    return { payload };
  }

  private decodeQuery(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // TODO: Implement query decoding with section emission
    return { payload: null };
  }

  private decodeTelemetry(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // TODO: Implement telemetry decoding with section emission
    return { payload: null };
  }

  private decodeWeather(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // TODO: Implement weather decoding with section emission
    return { payload: null };
  }

  private decodeRawGPS(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // TODO: Implement raw GPS decoding with section emission
    return { payload: null };
  }

  private decodeCapabilities(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // TODO: Implement capabilities decoding with section emission
    return { payload: null };
  }

  private decodeUserDefined(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // TODO: Implement user-defined decoding with section emission
    return { payload: null };
  }

  private decodeThirdParty(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
    // TODO: Implement third-party decoding with section emission
    return { payload: null };
  }

  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
  let offset = 0;
  const sourceStr = parts[0];
  const source = Address.fromString(sourceStr);

  offset += sourceStr.length + 1; // +1 for '>'

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

  offset += destinationStr.length;

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

    pathFields.push({
      type: FieldType.CHAR,
      name: `Path separator ${i}`,
      size: 1
    });
    pathFields.push({
      type: FieldType.STRING,
      name: `Repeater ${i}`,
      size: pathStr.length,
    });
    offset += pathStr.length;
  }

  const routingSection: PacketSegment = {
    name: 'Routing',
    data: encoder.encode(data.slice(0, routeSepIndex)),
    fields: [
      { type: FieldType.STRING, name: 'Source address', size: sourceStr.length },
      { type: FieldType.CHAR, name: 'Route separator', size: 1 },
      { type: FieldType.STRING, name: 'Destination address', size: destinationStr.length },
      ...pathFields,
      { type: FieldType.CHAR, name: 'Payload separator', size: 1 },
    ],
  };

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