"""Clip tools: MIDI notes, clip management, scenes."""

import json

from ..connection import get_connection


def create_clip(track_index: int, clip_index: int, length: float = 4.0) -> str:
    """Create a new MIDI clip in a track's clip slot.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        length: Length of the clip in beats (default 4.0 = one bar at 4/4).
    """
    result = get_connection().send_command("create_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
        "length": length,
    })
    return json.dumps(result)


def create_session_audio_clip(track_index: int, clip_index: int, file_path: str) -> str:
    """Create a new audio clip in a clip slot from an audio file.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        file_path: Absolute path to the audio file to load.
    """
    result = get_connection().send_command("create_session_audio_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
        "file_path": file_path,
    })
    return json.dumps(result)


def set_clip_name(track_index: int, clip_index: int, name: str) -> str:
    """Set the name of a clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        name: New name for the clip.
    """
    result = get_connection().send_command("set_clip_name", {
        "track_index": track_index,
        "clip_index": clip_index,
        "name": name,
    })
    return json.dumps(result)


def delete_clip(track_index: int, clip_index: int) -> str:
    """Delete a clip from a clip slot.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("delete_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def add_notes_to_clip(track_index: int, clip_index: int, notes: list) -> str:
    """Add MIDI notes to an existing clip.

    Each note is a dict with: pitch (0-127 MIDI note number), start_time (beat position),
    duration (in beats), velocity (0-127), and mute (boolean).

    Prefer add_notes_extended when you need probability, velocity_deviation, or
    release_velocity per note.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        notes: List of note dicts, each with pitch, start_time, duration, velocity, mute.
    """
    result = get_connection().send_command("add_notes_to_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
        "notes": notes,
    })
    return json.dumps(result)


def get_notes_from_clip(track_index: int, clip_index: int) -> str:
    """Read all MIDI notes from a clip.

    Returns a list of notes with pitch, start_time, duration, velocity, and mute.

    Prefer get_notes_extended when you need probability, velocity_deviation, or
    release_velocity per note.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("get_notes_from_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def fire_clip(track_index: int, clip_index: int) -> str:
    """Start playing a clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("fire_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def stop_clip(track_index: int, clip_index: int) -> str:
    """Stop playing a clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("stop_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def fire_scene(scene_index: int) -> str:
    """Fire (launch) a scene, triggering all clips in that row.

    Args:
        scene_index: Zero-based index of the scene.
    """
    result = get_connection().send_command("fire_scene", {"scene_index": scene_index})
    return json.dumps(result)


def create_scene(index: int = -1) -> str:
    """Create a new scene in the session.

    Args:
        index: Position to insert the scene. Use -1 to append at the end.
    """
    result = get_connection().send_command("create_scene", {"index": index})
    return json.dumps(result)



def remove_notes_from_clip(
    track_index: int,
    clip_index: int,
    from_pitch: int = 0,
    pitch_span: int = 128,
    from_time: float = 0.0,
    time_span: float = None,
) -> str:
    """Remove MIDI notes from a clip within a pitch and time range.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        from_pitch: Starting pitch (0-127, default 0).
        pitch_span: Number of pitches to span (default 128 = all).
        from_time: Start time in beats (default 0.0).
        time_span: Time span in beats. If None, uses clip length.
    """
    params = {
        "track_index": track_index,
        "clip_index": clip_index,
        "from_pitch": from_pitch,
        "pitch_span": pitch_span,
        "from_time": from_time,
    }
    if time_span is not None:
        params["time_span"] = time_span
    result = get_connection().send_command("remove_notes_from_clip", params)
    return json.dumps(result)


def clear_clip_notes(track_index: int, clip_index: int) -> str:
    """Remove all MIDI notes from a clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("clear_clip_notes", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def set_clip_loop(
    track_index: int,
    clip_index: int,
    looping: bool = None,
    loop_start: float = None,
    loop_end: float = None,
    start_marker: float = None,
    end_marker: float = None,
) -> str:
    """Set clip loop and marker properties.

    All loop parameters are optional; only provided values are changed.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        looping: Enable or disable looping.
        loop_start: Loop start position in beats.
        loop_end: Loop end position in beats.
        start_marker: Start marker position in beats.
        end_marker: End marker position in beats.
    """
    params = {
        "track_index": track_index,
        "clip_index": clip_index,
    }
    if looping is not None:
        params["looping"] = looping
    if loop_start is not None:
        params["loop_start"] = loop_start
    if loop_end is not None:
        params["loop_end"] = loop_end
    if start_marker is not None:
        params["start_marker"] = start_marker
    if end_marker is not None:
        params["end_marker"] = end_marker
    result = get_connection().send_command("set_clip_loop", params)
    return json.dumps(result)


def duplicate_clip(
    track_index: int,
    clip_index: int,
    target_track_index: int,
    target_clip_index: int,
) -> str:
    """Duplicate a clip to another clip slot.

    Args:
        track_index: Zero-based index of the source track.
        clip_index: Zero-based index of the source clip slot.
        target_track_index: Zero-based index of the destination track.
        target_clip_index: Zero-based index of the destination clip slot.
    """
    result = get_connection().send_command("duplicate_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
        "target_track_index": target_track_index,
        "target_clip_index": target_clip_index,
    })
    return json.dumps(result)


def set_clip_color(track_index: int, clip_index: int, color_index: int) -> str:
    """Set the color of a clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        color_index: Ableton color index to set.
    """
    result = get_connection().send_command("set_clip_color", {
        "track_index": track_index,
        "clip_index": clip_index,
        "color_index": color_index,
    })
    return json.dumps(result)


def quantize_clip(
    track_index: int, clip_index: int, grid: int, amount: float = 1.0
) -> str:
    """Quantize notes in a clip to a grid.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        grid: RecordingQuantization enum value (1=1/4, 2=1/8, 3=1/8T, 4=1/8+T,
              5=1/16, 6=1/16T, 7=1/16+T, 8=1/32).
        amount: Quantize strength from 0.0 to 1.0 (default 1.0 = full quantize).
    """
    result = get_connection().send_command("quantize_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
        "grid": grid,
        "amount": amount,
    })
    return json.dumps(result)


def duplicate_clip_loop(track_index: int, clip_index: int) -> str:
    """Duplicate the loop content of a clip, doubling its length.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("duplicate_clip_loop", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def crop_clip(track_index: int, clip_index: int) -> str:
    """Crop a clip to its loop boundaries.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("crop_clip", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def set_clip_muted(track_index: int, clip_index: int, muted: bool) -> str:
    """Set whether a clip is muted (deactivated).

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        muted: True to mute, False to unmute.
    """
    result = get_connection().send_command("set_clip_muted", {
        "track_index": track_index,
        "clip_index": clip_index,
        "muted": muted,
    })
    return json.dumps(result)


def set_clip_gain(track_index: int, clip_index: int, gain: float) -> str:
    """Set the gain of an audio clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        gain: Gain value to set.
    """
    result = get_connection().send_command("set_clip_gain", {
        "track_index": track_index,
        "clip_index": clip_index,
        "gain": gain,
    })
    return json.dumps(result)


def set_clip_pitch(
    track_index: int, clip_index: int, coarse: int = None, fine: float = None
) -> str:
    """Set the pitch transposition of an audio clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        coarse: Coarse pitch in semitones (-48 to 48).
        fine: Fine pitch in cents (-50.0 to 50.0).
    """
    params = {
        "track_index": track_index,
        "clip_index": clip_index,
    }
    if coarse is not None:
        params["coarse"] = coarse
    if fine is not None:
        params["fine"] = fine
    result = get_connection().send_command("set_clip_pitch", params)
    return json.dumps(result)


def set_clip_launch_mode(track_index: int, clip_index: int, mode: int) -> str:
    """Set the launch mode of a clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        mode: Launch mode (0=Trigger, 1=Gate, 2=Toggle, 3=Repeat).
    """
    result = get_connection().send_command("set_clip_launch_mode", {
        "track_index": track_index,
        "clip_index": clip_index,
        "mode": mode,
    })
    return json.dumps(result)


def set_clip_warp_mode(track_index: int, clip_index: int, warp_mode: int) -> str:
    """Set the warp mode of an audio clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        warp_mode: Warp mode index to set.
    """
    result = get_connection().send_command("set_clip_warp_mode", {
        "track_index": track_index,
        "clip_index": clip_index,
        "warp_mode": warp_mode,
    })
    return json.dumps(result)


def get_clip_envelope(
    track_index: int,
    clip_index: int,
    device_index: int,
    param_index: int,
    num_steps: int = 16,
) -> str:
    """Sample the automation envelope for a device parameter in a Session clip.

    The Ableton API does not expose individual breakpoints, so this samples
    the envelope value at evenly-spaced time positions across the clip.
    Returns envelope_exists=False if no envelope is set for the parameter.
    Only works for Session clips (not Arrangement clips).

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        device_index: Zero-based index of the device on the track.
        param_index: Zero-based index of the parameter on the device.
        num_steps: Number of sample points across the clip (2-128, default 16).
    """
    result = get_connection().send_command("get_clip_envelope", {
        "track_index": track_index,
        "clip_index": clip_index,
        "device_index": device_index,
        "param_index": param_index,
        "num_steps": num_steps,
    })
    return json.dumps(result)


def insert_clip_envelope_step(
    track_index: int,
    clip_index: int,
    device_index: int,
    param_index: int,
    time: float,
    value: float,
    curve: float = 0.0,
) -> str:
    """Insert an automation breakpoint into a clip's parameter envelope.

    Writes a single automation point at the given time with the given value.
    Creates the envelope if it does not exist yet. Only works for Session clips.
    Note: Arrangement clips will return an error.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        device_index: Zero-based index of the device on the track.
        param_index: Zero-based index of the parameter on the device.
        time: Time position in beats to insert the automation point.
        value: Automation value to set at the given time.
        curve: Curve shape (0.0 = linear, default 0.0).
    """
    result = get_connection().send_command("insert_clip_envelope_step", {
        "track_index": track_index,
        "clip_index": clip_index,
        "device_index": device_index,
        "param_index": param_index,
        "time": time,
        "value": value,
        "curve": curve,
    })
    return json.dumps(result)


def clear_clip_envelope(
    track_index: int,
    clip_index: int,
    device_index: int,
    param_index: int,
) -> str:
    """Clear the automation envelope for a specific device parameter in a clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        device_index: Zero-based index of the device on the track.
        param_index: Zero-based index of the parameter on the device.
    """
    result = get_connection().send_command("clear_clip_envelope", {
        "track_index": track_index,
        "clip_index": clip_index,
        "device_index": device_index,
        "param_index": param_index,
    })
    return json.dumps(result)


def clear_all_clip_envelopes(track_index: int, clip_index: int) -> str:
    """Clear all automation envelopes in a clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("clear_all_clip_envelopes", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def get_notes_extended(track_index: int, clip_index: int) -> str:
    """Read all MIDI notes from a clip with extended properties.

    Returns notes with pitch, start_time, duration, velocity, mute,
    probability, velocity_deviation, and release_velocity.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("get_notes_extended", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def add_notes_extended(track_index: int, clip_index: int, notes: list) -> str:
    """Add MIDI notes with extended properties to a clip.

    Each note is a dict with: pitch (0-127), start_time (beat position),
    duration (in beats), velocity (0-127), and optionally probability (0.0-1.0),
    velocity_deviation (-127 to 127), release_velocity (0-127), mute (boolean).

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        notes: List of note dicts with pitch, start_time, duration, velocity,
               and optional probability, velocity_deviation, release_velocity, mute.
    """
    result = get_connection().send_command("add_notes_extended", {
        "track_index": track_index,
        "clip_index": clip_index,
        "notes": notes,
    })
    return json.dumps(result)


def modify_notes(track_index: int, clip_index: int, modifications: list) -> str:
    """Modify properties of existing MIDI notes in a clip.

    Each modification dict must include pitch and start_time to identify the note,
    plus any properties to change: velocity, probability, velocity_deviation,
    release_velocity, mute.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        modifications: List of modification dicts, each with pitch, start_time,
                       and properties to modify.
    """
    result = get_connection().send_command("modify_notes", {
        "track_index": track_index,
        "clip_index": clip_index,
        "modifications": modifications,
    })
    return json.dumps(result)


def get_clip_properties(track_index: int, clip_index: int) -> str:
    """Get comprehensive properties of a clip.

    Returns name, color, color_index, is_audio_clip, is_midi_clip, length,
    start_marker, end_marker, looping, loop_start, loop_end, launch_mode,
    launch_quantization, velocity_amount, muted, and has_groove (if available).
    For audio clips also returns file_path, ram_mode, gain, and gain_display_string.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("get_clip_properties", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def set_clip_ram_mode(track_index: int, clip_index: int, ram_mode: bool) -> str:
    """Set RAM mode for an audio clip.

    When enabled, the clip's audio is loaded into RAM for playback.
    Only available for audio clips.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        ram_mode: True to enable RAM mode, False to disable.
    """
    result = get_connection().send_command("set_clip_ram_mode", {
        "track_index": track_index,
        "clip_index": clip_index,
        "ram_mode": ram_mode,
    })
    return json.dumps(result)


def set_clip_velocity_amount(track_index: int, clip_index: int, velocity_amount: float) -> str:
    """Set the velocity-to-volume amount for a clip.

    Controls how much note velocity affects clip volume.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        velocity_amount: Velocity-to-volume amount (0.0 to 1.0).
    """
    result = get_connection().send_command("set_clip_velocity_amount", {
        "track_index": track_index,
        "clip_index": clip_index,
        "velocity_amount": velocity_amount,
    })
    return json.dumps(result)


def set_clip_warping(track_index: int, clip_index: int, warping: bool) -> str:
    """Enable or disable warping on an audio clip.

    Warping allows the clip to follow the session tempo. Only available for audio clips.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        warping: True to enable warping, False to disable.
    """
    result = get_connection().send_command("set_clip_warping", {
        "track_index": track_index,
        "clip_index": clip_index,
        "warping": warping,
    })
    return json.dumps(result)


def get_clip_playing_position(track_index: int, clip_index: int) -> str:
    """Get the current playback position and state of a clip.

    Returns playing_position (beat position within the clip), is_playing,
    and is_triggered (clip is queued to play).

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("get_clip_playing_position", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def get_clip_fades(track_index: int, clip_index: int) -> str:
    """Get the fade in and fade out lengths of an audio clip.

    Returns fade_in_length and fade_out_length in beats. Only available for audio clips.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
    """
    result = get_connection().send_command("get_clip_fades", {
        "track_index": track_index,
        "clip_index": clip_index,
    })
    return json.dumps(result)


def set_clip_fades(
    track_index: int,
    clip_index: int,
    fade_in_length: float = None,
    fade_out_length: float = None,
) -> str:
    """Set the fade in and/or fade out lengths of an audio clip.

    Only provided values are changed. Returns the final fade_in_length and
    fade_out_length. Only available for audio clips.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        fade_in_length: Fade in length in beats.
        fade_out_length: Fade out length in beats.
    """
    params = {
        "track_index": track_index,
        "clip_index": clip_index,
    }
    if fade_in_length is not None:
        params["fade_in_length"] = fade_in_length
    if fade_out_length is not None:
        params["fade_out_length"] = fade_out_length
    result = get_connection().send_command("set_clip_fades", params)
    return json.dumps(result)


def replace_all_notes(track_index: int, clip_index: int, notes: list) -> str:
    """Atomically replace all MIDI notes in a clip.

    Removes every existing note then adds the provided notes in one operation.
    Each note is a dict with pitch, start_time, duration, and optionally velocity,
    mute, probability, velocity_deviation, release_velocity.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        notes: List of note dicts with pitch, start_time, duration, velocity,
               and optional probability, velocity_deviation, release_velocity, mute.
    """
    result = get_connection().send_command("replace_all_notes", {
        "track_index": track_index,
        "clip_index": clip_index,
        "notes": notes,
    })
    return json.dumps(result)


def remove_notes_extended(
    track_index: int,
    clip_index: int,
    from_pitch: int,
    pitch_span: int,
    from_time: float,
    time_span: float,
) -> str:
    """Remove MIDI notes from a clip within an explicit pitch and time range.

    All four range parameters are required. Use from_pitch=0, pitch_span=128
    to span all pitches; use from_time=0 and time_span=999999 to span the
    full clip.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        from_pitch: Starting MIDI pitch (0-127).
        pitch_span: Number of pitches to cover (1-128).
        from_time: Start time in beats.
        time_span: Duration in beats to remove notes within.
    """
    result = get_connection().send_command("remove_notes_extended", {
        "track_index": track_index,
        "clip_index": clip_index,
        "from_pitch": from_pitch,
        "pitch_span": pitch_span,
        "from_time": from_time,
        "time_span": time_span,
    })
    return json.dumps(result)


def set_clip_properties(
    track_index: int,
    clip_index: int,
    name: str = None,
    color: int = None,
    mute: bool = None,
    gain: float = None,
    pitch_coarse: int = None,
    pitch_fine: float = None,
    looping: bool = None,
    loop_start: float = None,
    loop_end: float = None,
    start_marker: float = None,
    end_marker: float = None,
) -> str:
    """Set multiple clip properties in a single call.

    Only provided properties are changed; omitted ones are left unchanged.

    Args:
        track_index: Zero-based index of the track.
        clip_index: Zero-based index of the clip slot.
        name: New name for the clip.
        color: Ableton color palette index.
        mute: True to mute (deactivate) the clip, False to unmute.
        gain: Gain value for audio clips.
        pitch_coarse: Coarse pitch transposition in semitones (-48 to 48).
        pitch_fine: Fine pitch transposition in cents (-50.0 to 50.0).
        looping: Enable or disable looping.
        loop_start: Loop start position in beats.
        loop_end: Loop end position in beats.
        start_marker: Start marker position in beats.
        end_marker: End marker position in beats.
    """
    params = {"track_index": track_index, "clip_index": clip_index}
    if name is not None:
        params["name"] = name
    if color is not None:
        params["color"] = color
    if mute is not None:
        params["mute"] = mute
    if gain is not None:
        params["gain"] = gain
    if pitch_coarse is not None:
        params["pitch_coarse"] = pitch_coarse
    if pitch_fine is not None:
        params["pitch_fine"] = pitch_fine
    if looping is not None:
        params["looping"] = looping
    if loop_start is not None:
        params["loop_start"] = loop_start
    if loop_end is not None:
        params["loop_end"] = loop_end
    if start_marker is not None:
        params["start_marker"] = start_marker
    if end_marker is not None:
        params["end_marker"] = end_marker
    result = get_connection().send_command("set_clip_properties", params)
    return json.dumps(result)


TOOLS = [
    create_clip,
    create_session_audio_clip,
    set_clip_name,
    delete_clip,
    add_notes_to_clip,
    get_notes_from_clip,
    fire_clip,
    stop_clip,
    fire_scene,
    create_scene,
    remove_notes_from_clip,
    clear_clip_notes,
    set_clip_loop,
    duplicate_clip,
    set_clip_color,
    quantize_clip,
    duplicate_clip_loop,
    crop_clip,
    set_clip_muted,
    set_clip_gain,
    set_clip_pitch,
    set_clip_launch_mode,
    set_clip_warp_mode,
    get_clip_envelope,
    insert_clip_envelope_step,
    clear_clip_envelope,
    clear_all_clip_envelopes,
    get_notes_extended,
    add_notes_extended,
    modify_notes,
    get_clip_properties,
    set_clip_ram_mode,
    set_clip_velocity_amount,
    set_clip_warping,
    get_clip_playing_position,
    get_clip_fades,
    set_clip_fades,
    replace_all_notes,
    remove_notes_extended,
    set_clip_properties,
]
