export interface HamokEncoder<U, R> {
	encode(data: U): R;
}

export interface HamokDecoder<U, R> {
	decode(data: R): U;
}

const EMPTY_MAP = new Map();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const EMPTY_SET = new Set<any>();

export function createHamokCodec<U, R>(encode: (input: U) => R, decode: (input: R) => U): HamokCodec<U, R> {
	return {
		encode,
		decode,
	};
}

export function createHamokJsonBinaryCodec<T>(): HamokCodec<T, Uint8Array> {
	const encoder = new TextEncoder();
	const decoder = new TextDecoder();

	return {
		encode: (data: T) => {
			const jsonString = JSON.stringify(data);
			const encoded = encoder.encode(jsonString);
            
			return encoded;
		},
		decode: (data: Uint8Array) => {
			const jsonString = decoder.decode(data);
			const decoded = JSON.parse(jsonString);
            
			return decoded;
		},
	};
}

export function createHamokJsonStringCodec<T>(): HamokCodec<T, string> {

	return {
		encode: (data: T) => JSON.stringify(data),
		decode: (data: string) => JSON.parse(data),
	};
}

export interface HamokCodec<U, R> extends HamokEncoder<U, R>, HamokDecoder<U, R> {}

/* eslint-disable @typescript-eslint/no-explicit-any */
export class FacadedHamokCodec<TIn = any, TOut = any> implements HamokCodec<TIn, TOut> {
	/* eslint-disable @typescript-eslint/no-explicit-any */
	public static wrap<U = any, R = any>(codec: HamokCodec<U, R>): FacadedHamokCodec<U, R> {
		const facadedCodec = new FacadedHamokCodec(codec);
        
		return facadedCodec;
	}
	private _codec: HamokCodec<TIn, TOut>;
	private constructor(firstEncoder: HamokCodec<TIn, TOut>) {
		this._codec = firstEncoder;
	}

	encode(data: TIn): TOut {
		return this._codec.encode(data);
	}

	decode(data: TOut): TIn {
		return this._codec.decode(data);
	}

	public then<TNextOut = TOut>(nextCodec: HamokCodec<TOut, TNextOut>): FacadedHamokCodec<TIn, TNextOut> {
		const actualCodec = this._codec;
        
		return FacadedHamokCodec.wrap<TIn, TNextOut>({
			encode(data: TIn): TNextOut {
				const encodedValue: TOut = actualCodec.encode(data);
                
				return nextCodec.encode(encodedValue);
			},
			decode(data: TNextOut): TIn {
				const decodedValue: TOut = nextCodec.decode(data);
                
				return actualCodec.decode(decodedValue);
			},
		});
	}
}

export function encodeCollection<K>(keys: IterableIterator<K>, keyCodec: HamokCodec<K, Uint8Array>): Uint8Array[] {
	const result: Uint8Array[] = [];

	for (const key of keys) {
		const encodedKey = keyCodec.encode(key);

		result.push(encodedKey);
	}
	
	return result;
}

export function decodeCollection<K>(keys: IterableIterator<Uint8Array>, keyCodec: HamokCodec<K, Uint8Array>): K[] {
	const result: K[] = [];

	for (const key of keys) {
		const decodedKey = keyCodec.decode(key);

		result.push(decodedKey);
	}
	
	return result;
}

export function encodeSet<K>(keys: ReadonlySet<K>, keyCodec: HamokCodec<K, Uint8Array>): Uint8Array[] {
	if (keys.size < 1) {
		return [];
	}
	
	return encodeCollection(keys.values(), keyCodec);
}

export function decodeSet<K>(keys: Uint8Array[], keyCodec: HamokCodec<K, Uint8Array>): ReadonlySet<K> {
	if (keys.length < 1) {
		return EMPTY_SET;
	}
	
	return new Set(decodeCollection(keys.values(), keyCodec));
}

export function encodeMap<K, V>(entries: ReadonlyMap<K, V>, keyCodec: HamokCodec<K, Uint8Array>, valueCodec: HamokCodec<V, Uint8Array>): [Uint8Array[], Uint8Array[]] {
	if (entries.size < 1) {
		return [ [], [] ];
	}
	const encodedKeys: Uint8Array[] = [];
	const encodedValues: Uint8Array[] = [];

	for (const [ key, value ] of entries) {
		const encodedKey = keyCodec.encode(key);
		const encodedValue = valueCodec.encode(value);

		encodedKeys.push(encodedKey);
		encodedValues.push(encodedValue);
	}
	
	return [ encodedKeys, encodedValues ];
}

export function decodeMap<K, V>(keys: Uint8Array[], values: Uint8Array[], keyCodec: HamokCodec<K, Uint8Array>, valueCodec: HamokCodec<V, Uint8Array>): ReadonlyMap<K, V> {
	if (keys.length < 1 || values.length < 1) {
		return EMPTY_MAP;
	}
	const result = new Map<K, V>();
	const length = Math.min(keys.length, values.length);

	for (let i = 0; i < length; ++i) {
		const key = keys[i];
		const value = values[i];
		const decodedKey = keyCodec.decode(key);
		const decodedValue = valueCodec.decode(value);

		result.set(decodedKey, decodedValue);
	}
	
	return result;
}

export function encodeNumber(value: number): Uint8Array {
	const buffer = new ArrayBuffer(4);
	const view = new DataView(buffer);

	view.setInt32(0, value);
	
	return new Uint8Array(buffer);
}

export function createStrToUint8ArrayCodec(encoding: BufferEncoding = 'utf-8'): HamokCodec<string, Uint8Array> {
	return {
		encode: (data: string) => Buffer.from(data, encoding),
		decode: (data: Uint8Array) => Buffer.from(data).toString(encoding),
	};
}
