module Psych
  module Visitors
    ###
    # YAMLTree builds a YAML ast given a ruby object.  For example:
    #
    #   builder = Psych::Visitors::YAMLTree.new
    #   builder << { :foo => 'bar' }
    #   builder.tree # => #<Psych::Nodes::Stream .. }
    #
    class YAMLTree < Psych::Visitors::Visitor
      def initialize options = {}, emitter = Psych::TreeBuilder.new
        super()
        @emitter  = emitter
        @st       = {}
        @ss       = ScalarScanner.new

        @dispatch_cache = Hash.new do |h,klass|
          method = "visit_#{(klass.name || '').split('::').join('_')}"

          method = respond_to?(method) ? method : h[klass.superclass]

          raise(TypeError, "Can't dump #{target.class}") unless method

          h[klass] = method
        end
      end

      def start encoding = Nodes::Stream::UTF8
        @emitter.start_stream(encoding).tap do
          @started = true
        end
      end

      def finish
        @emitter.end_stream.tap do
          @finished = true
        end
      end

      def tree
        finish unless finished?
      end

      def push object
        start unless started?
        @emitter.start_document [], [], false
        accept object
        @emitter.end_document
      end
      alias :<< :push

      def accept target
        # return any aliases we find
        if node = @st[target.object_id]
          node.anchor = target.object_id.to_s
          return @emitter.alias target.object_id.to_s
        end

        if target.respond_to?(:to_yaml)
          loc = target.method(:to_yaml).source_location.first
          if loc !~ /(syck\/rubytypes.rb|psych\/core_ext.rb)/
            unless target.respond_to?(:encode_with)
              if $VERBOSE
                warn "implementing to_yaml is deprecated, please implement \"encode_with\""
              end

              target.to_yaml(:nodump => true)
            end
          end
        end

        if target.respond_to?(:encode_with)
          dump_coder target
        else
          send(@dispatch_cache[target.class], target)
        end
      end

      def visit_Psych_Omap o
        seq = @emitter.start_sequence(nil, '!omap', false, Nodes::Sequence::BLOCK)
        register(o, seq)

        o.each { |k,v| visit_Hash k => v }
        @emitter.end_sequence
      end

      def visit_Object o
        tag = Psych.dump_tags[o.class]
        unless tag
          klass = o.class == Object ? nil : o.class.name
          tag   = ['!ruby/object', klass].compact.join(':')
        end

        map = @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
        register(o, map)

        dump_ivars o
        @emitter.end_mapping
      end

      def visit_Struct o
        tag = ['!ruby/struct', o.class.name].compact.join(':')

        register o, @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
        o.members.each do |member|
          @emitter.scalar member.to_s, nil, nil, true, false, Nodes::Scalar::ANY
          accept o[member]
        end
        dump_ivars o

        @emitter.end_mapping
      end

      def visit_Exception o
        tag = ['!ruby/exception', o.class.name].join ':'

        @emitter.start_mapping nil, tag, false, Nodes::Mapping::BLOCK

        v = o.__message  # maglev patch
        if v
          @emitter.scalar( 'message', nil, nil, true, false, Nodes::Scalar::ANY)
          accept(v)
        end
        # maglev, backtrace not included in output , not used by load 

        dump_ivars o

        @emitter.end_mapping
      end

      def visit_Regexp o
        @emitter.scalar o.inspect, nil, '!ruby/regexp', false, false, Nodes::Scalar::ANY
      end

      def visit_Time o
        formatted = o.strftime("%Y-%m-%d %H:%M:%S")
        if o.utc?
          formatted += ".%06dZ" % [o.usec]
        else
          formatted += ".%06d %+.2d:00" % [o.usec, o.gmt_offset / 3600]
        end

        @emitter.scalar formatted, nil, nil, true, false, Nodes::Scalar::ANY
      end

      def visit_Rational o
        @emitter.start_mapping(nil, '!ruby/object:Rational', false, Nodes::Mapping::BLOCK)

        [
          'denominator', o.denominator.to_s,
          'numerator', o.numerator.to_s
        ].each do |m|
          @emitter.scalar m, nil, nil, true, false, Nodes::Scalar::ANY
        end

        @emitter.end_mapping
      end

      def visit_Complex o
        @emitter.start_mapping(nil, '!ruby/object:Complex', false, Nodes::Mapping::BLOCK)

        ['real', o.real.to_s, 'image', o.imag.to_s].each do |m|
          @emitter.scalar m, nil, nil, true, false, Nodes::Scalar::ANY
        end

        @emitter.end_mapping
      end

      def visit_Integer o
        @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
      end
      alias :visit_TrueClass :visit_Integer
      alias :visit_FalseClass :visit_Integer
      alias :visit_Date :visit_Integer

      def visit_Float o
        if o.nan?
          @emitter.scalar '.nan', nil, nil, true, false, Nodes::Scalar::ANY
        elsif o.infinite?
          @emitter.scalar((o.infinite? > 0 ? '.inf' : '-.inf'),
            nil, nil, true, false, Nodes::Scalar::ANY)
        else
          @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
        end
      end

      def visit_String o
        plain = false
        quote = false
        style = Nodes::Scalar::ANY

        # GemStone:  MagLev does not yet have fdiv
        #if o.index("\x00") || o.count("^ -~\t\r\n").fdiv(o.length) > 0.3
        if o.index("\x00") || (o.count("^ -~\t\r\n").to_f / o.length) > 0.3
        # End GemStone
          str   = [o].pack('m').chomp
          tag   = '!binary' # FIXME: change to below when syck is removed
          #tag   = 'tag:yaml.org,2002:binary'
          style = Nodes::Scalar::LITERAL
        else
          str   = o
          tag   = nil
          quote = !(String === @ss.tokenize(o))
          plain = !quote
        end

        ivars = find_ivars o

        if ivars.empty?
          @emitter.scalar str, nil, tag, plain, quote, style
        else
          @emitter.start_mapping nil, '!str', false, Nodes::Mapping::BLOCK
          @emitter.scalar 'str', nil, nil, true, false, Nodes::Scalar::ANY
          @emitter.scalar str, nil, tag, plain, quote, style

          dump_ivars o

          @emitter.end_mapping
        end
      end

      def visit_Class o
        raise TypeError, "can't dump anonymous class #{o.class}"
      end

      def visit_Range o
        @emitter.start_mapping nil, '!ruby/range', false, Nodes::Mapping::BLOCK
        ['begin', o.begin, 'end', o.end, 'excl', o.exclude_end?].each do |m|
          accept m
        end
        @emitter.end_mapping
      end

      def visit_Hash o
        register(o, @emitter.start_mapping(nil, nil, true, Psych::Nodes::Mapping::BLOCK))

        o.each do |k,v|
          accept k
          accept v
        end

        @emitter.end_mapping
      end

      def visit_Psych_Set o
        register(o, @emitter.start_mapping(nil, '!set', false, Psych::Nodes::Mapping::BLOCK))

        o.each do |k,v|
          accept k
          accept v
        end

        @emitter.end_mapping
      end

      def visit_Array o
        register o, @emitter.start_sequence(nil, nil, true, Nodes::Sequence::BLOCK)
        o.each { |c| accept c }
        @emitter.end_sequence
      end

      def visit_NilClass o
        @emitter.scalar('', nil, 'tag:yaml.org,2002:null', false, false, Nodes::Scalar::ANY)
      end

      def visit_Symbol o
        @emitter.scalar ":#{o}", nil, nil, true, false, Nodes::Scalar::ANY
      end

      private
      # FIXME: remove this method once "to_yaml_properties" is removed
      def find_ivars target
        loc = target.method(:to_yaml_properties).source_location.first
        unless loc.start_with?(Psych::DEPRECATED) || loc.end_with?('rubytypes.rb')
          if $VERBOSE
            warn "#{loc}: to_yaml_properties is deprecated, please implement \"encode_with(coder)\""
          end
          return target.to_yaml_properties
        end

        target.instance_variables
      end

      def register target, yaml_obj
        @st[target.object_id] = yaml_obj
        yaml_obj
      end

      def dump_coder o
        tag = Psych.dump_tags[o.class]
        unless tag
          klass = o.class == Object ? nil : o.class.name
          tag   = ['!ruby/object', klass].compact.join(':')
        end

        c = Psych::Coder.new(tag)
        o.encode_with(c)
        emit_coder c
      end

      def emit_coder c
        case c.type
        when :scalar
          @emitter.scalar c.scalar, nil, c.tag, c.tag.nil?, false, Nodes::Scalar::ANY
        when :seq
          @emitter.start_sequence nil, c.tag, c.tag.nil?, Nodes::Sequence::BLOCK
          c.seq.each do |thing|
            accept thing
          end
          @emitter.end_sequence
        when :map
          @emitter.start_mapping nil, c.tag, c.implicit, c.style
          c.map.each do |k,v|
            @emitter.scalar k, nil, nil, true, false, Nodes::Scalar::ANY
            accept v
          end
          @emitter.end_mapping
        end
      end

      def dump_ivars target
        ivars = find_ivars target

        ivars.each do |iv|
          @emitter.scalar("#{iv.to_s.sub(/^@/, '')}", nil, nil, true, false, Nodes::Scalar::ANY)
          accept target.instance_variable_get(iv)
        end
      end
    end
  end
end
