import { Readable } from 'stream';
import { IFrame, IAddress } from './frame.types';
import { Address } from './address';
import { encodeHDLC } from './hdlc';


/**
 * Represents an AX.25 frame with source/destination addresses, control field, and information field.
 *
 * This class serves as a base for specific frame types (InformationFrame, SupervisoryFrame, UnnumberedFrame)
 * and provides common functionality such as parsing from raw bytes and encoding to AX.25 format.
 */
export class Frame implements IFrame {
	destination: IAddress;
	source: IAddress;
	repeaters: IAddress[];
	repeatedIndex: number | null;
	control: number;
	pid?: number;
	info: Uint8Array;
	raw?: Uint8Array;

	constructor(src: Address | string = '', dst: Address | string = '', control = 0, info?: Uint8Array) {
		this.source = typeof src === 'string' ? Address.fromString(src) : src;
		this.destination = typeof dst === 'string' ? Address.fromString(dst) : dst;
		this.repeaters = [];
		this.repeatedIndex = null;
		this.control = control;
		this.info = info ?? new Uint8Array(0);
	}

	toString() {
		// Format similar to common packet-radio programs
		const src = this.source.toString();
		const dst = this.destination.toString();
		let ctl = '';
		if (this instanceof UnnumberedFrame) {
			ctl = `${this.uType}${this.pf ? '^' : '-'}`;
		} else if (this instanceof SupervisoryFrame) {
			ctl = `${this.sType}${this.nr}${this.pf ? 'v' : '-'}`;
		} else if (this instanceof InformationFrame) {
			ctl = `${this.ns}${this.nr}${this.pf ? '^' : '-'}`;
		} else {
			ctl = `CTL${this.control.toString(16)}`;
		}

		// Repeater path formatting
		let viaStr = '';
		if (this.repeaters && this.repeaters.length > 0) {
			const path = this.repeaters.map((repeater, i) => {
				let s = repeater.toString();
				if (this.repeatedIndex === i) s += '*';
				return s;
			}).join(',');
			viaStr = ` via ${path}`;
		}

		const pidStr = typeof this.pid === 'number' ? ` pid ${this.pid.toString(16).toUpperCase().padStart(2, '0')}` : '';
		const lenStr = ` len=${this.info?.length ?? 0}`;

		// If info looks like printable ASCII, append it after a colon
		let infoText = '';
		if (this.info && this.info.length > 0) {
			const printable = Array.from(this.info).every(b => (b >= 0x20 && b <= 0x7e) || b === 0x09 || b === 0x0a || b === 0x0d);
			if (printable) {
				try {
					infoText = `: ${new TextDecoder().decode(this.info)}`;
				} catch {
					infoText = '';
				}
			}
		}

		return `fm ${src} to ${dst}${viaStr} ctl ${ctl}${pidStr}${lenStr}${infoText}`;
	}

