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

require 'time'
require 'date'
require_relative 'trivial_soap'
require_relative 'basic_types'
require_relative 'fault'
require_relative 'type_loader'
require_relative 'deserialization'

module RbVmomi

  IS_JRUBY = RUBY_PLATFORM == 'java'

  class DeserializationFailed < Exception; end

  class Connection < TrivialSoap
    NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'

    attr_accessor :rev
    attr_reader :profile
    attr_reader :profile_summary
    attr_accessor :profiling
    attr_reader :deserializer

    def initialize opts
      @ns = opts[:ns] or raise 'no namespace specified'
      @rev = opts[:rev] or raise 'no revision specified'
      @deserializer = Deserializer.new self
      reset_profiling
      @profiling = false
      super opts
    end

    def reset_profiling
      @profile = {}
      @profile_summary = {network_latency: 0, request_emit: 0, response_parse: 0, num_calls: 0}
    end

    def emit_request xml, method, descs, this, params
      xml.tag! method, xmlns: @ns do
        obj2xml xml, '_this', 'ManagedObject', false, this
        descs.each do |d|
          k = d['name']
          k = k.to_sym if !params.member?(k) && params.member?(k.to_sym)
          v = params[k]
          if not v == nil
            obj2xml xml, d['name'], d['wsdl_type'], d['is-array'], v
          else
            raise "missing required parameter #{d['name']}" unless d['is-optional']
          end
        end
      end
    end

    def parse_response resp, desc
      if resp.at('faultcode')
        detail = resp.at('detail')
        fault = detail && @deserializer.deserialize(detail.children.first, 'MethodFault')
        msg = resp.at('faultstring').text
        if fault
          raise RbVmomi::Fault.new(msg, fault)
        else
          raise "#{resp.at('faultcode').text}: #{msg}"
        end
      else
        if desc
          type = desc['is-task'] ? 'Task' : desc['wsdl_type']
          returnvals = resp.children.select(&:element?).map { |c| @deserializer.deserialize c, type }
          (desc['is-array'] && !desc['is-task']) ? returnvals : returnvals.first
        else
          nil
        end
      end
    end

    def call method, desc, this, params
      raise 'this is not a managed object' unless this.is_a? BasicTypes::ManagedObject
      raise 'parameters must be passed as a hash' unless params.is_a? Hash
      raise unless desc.is_a? Hash

      t1 = Time.now
      body = soap_envelope do |xml|
        emit_request xml, method, desc['params'], this, params
      end.target!

      t2 = Time.now
      resp, resp_size = request "#{@ns}/#{@rev}", body

      t3 = Time.now
      out = parse_response resp, desc['result']

      if @profiling
        t4 = Time.now
        @profile[method] ||= []
        profile_info = {
          network_latency: (t3 - t2),
          request_emit: t2 - t1,
          response_parse: t4 - t3,
          params: params,
          obj: this,
          backtrace: caller,
          request_size: body.length,
          response_size: resp_size,
        }
        @profile[method] << profile_info
        @profile_summary[:network_latency] += profile_info[:network_latency]
        @profile_summary[:response_parse] += profile_info[:response_parse]
        @profile_summary[:request_emit] += profile_info[:request_emit]
        @profile_summary[:num_calls] += 1
      end

      out
    end

    # hic sunt dracones
    def obj2xml xml, name, type, is_array, o, attrs={}
      expected = type(type)
      raise "expected array for '#{name}', got #{o.class.wsdl_name}" if is_array and not (o.is_a? Array or (o.is_a? Hash and expected == BasicTypes::KeyValue))

      case o
      when Array, BasicTypes::KeyValue
        if o.is_a? BasicTypes::KeyValue and expected != BasicTypes::KeyValue
          raise "expected #{expected.wsdl_name} for '#{name}', got KeyValue"
        elsif expected == BasicTypes::KeyValue and not is_array
          xml.tag! name, attrs do
            xml.tag! 'key', o[0].to_s
            xml.tag! 'value', o[1].to_s
          end
        else
          raise "expected #{expected.wsdl_name} for '#{name}', got array" unless is_array

          o.each do |e|
            obj2xml xml, name, expected.wsdl_name, false, e, attrs
          end
        end
      when BasicTypes::ManagedObject
        raise "expected #{expected.wsdl_name} for '#{name}', got #{o.class.wsdl_name} for field #{name.inspect}" if expected and not expected >= o.class and not expected == BasicTypes::AnyType

        xml.tag! name, o._ref, type: o.class.wsdl_name
      when BasicTypes::DataObject
        raise "expected #{expected.wsdl_name} for '#{name}', got #{o.class.wsdl_name} for field #{name.inspect}" if expected and not expected >= o.class and not expected == BasicTypes::AnyType

        xml.tag! name, attrs.merge('xsi:type' => o.class.wsdl_name) do
          o.class.full_props_desc.each do |desc|
            if o.props.member? desc['name'].to_sym
              v = o.props[desc['name'].to_sym]
              next if v.nil?

              obj2xml xml, desc['name'], desc['wsdl_type'], desc['is-array'], v
            end
          end
        end
      when BasicTypes::Enum
        xml.tag! name, o.value.to_s, attrs
      when Hash
        if expected == BasicTypes::KeyValue and is_array
          obj2xml xml, name, type, is_array, o.to_a, attrs
        else
          raise "expected #{expected.wsdl_name} for '#{name}', got a hash" unless expected <= BasicTypes::DataObject

          obj2xml xml, name, type, false, expected.new(o), attrs
        end
      when true, false
        raise "expected #{expected.wsdl_name} for '#{name}', got a boolean" unless [BasicTypes::Boolean, BasicTypes::AnyType].member? expected

        attrs['xsi:type'] = 'xsd:boolean' if expected == BasicTypes::AnyType
        xml.tag! name, (o ? '1' : '0'), attrs
      when Symbol, String
        if expected == BasicTypes::Binary
          attrs['xsi:type'] = 'xsd:base64Binary' if expected == BasicTypes::AnyType
          xml.tag! name, [o].pack('m').chomp.gsub("\n", ''), attrs
        else
          attrs['xsi:type'] = 'xsd:string' if expected == BasicTypes::AnyType
          xml.tag! name, o.to_s, attrs
        end
      when Integer
        attrs['xsi:type'] = 'xsd:long' if expected == BasicTypes::AnyType
        xml.tag! name, o.to_s, attrs
      when Float
        attrs['xsi:type'] = 'xsd:double' if expected == BasicTypes::AnyType
        xml.tag! name, o.to_s, attrs
      when DateTime
        attrs['xsi:type'] = 'xsd:dateTime' if expected == BasicTypes::AnyType
        xml.tag! name, o.strftime('%FT%T%:z'), attrs
      when Time
        attrs['xsi:type'] = 'xsd:dateTime' if expected == BasicTypes::AnyType
        xml.tag! name, o.iso8601, attrs
      when BasicTypes::Int
        attrs['xsi:type'] = 'xsd:int'
        xml.tag! name, o.to_s, attrs
      else raise "unexpected object class #{o.class} for '#{name}'"
      end
      xml
    rescue
      $stderr.puts "#{$!.class} while serializing #{name} (#{type}):"
      PP.pp o, $stderr
      raise
    end

    def self.type name
      raise unless name and (name.is_a? String or name.is_a? Symbol)

      name = $' if name.to_s =~ /^xsd:/
      case name.to_sym
      when :anyType then BasicTypes::AnyType
      when :boolean then BasicTypes::Boolean
      when :string then String
      when :int, :long, :short, :byte then Integer
      when :float, :double then Float
      when :dateTime then Time
      when :base64Binary then BasicTypes::Binary
      when :KeyValue then BasicTypes::KeyValue
      else
        first_char = name[0].chr
        name = '%s%s' % [first_char.upcase, name[1..-1]] if first_char.downcase == first_char

        if @loader.has? name
          const_get(name)
        else
          raise "no such type #{name.inspect}"
        end
      end
    end

    def type name
      self.class.type name
    end

    def instanceUuid
      nil
    end

    def self.extension_dirs
      @extension_dirs ||= []
    end

    def self.add_extension_dir dir
      extension_dirs << dir
      @loader.reload_extensions_dir dir if @loader
    end

    def self.reload_extensions
      @loader.reload_extensions
    end

    def self.loader; @loader; end

  protected

    def self.const_missing sym
      name = sym.to_s
      if @loader and @loader.has? name
        @loader.get(name)
      else
        super
      end
    end

    def self.method_missing sym, *a
      name = sym.to_s
      if @loader and @loader.has? name
        @loader.get(name).new(*a)
      else
        super
      end
    end

    def self.load_vmodl fn
      @loader = RbVmomi::TypeLoader.new fn, extension_dirs, self
      nil
    end
  end

end
