require 'strscan'

module ActiveLdap
  class DistinguishedName
    include GetTextSupport

    class Parser
      include GetTextSupport

      attr_reader :dn
      def initialize(source)
        @dn = nil
        source = source.to_s if source.is_a?(DN)
        unless source.is_a?(String)
          raise DistinguishedNameInputInvalid.new(source)
        end
        @source = source
      end

      def parse
        return @dn if @dn

        rdns = []
        scanner = StringScanner.new(@source)

        scanner.scan(/\s*/)
        raise rdn_is_missing if scanner.scan(/\s*\+\s*/)
        raise name_component_is_missing if scanner.scan(/\s*,\s*/)

        rdn = {}
        until scanner.eos?
          type = scan_attribute_type(scanner)
          skip_attribute_type_and_value_separator(scanner)
          value = scan_attribute_value(scanner)
          rdn[type] = value
          if scanner.scan(/\s*\+\s*/)
            raise rdn_is_missing if scanner.eos?
          elsif scanner.scan(/\s*\,\s*/)
            rdns << rdn
            rdn = {}
            raise name_component_is_missing if scanner.eos?
          else
            scanner.scan(/\s*/)
            rdns << rdn if scanner.eos?
          end
        end

        @dn = DN.new(*rdns)
        @dn
      end

      private
      ATTRIBUTE_TYPE_RE = /\s*([a-zA-Z][a-zA-Z\d\-]*|\d+(?:\.\d+)*)\s*/
      def scan_attribute_type(scanner)
        raise attribute_type_is_missing unless scanner.scan(ATTRIBUTE_TYPE_RE)
        scanner[1]
      end

      def skip_attribute_type_and_value_separator(scanner)
        raise attribute_value_is_missing unless scanner.scan(/\s*=\s*/)
      end

      HEX_PAIR = "(?:[\\da-fA-F]{2})"
      STRING_CHARS_RE = /[^,=\+<>\#;\\\"]*/ #
      PAIR_RE = /\\([,=\+<>\#;]|\\|\"| |(#{HEX_PAIR}))/ #
      HEX_STRING_RE = /\#(#{HEX_PAIR}+)/ #
      def scan_attribute_value(scanner)
        if scanner.scan(HEX_STRING_RE)
          value = scanner[1].scan(/../).collect do |hex_pair|
            hex_pair.hex
          end.pack("C*")
        elsif scanner.scan(/\"/)
          value = scan_quoted_attribute_value(scanner)
        else
          value = scan_not_quoted_attribute_value(scanner)
        end
        raise attribute_value_is_missing if value.blank?

        value
      end

      def scan_quoted_attribute_value(scanner)
        result = ""
        until scanner.scan(/\"/)
          scanner.scan(/([^\\\"]*)/)
          quoted_strings = scanner[1]
          pairs = collect_pairs(scanner)

          if scanner.eos? or (quoted_strings.empty? and pairs.empty?)
            raise found_unmatched_quotation
          end

          result << quoted_strings
          result << pairs
        end
        result
      end

      def scan_not_quoted_attribute_value(scanner)
        result = ""
        until scanner.eos?
          prev_size = result.size
          pairs = collect_pairs(scanner)
          strings = scanner.scan(STRING_CHARS_RE)
          result << pairs if !pairs.nil? and !pairs.empty?
          unless strings.nil?
            if scanner.peek(1) == ","
              result << strings.rstrip
            else
              result << strings
            end
          end
          break if prev_size == result.size
        end
        result
      end

      def collect_pairs(scanner)
        result = ""
        while scanner.scan(PAIR_RE)
          if scanner[2]
            result << [scanner[2].hex].pack("C*")
          else
            result << scanner[1]
          end
        end
        result.force_encoding("utf-8") if result.respond_to?(:force_encoding)
        result
      end

      def invalid_dn(reason)
        DistinguishedNameInvalid.new(@source, reason)
      end

      def name_component_is_missing
        invalid_dn(_("name component is missing"))
      end

      def rdn_is_missing
        invalid_dn(_("relative distinguished name (RDN) is missing"))
      end

      def attribute_type_is_missing
        invalid_dn(_("attribute type is missing"))
      end

      def attribute_value_is_missing
        invalid_dn(_("attribute value is missing"))
      end

      def found_unmatched_quotation
        invalid_dn(_("found unmatched quotation"))
      end
    end

    class << self
      def parse(source)
        Parser.new(source).parse
      end

      def escape_value(value)
        if /(\A | \z)/.match(value)
          '"' + value.gsub(/([\\\"])/, '\\\\\1') + '"'
        else
          value.gsub(/([,=\+<>#;\\\"])/, '\\\\\1')
        end
      end
    end

    attr_reader :rdns
    def initialize(*rdns)
      @rdns = rdns.collect do |rdn|
        if rdn.is_a?(Array) and rdn.size == 2
          {rdn[0] => rdn[1]}
        else
          rdn
        end
      end
    end

    def blank?
      @rdns.blank?
    end

    def +(other)
      self.class.new(*(@rdns + other.rdns))
    end

    def -(other)
      rdns = @rdns.dup
      normalized_rdns = normalize(@rdns)
      normalize(other.rdns).reverse_each do |rdn|
        if rdn == normalized_rdns.pop
          rdns.pop
        else
          raise ArgumentError, _("%s isn't sub DN of %s") % [other, self]
        end
      end
      self.class.new(*rdns)
    end

    def <<(rdn)
      @rdns << rdn
      self
    end

    def unshift(rdn)
      @rdns.unshift(rdn)
    end

    def parent
      return nil if @rdns.size <= 1
      self.class.new(*@rdns[1..-1])
    end

    def <=>(other)
      other = DN.parse(other) if other.is_a?(String)
      return nil unless other.is_a?(self.class)
      normalize_for_comparing(@rdns) <=>
        normalize_for_comparing(other.rdns)
    end

    def ==(other)
      case other
      when self.class
        normalize(@rdns) == normalize(other.rdns)
      when String
        parsed_other = nil
        begin
          parsed_other = self.class.parse(other)
        rescue DistinguishedNameInvalid
          return false
        end
        self == parsed_other
      else
        false
      end
    end

    def eql?(other)
      other.is_a?(self.class) and
        normalize(@rdns).to_s.eql?(normalize(other.rdns).to_s)
    end

    def hash
      normalize(@rdns).to_s.hash
    end

    def to_s
      klass = self.class
      @rdns.collect do |rdn|
        rdn.sort_by do |type, value|
          type.upcase
        end.collect do |type, value|
          "#{type}=#{klass.escape_value(value)}"
        end.join("+")
      end.join(",")
    end

    def to_str # for backward compatibility
      to_s
    end

    def to_human_readable_format
      to_s.inspect
    end

    private
    def normalize(rdns)
      rdns.collect do |rdn|
        normalized_rdn = {}
        rdn.each do |key, value|
          normalized_rdn[key.upcase] = value.upcase
        end
        normalized_rdn
      end
    end

    def normalize_for_comparing(rdns)
      normalize(rdns).collect do |rdn|
        rdn.sort_by do |key, value|
          key
        end
      end.collect do |key, value|
        [key, value]
      end
    end
  end

  DN = DistinguishedName
end
