# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors

# This file is part of Power Sequencer.

from math import floor, sqrt
from operator import attrgetter

import bpy

from .global_settings import SequenceTypes

max_channel = 32
min_channel = 1


def calculate_distance(x1, y1, x2, y2):
    return sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)


def convert_duration_to_frames(context, duration):
    return round(duration * context.scene.render.fps / context.scene.render.fps_base)


def find_linked(context, sequences, selected_sequences):
    """
    Takes a list of sequences and returns a list of all the sequences
    and effects that are linked in time
    Args:
        - sequences: a list of sequences
    Returns a list of all the linked sequences, but not the sequences passed to the function
    """
    start, end = get_frame_range(sequences, selected_sequences)
    sequences_in_range = [s for s in sequences if is_in_range(context, s, start, end)]
    effects = (s for s in sequences_in_range if s.type in SequenceTypes.EFFECT)
    selected_effects = (s for s in sequences if s.type in SequenceTypes.EFFECT)

    linked_sequences = []

    # Filter down to effects that have at least one of seq as input and
    # Append input sequences that aren't in the source list to linked_sequences
    for e in effects:
        if not hasattr(e, "input_2"):
            continue
        for s in sequences:
            if e.input_2 == s:
                linked_sequences.append(e)
                if e.input_1 not in sequences:
                    linked_sequences.append(e.input_1)
            elif e.input_1 == s:
                linked_sequences.append(e)
                if e.input_2 not in sequences:
                    linked_sequences.append(e.input_2)

    # Find inputs of selected effects that are not selected
    for e in selected_effects:
        try:
            if e.input_1 not in sequences:
                linked_sequences.append(e.input_1)
            if e.input_count == 2:
                if e.input_2 not in sequences:
                    linked_sequences.append(e.input_2)
        except AttributeError:
            continue

    return linked_sequences


def find_neighboring_markers(context, frame=None):
    """Returns a tuple containing the closest marker to the left and to the right of the frame"""
    markers = context.scene.timeline_markers

    if not (frame and markers):
        return None, None

    markers = sorted(markers, key=attrgetter("frame"))

    previous_marker, next_marker = None, None
    for m in markers:
        previous_marker = m if m.frame < frame else previous_marker
        if m.frame > frame:
            next_marker = m
            break

    return previous_marker, next_marker


def find_sequences_after(context, sequence):
    """
    Finds the strips following the sequences passed to the function
    Args:
        - Sequences, the sequences to check
    Returns all the strips after the sequence in the current context
    """
    return [s for s in context.sequences if s.frame_final_start > sequence.frame_final_start]


def find_snap_candidate(context, frame=0):
    """
    Returns the cut frame closest to the `frame` argument
    """
    snap_candidate = 1000000

    for s in context.sequences:
        start_to_frame = frame - s.frame_final_start
        end_to_frame = frame - s.frame_final_end

        distance_to_start = abs(start_to_frame)
        distance_to_end = abs(end_to_frame)

        candidate = (
            frame - start_to_frame
            if min(distance_to_start, distance_to_end) == distance_to_start
            else frame - end_to_frame
        )

        if abs(frame - candidate) < abs(frame - snap_candidate):
            snap_candidate = candidate

    return snap_candidate


def find_strips_mouse(context, frame, channel, select_linked=False):
    """
    Finds a list of sequences to select based on the frame and channel the mouse cursor is at
    Args:
        - frame: the frame the mouse or cursor is on
        - channel: the channel the mouse is hovering
        - select_linked: find and append the sequences linked in time if True
    Returns the sequence(s) under the mouse cursor as a list
    Returns an empty list if nothing found
    """
    sequences = [
        s
        for s in context.sequences
        if not s.lock and s.channel == channel and s.frame_final_start <= frame <= s.frame_final_end
    ]
    if select_linked:
        linked_strips = [
            s
            for s in context.sequences
            if s.frame_final_start == sequences[0].frame_final_start
            and s.frame_final_end == sequences[0].frame_final_end
        ]
        sequences.extend(linked_strips)
    return sequences


