# encoding: UTF-8

module Vines

  # A Config object is passed to the stream handlers to give them access
  # to server configuration information like virtual host names, storage
  # systems, etc. This class provides the DSL methods used in the
  # conf/config.rb file.
  class Config
    LOG_LEVELS = %w[debug info warn error fatal].freeze

    attr_reader :router

    @@instance = nil
    def self.configure(&block)
      @@instance = self.new(&block)
    end

    def self.instance
      @@instance
    end

    def initialize(&block)
      @certs = File.expand_path('conf/certs')
      @vhosts, @ports, @cluster = {}, {}, nil
      @null = Storage::Null.new
      @router = Router.new(self)
      instance_eval(&block)
      raise "must define at least one virtual host" if @vhosts.empty?

      unless @certs && File.directory?(@certs) && File.readable?(@certs)
        raise 'Must provide a readable certs directory'
      end
    end

    def certs(dir=nil)
      dir ? @certs = File.expand_path(dir) : @certs
    end

    def host(*names, &block)
      names = names.flatten.map {|name| name.downcase }
      dupes = names.uniq.size != names.size || (@vhosts.keys & names).any?
      raise "one host definition per domain allowed" if dupes
      names.each do |name|
        @vhosts[name] = Host.new(self, name, &block)
      end
    end

    %w[client server http component].each do |name|
      define_method(name) do |*args, &block|
        port = Vines::Config.const_get("#{name.capitalize}Port")
        raise "one #{name} port definition allowed" if @ports[name.to_sym]
        @ports[name.to_sym] = port.new(self, *args) do
          instance_eval(&block) if block
        end
      end
    end

    def cluster(&block)
      return @cluster unless block
      raise "one cluster definition allowed" if @cluster
      @cluster = Cluster.new(self, &block)
    end

    def log(level)
      const = Logger.const_get(level.to_s.upcase) rescue nil
      unless LOG_LEVELS.include?(level.to_s) && const
        raise "log level must be one of: #{LOG_LEVELS.join(', ')}"
      end
      Class.new.extend(Vines::Log).log.level = const
    end

    def ports
      @ports.values
    end

    # Return true if the domain is virtual hosted by this server.
    def vhost?(domain)
      !!vhost(domain)
    end

    # Return the Host config object for this domain if it's hosted by this
    # server.
    def vhost(domain)
      @vhosts[domain.to_s]
    end

    # Returns the storage system for the domain or a Storage::Null instance if
    # the domain is not hosted at this server.
    def storage(domain)
      host = vhost(domain)
      host ? host.storage : @null
    end

    # Returns the PubSub system for the domain or nil if pubsub is not enabled
    # for this domain.
    def pubsub(domain)
      host = @vhosts.values.find {|host| host.pubsub?(domain) }
      host.pubsubs[domain.to_s] if host
    end

    # Return true if the domain is a pubsub service hosted at a virtual host
    # at this server.
    def pubsub?(domain)
      @vhosts.values.any? {|host| host.pubsub?(domain) }
    end

    # Return true if all JIDs belong to components hosted by this server.
    def component?(*jids)
      !jids.flatten.index do |jid|
        !component_password(JID.new(jid).domain)
      end
    end

    # Return the password for the component or nil if it's not hosted here.
    def component_password(domain)
      host = @vhosts.values.find {|host| host.component?(domain) }
      host.password(domain) if host
    end

    # Return true if all of the JIDs are hosted by this server.
    def local_jid?(*jids)
      !jids.flatten.index do |jid|
        !vhost?(JID.new(jid).domain)
      end
    end

    # Return true if private XML fragment storage is enabled for this domain.
    def private_storage?(domain)
      host = vhost(domain)
      host.private_storage? if host
    end

    # Returns true if server-to-server connections are allowed with the
    # given domain.
    def s2s?(domain)
      @ports[:server] && @ports[:server].hosts.include?(domain.to_s)
    end

    # Return true if the server is a member of a cluster, serving the same
    # domains from different machines.
    def cluster?
      !!@cluster
    end

    # Retrieve the Port subclass with this name:
    # [:client, :server, :http, :component]
    def [](name)
      @ports[name] or raise ArgumentError.new("no port named #{name}")
    end

    # Return true if the two JIDs are allowed to send messages to each other.
    # Both domains must have enabled cross_domain_messages in their config files.
    def allowed?(to, from)
      to, from = JID.new(to), JID.new(from)
      return false                      if to.empty? || from.empty?
      return true                       if to.domain == from.domain # same domain always allowed
      return cross_domain?(to, from)    if local_jid?(to, from)     # both virtual hosted here
      return check_subdomains(to, from) if subdomain?(to, from)     # component/pubsub to component/pubsub
      return check_subdomain(to, from)  if subdomain?(to)           # to component/pubsub
      return check_subdomain(from, to)  if subdomain?(from)         # from component/pubsub
      return cross_domain?(to)          if local_jid?(to)           # from is remote
      return cross_domain?(from)        if local_jid?(from)         # to is remote
      return false
    end

    private

    # Return true if all of the JIDs are some kind of subdomain resource hosted
    # here (either a component or a pubsub domain).
    def subdomain?(*jids)
      !jids.flatten.index do |jid|
        !component?(jid) && !pubsub?(jid)
      end
    end

    # Return true if the third-level subdomain JIDs (components and pubsubs)
    # are allowed to communicate with each other. For example, a
    # tea.wonderland.lit component should be allowed to send messages to
    # pubsub.wonderland.lit because they share the second-level wonderland.lit
    # domain.
    def check_subdomains(to, from)
      sub1, sub2 = strip_domain(to), strip_domain(from)
      (sub1 == sub2) || cross_domain?(sub1, sub2)
    end

    # Return true if the third-level subdomain JID (component or pubsub) is
    # allowed to communicate with the other JID. For example,
    # pubsub.wonderland.lit should be allowed to send messages to
    # alice@wonderland.lit because they share the second-level wonderland.lit
    # domain.
    def check_subdomain(subdomain, jid)
      comp = strip_domain(subdomain)
      return true if comp.domain == jid.domain
      local_jid?(jid) ? cross_domain?(comp, jid) : cross_domain?(comp)
    end

    # Return the third-level JID's domain with the first subdomain stripped off
    # to create a second-level domain. For example, alice@tea.wonderland.lit
    # returns wonderland.lit.
    def strip_domain(jid)
      domain = jid.domain.split('.').drop(1).join('.')
      JID.new(domain)
    end

    # Return true if all JIDs are allowed to exchange cross domain messages.
    def cross_domain?(*jids)
      !jids.flatten.index do |jid|
        !vhost(jid.domain).cross_domain_messages?
      end
    end
  end
end
