module Spotify
  class API
    # @!group Playlist

    # @param [Playlist] playlist
    # @return [Boolean] true if playlist is loaded
    # @method playlist_is_loaded(playlist)
    attach_function :playlist_is_loaded, [ Playlist ], :bool

    # Attach callbacks to the playlist, used for getting change notifications.
    #
    # @example
    #   callbacks = Spotify::PlaylistCallbacks.new({
    #     tracks_added: proc do |playlist, tracks_pointer, count, position|
    #       puts "#{count} tracks added at #{position}."
    #     end,
    #     playlist_renamed: proc { |playlist| puts "Playlist renamed!" },
    #   })
    #   Spotify.playlist_add_callbacks(playlist, callbacks, nil) # => ok
    #
    # @note it is *very* important that the callbacks are not garbage collected before they are called!
    # @param [Playlist] playlist
    # @param [PlaylistCallbacks] playlist_callbacks
    # @param [FFI::Pointer] userdata
    # @return [Symbol] error code
    # @method playlist_add_callbacks(playlist, playlist_callbacks, userdata)
    attach_function :playlist_add_callbacks, [ Playlist, PlaylistCallbacks.by_ref, :userdata ], APIError

    # Remove playlist callbacks previously added with {#playlist_add_callbacks}.
    #
    # @see #playlist_add_callbacks
    # @param [Playlist] playlist
    # @param [PlaylistCallbacks] playlist_callbacks
    # @param [FFI::Pointer] userdata
    # @return [Symbol] error code
    # @method playlist_remove_callbacks(playlist, playlist_callbacks, userdata)
    attach_function :playlist_remove_callbacks, [ Playlist, PlaylistCallbacks.by_ref, :userdata ], APIError

    # @see #playlist_track
    # @note if playlist is not loaded, the function always return 0.
    # @param [Playlist] playlist
    # @return [Integer] number of tracks in the playlist
    # @method playlist_num_tracks(playlist)
    attach_function :playlist_num_tracks, [ Playlist ], :int

    # @see #playlist_num_tracks
    # @note if index is out of range, the function always return nil.
    # @param [Playlist] playlist
    # @param [Integer] index number between 0...{#playlist_num_tracks}
    # @return [Track, nil] track at index
    # @method playlist_track(playlist, index)
    attach_function :playlist_track, [ Playlist, :int ], Track

    # @see #playlist_num_tracks
    # @note if index is out of range, the function always return -1.
    # @param [Playlist] playlist
    # @param [Integer] index number between 0...{#playlist_num_tracks}
    # @return [Integer] time in seconds since unix epoch that track was added at index in the playlist
    # @method playlist_track_create_time(playlist, index)
    attach_function :playlist_track_create_time, [ Playlist, :int ], :int

    # @see #playlist_num_tracks
    # @note if index is out of range, the function always return nil.
    # @param [Playlist] playlist
    # @param [Integer] index number between 0...{#playlist_num_tracks}
    # @return [User, nil] user that added the track at index in the playlist
    # @method playlist_track_creator(playlist, index)
    attach_function :playlist_track_creator, [ Playlist, :int ], User

    # @see #playlist_num_tracks
    # @see #playlist_track_set_seen
    # @note if index is out of range, the function always return false.
    # @param [Playlist] playlist
    # @param [Integer] index number between 0...{#playlist_num_tracks}
    # @return [Boolean] true if playlist has been marked as seen with {#playlist_track_set_seen}.
    # @method playlist_track_seen(playlist, index)
    attach_function :playlist_track_seen, [ Playlist, :int ], :bool

    # Set `seen` flag on track. The flag can be retrieved by {#playlist_track_seen}.
    #
    # @see #playlist_num_tracks
    # @see #playlist_track_seen
    # @param [Playlist] playlist
    # @param [Integer] index number between 0...{#playlist_num_tracks}
    # @param [Boolean] seen
    # @return [Symbol] error code
    # @method playlist_track_set_seen(playlist, index, seen)
    attach_function :playlist_track_set_seen, [ Playlist, :int, :bool ], APIError

    # @see #playlist_num_tracks
    # @note if index is out of range, the function always return nil.
    # @param [Playlist] playlist
    # @param [Integer] index number between 0...{#playlist_num_tracks}
    # @return [String] message attached to a playlist item
    # @method playlist_track_message(playlist, index)
    attach_function :playlist_track_message, [ Playlist, :int ], UTF8String

    # @see #playlist_is_loaded
    # @note if playlist is not loaded, the function always return an empty string.
    # @param [Playlist] playlist
    # @return [String] name of the playlist
    # @method playlist_name(playlist)
    attach_function :playlist_name, [ Playlist ], UTF8String

    # Rename the playlist.
    #
    # @see #playlist_is_loaded
    # @note if playlist is not loaded, the function always return :permission_denied.
    # @param [Playlist] playlist
    # @param [String] new_name new name of the playlist
    # @return [Symbol] error code
    # @method playlist_rename(playlist, new_name)
    attach_function :playlist_rename, [ Playlist, UTF8String ], APIError

    # @param [Playlist] playlist
    # @return [User] owner of the playlist
    # @method playlist_owner(playlist)
    attach_function :playlist_owner, [ Playlist ], User

    # @see #playlist_is_loaded
    # @see #playlist_set_collaborative
    # @note if {#playlist_set_collaborative} was used, the final value will not be
    #       visible until after libspotify has negotiated with Spotify backend.
    # @note if playlist is not loaded, the function always return false.
    # @param [Playlist] playlist
    # @return [Boolean] true if the playlist is collaborative, i.e. editable by others.
    # @method playlist_is_collaborative(playlist)
    attach_function :playlist_is_collaborative, [ Playlist ], :bool

    # Set collaborative status on a playlist.
    #
    # @see #playlist_is_collaborative
    # @note the function always return :ok.
    # @param [Playlist] playlist
    # @param [Boolean] collaborative
    # @return [Symbol] error code
    # @method playlist_set_collaborative(playlist, collaborative)
    attach_function :playlist_set_collaborative, [ Playlist, :bool ], APIError

    # Set autolinking state for a playlist.
    #
    # If a playlist is set to autolink, unplayable tracks will be made playable by
    # linking them to an equivalent playable track when possible.
    #
    # @note the function always return :ok.
    # @param [Playlist] playlist
    # @param [Boolean] autolink
    # @return [Symbol] error code
    # @method playlist_set_autolink_tracks(playlist, autolink)
    attach_function :playlist_set_autolink_tracks, [ Playlist, :bool ], APIError

    # @see #playlist_is_loaded
    # @note if the playlist is not loaded, the function always return nil.
    # @param [Playlist] playlist
    # @return [String, nil] playlist description, if available
    # @method playlist_get_description(playlist)
    attach_function :playlist_get_description, [ Playlist ], UTF8String

    # Retrieve playlist image ID as a string.
    #
    # @example
    #   Spotify.playlist_get_image(playlist) # =>
    #
    # @see #playlist_is_loaded
    # @see #image_create
    # @note if the playlist is not loaded, the function always return nil.
    # @param [Playlist] playlist
    # @return [String, nil] image ID for playlist image, or nil if no image available.
    # @method playlist_get_image(playlist)
    attach_function :playlist_get_image, [ Playlist, :buffer_out ], :bool do |playlist|
      with_buffer(Spotify::ImageID) do |image_id_buffer|
        if sp_playlist_get_image(playlist, image_id_buffer)
          ImageID.from_native(image_id_buffer, nil)
        end
      end
    end

    # @see #playlist_is_loaded
    # @note if the playlist is not loaded, the function always return true.
    # @param [Playlist] playlist
    # @return [Boolean] true if the playlist has local changes that have not yet been acknowledged by Spotify backend
    # @method playlist_has_pending_changes(playlist)
    attach_function :playlist_has_pending_changes, [ Playlist ], :bool

    # Add tracks to the playlist.
    #
    # @example single track
    #   Spotify.playlist_add_tracks(playlist, track, offset, session) # => :ok
    #
    # @example array of tracks
    #   Spotify.playlist_add_tracks(playlist, tracks, offset, session) # => :ok
    #
    # @see #playlist_is_loaded
    # @note if the playlist is not loaded, the function always return an error.
    #
    # @param [Playlist] playlist
    # @param [Array<Track>, Track] tracks
    # @param [Integer] offset starting index to add tracks from
    # @param [Session] session
    # @return [Symbol] error code
    # @method playlist_add_tracks(playlist, tracks, offset, session)
    attach_function :playlist_add_tracks, [ Playlist, :array, :int, :int, Session ], APIError do |playlist, tracks, offset, session|
      tracks = Array(tracks)

      with_buffer(Spotify::Track, size: tracks.length) do |tracks_buffer|
        tracks_buffer.write_array_of_pointer(tracks)
        sp_playlist_add_tracks(playlist, tracks_buffer, tracks.length, offset, session)
      end
    end

    # Remove tracks from the playlist at the given indices.
    #
    # @example single index
    #   Spotify.playlist_remove_tracks(playlist, 3) # => :ok
    #
    # @example array of indices
    #   Spotify.playlist_remove_tracks(playlist, [1, 3]) # => :ok
    #
    # @see #playlist_is_loaded
    # @note if the playlist is not loaded, the function always return an error.
    # @note any index in indices_pointer must exist at most once, i.e. [0,1,2] is valid, [0,0,1] is not.
    # @param [Playlist] playlist
    # @param [Array<Integer>, Integer] indices_pointer pointer to array of track indices
    # @return [Symbol] error code
    # @method playlist_remove_tracks(playlist, indices)
    attach_function :playlist_remove_tracks, [ Playlist, :array, :int ], APIError do |playlist, indices|
      indices = Array(indices)

      with_buffer(:int, size: indices.length) do |indices_buffer|
        indices_buffer.write_array_of_int(indices)
        sp_playlist_remove_tracks(playlist, indices_buffer, indices.length)
      end
    end

    # Move tracks at the given indices to position index and forward.
    #
    # @example
    #   Spotify.playlist_reorder_tracks(playlist, [1, 7], 0) # => :ok
    #
    # @see #playlist_is_loaded
    # @note if the playlist is not loaded, the function always return an error.
    # @note any index in indices_pointer must exist at most once, i.e. [0,1,2] is valid, [0,0,1] is not.
    # @param [Playlist] playlist
    # @param [Array<Integer>] indices_pointer pointer to array of track indices
    # @param [Integer] index starting position of tracks to be placed at, number between 0..{#playlist_num_tracks}
    # @return [Symbol] error code
    # @method playlist_reorder_tracks(playlist, indices, index)
    attach_function :playlist_reorder_tracks, [ Playlist, :array, :int, :int ], APIError do |playlist, indices, index|
      indices = Array(indices)

      with_buffer(:int, size: indices.length) do |indices_buffer|
        indices_buffer.write_array_of_int(indices)
        sp_playlist_reorder_tracks(playlist, indices_buffer, indices.length, index)
      end
    end

    # @see #playlist_is_loaded
    # @see #playlist_update_subscribers
    # @note if the playlist is not loaded, the function always return 0.
    # @note if {#playlist_update_subscribers} have not been called, the function always return 0.
    # @param [Playlist] playlist
    # @return [Integer] number of playlist subscribers
    # @method playlist_num_subscribers(playlist)
    attach_function :playlist_num_subscribers, [ Playlist ], :uint

    # @example
    #   subscribers = Spotify.playlist_subscribers(playlist).to_a
    #   puts "Subscribers: ", subscribers.join(", ")
    #
    # @see #playlist_is_loaded
    # @see #playlist_update_subscribers
    # @see Subscribers
    # @note if the playlist is not loaded, the function always return an empty structure.
    # @note if {#playlist_update_subscribers} have not been called, the function always return an empty structure.
    # @param [Playlist] playlist
    # @return [Subscribers]
    # @method playlist_subscribers(playlist)
    attach_function :playlist_subscribers, [ Playlist ], Subscribers.auto_ptr
    attach_function :playlist_subscribers_free, [ Subscribers.by_ref ], APIError

    # Request download of the subscribers list.
    #
    # @note the function updates subscribers asynchronously, see {PlaylistCallbacks#subscribers_changed} for callback.
    # @param [Session] session
    # @param [Playlist] playlist
    # @return [Symbol] error code
    # @method playlist_update_subscribers(session, playlist)
    attach_function :playlist_update_subscribers, [ Session, Playlist ], APIError

    # @see https://developer.spotify.com/docs/libspotify/12.1.51/group__playlist.html#ga967ad87f0db702513ecda82546f667c5
    # @param [Session] session
    # @param [Playlist] playlist
    # @return [Boolean] true if playlist is loaded in memory, as opposed to only stored on disk.
    # @method playlist_is_in_ram(session, playlist)
    attach_function :playlist_is_in_ram, [ Session, Playlist ], :bool

    # Set if playlist should be loaded into memory, as opposed to only read from on disk.
    #
    # @see https://developer.spotify.com/docs/libspotify/12.1.51/group__playlist.html#ga967ad87f0db702513ecda82546f667c5
    # @param [Session] session
    # @param [Playlist] playlist
    # @param [Boolean] in_ram
    # @return [Symbol] error code
    # @method playlist_set_in_ram(session, playlist, in_ram)
    attach_function :playlist_set_in_ram, [ Session, Playlist, :bool ], APIError

    # Instantiate a Playlist from a Link.
    #
    # @param [Session] session
    # @param [Link] link
    # @return [Playlist, nil] playlist, or nil if link was not a valid playlist link
    # @method playlist_create(session, link)
    attach_function :playlist_create, [ Session, Link ], Playlist

    # @see #playlist_is_loaded
    # @see #playlist_set_offline_mode
    # @note if the playlist is not loaded, the function always return :no.
    # @param [Session] session
    # @param [Playlist] playlist
    # @return [Symbol] playlist offline status, one of :no, :yes, :downloading, or :waiting
    # @method playlist_get_offline_status(session, playlist)
    attach_function :playlist_get_offline_status, [ Session, Playlist ], :playlist_offline_status

    # @note if the playlist is not loaded, the function always return 0.
    # @note if the playlist is not marked for offline download, the function always return 0.
    # @param [Session] session
    # @param [Playlist] playlist
    # @return [Integer] percentage of playlist downloaded, 0..100
    # @method playlist_get_offline_download_completed(session, playlist)
    attach_function :playlist_get_offline_download_completed, [ Session, Playlist ], :int

    # Set if playlist should be marked for offline playback.
    #
    # @param [Session] session
    # @param [Playlist] playlist
    # @param [Boolean] offline true if playlist should be downloaded for offline usage
    # @return [Symbol] error code
    # @method playlist_set_offline_mode(session, playlist, offline)
    attach_function :playlist_set_offline_mode, [ Session, Playlist, :bool ], APIError

    attach_function :playlist_add_ref, [ Playlist ], APIError
    attach_function :playlist_release, [ Playlist ], APIError
  end
end
