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

require 'set'
require 'monitor'

module RbVmomi

  class TypeLoader
    def initialize fn, extension_dirs, namespace
      @extension_dirs = extension_dirs
      @namespace = namespace
      @lock = Monitor.new
      @db = {}
      @id2wsdl = {}
      @loaded = {}
      add_types Hash[BasicTypes::BUILTIN.map { |k| [k, nil] }]
      vmodl_database = File.open(fn, 'r') { |io| Marshal.load io }
      vmodl_database.reject! { |k, v| k =~ /^_/ }
      add_types vmodl_database
      preload
    end

    def preload
      names = (@namespace.constants + Object.constants).map(&:to_s).uniq.
                                                        select { |x| has? x }
      names.each { |x| get(x) }
    end

    # Reload all extensions for loaded VMODL types
    def reload_extensions
      @extension_dirs.each do |path|
        reload_extensions_dir path
      end
    end

    # Reload all extensions for loaded VMODL types from the given directory
    def reload_extensions_dir path
      loaded = Set.new(typenames.select { |x| @namespace.const_defined? x })
      Dir.open(path) do |dir|
        dir.each do |file|
          next unless file =~ /\.rb$/
          next unless loaded.member? $`

          file_path = File.join(dir, file)
          load file_path
        end
      end
    end

    def has? name
      raise unless name.is_a? String

      @db.member?(name) or BasicTypes::BUILTIN.member?(name)
    end

    def get name
      raise "name '#{name}' is #{name.class} expecting String" unless name.is_a? String

      first_char = name[0].chr
      name = '%s%s' % [first_char.upcase, name[1..-1]] if first_char.downcase == first_char

      return @loaded[name] if @loaded.member? name

      @lock.synchronize do
        return @loaded[name] if @loaded.member? name

        klass = make_type(name)
        @namespace.const_set name, klass
        load_extension name
        @loaded[name] = klass
      end
    end

    def add_types types
      @lock.synchronize do
        @db.merge! types
        @db = Hash[@db.map do |name, value|
          value['wsdl_name'] ||= name if value
          first_char = name[0].chr
          name = '%s%s' % [first_char.upcase, name[1..-1]] if first_char.downcase == first_char
          [name, value]
        end]
      end
    end

    def typenames
      @db.keys
    end

    private

    def load_extension name
      @extension_dirs.map { |x| File.join(x, "#{name}.rb") }.
                      select { |x| File.exist? x }.
                      each { |x| load x }
    end

    def make_type name
      name = name.to_s
      return BasicTypes.const_get(name) if BasicTypes::BUILTIN.member? name

      desc = @db[name] or raise "unknown VMODL type #{name}"
      case desc['kind']
      when 'data' then make_data_type name, desc
      when 'managed' then make_managed_type name, desc
      when 'enum' then make_enum_type name, desc
      when 'opaque' then make_opaque_type name, desc
      else raise desc.inspect
      end
    end

    def make_data_type name, desc
      superclass = get desc['wsdl_base']
      Class.new(superclass).tap do |klass|
        klass.init name, desc['props']
        klass.wsdl_name = desc['wsdl_name']
      end
    end

    def make_managed_type name, desc
      superclass = get desc['wsdl_base']
      Class.new(superclass).tap do |klass|
        klass.init name, desc['props'], desc['methods']
        klass.wsdl_name = desc['wsdl_name']
      end
    end

    def make_enum_type name, desc
      Class.new(BasicTypes::Enum).tap do |klass|
        klass.init name, desc['values']
        klass.wsdl_name = desc['wsdl_name']
      end
    end

    def make_opaque_type name, desc
      Class.new(BasicTypes::OpaqueObject).tap do |klass|
        klass.init name
        klass.wsdl_name = desc['wsdl_name']
      end
    end
  end

end
