import wave
from typing import List, NamedTuple, Tuple

import numpy as np
import numpy.typing as npt


class Sound(NamedTuple):
    """Audio sample- and meta- data."""

    data: np.ndarray
    rate: int
    width: int = 4


Correction = List[Tuple[float, float]]
"""List of (frequency, db) tuples."""


def write_wav(path: str, data: npt.ArrayLike, rate: int, width: int = 4):
    """
    Write n-channel float array with values between -1 and 1 to WAV file

    Params:
      path: Filename of WAV file.
      data: Sound sample data array.
      rate: Sample rate in Hz.
      width: Sample width in bytes.
    """
    if width not in [1, 2, 3, 4]:
        raise ValueError(f'Invalid sample width: {width}')
    data = np.asarray(data)
    ch = 1 if len(data.shape) < 2 else len(data)
    if width == 4:
        arr = np.empty_like(data, np.int32)
        np.rint((2 ** 31 - 1) * data, out=arr, casting='unsafe')
    elif width == 3:
        arr = np.empty_like(data, np.int32)
        np.rint((2 ** 31 - 2 ** 8) * data, out=arr, casting='unsafe')
        arr = arr.flatten(order='F').view(np.uint8)
        # Drop every 4th byte.
        arr = np.vstack([arr[1::4], arr[2::4], arr[3::4]])
    elif width == 2:
        arr = np.empty_like(data, np.int16)
        np.rint((2 ** 15 - 1) * data, out=arr, casting='unsafe')
    else:
        arr = np.empty_like(data, np.int8)
        np.rint((2 ** 7 - 1) * data, out=arr, casting='unsafe')
    with wave.open(path, 'wb') as wav:
        wav.setnchannels(ch)
        wav.setsampwidth(width)
        wav.setframerate(rate)
        wav.writeframes(arr.tobytes(order='F'))


def read_wav(path: str) -> Sound:
    """Read WAV file and return float32 arrays between -1 and 1."""
    with wave.open(path, 'rb') as wav:
        ch, width, rate, n, _, _ = wav.getparams()
        frames = wav.readframes(n)
    if width == 4:
        buff = np.frombuffer(frames, np.int32)
        norm = 2 ** 31 - 1
    elif width == 3:
        buff = np.frombuffer(frames, np.uint8)
        uints = buff[0::3].astype(np.uint32) << 8 \
            | buff[1::3].astype(np.uint32) << 16 \
            | buff[2::3].astype(np.uint32) << 24
        buff = uints.view(np.int32)
        norm = 2 ** 31 - 2 ** 8
    elif width == 2:
        buff = np.frombuffer(frames, np.int16)
        norm = 2 ** 15 - 1
    else:
        buff = np.frombuffer(frames, np.int8)
        norm = 2 ** 7 - 1
    data = buff.astype('f')
    data /= np.float32(norm)
    data = data.reshape((-1, ch)).T
    return Sound(data, rate, width)


def write_correction(path: str, correction: Correction):
    """Write (frequency, db) tuples to a space-separated file."""
    txt = '\n'.join(f'{freq} {db}' for freq, db in correction)
    with open(path, 'w') as f:
        f.write(txt)


def read_correction(path: str) -> Correction:
    """Read (frequency, db) tuples from comma- or space-separated file."""
    corr = []
    with open(path, 'r') as f:
        for line in f.readlines():
            try:
                freq, db, *_ = line.split(',' if ',' in line else None)
                corr.append((float(freq), float(db)))
            except ValueError:
                pass
    return corr