  /**
   * Parses a given AX.25 frame payload and returns the corresponding Frame instance.
   *
   * This function processes the AX.25 protocol frame structure, extracting address fields,
   * control field, and information field as appropriate. It supports parsing of I-frames,
   * S-frames, and U-frames, returning the correct subclass of `Frame` for each.
   *
   * @param payload - The raw AX.25 frame as a Uint8Array.
   * @returns {Frame} An instance of `InformationFrame`, `SupervisoryFrame`, or `UnnumberedFrame`
   *          depending on the type of AX.25 frame parsed.
   * @throws {Error} If the payload is malformed, missing required fields, or does not conform to AX.25.
   */
	static fromBytes(payload: Uint8Array): Frame {
		// Skip leading null bytes (common in some KISS/TNC output)
		let offset = 0;
		while (offset < payload.length && payload[offset] === 0x00) {
			offset++;
		}

		// Parse AX.25 addresses (each 7 bytes) until last address (bit 0 of SSID byte === 1)
		const addrs: Address[] = [];
		const repeaterFlags: boolean[] = [];
		while (offset + 7 <= payload.length) {
			const field = payload.slice(offset, offset + 7);
      const addr = Address.fromBytes(field);
      const isLast = addr.getExtension();
			const hasRepeated = addr.getCH();
			addrs.push(addr);
			repeaterFlags.push(hasRepeated);
			offset += 7;
			if (isLast) break;
		}

    // Need at least destination and source addresses
		if (addrs.length < 2) throw new Error('AX.25: not enough address fields');

		// next byte: control
		if (offset >= payload.length) throw new Error('AX.25: missing control byte');
		const control = payload[offset];
		offset += 1;

		// Addresses as Address objects
		const dst: Address = addrs[0];
		const src: Address = addrs[1];
		const repeaters: Address[] = addrs.length > 2 ? addrs.slice(2) : [];
		const repeatedIndex = repeaterFlags.length > 2 ? repeaterFlags.slice(2).lastIndexOf(true) : -1;

		// Decode control field (only 1-byte control supported here)
		// I-frame: bit0 == 0
		if ((control & 0x01) === 0) {
			const ns = (control >> 1) & 0x07;
			const pf = (control >> 4) & 0x01;
			const nr = (control >> 5) & 0x07;
			const info = payload.slice(offset);
			const f = new InformationFrame(src, dst, control, info, ns, nr, pf);
			f.repeaters = repeaters;
			f.repeatedIndex = repeatedIndex >= 0 ? repeatedIndex : null;
			f.raw = payload;
			return f;
		}

		// S-frame: bits 0-1 == 01
		if ((control & 0x03) === 0x01) {
			const sCode = (control >> 2) & 0x03;
			const pf = (control >> 4) & 0x01;
			const nr = (control >> 5) & 0x07;
			let sType: 'RR' | 'RNR' | 'REJ' | 'SREJ' = 'RR';
			switch (sCode) {
				case 0:
					sType = 'RR';
					break;
				case 1:
					sType = 'RNR';
					break;
				case 2:
					sType = 'REJ';
					break;
				case 3:
					sType = 'SREJ';
					break;
			}
			const f = new SupervisoryFrame(src, dst, control, sType, nr, pf);
			f.repeaters = repeaters;
			f.repeatedIndex = repeatedIndex >= 0 ? repeatedIndex : null;
			f.raw = payload;
			return f;
		}

		// U-frame: bits 0-1 == 11
		{
			const pf = (control >> 4) & 0x01;
			// common U-frame types (not exhaustive)
			let uType = `U(${control.toString(16)})`;
			switch (control) {
				case 0x03:
					uType = 'UI';
					break;
				case 0x2f:
					uType = 'SABM';
					break;
				case 0x63:
					uType = 'UA';
					break;
				case 0x43:
					uType = 'DISC';
					break;
				case 0x0f:
					uType = 'SNRM';
					break;
			}
			let uPid: number | undefined;
			let info: Uint8Array = new Uint8Array(0);
			if (uType === 'UI') {
				if (offset < payload.length) {
					uPid = payload[offset];
					offset += 1;
					info = payload.slice(offset);
				}
			}
			const f = new UnnumberedFrame(src, dst, control, uType, pf, info);
			f.repeaters = repeaters;
			f.repeatedIndex = repeatedIndex >= 0 ? repeatedIndex : null;
			f.pid = uPid;
			f.raw = payload;
			return f;
		}
	}
}

export class InformationFrame extends Frame {
	ns: number;
	nr: number;
	pf: number;

	constructor(src: Address, dst: Address, control: number, info: Uint8Array, ns: number, nr: number, pf: number) {
		super(src, dst, control, info);
		this.ns = ns;
		this.nr = nr;
		this.pf = pf;
	}
}

export class SupervisoryFrame extends Frame {
	sType: 'RR' | 'RNR' | 'REJ' | 'SREJ';
	nr: number;
	pf: number;

	constructor(src: Address, dst: Address, control: number, sType: 'RR' | 'RNR' | 'REJ' | 'SREJ', nr: number, pf: number) {
		super(src, dst, control, new Uint8Array(0));
		this.sType = sType;
		this.nr = nr;
		this.pf = pf;
	}
}

export class UnnumberedFrame extends Frame {
	uType: string;
	pf: number;

	constructor(src: Address, dst: Address, control: number, uType: string, pf: number, info?: Uint8Array) {
		super(src, dst, control, info);
		this.uType = uType;
		this.pf = pf;
	}
}

export const buildIControl: (ns: number, nr: number, pf?: number) => number = (ns, nr, pf = 0) => {
	// bit0 = 0 for I-frames; ns in bits 1-3, pf in bit4, nr in bits 5-7
	return ((ns & 0x07) << 1) | ((pf & 0x01) << 4) | ((nr & 0x07) << 5);
};


