require 'erb'
require 'builder'

require 'active_ldap/ldif'

module ActiveLdap
  class Xml
    class Serializer
      PRINTABLE_STRING = /[\x20-\x7e\t\r\n]*/n

      def initialize(dn, attributes, schema, options={})
        @dn = dn
        @attributes = attributes
        @schema = schema
        @options = options
      end

      def to_s
        root = @options[:root]
        indent = @options[:indent] || 2
        xml = @options[:builder] || Builder::XmlMarkup.new(:indent => indent)
        xml.tag!(root) do
          target_attributes.each do |key, values|
            values = normalize_values(values).sort_by {|value, _| value}
            if @schema.attribute(key).single_value?
              next if values.empty?
              serialize_attribute_value(xml, key, *values[0])
            else
              serialize_attribute_values(xml, key, values)
            end
          end
        end
      end

      private
      def target_attributes
        except_dn = false
        only = @options[:only] || []
        except = @options[:except] || []
        if !only.empty?
          attributes = []
          except_dn = true
          only.each do |name|
            if name == "dn"
              except_dn = false
            elsif @attributes.has_key?(name)
              attributes << [name, @attributes[name]]
            end
          end
        elsif !except.empty?
          attributes = @attributes.dup
          except.each do |name|
            if name == "dn"
              except_dn = true
            else
              attributes.delete(name)
            end
          end
        else
          attributes = @attributes.dup
        end
        attributes = attributes.sort_by {|key, values| key}
        attributes.unshift(["dn", [@dn]]) unless except_dn
        attributes
      end

      def normalize_values(values)
        targets = []
        values.each do |value|
          targets.concat(normalize_value(value))
        end
        targets
      end

      def normalize_value(value, options=[])
        targets = []
        case value
        when Hash
          value.each do |real_option, real_value|
            targets.concat(normalize_value(real_value, options + [real_option]))
          end
        when Array
          value.each do |real_value|
            targets.concat(normalize_value(real_value, options))
          end
        when DN
          targets.concat(normalize_value(value.to_s, options))
        when nil
          # ignore
        else
          if /\A#{PRINTABLE_STRING}\z/ !~ value
            value = [value].pack("m").gsub(/\n/u, '')
            options += ["base64"]
          end
          xml_attributes = {}
          options.each do |name, val|
            xml_attributes[name] = val || "true"
          end
          targets << [value, xml_attributes]
        end
        targets
      end

      def serialize_attribute_values(xml, name, values)
        return if values.blank?

        if name == "dn" or @options[:type].to_s.downcase == "ldif"
          values.each do |value, xml_attributes|
            serialize_attribute_value(xml, name, value, xml_attributes)
          end
        else
          plural_name = name.pluralize
          attributes = @options[:skip_types] ? {} : {"type" => "array"}
          xml.tag!(plural_name, attributes) do
            values.each do |value, xml_attributes|
              serialize_attribute_value(xml, name, value, xml_attributes)
            end
          end
        end
      end

      def serialize_attribute_value(xml, name, value, xml_attributes)
        xml.tag!(name, value, xml_attributes)
      end
    end

    def initialize(dn, attributes, schema)
      @dn = dn
      @attributes = attributes
      @schema = schema
    end

    def to_s(options={})
      Serializer.new(@dn, @attributes, @schema, options).to_s
    end
  end

  XML = Xml
end
