import { BlnM, DescriptorUnitsValue, parseAngle, parsePercent, parseUnitsToNumber, readVersionAndDescriptor } from './descriptor';
import { BlendMode, PatternInfo } from './psd';
import { checkSignature, createReader, readBytes, readDataRLE, readInt16, readInt32, readPascalString, readPattern, readSignature, readUint16, readUint32, readUint8, skipBytes } from './psdReader';

export interface Abr {
	brushes: Brush[];
	samples: SampleInfo[];
	patterns: PatternInfo[];
}

export interface SampleInfo {
	id: string;
	bounds: { x: number; y: number; w: number; h: number; };
	alpha: Uint8Array;
}

export interface BrushDynamics {
	control: 'off' | 'fade' | 'pen pressure' | 'pen tilt' | 'stylus wheel' | 'initial direction' | 'direction' | 'initial rotation' | 'rotation';
	steps: number; // for fade
	jitter: number;
	minimum: number;
}

const dynamicsControl = ['off', 'fade', 'pen pressure', 'pen tilt', 'stylus wheel', 'initial direction', 'direction', 'initial rotation', 'rotation'];

type DynamicBrushShapeShape = 'round point' | 'round blunt' | 'round curve' | 'round angle' | 'round fan' | 'flat point' | 'flat blunt' | 'flat curve' | 'flat angle' | 'flat fan';

const dynamicBrushShapeShapes: DynamicBrushShapeShape[] = ['round point', 'round blunt', 'round curve', 'round angle', 'round fan', 'flat point', 'flat blunt', 'flat curve', 'flat angle', 'flat fan'];

type TipsBrushShapeShape = 'erodible point' | 'erodible flat' | 'erodible round' | 'erodible square' | 'erodible triangle' | 'custom';

const tipsBrushShapeShapes: TipsBrushShapeShape[] = ['erodible point', 'erodible flat', 'erodible round', 'erodible square', 'erodible triangle', 'custom'];

export type BrushShape = ComputedBrushShape | SampledBrushShape | TipsBrushShape | DynamicBrushShape;

interface ComputedBrushShape {
	type: 'computed';
	size: number;
	angle: number;
	roundness: number;
	hardness: number;
	spacingOn: boolean;
	spacing: number;
	flipX: boolean;
	flipY: boolean;
}

interface SampledBrushShape {
	type: 'sampled';
	name: string;
	size: number;
	angle: number;
	roundness: number;
	spacingOn: boolean;
	spacing: number;
	flipX: boolean;
	flipY: boolean;
	sampledData: string;
}

interface TipsBrushShape {
	type: 'tips';
	angle: number;
	size: number;
	shape: DynamicBrushShapeShape;
	physics: boolean;
	spacing: number,
	spacingOn: boolean;
	flipX: boolean;
	flipY: boolean;
	tipsType: TipsBrushShapeShape;
	tipsLengthRatio: number;
	tipsHardness: number;
	tipsGridSize?: number;
	tipsErodibleTipHeightMap?: number[];
	tipsAirbrushCutoffAngle: number;
	tipsAirbrushGranularity: number;
	tipsAirbrushStreakiness: number;
	tipsAirbrushSplatSize: number;
	tipsAirbrushSplatCount: number;
}

interface DynamicBrushShape {
	type: 'dynamic';
	size: number;
	angle: number;
	shape: DynamicBrushShapeShape;
	density: number;
	length: number;
	clumping: number; // bristles
	thickness: number;
	stiffness: number;
	physics: boolean;
	spacing: number;
	spacingOn: boolean;
	flipX: boolean;
	flipY: boolean;
}

