# Copyright (c) 2023 M.J.N. Corino, The Netherlands
#
# This software is released under the MIT license.
# 
# Some parts are
# Copyright 2004-2007, wxRuby development team
# released under the MIT-like wxRuby2 license

# WxRuby Extensions - Keyword Constructors for wxRuby3
# Adapted from wxRuby2.

module Wx
  # = WxSugar - Keyword Constructors Classes
  #
  # This extension defines the keyword parameters for +new+ methods for
  # widgets, windows and frames. It's for use with *Keyword Constructors*
  # and is no use on its own - except if you are looking for a bug or want
  # to add a  missing class.
  #
  # For each class, the parameters *must* be declared in the order that
  # they are supplied to wxRuby. A parameter is specified by a symbol
  # name, and, optionally, a default argument which will be of whatever type
  # the wxRuby core library accepts.
  # To support complex (context dependent) defaults and/or autoconversion
  # of arguments for backwards the specified default can also be a lambda
  # or  a proc accepting a single argument. This argument will be the
  # value of the specified parameter (or nil) and the lambda or proc
  # should return the constructed or converted argument value to be used.
  #
  # Some common parameters to constructors such as size, position, title,
  # id and so forth always have a standard default argumnet, which is
  # defined in keyword_ctors. In these cases, it is not necessary to
  # supply the default argument in the definition.
  @defined_kw_classes = {}

  # accepts a string unadorned name of a WxWidgets class, and block, which
  # defines the constructor parameters and style flags for that class.
  # If the named class exists in the available WxRuby, the block is run and
  # the class may use keyword constructors. If the class is not available, the
  # block is ignored.
  def self.define_keyword_ctors(klass, &block)
    klass_name = ::String === klass ? klass : klass.name
    # check this class hasn't already been defined
    if @defined_kw_classes[klass_name]
      raise ArgumentError, "Keyword ctor for #{klass_name} already defined"
    else
      @defined_kw_classes[klass_name] = true
    end

    begin
      klass =  Wx::const_get(klass_name)
    rescue NameError
      STDERR.puts "WARNING: cannot define keyword ctor for #{klass_name}" if Wx::DEBUG
      return nil
    end if ::String === klass
    # If the klass inherited from a class which already has keyword ctor defined
    # it will already have a param_spec class attribute defined.
    # We need to clear it here because we're going to define a new param_spec for
    # this specific derivative.
    unless klass.respond_to?(:param_spec)
      klass.include Wx::KeywordConstructor::ParamSpec
    end
    klass.param_spec = []
    klass.include Wx::KeywordConstructor
    klass.instance_eval(&block)
  end

  module KeywordConstructor

    # This module defines an inheritable class attribute like the ones defined
    # by the Rails #class_attribute method.
    module ParamSpec

      if (RUBY_VERSION.split('.').collect {|v| v.to_i} <=> [2,7,0]) < 0
        PARAM_SPEC_ARG = [:param_spec]
      else
        PARAM_SPEC_ARG = [:param_spec, false]
      end

      def self.included(mod)
        mod.class_eval do
          def self.param_spec
            nil
          end

          def self.param_spec?
            !!param_spec
          end

          def self.param_spec=(val)
            singleton_class.class_eval do
              if method_defined?(*PARAM_SPEC_ARG) || private_method_defined?(*PARAM_SPEC_ARG)
                begin
                  remove_method(:param_spec)
                rescue NameError;
                end
              end
              define_method(:param_spec) { val }
            end

            if singleton_class?
              class_eval do
                if method_defined?(*PARAM_SPEC_ARG) || private_method_defined?(*PARAM_SPEC_ARG)
                  begin
                    remove_method(:param_spec)
                  rescue NameError;
                  end
                end

                def param_spec
                  defined?(@param_spec) ? @param_spec : singleton_class.param_spec
                end
              end
            end
            val
          end

          if method_defined?(*PARAM_SPEC_ARG) || private_method_defined?(*PARAM_SPEC_ARG)
            begin
              remove_method(:param_spec)
            rescue NameError;
            end
          end

          def param_spec
            defined?(@param_spec) ? @param_spec : self.class.param_spec
          end

          def param_spec?
            !!param_spec
          end
        end
      end
    end

    # This module adds the class methods needed to define and use keyword ctors.
    module ClassMethods

      # Common Wx constructor argument keywords, with their default values.
      STANDARD_DEFAULTS = {
        :id        => Wx::StandardID::ID_ANY,
        :size      => Wx::DEFAULT_SIZE,
        :pos       => Wx::DEFAULT_POSITION,
        :style     => 0,
        :title     => '',
        :validator => Wx::DEFAULT_VALIDATOR,
        :choices   => [] # for Choice, ComboBox etc
      }

      # Adds a list of named parameters *params* to the parameter
      # specification for this Wx class's constructor. Each parameter
      # should be specified as a either a common known symbol, such as
      # +:size+ or +:pos:+ or +:style:+ (corresponding to the common
      # constructor arguments in WxWidgets API), or a single-key with the
      # key the name of the argument, and the value a default value.
      # 
      # Parameters should be specified in the order they occur in the Wx
      # API constructor
      def wx_ctor_params(*params)
        self.param_spec += params.map do | param |
          param.kind_of?(Hash) ? 
            Parameter[ param.keys.first, param.values.first ] : 
            Parameter[ param, STANDARD_DEFAULTS[param] ]
        end
      end

      if (RUBY_VERSION.split('.').collect {|v| v.to_i} <=> [3,0,0]) <= 0
        def args_as_list(*mixed_args)
          Wx::args_as_list(param_spec, *mixed_args)
        end
      else
        def args_as_list(*args, **kwargs)
          Wx::args_as_list(param_spec, *args, **kwargs)
        end
      end

      def describe_constructor(txt = '')
        param_spec.inject(txt) do | desc, param |
          if Proc === param.default_or_proc
            desc << ":#{param.name} => (#{param.default_or_proc.call(nil).class.name})\n"
          else
            desc << ":#{param.name} => (#{param.default_or_proc.class.name})\n"
          end
        end
      end
    end

    def self.included(klass)
      klass.extend ClassMethods

      # This defines the overridden ctor that accepts and use the keyword param specs.
      klass.module_eval do

        alias :pre_wx_kwctor_init :initialize

        # The new definition of initialize; accepts a parent arg
        # mixed_args, which may zero or more position args, optionally
        # terminated with hash keyword args, and an optional block
        wx_redefine_method :initialize do |parent = :default_ctor, *args, **kwargs, &block|
          # allow zero-args ctor for use with XRC
          if parent == :default_ctor
            pre_wx_kwctor_init
            return
          end

          real_args = begin
                        [ parent ] + self.class.args_as_list(*args, **kwargs)
                      rescue => err
                        msg = "Error initializing #{self.inspect}\n"+
                          " : #{err.message} \n" +
                          "Provided are #{[parent] + args + [kwargs]} \n" +
                          "Correct parameters for #{self.class.name}.new are:\n" +
                          self.class.describe_constructor(":parent => (Wx::Window)\n")

                        new_err = err.class.new(msg)
                        new_err.set_backtrace(caller)
                        Kernel.raise new_err
                      end
          begin
            pre_wx_kwctor_init(*real_args)
          rescue => err
            msg = "Error initializing #{self.inspect}\n"+
                  " : #{err.message} \n" +
                  "Provided are #{real_args} \n" +
                  "Correct parameters for #{self.class.name}.new are:\n" +
                   self.class.describe_constructor(":parent => (Wx::Window)\n")

            new_err = err.class.new(msg)
            new_err.set_backtrace(caller)
            Kernel.raise new_err
          end

          # If a block was given, pass the newly created Window instance
          # into it; use block
          if block
            if block.arity == -1 or block.arity == 0
              self.instance_eval(&block)
            elsif block.arity == 1
              block.call(self)
            else
              Kernel.raise ArgumentError,
                           "Block to initialize should accept a single argument or none"
            end
          end
        end
      end
    end

  end
end
