# -*- coding: utf-8 -*-
require "set"

module Cinch
  class Channel
    include Syncable
    @channels = {}

    class << self
      # Finds or creates a channel.
      #
      # @param [String] name name of a channel
      # @param [Bot] bot a bot
      # @return [Channel]
      # @see Bot#Channel
      def find_ensured(name, bot)
        downcased_name = name.irc_downcase(bot.irc.isupport["CASEMAPPING"])
        @channels[downcased_name] ||= new(name, bot)
        @channels[downcased_name]
      end

      # Finds a channel.
      #
      # @param [String] name name of a channel
      # @return [Channel, nil]
      def find(name)
        @channels[name]
      end

      # @return [Array<Channel>] Returns all channels
      def all
        @channels.values
      end
    end

    # @return [Bot]
    attr_reader :bot

    # @return [String]
    attr_reader :name

    # @return [Array<User>]
    attr_reader :users
    synced_attr_reader :users

    # @return [String]
    attr_accessor :topic
    synced_attr_reader :topic

    # @return [Array<Ban>]
    attr_reader :bans
    synced_attr_reader :bans

    # @return [Array<String>]
    attr_reader :modes
    synced_attr_reader :modes
    def initialize(name, bot)
      @bot   = bot
      @name  = name
      @users = {}
      @bans  = []

      @modes = {}
      # TODO raise if not a channel

      @topic = nil

      @in_channel = false

      @synced_attributes  = Set.new
      @when_requesting_synced_attribute = lambda {|attr|
        unless @in_channel
          unsync(attr)
          case attr
          when :users
            @bot.raw "NAMES #@name"
          when :topic
            @bot.raw "TOPIC #@name"
          when :bans
            @bot.raw "MODE #@name +b"
          when :modes
            @bot.raw "MODE #@name"
          end
        end
      }
    end

    attr_accessor :limit
    # @return [Number]
    def limit
      @modes["l"].to_i
    end

    def limit=(val)
      if val == -1 or val.nil?
        mode "-l"
      else
        mode "+l #{val}"
      end
    end

    # @return [Boolean] true if the channel is secret (+s)
    attr_accessor :secret
    undef_method "secret"
    undef_method "secret="
    def secret
      @modes["s"]
    end
    alias_method :secret?, :secret

    def secret=(bool)
      if bool
        mode "+s"
      else
        mode "-s"
      end
    end

    # @return [Boolean] true if the channel is moderated (only users
    #   with +o and +v are able to send messages)
    attr_accessor :moderated
    undef_method "moderated"
    undef_method "moderated="
    def moderated
      @modes["m"]
    end
    alias_method :moderated?, :moderated

    def moderated=(val)
      if bool
        mode "+m"
      else
        mode "-m"
      end
    end

    # @return [Boolean] true if the channel is invite only (+i)
    attr_accessor :invite_only
    undef_method "invite_only"
    undef_method "invite_only="
    def invite_only
      @modes["i"]
    end
    alias_method :invite_only?, :invite_only

    def invite_only=(bool)
      if bool
        mode "+i"
      else
        mode "-i"
      end
    end

    # @return [String, nil]
    attr_accessor :key
    undef_method "key"
    undef_method "key="
    def key
      @modes["k"]
    end

    def key=(new_key)
      if new_key.nil?
        mode "-k #{key}"
      else
        mode "+k #{new_key}"
      end
    end

    # Sets or unsets modes. Most of the time you won't need this but
    # use setter methods like {Channel#invite_only=}.
    #
    # @param [String] s a mode string
    # @return [void]
    # @example
    #   channel.mode "+n"
    def mode(s)
      @bot.raw "MODE #@name #{s}"
    end

    # @api private
    # @return [void]
    def sync_modes(all = true)
      unsync :users
      unsync :bans
      unsync :modes
      @bot.raw "NAMES #@name" if all
      @bot.raw "MODE #@name +b" # bans
      @bot.raw "MODE #@name"
    end

    # @return [Boolean] true if `user` is opped in the channel
    def opped?(user)
      user = User.find_ensured(user, @bot) unless user.is_a?(User)
      @users[user] == "@"
    end

    # @return [Boolean] true if `user` is voiced in the channel
    def voiced?(user)
      user = User.find_ensured(user, @bot) unless user.is_a?(User)
      @users[user] == "+"
    end

    # Bans someone from the channel.
    #
    # @param [Ban, Mask, User, String] target the mask to ban
    # @return [Mask] the mask used for banning
    def ban(target)
      mask = Mask.from(target)

      @bot.raw "MODE #@name +b #{mask}"
      mask
    end

    # Unbans someone from the channel.
    #
    # @param [Ban, Mask, User, String] target the mask to unban
    # @return [Mask] the mask used for unbanning
    def unban(target)
      mask = Mask.from(target)

      @bot.raw "MODE #@name -b #{mask}"
      mask
    end

    # @param [String, User] user the user to op
    # @return [void]
    def op(user)
      @bot.raw "MODE #@name +o #{user}"
    end

    # @param [String, User] user the user to deop
    # @return [void]
    def deop(user)
      @bot.raw "MODE #@name -o #{user}"
    end

    # @param [String, User] user the user to voice
    # @return [void]
    def voice(user)
      @bot.raw "MODE #@name +v #{user}"
    end

    # @param [String, User] user the user to devoice
    # @return [void]
    def devoice(user)
      @bot.raw "MODE #@name -v #{user}"
    end

    # @api private
    # @return [void]
    def add_user(user, mode = nil)
      @in_channel = true if user == @bot
      @users[user] = mode # TODO can a user have more than one mode?
    end

    # @api private
    # @return [void]
    def remove_user(user)
      @in_channel = false if user == @bot
      @users.delete(user)
    end

    # Removes all users
    #
    # @api private
    # @return [void]
    def clear_users
      @users.clear
    end

    # Send a message to the channel.
    #
    # @param [String] message the message
    # @return [void]
    # @see #safe_send
    def send(message)
      @bot.msg(@name, message)
    end
    alias_method :privmsg, :send
    alias_method :msg, :send

    # Send a message to the channel, but remove any non-printable
    # characters. The purpose of this method is to send text from
    # untrusted sources, like other users or feeds.
    #
    # Note: this will **break** any mIRC color codes embedded in the
    # string.
    #
    # @param (see #send)
    # @return (see #send)
    # @see #send
    # @todo Handle mIRC color codes more gracefully.
    def safe_send(message)
      @bot.safe_msg(@name, message)
    end
    alias_method :safe_privmsg, :safe_send
    alias_method :safe_msg, :safe_send



    # Send a CTCP to the channel.
    #
    # @param [String] message the ctcp message
    # @return [void]
    def ctcp(message)
      send "\001#{message}\001"
    end

    # Invoke an action (/me) in the channel.
    #
    # @param [String] message the message
    # @return [void]
    # @see #safe_action
    def action(message)
      @bot.action(@name, message)
    end

    # Invoke an action (/me) in the channel but remove any
    # non-printable characters. The purpose of this method is to send
    # text from untrusted sources, like other users or feeds.
    #
    # Note: this will **break** any mIRC color codes embedded in the
    # string.
    #
    # @param (see #action)
    # @return (see #action)
    # @see #action
    # @todo Handle mIRC color codes more gracefully.
    def safe_action(message)
      @bot.safe_action(@name, message)
    end


    # Invites a user to the channel.
    #
    # @param [String, User] user the user to invite
    # @return [void]
    def invite(user)
      @bot.raw("INVITE #{user} #@name")
    end

    # Sets the topic.
    #
    # @param [String] new_topic the new topic
    # @raise [Exceptions::TopicTooLong]
    def topic=(new_topic)
      if new_topic.size > @bot.irc.isupport["TOPICLEN"] && @bot.strict?
        raise Exceptions::TopicTooLong, new_topic
      end

      @bot.raw "TOPIC #@name :#{new_topic}"
    end

    # Kicks a user from the channel.
    #
    # @param [String, User] user the user to kick
    # @param [String] a reason for the kick
    # @raise [Exceptions::KickReasonTooLong]
    # @return [void]
    def kick(user, reason = nil)
      if reason.to_s.size > @bot.irc.isupport["KICKLEN"] && @bot.strict?
        raise Exceptions::KickReasonTooLong, reason
      end

      @bot.raw("KICK #@name #{user} :#{reason}")
    end

    # Invites a user to the channel.
    #
    # @param [String, User] user the user to invite
    # @return [void]
    def invite(user)
      @bot.raw "INVITE #{user} #@name"
    end

    # Causes the bot to part from the channel.
    #
    # @param [String] message the part message.
    # @return [void]
    def part(message = nil)
      @bot.raw "PART #@name :#{message}"
    end

    # Joins the channel
    #
    # @param [String] key the channel key, if any. If none is
    #   specified but @key is set, @key will be used
    # @return [void]
    def join(key = nil)
      if key.nil? and self.key != true
        key = self.key
      end
      @bot.raw "JOIN #{[@name, key].compact.join(" ")}"
    end

    # @return [Boolean]
    def ==(other)
      @name == other.to_s
    end
    alias_method :eql?, "=="

    # @return [Fixnum]
    def hash
      @name.hash
    end

    # @return [String]
    def to_s
      @name
    end
    alias_method :to_str, :to_s

    # @return [String]
    def inspect
      "#<Channel name=#{@name.inspect}>"
    end
  end
end
