# -*- encoding: utf-8 -*-

require 'webgen/node'

module Webgen

  # Represents a tree of nodes.
  class Tree

    # The dummy root.
    #
    # This is the default node that gets created when the Tree is created so that the real root node
    # can be treated like any other node. It has only one child, namely the real root node of the
    # tree.
    attr_reader :dummy_root

    # Direct access to the hashes for node resolving.
    #
    # Only use this for reading purposes! If you just want to get a specific node for an
    # alcn/acn/destination path, use #node instead.
    attr_reader :node_access

    # The website to which the Tree object belongs.
    attr_reader :website

    # Create a new Tree object for the website.
    def initialize(website)
      @website = website
      @node_access = {:alcn => {}, :acn => {}, :dest_path => {}, :translation_key => {}}
      @dummy_root = Node.new(self, '', '')
    end

    # The real root node of the tree.
    def root
      @dummy_root.children.first
    end

    # Access a node via its +:alcn+.
    def [](path)
      @node_access[:alcn][path]
    end

    # Access a node via a +path+ of a specific +type+.
    #
    # If type is +:alcn+ (default) then +path+ has to be an absolute localized canonical name, if
    # type is +acn+ then +path+ has to be an absolute canonical name and if type is +:dest_path+
    # then +path+ needs to be a destination path.
    #
    # Returns the requested Node or +nil+ if such a node does not exist.
    def node(path, type = :alcn)
      case type
      when :alcn then @node_access[type][path]
      when :acn then @node_access[type][path] && @node_access[type][path].first
      when :dest_path then @node_access[type][path]
      else
        raise ArgumentError, "Unknown type '#{type}' for resolving path <#{path}>"
      end
    end

    # Return the node representing the given +path+ which can be an alcn/acn/destination path (name
    # resolution is done in the specified order). The path has to be absolute, i.e. starting with a
    # slash.
    #
    # If the +path+ is an alcn and a node is found, it is returned. If the +path+ is an acn, the
    # correct localized node according to +lang+ is returned or if no such node exists but an
    # unlocalized version does, the unlocalized node is returned. If the +path+ is a destination
    # path, the node with this destination path is returned.
    #
    # If no node is found for the given path or if the path is invalid, +nil+ is returned and, if
    # +msg_on_failure+ is +true+, the message :node_resolution_failed (with parameters +path+ and
    # +lang+) is dispatched.
    def resolve_node(path, lang, msg_on_failure = false)
      node = self.node(path, :alcn)
      if !node || node.acn == path
        (node = (self.node(path, :acn) || self.node(path + '/', :acn))) && (node = translate_node(node, lang))
      end
      node = self.node(path, :dest_path) if !node
      @website.blackboard.dispatch_msg(:node_resolution_failed, path, lang) if !node && msg_on_failure
      node
    end

    # Return the translation of the node to the language +lang+ or, if no such node exists, an
    # unlocalized version of the node. If no such node is found either, +nil+ is returned.
    def translate_node(node, lang)
      avail = @node_access[:translation_key][translation_key(node)]
      avail.find do |n|
        n = n.parent while n.is_fragment?
        n.lang == lang
      end || avail.find do |n|
        n = n.parent while n.is_fragment?
        n.lang.nil?
      end
    end

    # Return all translations of the node.
    def translations(node)
      @node_access[:translation_key][translation_key(node)].dup
    end

    # Utility method called by Node#initialize. This method should not be used directly!
    def register_node(node)
      if @node_access[:alcn].has_key?(node.alcn)
        raise "Can't have two nodes with same alcn: #{node}"
      else
        @node_access[:alcn][node.alcn] = node
      end
      (@node_access[:acn][node.acn] ||= []) << node
      (@node_access[:translation_key][translation_key(node)] ||= []) << node
      if node.meta_info['no_output']
        # ignore node dest path
      elsif @node_access[:dest_path].has_key?(node.dest_path)
        raise "Can't have two nodes with same destination path: #{node.dest_path}"
      else
        @node_access[:dest_path][node.dest_path] = node
      end
    end

    # Return the translation key for the node.
    def translation_key(node)
      node.meta_info['translation_key'] || node.acn
    end
    private :translation_key

    # Delete the node and all of its children from the tree.
    #
    # The message +:before_node_deleted+ is sent with the to-be-deleted node before the node is
    # actually deleted from the tree.
    def delete_node(node)
      return if node.nil? || !node.kind_of?(Node) || node == @dummy_root

      node.children.dup.each {|child| delete_node(child)}

      @website.blackboard.dispatch_msg(:before_node_deleted, node)
      node.parent.children.delete(node)
      @node_access[:alcn].delete(node.alcn)
      @node_access[:acn][node.acn].delete(node)
      @node_access[:translation_key][translation_key(node)].delete(node)
      @node_access[:dest_path].delete(node.dest_path) unless node.meta_info['no_output']
    end

  end

end
