require 'yaml'
require 'puppet/network'
require 'puppet/network/format'

module Puppet::Network::FormatHandler
  class FormatError < Puppet::Error; end

  class FormatProtector
    attr_reader :format

    def protect(method, args)
        Puppet::Network::FormatHandler.format(format).send(method, *args)
    rescue => details
        direction = method.to_s.include?("intern") ? "from" : "to"
        error = FormatError.new("Could not #{method} #{direction} #{format}: #{details}")
        error.set_backtrace(details.backtrace)
        raise error
    end

    def initialize(format)
      @format = format
    end

    [:intern, :intern_multiple, :render, :render_multiple, :mime].each do |method|
      define_method(method) do |*args|
        protect(method, args)
      end
    end
  end

  @formats = {}
  def self.create(*args, &block)
    instance = Puppet::Network::Format.new(*args)
    instance.instance_eval(&block) if block_given?

    @formats[instance.name] = instance
    instance
  end

  def self.create_serialized_formats(name,options = {},&block)
    ["application/x-#{name}", "application/#{name}", "text/x-#{name}", "text/#{name}"].each { |mime_type|
      create name, {:mime => mime_type}.update(options), &block
    }
  end

  def self.extended(klass)
    klass.extend(ClassMethods)

    # LAK:NOTE This won't work in 1.9 ('send' won't be able to send
    # private methods, but I don't know how else to do it.
    klass.send(:include, InstanceMethods)
  end

  def self.format(name)
    @formats[name.to_s.downcase.intern]
  end

  def self.format_by_extension(ext)
    @formats.each do |name, format|
      return format if format.extension == ext
    end
    nil
  end

  # Provide a list of all formats.
  def self.formats
    @formats.keys
  end

  # Return a format capable of handling the provided mime type.
  def self.mime(mimetype)
    mimetype = mimetype.to_s.downcase
    @formats.values.find { |format| format.mime == mimetype }
  end

  # Use a delegator to make sure any exceptions generated by our formats are
  # handled intelligently.
  def self.protected_format(name)
    name = format_to_canonical_name(name)
    @format_protectors ||= {}
    @format_protectors[name] ||= FormatProtector.new(name)
    @format_protectors[name]
  end

  # Return a format name given:
  #  * a format name
  #  * a mime-type
  #  * a format instance
  def self.format_to_canonical_name(format)
    case format
    when Puppet::Network::Format
      out = format
    when %r{\w+/\w+}
      out = mime(format)
    else
      out = format(format)
    end
    raise ArgumentError, "No format match the given format name or mime-type (#{format})" if out.nil?
    out.name
  end

  module ClassMethods
    def format_handler
      Puppet::Network::FormatHandler
    end

    def convert_from(format, data)
      format_handler.protected_format(format).intern(self, data)
    end

    def convert_from_multiple(format, data)
      format_handler.protected_format(format).intern_multiple(self, data)
    end

    def render_multiple(format, instances)
      format_handler.protected_format(format).render_multiple(instances)
    end

    def default_format
      supported_formats[0]
    end

    def support_format?(name)
      Puppet::Network::FormatHandler.format(name).supported?(self)
    end

    def supported_formats
      result = format_handler.formats.collect { |f| format_handler.format(f) }.find_all { |f| f.supported?(self) }.collect { |f| f.name }.sort do |a, b|
        # It's an inverse sort -- higher weight formats go first.
        format_handler.format(b).weight <=> format_handler.format(a).weight
      end

      result = put_preferred_format_first(result)

      Puppet.debug "#{friendly_name} supports formats: #{result.map{ |f| f.to_s }.sort.join(' ')}; using #{result.first}"

      result
    end

    private

    def friendly_name
      if self.respond_to? :indirection
        indirection.name
      else
        self
      end
    end

    def put_preferred_format_first(list)
      preferred_format = Puppet.settings[:preferred_serialization_format].to_sym
      if list.include?(preferred_format)
        list.delete(preferred_format)
        list.unshift(preferred_format)
      else
        Puppet.debug "Value of 'preferred_serialization_format' (#{preferred_format}) is invalid for #{friendly_name}, using default (#{list.first})"
      end
      list
    end
  end

  module InstanceMethods
    def render(format = nil)
      format ||= self.class.default_format

      Puppet::Network::FormatHandler.protected_format(format).render(self)
    end

    def mime(format = nil)
      format ||= self.class.default_format

      Puppet::Network::FormatHandler.protected_format(format).mime
    end

    def support_format?(name)
      self.class.support_format?(name)
    end
  end
end

require 'puppet/network/formats'