export interface Brush {
	name: string;
	shape: BrushShape;
	shapeDynamics?: {
		sizeDynamics: BrushDynamics;
		minimumDiameter: number;
		tiltScale: number;
		angleDynamics: BrushDynamics; // jitter 0-1 -> 0-360 deg ?
		roundnessDynamics: BrushDynamics;
		minimumRoundness: number;
		flipX: boolean;
		flipY: boolean;
		brushProjection: boolean;
	};
	scatter?: {
		bothAxes: boolean;
		scatterDynamics: BrushDynamics;
		countDynamics: BrushDynamics;
		count: number;
	};
	texture?: {
		id: string;
		name: string;
		invert: boolean;
		scale: number;
		brightness: number;
		contrast: number;
		blendMode: BlendMode;
		depth: number;
		depthMinimum: number;
		depthDynamics: BrushDynamics;
		textureEachTip: boolean;
	};
	dualBrush?: {
		flip: boolean;
		shape: BrushShape;
		blendMode: BlendMode;
		useScatter: boolean;
		spacing: number;
		count: number;
		bothAxes: boolean;
		countDynamics: BrushDynamics;
		scatterDynamics: BrushDynamics;
	};
	colorDynamics?: {
		foregroundBackground: BrushDynamics;
		hue: number;
		saturation: number;
		brightness: number;
		purity: number;
		perTip: boolean;
	};
	transfer?: {
		flowDynamics: BrushDynamics;
		opacityDynamics: BrushDynamics;
		wetnessDynamics: BrushDynamics;
		mixDynamics: BrushDynamics;
	};
	brushPose?: {
		overrideAngle: boolean;
		overrideTiltX: boolean;
		overrideTiltY: boolean;
		overridePressure: boolean;
		pressure: number;
		tiltX: number;
		tiltY: number;
		angle: number;
	};
	noise: boolean;
	wetEdges: boolean;
	// TODO: build-up
	// TODO: smoothing
	protectTexture?: boolean;
	spacing: number;
	brushGroup?: undefined; // ?
	interpretation?: boolean; // ?
	useBrushSize: boolean; // ?
	toolOptions?: {
		type: 'brush' | 'mixer brush' | 'smudge brush';
		brushPreset: boolean;
		flow: number; // 0-100
		wetness?: number; // 0-100
		dryness?: number; // 0-100
		mix?: number; // 0-100
		smooth: number; // ?
		mode: BlendMode;
		opacity: number; // 0-100
		smoothing: boolean;
		smoothingValue: number;
		smoothingRadiusMode: boolean;
		smoothingCatchup: boolean;
		smoothingCatchupAtEnd: boolean;
		smoothingZoomCompensation: boolean;
		pressureSmoothing: boolean;
		usePressureOverridesSize: boolean;
		usePressureOverridesOpacity: boolean;
		useLegacy: boolean;
		autoFill?: boolean;
		autoClean?: boolean;
		loadSolidColorOnly?: boolean;
		sampleAllLayers?: boolean;
		flowDynamics?: BrushDynamics;
		opacityDynamics?: BrushDynamics;
		sizeDynamics?: BrushDynamics;
		smudgeFingerPainting?: boolean;
		smudgeSampleAllLayers?: boolean;
		strength?: number; // 0-100
	};
}

// internal

interface PhryDescriptor {
	hierarchy: ({} | {
		'Nm  ': string;
		zuid: string;
	})[];
}

interface DynamicsDescriptor {
	bVTy: number;
	fStp: number;
	jitter: DescriptorUnitsValue;
	'Mnm ': DescriptorUnitsValue;
}

type BrushShapeDescriptor = ComputedBrushDescriptor | SampledBrushDescriptor | TipsBrushDescriptor | DynamicBrushDescriptor;

interface ComputedBrushDescriptor {
	_name: '';
	_classID: 'computedBrush',
	Dmtr: DescriptorUnitsValue,
	Hrdn: DescriptorUnitsValue,
	Angl: DescriptorUnitsValue,
	Rndn: DescriptorUnitsValue,
	Spcn: DescriptorUnitsValue,
	Intr: boolean,
	flipX: boolean,
	flipY: boolean,
}

interface SampledBrushDescriptor {
	_name: '';
	_classID: 'sampledBrush';
	Dmtr: DescriptorUnitsValue;
	Angl: DescriptorUnitsValue;
	Rndn: DescriptorUnitsValue;
	'Nm  ': string;
	Spcn: DescriptorUnitsValue;
	Intr: boolean;
	flipX: boolean;
	flipY: boolean;
	sampledData: string;
}

