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

require 'webgen/extension_manager'
require 'webgen/error'
require 'webgen/utils/tag_parser'

module Webgen

  # Namespace for all webgen tags.
  #
  # == About
  #
  # A tag object is a webgen extension that handles specific webgen tags. webgen tags are used to
  # add dynamic content to page and template files (or any other file for that matter) and are made
  # for ease of use.
  #
  # == Implementing a tag
  #
  # A tag object only needs to respond to the method +call+ which needs to accept three parameters:
  #
  # [tag] The name of the tag which should be processed (useful for tag objects which can process
  #       different tags).
  #
  # [body] Holds the body value for the tag if any.
  #
  # [context] Holds all relevant information for processing -- have a look at the Webgen::Context
  #           class to see what is available. The special key :config is set to an
  #           Webgen::Configuration object that should be used to retrieve configuration option
  #           values because the values might be changed due to options set directly via the tag
  #           syntax.
  #
  # The method has to return the result of the processing and, optionally, a boolean value
  # specifying if the result should further be processed (ie. webgen tags replaced).
  #
  # This allows one to implement a tag object as a class with a class method called +call+. Or as a
  # Proc object.
  #
  # The tag object has to be registered so that webgen knows about it, see #register for more
  # information.
  #
  # == Tag options
  #
  # webgen tags allow the specification of options in the tag definition. When registering a tag,
  # one can specify which options are mandatory, i.e. which options always have to be set directly
  # for the tag. The value of the option :config_prefix for the #register method is used to resolve
  # partially stated configuration entries.
  #
  # == Sample Tag
  #
  # Following is a simple tag class example which just reverses the body text and adds some
  # information about the context to the result.
  #
  # Put the following into the ext/init.rb file of your webgen website:
  #
  #   class Reverser
  #
  #     def self.call(tag, body, context)
  #       result = context[:config]['tag.reverser.do_reverse'] ? body.reverse : body
  #       result << "\nNode: " << context.content_node.alcn << " (" << context.content_node['title'] << ")"
  #       result << "\nReference node: " << context.ref_node.alcn
  #       result
  #     end
  #
  #   end
  #
  #   website.config.define_option('tag.reverser.do_reverse', nil)
  #   website.ext.tag.register(Reverser, :names => 'reverse',
  #                            :config_prefix => 'tag.reverser',
  #                            :mandatory => ['do_reverse'])
  #
  # Then you can use the reverser tag as follows in a page file:
  #
  #   {reverse:: {do_reverse: true}}This text is reversed{reverse}
  #
  class Tag

    include Webgen::ExtensionManager

    # Render the tag template for the given tag and return the result.
    #
    # The value of the configuration option 'tag.<TAG>.template' (where '<TAG>' is replaced with
    # +tag+) is used as template path.
    #
    # If the template node cannot be found, an empty string is returned.
    def self.render_tag_template(context, tag)
      path = context[:config]["tag.#{tag}.template"]
      if path && template_node = context.ref_node.resolve(path, context.dest_node.lang, true)
        context.website.ext.item_tracker.add(context.dest_node, :template_chain, template_node)
        context.render_block(:name => "tag.#{tag}", :node => 'first',
                             :chain => [*template_node.template_chain, template_node, context.content_node])
      else
        context.website.logger.warn { "Template node '#{path}' for tag '#{tag}' not found" }
        ''
      end
    end


    def initialize(website) # :nodoc:
      super()
      @parser = nil
      website.blackboard.add_listener(:website_initialized, 'tag') do
        @parser = Webgen::Utils::TagParser.new(website.config['tag.prefix'])
      end
    end

    # Register a tag.
    #
    # The parameter +klass+ can either be a String containing the name of a class/module (which has
    # to respond to :call) or an object that responds to :call. If the class is located under this
    # namespace, only the class name without the hierarchy part is needed, otherwise the full
    # class/module name including parent module/class names is needed.
    #
    # Instead of registering an object that responds to :call, you can also provide a block that
    # processes a tag.
    #
    # === Options:
    #
    # [:names] The tag name or an array of tag names. If not set, it defaults to the lowercase
    #          version of the class name (without the hierarchy part).
    #
    #          The name :default is used for specifying the default tag which is called if an
    #          unknown tag name is encountered.
    #
    # [:config_prefix] The configuration prefix, i.e. the part of a configuration option name that
    #                  does not need to be specified. Defaults to the full class name without the
    #                  Webgen module downcased and all "::" substituted with "." (e.g.
    #                  Webgen::Tag::Menu → tag.menu). Needs to be specified when a block is used!
    #
    # [:mandatory] A list of configuration option names whose values always need to be provided. The
    #              first configuration option name is used as the default mandatory option (used
    #              when only a string is provided in the tag definition).
    #
    # === Examples:
    #
    #   tag.register('Date')    # registers Webgen::Tag::Date
    #
    #   tag.register('::Date')  # registers Date !!!
    #
    #   tag.register('MyModule::Date', names: ['mydate', 'date'])
    #
    #   tag.register('date', config_prefix: 'tag.date') do |tag, body, context|
    #     Time.now.strftime(param('tag.date.format'))
    #   end
    #
    def register(klass, options = {}, &block)
      if block_given? && !options[:config_prefix]
        raise ArgumentError, "The option :config_prefix needs to be specified when registering a tag using a block"
      end

      names = [options.delete(:names)].flatten.compact
      options[:name] = names.shift
      name = do_register(klass, options, true, &block)
      ext_data(name).mandatory = options[:mandatory] || []
      ext_data(name).config_prefix = options[:config_prefix] ||
        Webgen::Utils.snake_case(ext_data(name).object.to_s.gsub(/::/, '.').gsub(/^Webgen\./, ''))
      ext_data(name).initialized = false
      names.each {|n| @extensions[n.to_sym] = @extensions[name]}
    end

    # Process the +tag+ and return the result.
    #
    # The parameter +params+ (can be a Hash, a String or nil) needs to contain the parameters for
    # the tag and +body+ is the optional body for the tag. +context+ needs to be a valid
    # Webgen::Context object.
    def call(tag, params, body, context)
      result = ''
      tdata = tag_data(tag, context)
      if !tdata.nil?
        context = context.clone(:config => create_config(tag, params, tdata, context))
        result, process_output = tdata.object.call(tag, body, context)
        if process_output
          context.content = result
          result = context.website.ext.content_processor.call('tags', context).content
        end
      else
        raise Webgen::RenderError.new("No tag processor for '#{tag}' found", 'tag',
                                      context.dest_node, context.ref_node)
      end
      result
    rescue Webgen::Error => e
      e.path = context.dest_node if e.path.to_s.empty?
      e.location = "tag.#{tag}" unless e.location
      raise
    rescue Exception => e
      raise Webgen::RenderError.new(e, "tag.#{tag}", context.dest_node, context.ref_node)
    end

    # See Webgen::Utils::TagParser#replace_tags.
    def replace_tags(str, &block)  #:yields: tag_name, params, body
      @parser.replace_tags(str, &block)
    end

    #######
    private
    #######

    # Create the Webgen::Configuration object from the parameters and the given configuration
    # prefix.
    def create_config(tag, params, tdata, context)
      values = case params
               when Hash
                 values_from_hash(tag, params, tdata, context)
               when String, Array, TrueClass, FalseClass, Numeric
                 values_for_default_mandatory(tag, params, tdata, context)
               when NilClass then {}
               else
                 raise Webgen::RenderError.new("Invalid parameter type (#{params.class})",
                                               "tag", context.dest_node, context.ref_node)
               end

      if !tdata.mandatory.all? {|k| values.has_key?(k)}
        raise Webgen::RenderError.new("Not all mandatory parameters set", "tag", context.dest_node, context.ref_node)
      end
      config = context.website.config.dup
      config.set_values(values)
      config.freeze
      config
    end

    # Return a hash containing valid configuration options by taking key-value pairs from +params+.
    def values_from_hash(tag, params, tdata, context)
      result = {}
      params.each do |key, value|
        if context.website.config.option?(key)
          result[key] = value
        elsif context.website.config.option?(tdata.config_prefix + '.' + key)
          result[tdata.config_prefix + '.' + key] = value
        else
          context.website.logger.warn do
            ["Invalid configuration option '#{key}' for tag '#{tag}' found in <#{context.ref_node}>",
             "Remove the invalid key '#{key}' to fix the warning."]
          end
        end
      end
      result
    end

    # Return a hash containing valid configuration options by setting the default mandatory
    # parameter for +tag+ to +value+.
    def values_for_default_mandatory(tag, value, tdata, context)
      if tdata.mandatory.first.nil?
        context.website.logger.error do
          ["No default mandatory option specified for tag '#{tag}' but set in <#{context.ref_node}>",
           "Use the {key: value} syntax for assigning the value '#{value}' to the intended tag option!"]
        end
        {}
      else
        {tdata.mandatory.first => value}
      end
    end

    # Return the tag data for +tag+ or +nil+ if +tag+ is unknown.
    def tag_data(tag, context)
      tdata = @extensions[tag.to_sym] || @extensions[:default]
      if tdata && !tdata.initialized
        tdata.object = resolve_class(tdata.object)
        tdata.mandatory.each_with_index do |o, index|
          next if context.website.config.option?(o)
          o = tdata.config_prefix + '.' + o
          if context.website.config.option?(o)
            tdata.mandatory[index] = o
          else
            raise ArgumentError, "Invalid configuration option name '#{o}' specified as mandatory option for tag '#{tag}'"
          end
        end
        tdata.initialized = true
      end
      tdata
    end

  end

end
