# frozen_string_literal: true

require 'discordrb/events/generic'
require 'discordrb/data'

module Discordrb::Events
  # Module to make sending messages easier with the presence of a text channel in an event
  module Respondable
    # @return [Channel] the channel in which this event occurred
    attr_reader :channel

    # Sends a message to the channel this message was sent in, right now. It is usually preferable to use {#<<} instead
    # because it avoids rate limiting problems
    # @param content [String] The message to send to the channel
    # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
    # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
    # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
    # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
    # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
    # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
    # @return [Discordrb::Message] the message that was sent
    def send_message(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
      channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference, components)
    end

    # The same as {#send_message}, but yields a {Webhooks::Embed} for easy building of embedded content inside a block.
    # @see Channel#send_embed
    # @param message [String] The message that should be sent along with the embed. If this is the empty string, only the embed will be shown.
    # @param embed [Discordrb::Webhooks::Embed, nil] The embed to start the building process with, or nil if one should be created anew.
    # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
    # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
    # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
    # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
    # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
    # @yield [embed] Yields the embed to allow for easy building inside a block.
    # @yieldparam embed [Discordrb::Webhooks::Embed] The embed from the parameters, or a new one.
    # @return [Message] The resulting message.
    def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowed_mentions = nil, message_reference = nil, components = nil, &block)
      channel.send_embed(message, embed, attachments, tts, allowed_mentions, message_reference, components, &block)
    end

    # Sends a temporary message to the channel this message was sent in, right now.
    # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
    # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
    # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
    # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
    # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
    # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
    # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
    def send_temporary_message(content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, components = nil)
      channel.send_temporary_message(content, timeout, tts, embed, attachments, allowed_mentions, components)
    end

    # Adds a string to be sent after the event has finished execution. Avoids problems with rate limiting because only
    # one message is ever sent. If it is used multiple times, the strings will bunch up into one message (separated by
    # newlines)
    # @param message [String] The message to send to the channel
    def <<(message)
      addition = "#{message}\n"
      @saved_message = @saved_message ? @saved_message + addition : addition
      nil
    end

    # Drains the currently saved message, which clears it out, resulting in everything being saved before being
    # thrown away and nothing being sent to the channel (unless there is something saved after this).
    # @see #<<
    def drain
      @saved_message = ''
      nil
    end

    # Drains the currently saved message into a result string. This prepends it before that string, clears the saved
    # message and returns the concatenation.
    # @param result [String] The result string to drain into.
    # @return [String] a string formed by concatenating the saved message and the argument.
    def drain_into(result)
      return if result.is_a?(Discordrb::Message)

      result = (@saved_message.nil? ? '' : @saved_message.to_s) + (result.nil? ? '' : result.to_s)
      drain
      result
    end

    alias_method :send, :send_message
    alias_method :respond, :send_message
    alias_method :send_temp, :send_temporary_message
  end

  # Event raised when a text message is sent to a channel
  class MessageEvent < Event
    include Respondable

    # @return [Message] the message which triggered this event.
    attr_reader :message

    # @return [String] the message that has been saved by calls to {#<<} and will be sent to Discord upon completion.
    attr_reader :saved_message

    # @return [File] the file that has been saved by a call to {#attach_file} and will be sent to Discord upon completion.
    attr_reader :file

    # @return [String] the filename set in {#attach_file} that will override the original filename when sent.
    attr_reader :filename

    # @return [true, false] Whether or not this file should appear as a spoiler. Set by {#attach_file}
    attr_reader :file_spoiler

    # @!attribute [r] author
    #   @return [Member, User] who sent this message.
    #   @see Message#author
    # @!attribute [r] channel
    #   @return [Channel] the channel in which this message was sent.
    #   @see Message#channel
    # @!attribute [r] content
    #   @return [String] the message's content.
    #   @see Message#content
    # @!attribute [r] timestamp
    #   @return [Time] the time at which the message was sent.
    #   @see Message#timestamp
    delegate :author, :channel, :content, :timestamp, to: :message

    # @!attribute [r] server
    #   @return [Server, nil] the server where this message was sent, or nil if it was sent in PM.
    #   @see Channel#server
    delegate :server, to: :channel

    def initialize(message, bot)
      @bot = bot
      @message = message
      @channel = message.channel
      @saved_message = ''
      @file = nil
      @filename = nil
      @file_spoiler = nil
    end

    # Sends file with a caption to the channel this message was sent in, right now.
    # It is usually preferable to use {#<<} and {#attach_file} instead
    # because it avoids rate limiting problems
    # @param file [File] The file to send to the channel
    # @param caption [String] The caption attached to the file
    # @param filename [String] Overrides the filename of the uploaded file
    # @param spoiler [true, false] Whether or not this file should appear as a spoiler.
    # @return [Discordrb::Message] the message that was sent
    # @example Send a file from disk
    #   event.send_file(File.open('rubytaco.png', 'r'))
    def send_file(file, caption: nil, filename: nil, spoiler: nil)
      @message.channel.send_file(file, caption: caption, filename: filename, spoiler: spoiler)
    end

    # Attaches a file to the message event and converts the message into
    # a caption.
    # @param file [File] The file to be attached
    # @param filename [String] Overrides the filename of the uploaded file
    # @param spoiler [true, false] Whether or not this file should appear as a spoiler.
    def attach_file(file, filename: nil, spoiler: nil)
      raise ArgumentError, 'Argument is not a file!' unless file.is_a?(File)

      @file = file
      @filename = filename
      @file_spoiler = spoiler
      nil
    end

    # Detaches a file from the message event.
    def detach_file
      @file = nil
      @filename = nil
      @file_spoiler = nil
    end

    # @return [true, false] whether or not this message was sent by the bot itself
    def from_bot?
      @message.user.id == @bot.profile.id
    end

    # Utility method to get the voice bot for the current server
    # @return [VoiceBot, nil] the voice bot connected to this message's server, or nil if there is none connected
    def voice
      @bot.voice(@message.channel.server.id)
    end

    alias_method :user, :author
    alias_method :text, :content
  end

  # Event handler for MessageEvent
  class MessageEventHandler < EventHandler
    def matches?(event)
      # Check for the proper event type
      return false unless event.is_a? MessageEvent

      [
        matches_all(@attributes[:starting_with] || @attributes[:start_with], event.content) do |a, e|
          case a
          when String
            e.start_with? a
          when Regexp
            (e =~ a)&.zero?
          end
        end,
        matches_all(@attributes[:ending_with] || @attributes[:end_with], event.content) do |a, e|
          case a
          when String
            e.end_with? a
          when Regexp
            !(e =~ Regexp.new("#{a}$")).nil?
          end
        end,
        matches_all(@attributes[:containing] || @attributes[:contains], event.content) do |a, e|
          case a
          when String
            e.include? a
          when Regexp
            (e =~ a)
          end
        end,
        matches_all(@attributes[:in], event.channel) do |a, e|
          case a
          when String
            # Make sure to remove the "#" from channel names in case it was specified
            a.delete('#') == e.name
          when Integer
            a == e.id
          else
            a == e
          end
        end,
        matches_all(@attributes[:from], event.author) do |a, e|
          case a
          when String
            a == e.name
          when Integer
            a == e.id
          when :bot
            e.current_bot?
          else
            a == e
          end
        end,
        matches_all(@attributes[:with_text] || @attributes[:content] || @attributes[:exact_text], event.content) do |a, e|
          case a
          when String
            e == a
          when Regexp
            match = a.match(e)
            match ? (e == match[0]) : false
          end
        end,
        matches_all(@attributes[:after], event.timestamp) { |a, e| a > e },
        matches_all(@attributes[:before], event.timestamp) { |a, e| a < e },
        matches_all(@attributes[:private], event.channel.private?) { |a, e| !e == !a }
      ].reduce(true, &:&)
    end

    # @see EventHandler#after_call
    def after_call(event)
      if event.file.nil?
        event.send_message(event.saved_message) unless event.saved_message.empty?
      else
        event.send_file(event.file, caption: event.saved_message, filename: event.filename, spoiler: event.file_spoiler)
      end
    end
  end

  # @see Discordrb::EventContainer#mention
  class MentionEvent < MessageEvent; end

  # Event handler for {MentionEvent}
  class MentionEventHandler < MessageEventHandler; end

  # @see Discordrb::EventContainer#pm
  class PrivateMessageEvent < MessageEvent; end

  # Event handler for {PrivateMessageEvent}
  class PrivateMessageEventHandler < MessageEventHandler; end

  # A subset of MessageEvent that only contains a message ID and a channel
  class MessageIDEvent < Event
    include Respondable

    # @return [Integer] the ID associated with this event
    attr_reader :id

    # @!visibility private
    def initialize(data, bot)
      @id = data['id'].to_i
      @channel = bot.channel(data['channel_id'].to_i)
      @saved_message = ''
      @bot = bot
    end
  end

  # Event handler for {MessageIDEvent}
  class MessageIDEventHandler < EventHandler
    def matches?(event)
      # Check for the proper event type
      return false unless event.is_a? MessageIDEvent

      [
        matches_all(@attributes[:id], event.id) do |a, e|
          a.resolve_id == e.resolve_id
        end,
        matches_all(@attributes[:in], event.channel) do |a, e|
          case a
          when String
            # Make sure to remove the "#" from channel names in case it was specified
            a.delete('#') == e.name
          when Integer
            a == e.id
          else
            a == e
          end
        end
      ].reduce(true, &:&)
    end
  end

  # Raised when a message is edited
  # @see Discordrb::EventContainer#message_edit
  class MessageEditEvent < MessageEvent; end

  # Event handler for {MessageEditEvent}
  class MessageEditEventHandler < MessageEventHandler; end

  # Raised when a message is deleted
  # @see Discordrb::EventContainer#message_delete
  class MessageDeleteEvent < MessageIDEvent; end

  # Event handler for {MessageDeleteEvent}
  class MessageDeleteEventHandler < MessageIDEventHandler; end

  # Raised whenever a MESSAGE_UPDATE is received
  # @see Discordrb::EventContainer#message_update
  class MessageUpdateEvent < MessageEvent; end

  # Event handler for {MessageUpdateEvent}
  class MessageUpdateEventHandler < MessageEventHandler; end
end