interface TipsBrushDescriptor {
	_name: '';
	_classID: 'dTips';
	Angl: DescriptorUnitsValue;
	Dmtr: DescriptorUnitsValue;
	dtipsType: number;
	'Shp ': number;
	dtipsLengthRatio: DescriptorUnitsValue;
	dtipsHardness: DescriptorUnitsValue;
	dtipsGridSize: number;
	dtipsErodibleTipHeightMap?: Uint8Array;
	physics: boolean;
	dtipsAirbrushCutoffAngle: number;
	dtipsAirbrushGranularity: DescriptorUnitsValue;
	dtipsAirbrushStreakiness: DescriptorUnitsValue;
	dtipsAirbrushSplatSize: DescriptorUnitsValue;
	dtipsAirbrushSplatCount: number;
	Spcn: DescriptorUnitsValue,
	Intr: boolean;
	flipX: boolean;
	flipY: boolean;
}

interface DynamicBrushDescriptor {
	_name: '';
	_classID: 'dBrush';
	'Shp ': number;
	Angl: DescriptorUnitsValue;
	Dmtr: DescriptorUnitsValue;
	Dnst: DescriptorUnitsValue;
	Lngt: DescriptorUnitsValue;
	clumping: DescriptorUnitsValue;
	thickness: DescriptorUnitsValue;
	stiffness: DescriptorUnitsValue;
	physics: boolean;
	Spcn: DescriptorUnitsValue;
	Intr: boolean;
	flipX: boolean;
	flipY: boolean;
}

interface DescDescriptor {
	Brsh: {
		'Nm  ': string;
		Brsh: BrushShapeDescriptor;
		useTipDynamics: boolean;
		flipX: boolean;
		flipY: boolean;
		brushProjection: boolean;
		minimumDiameter: DescriptorUnitsValue;
		minimumRoundness: DescriptorUnitsValue;
		tiltScale: DescriptorUnitsValue;
		szVr: DynamicsDescriptor;
		angleDynamics: DynamicsDescriptor;
		roundnessDynamics: DynamicsDescriptor;
		useScatter: boolean;
		Spcn: DescriptorUnitsValue;
		'Cnt ': number;
		bothAxes: boolean;
		countDynamics: DynamicsDescriptor;
		scatterDynamics: DynamicsDescriptor;
		dualBrush: { useDualBrush: false; } | {
			useDualBrush: true;
			Flip: boolean;
			Brsh: BrushShapeDescriptor;
			BlnM: string;
			useScatter: boolean;
			Spcn: DescriptorUnitsValue;
			'Cnt ': number;
			bothAxes: boolean;
			countDynamics: DynamicsDescriptor;
			scatterDynamics: DynamicsDescriptor;
		};
		brushGroup: { useBrushGroup: false; };
		useTexture: boolean;
		TxtC: boolean;
		interpretation: boolean;
		textureBlendMode: string;
		textureDepth: DescriptorUnitsValue;
		minimumDepth: DescriptorUnitsValue;
		textureDepthDynamics: DynamicsDescriptor;
		Txtr?: {
			'Nm  ': string;
			Idnt: string;
		};
		textureScale: DescriptorUnitsValue;
		InvT: boolean;
		protectTexture: boolean;
		textureBrightness: number;
		textureContrast: number;
		usePaintDynamics: boolean;
		prVr?: DynamicsDescriptor;
		opVr?: DynamicsDescriptor;
		wtVr?: DynamicsDescriptor;
		mxVr?: DynamicsDescriptor;
		useColorDynamics: boolean;
		clVr?: DynamicsDescriptor;
		'H   '?: DescriptorUnitsValue;
		Strt?: DescriptorUnitsValue;
		Brgh?: DescriptorUnitsValue;
		purity?: DescriptorUnitsValue;
		colorDynamicsPerTip?: true;
		Wtdg: boolean;
		Nose: boolean;
		'Rpt ': boolean;
		useBrushSize: boolean;
		useBrushPose: boolean;
		overridePoseAngle?: boolean;
		overridePoseTiltX?: boolean;
		overridePoseTiltY?: boolean;
		overridePosePressure?: boolean;
		brushPosePressure?: DescriptorUnitsValue;
		brushPoseTiltX?: number;
		brushPoseTiltY?: number;
		brushPoseAngle?: number;
		toolOptions?: {
			_classID: string;
			brushPreset: boolean;
			flow?: number;
			wetness?: number;
			dryness?: number;
			mix?: number;
			Smoo?: number;
			'Md  ': string;
			Opct?: number;
			smoothing?: boolean;
			smoothingValue?: number;
			smoothingRadiusMode?: boolean;
			smoothingCatchup?: boolean;
			smoothingCatchupAtEnd?: boolean;
			smoothingZoomCompensation?: boolean;
			pressureSmoothing?: boolean;
			usePressureOverridesSize?: boolean;
			usePressureOverridesOpacity?: boolean;
			useLegacy: boolean;
			autoFill?: boolean;
			autoClean?: boolean;
			loadSolidColorOnly?: boolean;
			sampleAllLayers?: boolean;
			'Prs '?: number;
			MgcE?: boolean; // TODO: ???
			ErsB?: number; // TODO: ???
			prVr?: DynamicsDescriptor;
			opVr?: DynamicsDescriptor;
			szVr?: DynamicsDescriptor;
			SmdF?: boolean;
			SmdS?: boolean;
		};
	}[];
}