def get_frame_range(sequences, get_from_start=False):
    """
    Returns a tuple with the minimum and maximum frames of the
    list of passed sequences.
    Args:
        - sequences, the sequences to use
        - get_from_start, the returned start frame is set to 1 if
        this boolean is True
    """
    start, end = -1, -1
    start = (
        1
        if get_from_start
        else min(sequences, key=attrgetter("frame_final_start")).frame_final_start
    )
    end = max(sequences, key=attrgetter("frame_final_end")).frame_final_end
    return start, end


def get_channel_range(sequences):
    """
    Returns a tuple with the minimum and maximum channels of the
    list of passed sequences.
    """
    start = min(sequences, key=attrgetter("channel")).channel
    end = max(sequences, key=attrgetter("channel")).channel
    return start, end


def get_mouse_frame_and_channel(context, event):
    """
    Convert mouse coordinates from the event, from
    pixels to frame, channel.
    Returns a tuple of frame, channel as integers
    """
    view2d = context.region.view2d
    frame, channel = view2d.region_to_view(event.mouse_region_x, event.mouse_region_y)
    return round(frame), floor(channel)


def is_in_range(context, sequence, start, end):
    """
    Checks if a single sequence's start or end is in the range
    Args:
        - sequence: the sequence to check for
        - start, end: the start and end frames
    Returns True if the sequence is within the range, False otherwise
    """
    s_start = sequence.frame_final_start
    s_end = sequence.frame_final_end
    return start <= s_start <= end or start <= s_end <= end


def set_preview_range(context, start, end):
    """Sets the preview range and timeline render range"""
    if not (start and end) and start != 0:
        raise AttributeError("Missing start or end parameter")

    scene = context.scene

    scene.frame_start = start
    scene.frame_end = end
    scene.frame_preview_start = start
    scene.frame_preview_end = end


def slice_selection(context, sequences, range_block=0):
    """
    Takes a list of sequences and breaks it down
    into multiple lists of connected sequences
    Returns a list of lists of sequences,
    each list corresponding to a block of sequences
    that are connected in time and sorted by frame_final_start
    """
    if not sequences:
        return []

    # Indicates the index number of the lists from the "broken_selection" list
    index = -1
    block_end = 0
    broken_selection = []
    sorted_sequences = sorted(sequences, key=attrgetter("frame_final_start"))

    for s in sorted_sequences:
        if not broken_selection or (block_end + 1 + range_block < s.frame_final_start):
            broken_selection.append([s])
            block_end = s.frame_final_end
            index += 1
            continue
        block_end = max(block_end, s.frame_final_end)
        broken_selection[index].append(s)

    return broken_selection


def trim_strips(context, frame_start, frame_end, to_trim, to_delete=[]):
    """
    Removes the footage and audio between frame_start and frame_end.
    You must pass the list of strips to trim to the function for it to work.
    """
    trim_start = min(frame_start, frame_end)
    trim_end = max(frame_start, frame_end)

    to_trim = [s for s in to_trim if s.type in SequenceTypes.CUTABLE]
    initial_selection = context.selected_sequences

    for s in to_trim:
        strips_in_target_channel = []
        # Cut strip longer than the trim range in three
        is_strip_longer_than_trim_range = (
            s.frame_final_start < trim_start and s.frame_final_end > trim_end
        )
        if is_strip_longer_than_trim_range:
            bpy.ops.sequencer.select_all(action="DESELECT")
            s.select = True
            bpy.ops.sequencer.split(frame=trim_start, type="SOFT", side="RIGHT")
            bpy.ops.sequencer.split(frame=trim_end, type="SOFT", side="LEFT")
            to_delete.append(context.selected_sequences[0])

            for c in context.sequences:
                if c.channel == s.channel:
                    strips_in_target_channel.append(c)

            if s in initial_selection:
                initial_selection.append(strips_in_target_channel[0])
            continue

        # Resize strips that overlap the trim range
        elif s.frame_final_start < trim_end and s.frame_final_end > trim_end:
            s.frame_final_start = trim_end
        elif s.frame_final_end > trim_start and s.frame_final_start < trim_start:
            s.frame_final_end = trim_start

    for s in to_delete:
        bpy.context.sequences.remove(s)

    for s in initial_selection:
        s.select = True
    return {"FINISHED"}