export const buildSControl: (sType: 'RR' | 'RNR' | 'REJ' | 'SREJ', nr: number, pf?: number) => number = (sType, nr, pf = 0) => {
	const sCode = sType === 'RR' ? 0 : sType === 'RNR' ? 1 : sType === 'REJ' ? 2 : 3;
	// bits 0-1 = 01
	return 0x01 | (sCode << 2) | ((pf & 0x01) << 4) | ((nr & 0x07) << 5);
};

export const buildUControl: (uType: string, pf?: number) => number = (uType, pf = 0) => {
	// common mappings
	switch (uType) {
		case 'UI':
			return 0x03 | ((pf & 0x01) << 4);
		case 'SABM':
			return 0x2f | ((pf & 0x01) << 4);
		case 'UA':
			return 0x63 | ((pf & 0x01) << 4);
		case 'DISC':
			return 0x43 | ((pf & 0x01) << 4);
		default:
			return 0x03 | ((pf & 0x01) << 4);
	}
};

// Extended control builders (2-byte) - minimal support for encoding larger NS/NR
export const buildIControlExtended: (ns: number, nr: number, pf?: number) => Uint8Array = (ns, nr, pf = 0) => {
	const b1 = ((ns & 0x7f) << 1) | ((pf & 0x01) << 4);
	const b2 = ((nr & 0x7f) << 1);
	return Uint8Array.from([b1 & 0xff, b2 & 0xff]);
};

export const buildSControlExtended: (sType: 'RR' | 'RNR' | 'REJ' | 'SREJ', nr: number, pf?: number) => Uint8Array = (sType, nr, pf = 0) => {
	const sCode = sType === 'RR' ? 0 : sType === 'RNR' ? 1 : sType === 'REJ' ? 2 : 3;
	const b1 = 0x01 | (sCode << 2) | ((pf & 0x01) << 4);
	const b2 = ((nr & 0x7f) << 1);
	return Uint8Array.from([b1 & 0xff, b2 & 0xff]);
};

export const encodeFrame: (frame: Frame) => Uint8Array = (frame) => {
	const parts: number[] = [];
	// addresses: destination then source; mark source as last
	parts.push(...(frame.destination as IAddress).toBytes(false));
	parts.push(...(frame.source as IAddress).toBytes(true));
	// control
	parts.push(frame.control & 0xff);

	if (frame instanceof UnnumberedFrame) {
		if (frame.uType === 'UI') {
			parts.push(frame.pid ?? 0xf0);
			parts.push(...(frame.info ?? new Uint8Array(0)));
		}
		// other U-frames have no pid/info in AX.25 common usage
	} else if (frame instanceof InformationFrame) {
		parts.push(...(frame.info ?? new Uint8Array(0)));
	} else if (frame instanceof SupervisoryFrame) {
		// S-frames typically carry no PID/info
	}

	return Uint8Array.from(parts);
};

export const toHDLC: (frame: Frame) => Uint8Array = (frame) => {
	const ax25 = encodeFrame(frame);
	return encodeHDLC(ax25, { includeFcs: true });
};

export const toWire: (frame: Frame) => Buffer = (frame) => {
	const h = toHDLC(frame);
	return Buffer.from(h.buffer, h.byteOffset, h.byteLength);
};

export const toWireStream: (frame: Frame) => Readable = (frame) => {
	const buf = toWire(frame);
	const r = new Readable();
	r.push(buf);
	r.push(null);
	return r;
};

const addrsRaw: (payload: Uint8Array, offset: number) => Uint8Array = (payload, offset) => payload.slice(offset, offset + 7);

export const encodeAX25: (dst: Address | string, src: Address | string, info: Uint8Array, options?: { ui?: boolean }) => Uint8Array = (dst, src, info, options) => {
	// dst and src should be like 'CALL-SSID' or 'CALL'
	const dstAddr = typeof dst === 'string' ? Address.fromString(dst) : dst;
	const srcAddr = typeof src === 'string' ? Address.fromString(src) : src;
	const parts: number[] = [];
	const dfield = dstAddr.toBytes(false);
	const sfield = srcAddr.toBytes(true); // mark source as last address
	parts.push(...dfield, ...sfield);
	// control
	parts.push(options?.ui ? 0x03 : 0x03);
	// PID - use 0xf0 (no layer 3) as default for UI-frames
	parts.push(0xf0);
	parts.push(...info);
	return Uint8Array.from(parts);
}