const toBrushType: { [key: string]: 'brush' | 'mixer brush' | 'smudge brush'; } = {
	_: 'brush',
	MixB: 'mixer brush',
	SmTl: 'smudge brush',
	// PbTl
	// ErTl
};

function parseDynamics(desc: DynamicsDescriptor): BrushDynamics {
	return {
		control: dynamicsControl[desc.bVTy] as any,
		steps: desc.fStp,
		jitter: parsePercent(desc.jitter),
		minimum: parsePercent(desc['Mnm ']),
	};
}

function parseBrushShape(desc: BrushShapeDescriptor): BrushShape {
	switch (desc._classID) {
		case 'computedBrush': {
			return {
				type: 'computed',
				size: parseUnitsToNumber(desc.Dmtr, 'Pixels'),
				angle: parseAngle(desc.Angl),
				roundness: parsePercent(desc.Rndn),
				spacingOn: desc.Intr,
				spacing: parsePercent(desc.Spcn),
				flipX: desc.flipX,
				flipY: desc.flipY,
				hardness: parsePercent(desc.Hrdn),
			};
		}
		case 'sampledBrush': {
			return {
				type: 'sampled',
				size: parseUnitsToNumber(desc.Dmtr, 'Pixels'),
				angle: parseAngle(desc.Angl),
				roundness: parsePercent(desc.Rndn),
				spacingOn: desc.Intr,
				spacing: parsePercent(desc.Spcn),
				flipX: desc.flipX,
				flipY: desc.flipY,
				name: desc['Nm  '],
				sampledData: desc.sampledData,
			};
		}
		case 'dBrush':
			return {
				type: 'dynamic',
				shape: dynamicBrushShapeShapes[desc['Shp ']],
				angle: parseAngle(desc.Angl),
				size: parseUnitsToNumber(desc.Dmtr, 'Pixels'),
				density: parsePercent(desc.Dnst),
				length: parsePercent(desc.Lngt),
				clumping: parsePercent(desc.clumping),
				thickness: parsePercent(desc.thickness),
				stiffness: parsePercent(desc.stiffness),
				physics: desc.physics,
				spacing: parsePercent(desc.Spcn),
				spacingOn: desc.Intr,
				flipX: desc.flipX,
				flipY: desc.flipY,
			};
		case 'dTips': {
			return {
				type: 'tips',
				angle: parseAngle(desc.Angl),
				size: parseUnitsToNumber(desc.Dmtr, 'Pixels'),
				shape: dynamicBrushShapeShapes[desc['Shp ']],
				physics: desc.physics,
				spacing: parsePercent(desc.Spcn),
				spacingOn: desc.Intr,
				flipX: desc.flipX,
				flipY: desc.flipY,
				// tips:
				tipsType: tipsBrushShapeShapes[desc.dtipsType],
				tipsLengthRatio: parsePercent(desc.dtipsLengthRatio),
				tipsHardness: parsePercent(desc.dtipsHardness),
				...(desc.dtipsGridSize && desc.dtipsErodibleTipHeightMap ? {
					tipsGridSize: desc.dtipsGridSize,
					tipsErodibleTipHeightMap: parseHeightmap(desc.dtipsErodibleTipHeightMap),
				} : {}),
				// airbrush
				tipsAirbrushCutoffAngle: desc.dtipsAirbrushCutoffAngle,
				tipsAirbrushGranularity: parsePercent(desc.dtipsAirbrushGranularity),
				tipsAirbrushStreakiness: parsePercent(desc.dtipsAirbrushStreakiness),
				tipsAirbrushSplatSize: parsePercent(desc.dtipsAirbrushSplatSize),
				tipsAirbrushSplatCount: desc.dtipsAirbrushSplatCount,
			};
		}
		default:
			console.log(require('util').inspect(desc, false, 99, true));
			throw new Error(`Unknown brush classId: ${(desc as any)._classID}`);
	}
}