def find_closest_surrounding_cuts(context, frame):
    """
    Returns a tuple of (strip_before, strip_after), the two closest sequences around a gap.
    If the frame is in the middle of a strip, both strips may be the same.
    """
    strip_before = max(
        context.sequences,
        key=lambda s: s.frame_final_end
        if s.frame_final_end <= frame
        else s.frame_final_start
        if s.frame_final_start <= frame
        else 0,
    )
    strip_after = min(
        context.sequences,
        key=lambda s: s.frame_final_start
        if s.frame_final_start >= frame
        else s.frame_final_end
        if s.frame_final_end >= frame
        else 1000000,
    )
    return strip_before, strip_after


def find_closest_surrounding_cuts_frames(context, frame):
    before, after = find_closest_surrounding_cuts(context, frame)
    if before == after:
        frame_left, frame_right = before.frame_final_start, before.frame_final_end
    else:
        frame_left, frame_right = before.frame_final_end, after.frame_final_start
    return frame_left, frame_right


def get_sequences_under_cursor(context):
    frame = context.scene.frame_current
    under_cursor = [
        s
        for s in context.sequences
        if s.frame_final_start <= frame and s.frame_final_end >= frame and not s.lock
    ]
    return under_cursor


def ripple_move(context, sequences, duration_frames, delete=False):
    """Moves sequences in the list and ripples the change to all sequences after them, in the corresponding channels
    The `duration_frames` can be positive or negative.
    If `delete` is True, deletes every sequence in `sequences`.
    """
    channels = {s.channel for s in sequences}
    first_strip = min(sequences, key=lambda s: s.frame_final_start)
    to_ripple = [
        s
        for s in context.sequences
        if s.channel in channels and s.frame_final_start >= first_strip.frame_final_start
    ]

    if delete:
        bpy.ops.sequencer.select_all(action="DESELECT")
        for s in sequences:
            s.select = True
        bpy.ops.sequencer.delete()
    else:
        to_ripple = set(to_ripple + sequences)

    move_selection(context, to_ripple, duration_frames, 0)


def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=True):
    """
    Returns a tuple of two lists: (strips_inside_range, strips_overlapping_range)
    strips_inside_range are strips entirely contained in the frame range.
    strips_overlapping_range are strips that only overlap the frame range.
    Args:
        - frame_start, the start of the frame range
        - frame_end, the end of the frame range
        - sequences (optional): only work with these sequences.
        If it doesn't receive any, the function works with all the sequences in the current context
        - find_overlapping (optional): find and return a list of strips that overlap the
        frame range
    """
    strips_inside_range = []
    strips_overlapping_range = []
    if not sequences:
        sequences = bpy.context.sequences
    for s in sequences:
        if (
            frame_start <= s.frame_final_start <= frame_end
            and frame_start <= s.frame_final_end <= frame_end
        ):
            strips_inside_range.append(s)
        elif find_overlapping:
            if (
                frame_start < s.frame_final_end <= frame_end
                or frame_start <= s.frame_final_start < frame_end
            ):
                strips_overlapping_range.append(s)

        if find_overlapping:
            if s.frame_final_start < frame_start and s.frame_final_end > frame_end:
                strips_overlapping_range.append(s)
    return strips_inside_range, strips_overlapping_range


def move_selection(context, sequences, frame_offset, channel_offset=0):
    """Offsets the selected `sequences` horizontally and vertically and preserves
    the current selected sequences.
    """
    if not sequences:
        return
    initial_selection = context.selected_sequences
    bpy.ops.sequencer.select_all(action="DESELECT")
    for s in sequences:
        s.select = True
    bpy.ops.transform.seq_slide(value=(frame_offset, channel_offset))
    bpy.ops.sequencer.select_all(action="DESELECT")
    for s in initial_selection:
        s.select = True
