# frozen_string_literal: true
# Copyright (c) 2011-2017 VMware, Inc.  All Rights Reserved.
# SPDX-License-Identifier: MIT

require 'time'

module RbVmomi

  class NewDeserializer
    NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'

    DEMANGLED_ARRAY_TYPES = {
      'AnyType' => 'xsd:anyType',
      'DateTime' => 'xsd:dateTime',
    }
    %w(Boolean String Byte Short Int Long Float Double).each do |x|
      DEMANGLED_ARRAY_TYPES[x] = "xsd:#{x.downcase}"
    end

    BUILTIN_TYPE_ACTIONS = {
      'xsd:string' => :string,
      'xsd:boolean' => :boolean,
      'xsd:byte' => :int,
      'xsd:short' => :int,
      'xsd:int' => :int,
      'xsd:long' => :int,
      'xsd:float' => :float,
      'xsd:dateTime' => :date,
      'PropertyPath' => :string,
      'MethodName' => :string,
      'TypeName' => :string,
      'xsd:base64Binary' => :binary,
      'KeyValue' => :keyvalue,
    }

    BUILTIN_TYPE_ACTIONS.dup.each do |k, v|
      BUILTIN_TYPE_ACTIONS[$'] = v if k =~ /^xsd:/
    end

    def initialize conn
      @conn = conn
      @loader = conn.class.loader
    end

    def deserialize node, type=nil
      type_attr = node['type']

      # Work around for 1.5.x which doesn't populate node['type']
      # XXX what changed
      type_attr = node.attributes['type'].value if node.attributes['type'] and not type_attr

      type = type_attr if type_attr

      if action = BUILTIN_TYPE_ACTIONS[type]
        case action
        when :string
          node.content
        when :boolean
          node.content == '1' || node.content == 'true'
        when :int
          node.content.to_i
        when :float
          node.content.to_f
        when :date
          leaf_date node
        when :binary
          leaf_binary node
        when :keyvalue
          leaf_keyvalue node
        else raise
        end
      else
        type = type.split(':', 2)[1] if type =~ /:/
        if type =~ /^ArrayOf/
          type = DEMANGLED_ARRAY_TYPES[$'] || $'
          return node.children.select(&:element?).map { |c| deserialize c, type }
        end
        type = type.split(':', 2)[1] if type =~ /:/

        klass = @loader.get(type) or raise "no such type '#{type}'"
        case klass.kind
        when :data
          traverse_data node, klass
        when :enum
          node.content
        when :managed
          leaf_managed node, klass
        when :opaque
          leaf_opaque node, klass
        else raise
        end
      end
    end

    def traverse_data node, klass
      obj = klass.new nil
      props = obj.props
      children = node.children.select(&:element?)
      i = 0

      klass.full_props_desc.each do |desc|
        name = desc['name']
        child_type = desc['wsdl_type']

        # Ignore unknown fields
        while child = children[i] and not klass.full_props_set.member? child.name
          i += 1
        end

        if desc['is-array']
          a = []
          while ((child = children[i]) && (child.name == name))
            child = children[i]
            a << deserialize(child, child_type)
            i += 1
          end
          props[name.to_sym] = a
        elsif ((child = children[i]) && (child.name == name))
          props[name.to_sym] = deserialize(child, child_type)
          i += 1
        end
      end

      obj
    end

    def leaf_managed node, klass
      type_attr = node['type']
      klass = @loader.get(type_attr) if type_attr
      klass.new(@conn, node.content)
    end

    def leaf_opaque node, klass
      klass.new(@conn, node.content)
    end

    def leaf_date node
      Time.parse node.content
    end

    def leaf_binary node
      node.content.unpack('m')[0]
    end

    # XXX does the value need to be deserialized?
    def leaf_keyvalue node
      h = {}
      node.children.each do |child|
        next unless child.element?

        h[child.name] = child.content
      end
      [h['key'], h['value']]
    end
  end

  class OldDeserializer
    NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'

    def initialize conn
      @conn = conn
    end

    def deserialize xml, typename=nil
      if IS_JRUBY
        type_attr = xml.attribute_nodes.find { |a| a.name == 'type' &&
                                                   a.namespace &&
                                                   a.namespace.prefix == 'xsi' }
      else
        type_attr = xml.attribute_with_ns('type', NS_XSI)
      end
      typename = (type_attr || typename).to_s

      if typename =~ /^ArrayOf/
        typename = demangle_array_type $'
        return xml.children.select(&:element?).map { |c| deserialize c, typename }
      end

      t = @conn.type typename
      if t <= BasicTypes::DataObject
        props_desc = t.full_props_desc
        h = {}
        props_desc.select { |d| d['is-array'] }.each { |d| h[d['name'].to_sym] = [] }
        xml.children.each do |c|
          next unless c.element?

          field = c.name.to_sym
          d = t.find_prop_desc(field.to_s) or next
          o = deserialize c, d['wsdl_type']
          if h[field].is_a? Array
            h[field] << o
          else
            h[field] = o
          end
        end
        t.new h
      elsif t == BasicTypes::ManagedObjectReference
        @conn.type(xml['type']).new @conn, xml.text
      elsif t <= BasicTypes::ManagedObject
        @conn.type(xml['type'] || t.wsdl_name).new @conn, xml.text
      elsif t <= BasicTypes::Enum
        xml.text
      elsif t <= BasicTypes::KeyValue
        h = {}
        xml.children.each do |c|
          next unless c.element?

          h[c.name] = c.text
        end
        [h['key'], h['value']]
      elsif t <= String
        xml.text
      elsif t <= Symbol
        xml.text.to_sym
      elsif t <= Integer
        xml.text.to_i
      elsif t <= Float
        xml.text.to_f
      elsif t <= Time
        Time.parse xml.text
      elsif t == BasicTypes::Boolean
        xml.text == 'true' || xml.text == '1'
      elsif t == BasicTypes::Binary
        xml.text.unpack('m')[0]
      elsif t == BasicTypes::AnyType
        raise 'attempted to deserialize an AnyType'
      else raise "unexpected type #{t.inspect} (#{t.ancestors * '/'})"
      end
    rescue
      RbVmomi.logger.error("#{$!.class} while deserializing #{xml.name} (#{typename}):\n#{xml.to_s}")
      raise
    end

    def demangle_array_type x
      case x
      when 'AnyType' then 'anyType'
      when 'DateTime' then 'dateTime'
      when 'Boolean', 'String', 'Byte', 'Short', 'Int', 'Long', 'Float', 'Double' then x.downcase
      else x
      end
    end
  end

  if ENV['RBVMOMI_NEW_DESERIALIZER'] == '1' || true # Always use new one now
    Deserializer = NewDeserializer
  else
    Deserializer = OldDeserializer
  end

end