function parseHeightmap(array: Uint8Array) {
	const result: number[] = [];
	for (let i = 0; i < array.byteLength; i++) {
		result.push(array[i]);
	}
	return result;
	// const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
	// const result: number[] = [];
	// for (let i = 0, len = (array.byteLength / 4) | 0; i < len; i++) {
	// 	result.push(view.getInt32(i * 4)); ????
	// }
	// return result;
}

export function readAbr(buffer: ArrayBufferView, options: { logMissingFeatures?: boolean; } = {}): Abr {
	const reader = createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength);
	const version = readInt16(reader);
	const samples: SampleInfo[] = [];
	const brushes: Brush[] = [];
	const patterns: PatternInfo[] = [];

	if (version === 1 || version === 2) {
		throw new Error(`Unsupported ABR version (${version})`); // TODO: ...
	} else if (version === 6 || version === 7 || version === 9 || version === 10) {
		const minorVersion = readInt16(reader);
		if (minorVersion !== 1 && minorVersion !== 2) throw new Error('Unsupported ABR minor version');

		while (reader.offset < reader.view.byteLength) {
			checkSignature(reader, '8BIM');
			const type = readSignature(reader) as 'samp' | 'desc' | 'patt' | 'phry';
			let size = readUint32(reader);
			const end = reader.offset + size;

			switch (type) {
				case 'samp': {
					while (reader.offset < end) {
						let brushLength = readUint32(reader);
						while (brushLength & 0b11) brushLength++; // pad to 4 byte alignment
						const brushEnd = reader.offset + brushLength;

						const id = readPascalString(reader, 1);

						// v1 - Skip the Int16 bounds rectangle and the unknown Int16.
						// v2 - Skip the unknown bytes.
						skipBytes(reader, minorVersion === 1 ? 10 : 264);

						const y = readInt32(reader);
						const x = readInt32(reader);
						const h = readInt32(reader) - y;
						const w = readInt32(reader) - x;
						if (w <= 0 || h <= 0) throw new Error('Invalid bounds');

						const bithDepth = readInt16(reader);
						const compression = readUint8(reader); // 0 - raw, 1 - RLE
						const alpha = new Uint8Array(w * h);

						if (bithDepth === 8) {
							if (compression === 0) {
								alpha.set(readBytes(reader, alpha.byteLength));
							} else if (compression === 1) {
								readDataRLE(reader, { width: w, height: h, data: alpha }, w, h, bithDepth, 1, [0], false);
							} else {
								throw new Error('Invalid compression');
							}
						} else if (bithDepth === 16) {
							if (compression === 0) {
								for (let i = 0; i < alpha.byteLength; i++) {
									alpha[i] = readUint16(reader) >> 8; // convert to 8bit values
								}
							} else if (compression === 1) {
								throw new Error('not implemented (16bit RLE)'); // TODO: ...
							} else {
								throw new Error('Invalid compression');
							}
						} else {
							throw new Error('Invalid depth');
						}

						samples.push({ id, bounds: { x, y, w, h }, alpha });
						reader.offset = brushEnd;
					}
					break;
				}
				case 'desc': {
					const desc: DescDescriptor = readVersionAndDescriptor(reader, true);
					// console.log(require('util').inspect(desc, false, 99, true));
					// require('fs').writeFileSync('test.log', require('util').inspect(desc, false, 99, false), 'utf8');

					for (const brush of desc.Brsh) {
						const b: Brush = {
							name: brush['Nm  '],
							shape: parseBrushShape(brush.Brsh),
							spacing: parsePercent(brush.Spcn),
							// TODO: brushGroup ???
							wetEdges: brush.Wtdg,
							noise: brush.Nose,
							// TODO: TxtC ??? smoothing / build-up ?
							// TODO: 'Rpt ' ???
							useBrushSize: brush.useBrushSize, // ???
						};

						if (brush.interpretation != null) b.interpretation = brush.interpretation;
						if (brush.protectTexture != null) b.protectTexture = brush.protectTexture;

						if (brush.useTipDynamics) {
							b.shapeDynamics = {
								tiltScale: parsePercent(brush.tiltScale),
								sizeDynamics: parseDynamics(brush.szVr),
								angleDynamics: parseDynamics(brush.angleDynamics),
								roundnessDynamics: parseDynamics(brush.roundnessDynamics),
								flipX: brush.flipX,
								flipY: brush.flipY,
								brushProjection: brush.brushProjection,
								minimumDiameter: parsePercent(brush.minimumDiameter),
								minimumRoundness: parsePercent(brush.minimumRoundness),
							};
						}

						if (brush.useScatter) {
							b.scatter = {
								count: brush['Cnt '],
								bothAxes: brush.bothAxes,
								countDynamics: parseDynamics(brush.countDynamics),
								scatterDynamics: parseDynamics(brush.scatterDynamics),
							};
						}

						if (brush.useTexture && brush.Txtr) {
							b.texture = {
								id: brush.Txtr.Idnt,
								name: brush.Txtr['Nm  '],
								blendMode: BlnM.decode(brush.textureBlendMode),
								depth: parsePercent(brush.textureDepth),
								depthMinimum: parsePercent(brush.minimumDepth),
								depthDynamics: parseDynamics(brush.textureDepthDynamics),
								scale: parsePercent(brush.textureScale),
								invert: brush.InvT,
								brightness: brush.textureBrightness,
								contrast: brush.textureContrast,
								textureEachTip: !!brush.TxtC,
							};
						}

						const db = brush.dualBrush;
						if (db && db.useDualBrush) {
							b.dualBrush = {
								flip: db.Flip,
								shape: parseBrushShape(db.Brsh),
								blendMode: BlnM.decode(db.BlnM),
								useScatter: db.useScatter,
								spacing: parsePercent(db.Spcn),
								count: db['Cnt '],
								bothAxes: db.bothAxes,
								countDynamics: parseDynamics(db.countDynamics),
								scatterDynamics: parseDynamics(db.scatterDynamics),
							};
						}

						if (brush.useColorDynamics) {
							b.colorDynamics = {
								foregroundBackground: parseDynamics(brush.clVr!),
								hue: parsePercent(brush['H   ']!),
								saturation: parsePercent(brush.Strt!),
								brightness: parsePercent(brush.Brgh!),
								purity: parsePercent(brush.purity!),
								perTip: brush.colorDynamicsPerTip!,
							};
						}

						if (brush.usePaintDynamics) {
							b.transfer = {
								flowDynamics: parseDynamics(brush.prVr!),
								opacityDynamics: parseDynamics(brush.opVr!),
								wetnessDynamics: parseDynamics(brush.wtVr!),
								mixDynamics: parseDynamics(brush.mxVr!),
							};
						}

						if (brush.useBrushPose) {
							b.brushPose = {
								overrideAngle: brush.overridePoseAngle!,
								overrideTiltX: brush.overridePoseTiltX!,
								overrideTiltY: brush.overridePoseTiltY!,
								overridePressure: brush.overridePosePressure!,
								pressure: parsePercent(brush.brushPosePressure!),
								tiltX: brush.brushPoseTiltX!,
								tiltY: brush.brushPoseTiltY!,
								angle: brush.brushPoseAngle!,
							};
						}

						const to = brush.toolOptions;
						if (to) {
							b.toolOptions = {
								type: toBrushType[to._classID] || 'brush',
								brushPreset: to.brushPreset,
								flow: to.flow ?? 100,
								smooth: to.Smoo ?? 0,
								mode: BlnM.decode(to['Md  '] || 'BlnM.Nrml'), // sometimes mode is missing
								opacity: to.Opct ?? 100,
								smoothing: !!to.smoothing,
								smoothingValue: to.smoothingValue || 0,
								smoothingRadiusMode: !!to.smoothingRadiusMode,
								smoothingCatchup: !!to.smoothingCatchup,
								smoothingCatchupAtEnd: !!to.smoothingCatchupAtEnd,
								smoothingZoomCompensation: !!to.smoothingZoomCompensation,
								pressureSmoothing: !!to.pressureSmoothing,
								usePressureOverridesSize: !!to.usePressureOverridesSize,
								usePressureOverridesOpacity: !!to.usePressureOverridesOpacity,
								useLegacy: !!to.useLegacy,
							};

							if (to.prVr) b.toolOptions.flowDynamics = parseDynamics(to.prVr);
							if (to.opVr) b.toolOptions.opacityDynamics = parseDynamics(to.opVr);
							if (to.szVr) b.toolOptions.sizeDynamics = parseDynamics(to.szVr);
							if ('wetness' in to) b.toolOptions.wetness = to.wetness;
							if ('dryness' in to) b.toolOptions.dryness = to.dryness;
							if ('mix' in to) b.toolOptions.mix = to.mix;
							if ('autoFill' in to) b.toolOptions.autoFill = to.autoFill;
							if ('autoClean' in to) b.toolOptions.autoClean = to.autoClean;
							if ('loadSolidColorOnly' in to) b.toolOptions.loadSolidColorOnly = to.loadSolidColorOnly;
							if ('sampleAllLayers' in to) b.toolOptions.sampleAllLayers = to.sampleAllLayers;
							if ('SmdF' in to) b.toolOptions.smudgeFingerPainting = to.SmdF;
							if ('SmdS' in to) b.toolOptions.smudgeSampleAllLayers = to.SmdS;
							if ('Prs ' in to) b.toolOptions.strength = to['Prs '];
							if ('SmdF' in to) b.toolOptions.smudgeFingerPainting = to.SmdF;
							if ('SmdS' in to) b.toolOptions.smudgeSampleAllLayers = to.SmdS;
						}

						brushes.push(b);
					}
					break;
				}
				case 'patt': {
					while (reader.offset < end) {
						patterns.push(readPattern(reader));
					}
					reader.offset = end;
					break;
				}
				case 'phry': {
					// TODO: what is this ?
					const desc: PhryDescriptor = readVersionAndDescriptor(reader);
					// example:
					// hierarchy: [
					// 	{
					// 		'Nm  ': 'PRE_EXPORT ',
					// 		zuid: '965209f2-6f35-9a40-aa43-485684382172'
					// 	},
					// 	{},
					//  ...
					// ]
					if (options.logMissingFeatures) {
						if (desc.hierarchy?.length) {
							// console.log('unhandled phry section', desc);
						}
					}
					break;
				}
				default:
					throw new Error(`Invalid brush type: ${type}`);
			}

			// align to 4 bytes
			while (size % 4) {
				reader.offset++;
				size++;
			}
		}
	} else {
		throw new Error(`Unsupported ABR version (${version})`);
	}

	return { samples, patterns, brushes };
}
