import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core/safe";
import {
	FLASH_MAX_PAGE_SIZE,
	NVM3_MIN_PAGE_SIZE,
	NVM3_PAGE_COUNTER_MASK,
	NVM3_PAGE_COUNTER_SIZE,
	NVM3_PAGE_HEADER_SIZE,
	NVM3_PAGE_MAGIC,
	PageStatus,
	PageWriteSize,
} from "./consts";
import { NVM3Object, readObjects } from "./object";
import { computeBergerCode, validateBergerCode } from "./utils";

export interface NVM3PageHeader {
	offset: number;
	version: number;
	eraseCount: number;
	status: PageStatus;
	encrypted: boolean;
	pageSize: number;
	writeSize: PageWriteSize;
	memoryMapped: boolean;
	deviceFamily: number;
}

export interface NVM3Page {
	header: NVM3PageHeader;
	objects: NVM3Object[];
}

// The page size field has a value from 0 to 7 describing page sizes from 512 to 65536 bytes
export function pageSizeToBits(pageSize: number): number {
	return Math.ceil(Math.log2(pageSize) - Math.log2(NVM3_MIN_PAGE_SIZE));
}

export function pageSizeFromBits(bits: number): number {
	return NVM3_MIN_PAGE_SIZE * Math.pow(2, bits);
}

export function readPage(
	buffer: Buffer,
	offset: number,
): { page: NVM3Page; bytesRead: number } {
	const version = buffer.readUInt16LE(offset);
	const magic = buffer.readUInt16LE(offset + 2);
	if (magic !== NVM3_PAGE_MAGIC) {
		throw new ZWaveError(
			"Not a valid NVM3 page!",
			ZWaveErrorCodes.NVM_InvalidFormat,
		);
	}
	if (version !== 0x01) {
		throw new ZWaveError(
			`Unsupported NVM3 page version: ${version}`,
			ZWaveErrorCodes.NVM_NotSupported,
		);
	}

	// The erase counter is saved twice, once normally, once inverted
	let eraseCount = buffer.readUInt32LE(offset + 4);
	const eraseCountCode = eraseCount >>> NVM3_PAGE_COUNTER_SIZE;
	eraseCount &= NVM3_PAGE_COUNTER_MASK;
	validateBergerCode(eraseCount, eraseCountCode, NVM3_PAGE_COUNTER_SIZE);

	let eraseCountInv = buffer.readUInt32LE(offset + 8);
	const eraseCountInvCode = eraseCountInv >>> NVM3_PAGE_COUNTER_SIZE;
	eraseCountInv &= NVM3_PAGE_COUNTER_MASK;
	validateBergerCode(
		eraseCountInv,
		eraseCountInvCode,
		NVM3_PAGE_COUNTER_SIZE,
	);

	if (eraseCount !== (~eraseCountInv & NVM3_PAGE_COUNTER_MASK)) {
		throw new ZWaveError(
			"Invalid erase count!",
			ZWaveErrorCodes.NVM_InvalidFormat,
		);
	}

	// Page status
	const status = buffer.readUInt32LE(offset + 12);

	const devInfo = buffer.readUInt16LE(offset + 16);
	const deviceFamily = devInfo & 0x7ff;
	const writeSize = (devInfo >> 11) & 0b1;
	const memoryMapped = !!((devInfo >> 12) & 0b1);
	const pageSize = pageSizeFromBits((devInfo >> 13) & 0b111);

	// Application NVM pages seem to get written with a page size of 0xffff
	const actualPageSize = Math.min(pageSize, FLASH_MAX_PAGE_SIZE);

	if (buffer.length < offset + actualPageSize) {
		throw new ZWaveError(
			"Incomplete page in buffer!",
			ZWaveErrorCodes.NVM_InvalidFormat,
		);
	}

	const formatInfo = buffer.readUInt16LE(offset + 18);

	const encrypted = !(formatInfo & 0b1);

	const header: NVM3PageHeader = {
		offset,
		version,
		eraseCount,
		status,
		encrypted,
		pageSize,
		writeSize,
		memoryMapped,
		deviceFamily,
	};
	const bytesRead = actualPageSize;
	const data = buffer.slice(offset + 20, offset + bytesRead);

	const { objects } = readObjects(data);

	return {
		page: { header, objects },
		bytesRead,
	};
}

export function writePageHeader(
	header: Omit<NVM3PageHeader, "offset">,
): Buffer {
	const ret = Buffer.alloc(NVM3_PAGE_HEADER_SIZE);

	ret.writeUInt16LE(header.version, 0);
	ret.writeUInt16LE(NVM3_PAGE_MAGIC, 2);

	let eraseCount = header.eraseCount & NVM3_PAGE_COUNTER_MASK;
	const eraseCountCode = computeBergerCode(
		eraseCount,
		NVM3_PAGE_COUNTER_SIZE,
	);
	eraseCount |= eraseCountCode << NVM3_PAGE_COUNTER_SIZE;
	ret.writeInt32LE(eraseCount, 4);

	let eraseCountInv = ~header.eraseCount & NVM3_PAGE_COUNTER_MASK;
	const eraseCountInvCode = computeBergerCode(
		eraseCountInv,
		NVM3_PAGE_COUNTER_SIZE,
	);
	eraseCountInv |= eraseCountInvCode << NVM3_PAGE_COUNTER_SIZE;
	ret.writeInt32LE(eraseCountInv, 8);

	ret.writeUInt32LE(header.status, 12);

	const devInfo =
		(header.deviceFamily & 0x7ff) |
		((header.writeSize & 0b1) << 11) |
		((header.memoryMapped ? 1 : 0) << 12) |
		(pageSizeToBits(header.pageSize) << 13);
	ret.writeUInt16LE(devInfo, 16);

	const formatInfo = header.encrypted ? 0xfffe : 0xffff;
	ret.writeUInt16LE(formatInfo, 18);

	return ret;
}
