import * as React from 'react';
import MusicalKeyboard, { StyledAccidentalKey, StyledNaturalKey, KeyboardMap } from '@theoryofnekomata/react-musical-keyboard';

const useFrequenciesForm = () => {
	const [baseFrequency, setBaseFrequency] = React.useState(440);
	const [baseKey, setBaseKey] = React.useState(69);
	const [stretchFactorNumerator, setStretchFactorNumerator] = React.useState(12.125);
	const [audioContext, setAudioContext] = React.useState<AudioContext>();
	const [keyChannels, setKeyChannels] = React.useState([] as { key: number, velocity: number, channel: number, oscillator: OscillatorNode }[]);
	const [gain, setGain] = React.useState<GainNode>();
	const [equalDivisionOfTheOctave] = React.useState(12);

	const handleBaseKeyChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => {
		setBaseKey(e.currentTarget.valueAsNumber);
	};

	const handleBaseFrequencyChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => {
		setBaseFrequency(e.currentTarget.valueAsNumber);
	};

	const handleStretchFactorFineChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => {
		const { form, valueAsNumber } = e.currentTarget;

		setStretchFactorNumerator(valueAsNumber);

		if (!form) {
			return;
		}

		const coarse = form.elements.namedItem('stretchFactorNumeratorCoarse');
		if (!coarse) {
			return;
		}

		if (!('value' in coarse)) {
			return;
		}

		coarse.value = valueAsNumber.toString();
	};

	const handleStretchFactorCoarseChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => {
		const { form, valueAsNumber } = e.currentTarget;

		setStretchFactorNumerator(valueAsNumber);

		if (!form) {
			return;
		}

		const fine = form.elements.namedItem('stretchFactorNumeratorFine');
		if (!fine) {
			return;
		}

		if (!('value' in fine)) {
			return;
		}

		fine.value = valueAsNumber.toString();
	};

	React.useEffect(() => {
		const audioContext = new AudioContext();
		setAudioContext(audioContext);

		const gainNode = audioContext.createGain();
		gainNode?.gain.setValueAtTime(0.05, audioContext.currentTime);
		setGain(gainNode);

		gainNode.connect(audioContext.destination);
	}, []);

	const playSound = (keys: { velocity: number, channel: number, key: number }[]) => {
		if (!audioContext) {
			return;
		}

		if (!gain) {
			return;
		}

		setKeyChannels((oldOscillators) => {
			const activeKeys = keys.map(k => `${k.channel}:${k.key}`);
			const oscillatorsToCancel = oldOscillators.filter((k) => !activeKeys.includes(`${k.channel}:${k.key}`));
			for (let i = 0; i < oscillatorsToCancel.length; i += 1) {
				oldOscillators[i]?.oscillator?.stop();
				oldOscillators[i]?.oscillator?.disconnect();
			}

			return keys.map((k) => {
				const existingOscillator = oldOscillators.find((o) => o.key === k.key && o.channel === k.channel);
				if (existingOscillator) {
					return existingOscillator;
				}

				const oscillator = audioContext.createOscillator();
				const f = (
					baseFrequency * (
						2 ** (
							1 / equalDivisionOfTheOctave
						)
					) ** (
						(
							k.key - baseKey
						) * (
							stretchFactorNumerator / equalDivisionOfTheOctave
						)
					)
				);
				oscillator?.frequency.setValueAtTime(f, audioContext.currentTime);
				oscillator.type = 'square';
				oscillator.connect(gain);
				oscillator.start();
				return {
					...k,
					oscillator,
				};
			});

		});
	};

	return {
		baseFrequency,
		baseKey,
		stretchFactorNumerator,
		handleBaseFrequencyChange,
		handleBaseKeyChange,
		handleStretchFactorCoarseChange,
		handleStretchFactorFineChange,
		equalDivisionOfTheOctave,
		playSound,
		keyChannels,
	};
};

const PITCH_CLASSES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] as const;

