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

require 'pp'
require 'set'

module RbVmomi
  module BasicTypes

    BUILTIN = Set.new %w(ManagedObject DataObject TypeName PropertyPath ManagedObjectReference MethodName MethodFault LocalizedMethodFault KeyValue OpaqueObject)

    class Base
      class << self
        attr_accessor :wsdl_name

        def init wsdl_name=self.name
          @wsdl_name = wsdl_name
        end

        def to_s
          @wsdl_name
        end
      end

      init
    end

    class ObjectWithProperties < Base
      class << self
        attr_accessor :props_desc

        def init name=self.name, props=[]
          super name
          @props_desc = props
          @props_desc.each do |d|
            sym = d['name'].to_sym
            define_method(sym) { _get_property sym }
            define_method(:"#{sym}=") { |x| _set_property sym, x }
          end
        end

        def full_props_set
          @full_props_set ||= Set.new(full_props_desc.map { |x| x['name'] })
        end

        def full_props_desc
          @full_props_desc ||= (self == ObjectWithProperties ? [] : superclass.full_props_desc) + props_desc
        end

        def find_prop_desc name
          full_props_desc.find { |x| x['name'] == name.to_s }
        end
      end

      def _get_property sym
        raise 'unimplemented'
      end

      def _set_property sym, val
        raise 'unimplemented'
      end

      init
    end

    class ObjectWithMethods < ObjectWithProperties
      class << self
        attr_accessor :methods_desc

        def init name=self.name, props=[], methods={}
          super name, props
          @methods_desc = methods

          @methods_desc.each do |k, d|
            sym = k.to_sym
            define_method(sym) { |*args| _call sym, *args }
            define_method(:"#{sym}!") { |*args| _call sym, *args }
          end
        end

        # XXX cache
        def full_methods_desc
          (self == ObjectWithMethods ? {} : superclass.full_methods_desc).merge methods_desc
        end
      end

      init
    end

    class DataObject < ObjectWithProperties
      attr_reader :props

      def self.kind; :data end

      def initialize props={}
        # Deserialization fast path
        if props == nil
          @props = {}
          return
        end

        @props = Hash[props.map { |k, v| [k.to_sym, v] }]
        #self.class.full_props_desc.each do |desc|
          #fail "missing required property #{desc['name'].inspect} of #{self.class.wsdl_name}" if @props[desc['name'].to_sym].nil? and not desc['is-optional']
        #end
        @props.each do |k, v|
          raise "unexpected property name #{k}" unless self.class.find_prop_desc(k)
        end
      end

      def initialize_copy(source)
        super
        @props = @props.dup
      end

      def _get_property sym
        @props[sym]
      end

      def [] sym
        _get_property sym
      end

      def _set_property sym, val
        @props[sym] = val
      end

      def []= sym, val
        _set_property sym, val
      end

      def == o
        return false unless o.class == self.class

        hash == o.hash
      end

      alias eql? ==

      def hash
        props.hash
      end

      def pretty_print q
        q.text self.class.wsdl_name
        q.group 2 do
          q.text '('
          q.breakable
          props = @props.sort_by { |k, v| k.to_s }
          q.seplist props, nil, :each do |e|
            k, v = e
            q.group do
              q.text k.to_s
              q.text ': '
              q.pp v
            end
          end
        end
        q.breakable
        q.text ')'
      end

      def as_hash(val)
        if val.kind_of?(Array)
          val.map { |v| as_hash(v) }
        elsif val.respond_to?(:to_hash)
          val.to_hash
        else
          val
        end
      end

      def to_hash
        props.transform_values { |v| as_hash(v) }
      end

      def to_json(options = nil)
        to_hash.merge(JSON.create_id => self.class.name).to_json
      end

      init
    end

    module ObjectWithRefAndConnection
      def initialize connection, ref
        super()
        @connection = connection
        @soap = @connection # XXX deprecated
        @ref = ref
      end

      def _connection
        @connection
      end

      def _ref
        @ref
      end

      def to_s
        "#{self.class.wsdl_name}(#{@ref.inspect})"
      end

      def to_hash
        to_s
      end

      def pretty_print pp
        pp.text to_s
      end

      def == x
        out = x.hash == hash
        out = (x._connection.instanceUuid == self._connection.instanceUuid) if out && x._connection.host
        out
      end

      alias eql? ==

      def hash
        [self.class, @ref].hash
      end
    end

    class ManagedObject < ObjectWithMethods
      include ObjectWithRefAndConnection

      def self.kind; :managed end

      def _get_property sym
        ret = @connection.propertyCollector.RetrieveProperties(specSet: [{
          propSet: [{ type: self.class.wsdl_name, pathSet: [sym.to_s] }],
          objectSet: [{ obj: self }],
        }])[0]

        if !ret
          return nil
        elsif ret.propSet.empty?
          return nil if ret.missingSet.empty?

          raise ret.missingSet[0].fault
        else
          ret.propSet[0].val
        end
      end

      def _set_property sym, val
        raise 'unimplemented'
      end

      def _call method, o={}
        raise 'parameters must be passed as a hash' unless o.is_a? Hash

        desc = self.class.full_methods_desc[method.to_s] or raise 'unknown method'
        @connection.call method, desc, self, o
      end

      def [] k
        _get_property k
      end

      init 'ManagedObject'
    end

    class Enum < Base
      class << self
        attr_accessor :values

        def init name=self.name, values=[]
          super name
          @values = values
        end
      end

      def self.kind; :enum end

      attr_reader :value

      def initialize value
        @value = value
      end

      def to_hash
        value
      end

      init
    end

    class MethodFault < DataObject
      init 'MethodFault', [
        {
          'name' => 'faultCause',
          'wsdl_type' => 'LocalizedMethodFault',
          'is-array' => false,
          'is-optional' => true,
        }, {
          'name' => 'faultMessage',
          'wsdl_type' => 'LocalizableMessage',
          'is-array' => true,
          'is-optional' => true,
        },
      ]

      def self.=== exn
        exn.class == RbVmomi::Fault and self <= exn.fault.class
      end
    end

    class LocalizedMethodFault < DataObject
      init 'LocalizedMethodFault', [
        {
          'name' => 'fault',
          'wsdl_type' => 'MethodFault',
          'is-array' => false,
          'is-optional' => false,
        }, {
          'name' => 'localizedMessage',
          'wsdl_type' => 'xsd:string',
          'is-array' => false,
          'is-optional' => true,
        },
      ]

      def exception
        RbVmomi::Fault.new(self.localizedMessage, self.fault)
      end
    end

    class RuntimeFault < MethodFault
      init
    end

    class MethodName < String
      def self.wsdl_name; 'MethodName' end
    end

    class PropertyPath < String
      def self.wsdl_name; 'PropertyPath' end
    end

    class TypeName < String
      def self.wsdl_name; 'TypeName' end
    end

    class ManagedObjectReference
      def self.wsdl_name; 'ManagedObjectReference' end
    end

    class Boolean
      def self.wsdl_name; 'xsd:boolean' end
    end

    class AnyType
      def self.wsdl_name; 'xsd:anyType' end
    end

    class Binary
      def self.wsdl_name; 'xsd:base64Binary' end
    end

    class ::Class
      def wsdl_name; self.class.name end
    end

    class ::String
      def self.wsdl_name; 'xsd:string' end
    end

    class ::Integer
      def self.wsdl_name; 'xsd:int' end
    end

    class ::Float
      def self.wsdl_name; 'xsd:float' end
    end

    class Int
      def self.wsdl_name; 'xsd:int' end

      def initialize x
        @val = x
      end

      def to_s
        @val.to_s
      end
    end

    class KeyValue
      def self.wsdl_name; 'KeyValue' end
      attr_accessor :key, :value

      def initialize k, v
        @key = k
        @value = v
      end

      def [] i
        if i == 0 then @key
        elsif i == 1 then @value
        else raise "invalid index #{i.inspect}"
        end
      end
    end

    class OpaqueObject < Base
      include ObjectWithRefAndConnection

      def self.kind; :opaque end

      init 'OpaqueObject'
    end
  end
end
