import { toUint8Array } from "../bytes/index.js";
const HEX_CHARACTER_REGEX = /^[0-9a-f]$/i;
const COMMON_SEPARATORS = [" ", "\t", "\n", "\r", ":", "-", "."];
function resolveSeparators(options) {
    if (options.separators === "none") {
        return [];
    }
    if (!options.separators || options.separators === "common") {
        return COMMON_SEPARATORS;
    }
    return options.separators;
}
function validateSeparator(separator) {
    if (!separator) {
        throw new TypeError("Hex separators must be non-empty strings");
    }
}
function matchSeparator(text, index, separators) {
    for (const separator of separators) {
        if (text.startsWith(separator, index)) {
            return separator;
        }
    }
    return undefined;
}
function detectCase(text) {
    const hasUpper = /[A-F]/.test(text);
    const hasLower = /[a-f]/.test(text);
    return hasUpper && !hasLower ? "upper" : "lower";
}
function detectLineSeparator(text) {
    const match = /\r\n|\n/.exec(text);
    if (!match) {
        return undefined;
    }
    return match[0] === "\r\n" ? "\r\n" : "\n";
}
function compactForDetection(text) {
    return text.replace(/[^0-9a-f]/gi, "");
}
function detectGroup(text) {
    const segments = text.match(/[0-9A-Fa-f]+|[^0-9A-Fa-f]+/g) ?? [];
    if (segments.length < 3) {
        return undefined;
    }
    const hexSegments = segments.filter((_, index) => index % 2 === 0);
    const separators = segments.filter((_, index) => index % 2 === 1);
    const separator = separators[0];
    if (!separator || separators.some((item) => item !== separator)) {
        return undefined;
    }
    if (hexSegments.some((segment) => segment.length === 0 || segment.length % 2 !== 0)) {
        return undefined;
    }
    const firstLength = hexSegments[0]?.length ?? 0;
    if (!firstLength) {
        return undefined;
    }
    if (hexSegments.slice(0, -1).some((segment) => segment.length !== firstLength)) {
        return undefined;
    }
    if ((hexSegments[hexSegments.length - 1]?.length ?? 0) > firstLength) {
        return undefined;
    }
    return {
        size: firstLength / 2,
        separator,
    };
}
function detectFormat(text) {
    const trimmed = text.trim();
    const prefix = /^0x/i.test(trimmed) ? "0x" : "";
    const body = prefix ? trimmed.slice(2) : trimmed;
    const lineSeparator = detectLineSeparator(body);
    const lines = body.split(/\r\n|\n/).filter((line) => line.length > 0);
    const sampleLine = lines[0]?.trim() ?? "";
    const group = detectGroup(sampleLine);
    const format = {
        case: detectCase(trimmed),
        prefix,
    };
    if (group) {
        format.group = group;
    }
    if (lineSeparator && lines.length > 1) {
        const firstLineBytes = compactForDetection(lines[0] ?? "").length / 2;
        if (firstLineBytes > 0 && lines.slice(0, -1).every((line) => compactForDetection(line).length / 2 === firstLineBytes)) {
            format.line = {
                bytesPerLine: firstLineBytes,
                separator: lineSeparator,
            };
        }
    }
    return format;
}
function normalizeText(text, options) {
    const allowPrefix = options.allowPrefix ?? true;
    const separators = [...resolveSeparators(options)].sort((left, right) => right.length - left.length);
    for (const separator of separators) {
        validateSeparator(separator);
    }
    let working = text.trim();
    if (/^0x/i.test(working)) {
        if (!allowPrefix) {
            throw new TypeError("Hexadecimal text must not include a 0x prefix");
        }
        working = working.slice(2);
    }
    let normalized = "";
    let lastTokenWasSeparator = false;
    for (let index = 0; index < working.length;) {
        const character = working[index] ?? "";
        if (HEX_CHARACTER_REGEX.test(character)) {
            normalized += character;
            lastTokenWasSeparator = false;
            index += 1;
            continue;
        }
        const separator = matchSeparator(working, index, separators);
        if (!separator) {
            throw new TypeError("Input is not valid hexadecimal text");
        }
        if (options.strict && (lastTokenWasSeparator || normalized.length === 0)) {
            throw new TypeError("Hexadecimal text contains misplaced separators");
        }
        lastTokenWasSeparator = true;
        index += separator.length;
    }
    if (options.strict && lastTokenWasSeparator && normalized.length > 0) {
        throw new TypeError("Hexadecimal text must not end with a separator");
    }
    if (normalized.length % 2 !== 0) {
        if (!options.allowOddLength) {
            throw new TypeError("Hexadecimal text must contain an even number of characters");
        }
        normalized = `0${normalized}`;
    }
    return normalized.toLowerCase();
}
function groupPairs(pairs, group) {
    if (!group) {
        return pairs.join("");
    }
    if (!Number.isInteger(group.size) || group.size < 1) {
        throw new RangeError("Hex group size must be a positive integer");
    }
    const chunks = [];
    for (let index = 0; index < pairs.length; index += group.size) {
        chunks.push(pairs.slice(index, index + group.size).join(""));
    }
    return chunks.join(group.separator);
}
export function normalize(text, options = {}) {
    return normalizeText(text, options);
}
export function is(text, options = {}) {
    if (typeof text !== "string") {
        return false;
    }
    try {
        normalize(text, options);
        return true;
    }
    catch {
        return false;
    }
}
export function encode(data, options = {}) {
    const bytes = toUint8Array(data);
    const casing = options.case ?? "lower";
    const pairs = Array.from(bytes, (byte) => {
        const text = byte.toString(16).padStart(2, "0");
        return casing === "upper" ? text.toUpperCase() : text;
    });
    let body = "";
    if (options.line) {
        const bytesPerLine = options.line.bytesPerLine;
        if (!Number.isInteger(bytesPerLine) || bytesPerLine < 1) {
            throw new RangeError("Hex bytesPerLine must be a positive integer");
        }
        const separator = options.line.separator ?? "\n";
        const lines = [];
        for (let index = 0; index < pairs.length; index += bytesPerLine) {
            lines.push(groupPairs(pairs.slice(index, index + bytesPerLine), options.group));
        }
        body = lines.join(separator);
    }
    else {
        body = groupPairs(pairs, options.group);
    }
    return `${options.prefix ?? ""}${body}`;
}
export function decode(text, options = {}) {
    const normalized = normalize(text, options);
    const result = new Uint8Array(normalized.length / 2);
    for (let i = 0; i < normalized.length; i += 2) {
        result[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16);
    }
    return result;
}
export function parse(text, options = {}) {
    const normalized = normalize(text, options);
    return {
        bytes: decode(normalized),
        format: detectFormat(text),
        normalized,
    };
}
export function format(data, value) {
    return encode(data, value);
}
export const formats = {
    compact: Object.freeze({}),
    upper: Object.freeze({ case: "upper" }),
    colon: Object.freeze({ group: { size: 1, separator: ":" } }),
    colonUpper: Object.freeze({ case: "upper", group: { size: 1, separator: ":" } }),
    groupsOf4: Object.freeze({ group: { size: 4, separator: " " } }),
    prefixed: Object.freeze({ prefix: "0x" }),
};
export const hex = { encode, decode, format, formats, is, normalize, parse };