export const FrequenciesForm = () => {
	const [showForm, setShowForm] = React.useState(false);
	const {
		handleStretchFactorCoarseChange,
		handleStretchFactorFineChange,
		baseKey,
		baseFrequency,
		handleBaseFrequencyChange,
		handleBaseKeyChange,
		stretchFactorNumerator,
		equalDivisionOfTheOctave,
		playSound,
		keyChannels,
	} = useFrequenciesForm();

	React.useEffect(() => {
		setShowForm(true);
	}, []);

	React.useEffect(() => {

	}, []);

	return (
		<div>
			{showForm && (
				<div className="print-hidden">
					<form>
						<input
							type="number"
							defaultValue={baseFrequency}
							name="baseFrequency"
							onChange={handleBaseFrequencyChange}
						/>
						<input
							type="number"
							defaultValue={baseKey}
							name="baseKey"
							onChange={handleBaseKeyChange}
						/>
						<input
							type="range"
							min={equalDivisionOfTheOctave * 0.95}
							max={equalDivisionOfTheOctave * 1.05}
							defaultValue={stretchFactorNumerator}
							name="stretchFactorNumeratorFine"
							onChange={handleStretchFactorFineChange}
							step="any"
						/>
						<input
							type="number"
							min={equalDivisionOfTheOctave * 0.95}
							max={equalDivisionOfTheOctave * 1.05}
							defaultValue={stretchFactorNumerator}
							name="stretchFactorNumeratorCoarse"
							onChange={handleStretchFactorCoarseChange}
							step="any"
						/>
					</form>
					<div style={{ position: 'relative', backgroundColor: 'black', }}>
						{/* @ts-ignore */}
						<MusicalKeyboard
							hasMap
							startKey={0}
							endKey={127}
							height={50}
							keyComponents={{
								accidental: StyledAccidentalKey,
								natural: StyledNaturalKey,
							}}
							keyChannels={keyChannels}
						>
							<KeyboardMap
								channel={0}
								onChange={playSound}
							/>
						</MusicalKeyboard>
					</div>
				</div>
			)}

			<table
				className="alternate-rows"
				style={{
					fontSize: '0.75em',
				}}
			>
				<caption>
					MIDI note frequencies and their stretched counterparts
					(base frequency={baseFrequency} Hz for key #{baseKey}, stretch factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}).
					First figures indicate non-stretched frequencies, second figures indicate stretched frequencies. Parenthesized figures represent difference
					between the frequencies. MIDI note 60 = Middle C.
				</caption>
				<thead>
					<tr>
						<th rowSpan={2}>
							Octave
						</th>
						<th colSpan={12}>
							Pitch Class
						</th>
					</tr>
					<tr>
						{PITCH_CLASSES.map((c) => (
							<th
								key={c}
								style={{ textAlign: 'right' }}
							>
								{c}
							</th>
						))}
					</tr>
				</thead>
				<tbody>
					{new Array(11).fill(0).map((_, octave) => {
						return (
							<tr>
								<th>{octave}</th>
								{PITCH_CLASSES.map((_, pitchClassIndex) => {
									const i = (octave * PITCH_CLASSES.length) + pitchClassIndex;
									const nonStretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** (i - baseKey));
									const stretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** ((i - baseKey) * (stretchFactorNumerator / equalDivisionOfTheOctave)));
									const difference = (stretched - nonStretched);
									return (
										<td
											style={{ textAlign: 'right' }}
										>
											{nonStretched.toFixed(2)}
											<br />{' '}
											{stretched.toFixed(2)}
											<br />{' '}
											<small>
												({difference.toFixed(2)})
											</small>
										</td>
									);
								})}
							</tr>
						);
					})}
				</tbody>
			</table>
			{false && (
				<table>
					<caption>
						MIDI note frequencies and their stretched counterparts
						(base frequency={baseFrequency} Hz for key #{baseKey}, stretch factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)})
					</caption>
					<thead>
						<tr>
							<th>
								MIDI Note Number
							</th>
							<th>
								Key
							</th>
							<th>
								Frequency
								<br />
								{' '}
								(non-stretched, in Hz)
							</th>
							<th>
								Frequency
								<br />
								{' '}
								(stretched, in Hz)
							</th>
							<th>
								Difference (in Hz)
							</th>
						</tr>
					</thead>
					<tbody>
						{new Array(128).fill(0).map((_, i) => {
							const nonStretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** (i - baseKey));
							const stretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** ((i - baseKey) * (stretchFactorNumerator / equalDivisionOfTheOctave)));
							const difference = (stretched - nonStretched);
							return (
								<tr key={i}>
									<th>
										{i}
									</th>
									<th>
										{PITCH_CLASSES[i % PITCH_CLASSES.length]}
										{Math.floor(i / PITCH_CLASSES.length)}
									</th>
									<td style={{ textAlign: 'right' }}>
										{nonStretched.toFixed(5)}
									</td>
									<td style={{ textAlign: 'right' }}>
										{stretched.toFixed(5)}
									</td>
									<td style={{ textAlign: 'right' }}>
										{difference.toFixed(5)}
									</td>
								</tr>
							);
						})}
					</tbody>
				</table>
			)}
		</div>
	);
};
