# Copyright (c) 2023 M.J.N. Corino, The Netherlands
#
# This software is released under the MIT license.

###
# wxRuby3 wxWidgets interface generation templates
###

require 'set'
require 'yaml'

require_relative './base'
require_relative './analyzer'

module WXRuby3

  class DocGenerator < Generator

    class << self

      private

      def get_constants_db
        script = <<~__SCRIPT
          require 'json'
          WX_GLOBAL_CONSTANTS=false
          require 'wx'
          def handle_module(mod, table)
            Wx.delayed_constants_for(mod).each do |key, delayed_const|
              table[key.sym.to_s] = { type: true, value: delayed_const.to_s }
            end
            mod.constants.each do |c|
              a_const = mod.const_get(c)
              if (::Module === a_const || ::Class === a_const) && a_const.name.start_with?('Wx::')  # Wx:: Package submodule or Class (possibly Enum)
                handle_module(a_const, table[c.to_s] = {})
              elsif Wx::Enum === a_const
                table[c.to_s] = { type: true, value: "\#{a_const.class}.new(\#{a_const.to_i})" } 
              elsif !(::Hash === a_const || ::Array === a_const)
                case a_const
                when Wx::Size
                  table[c.to_s] = { type: true, value: "Wx::Size.new(\#{a_const.width}, \#{a_const.height})" }
                when Wx::Point
                  table[c.to_s] = { type: true, value: "Wx::Point.new(\#{a_const.x}, \#{a_const.y})" }
                else 
                  table[c.to_s] = { type: true, value: a_const } unless c == :THE_APP
                end
              end
            end
          end
          table = { 'Wx' => {}}
          handle_module(Wx, table['Wx'])
          STDOUT.puts JSON.dump(table)
        __SCRIPT
        STDERR.puts "* executing constants collection script:\n#{script}" if Director.trace?
        begin
          tmpfile = Tempfile.new('script')
          ftmp_name = tmpfile.path.dup
          tmpfile << script
          tmpfile.close(false)
          result = if Director.trace?
                     Config.instance.run(ftmp_name, capture: :out, verbose: false)
                   else
                     Config.instance.run(ftmp_name, capture: :no_err, verbose: false)
                   end
          STDERR.puts "* got constants collection output:\n#{result}" if Director.trace?
          begin
            db = JSON.load(result)
          rescue Exception
            File.open('constants_raw.json', "w") { |f| f << result } if Director.verbose?
            ::Kernel.raise RuntimeError, "Exception loading constants collection result: #{$!.message.slice(0, 512)}", cause: nil
          end
          File.open('constants.json', "w") { |f| f << JSON.pretty_generate(db) } if Director.verbose?
          return db
        ensure
          File.unlink(ftmp_name)
        end
      end

      def get_constants_xref_db(const_tbl = nil, mods = [])
        xref_tbl = {}
        (const_tbl || constants_db).each_pair do |constnm, constspec|
          unless constspec.has_key?('type')
            xref_tbl[constnm] = { 'mod' => mods.join('::'), 'table' => constspec }
            xref_tbl.merge!(get_constants_xref_db(constspec, mods + [constnm]))
          else
            xref_tbl[constnm] = constspec.merge({'mod' => mods.join('::') })
          end
        end
        File.open('constants_xrefs.json', "w") { |f| f << JSON.pretty_generate(xref_tbl) } if Director.verbose?
        xref_tbl
      end

      public

      def constants_db
        @constants_db ||= get_constants_db
      end

      def constants_xref_db
        @constants_xref_db ||= get_constants_xref_db
      end

    end

    class XMLTransformer

      include DirectorSpecsHelper

      private

      def event_section(f = true)
        @event_section = !!f
      end

      def event_section?
        !!@event_section
      end

      def event_list(f = true)
        @event_list = !!f
      end

      def event_list?
        !!@event_list
      end

      def no_ref(&block)
        was_no_ref = @no_ref
        @no_ref = true
        begin
          return block.call
        ensure
          @no_ref = was_no_ref
        end
      end

      def no_ref?
        !!@no_ref
      end

      def no_idents(&block)
        was_no_idents = @no_idents
        @no_idents = true
        begin
          return block.call
        ensure
          @no_idents = was_no_idents
        end
      end

      def no_idents?
        !!@no_idents
      end

      def _ident_to_ref(idstr)
        idstr.sub(/(.*)(\(.*\))?/) { |_| "{#{$1}}#{$2}" }
      end

      def text_to_doc(node)
        text = node.text
        # handle left-over doxygen tags
        text.gsub!(/#(\w)/, '\#\1')
        text.gsub!(/@(end)?code/, '')
        text.gsub!('@subsection', '##')
        text = '' if text.strip == '##' # no empty headings
        text.gsub!('@remarks', '')
        text.gsub!(/@see.*\n/, '')
        text.gsub!('@ref', '')
        text.gsub!(/(\W|\A)nullptr(\W|\Z)/, '\1nil\2')
        unless no_ref?
          # auto create references for any ids explicitly declared such
          text.gsub!(/\W?(wx\w+(::\w+)?(\(.*\))?)/) do |s|
            if $1 == 'wxWidgets'
              s
            else
              if s==$1
                doc_id, known_id = _ident_str_to_doc($1)
                known_id ? _ident_to_ref(doc_id) : doc_id
              else
                doc_id, known_id = _ident_str_to_doc($1)
                "#{s[0]}#{known_id ? _ident_to_ref(doc_id) : doc_id}"
              end
            end
          end
          text.gsub!(/WX(K_[A-Z]+)/) { "{Wx::KeyCode::#{$1}}"}
        end
        if event_section?
          case text
          when /Event macros for events emitted by this class:/
            event_list(true)
            'Event handler methods for events emitted by this class:'
          when /Event macros:/
            event_list(true)
            'Event handler methods:'
          else
            text
          end
        else
          text
        end
      end

      def computeroutput_to_doc(node)
        if event_section?
          node_to_doc(node)
        elsif /\A[\w:\.]+\Z/ =~ node.text # only a single word/identifier?
          node_to_doc(node)
        else
          no_ref do
            "<code>#{node_to_doc(node)}</code>"
          end
        end
      end

      def bold_to_doc(node)
        "<b>#{node_to_doc(node)}</b>"
      end

      def sp_to_doc(node)
        " #{node_to_doc(node)}"
      end

      def nonbreakablespace_to_doc(node)
        sp_to_doc(node)
      end

      def linebreak_to_doc(node)
        "#{node_to_doc(node)}\n"
      end

      def programlisting_to_doc(node)
        no_idents do
          "\n```\n  #{node_to_doc(node).split("\n").join("\n  ")}\n```\n"
        end
      end

      def onlyfor_to_doc(node)
        '' # handled elsewhere
      end

      def simplesect_to_doc(node)
        case node['kind']
        when 'since' # get rid of 'Since' notes
          ''
        when 'see'
          no_ref do
            @see_list.concat node_to_doc(node).split(',')
          end
          ''
        when 'note'
          <<~__NOTE

            <div class="wxrb-note">
            <b>Note:</b>
            <p>#{node_to_doc(node)}</p>
            </div>
          __NOTE
        when 'remark'
          <<~__NOTE

            <div class="wxrb-remark">
            <b>Remark:</b>
            <p>#{node_to_doc(node)}</p>
            </div>
          __NOTE
        else
          node_to_doc(node)
        end
      end

      def _arglist_to_doc(args)
        args.split(',').collect do |a|
          a = a.gsub(/const\s+/, '')
          a.tr!('*&[]', '')
          a.split(' ').last
        end.join(',').strip
      end
      private :_arglist_to_doc

      def _is_method?(itmname, clsnm=nil)
        spec = clsnm ? Director::Spec.class_index[clsnm] : self
        if spec
          if clsnm
            if clsdef = spec.def_item(clsnm)
              if itmdef = clsdef.find_item(itmname)
                return Extractor::FunctionDef === itmdef
              end
            end
          else
            if itmdef = spec.def_item(itmname)
              return Extractor::FunctionDef === itmdef
            end
          end
        end
        false
      end
      private :_is_method?

      def _is_static_method?(clsnm, mtdname)
        if clsspec = Director::Spec.class_index[clsnm]
          if clsdef = clsspec.def_item(clsnm)
            if mtdef = clsdef.find_item(mtdname)
              return Extractor::MethodDef === mtdef && mtdef.is_static
            end
          end
        end
        false
      end
      private :_is_static_method?

      def _ident_str_to_doc(s, ref_scope = nil)
        return s if no_idents?
        return s if s.start_with?('wxRuby')
        return 'WXOSX' if s.start_with?('wxMac')
        return s.sub(/\Awx/, 'WX') if %w[wxMSW wxOSX wxGTK wxX11 wxUNIVERSAL].any? { |w| s.start_with?(w) }
        nmlist = s.split('::')
        nm_str = nmlist.shift.to_s
        constnm = rb_wx_name(nm_str)
        if nmlist.empty?  # unscoped id?
          if /(\w+)\s*\(([^\)]*)\)/ =~ nm_str   # method with arglist?
            fn = $1
            args = _arglist_to_doc($2)
            if ref_scope
              mtdsig = if ref_scope == fn
                         args.empty? ? 'initialize' : "initialize(#{args})"
                       else
                         args.empty? ? "#{rb_method_name(fn)}" : "#{rb_method_name(fn)}(#{args})"
                       end
              sep = _is_static_method?(ref_scope, fn) ? '.' : '#'
              constnm = rb_wx_name(ref_scope)
              if DocGenerator.constants_xref_db.has_key?(constnm)
                ["#{DocGenerator.constants_xref_db[constnm]['mod']}::#{constnm}#{sep}#{mtdsig}", true]
              else
                "Wx::#{constnm}#{sep}#{mtdsig}"
              end
            else
              mtdsig = args.empty? ? "#{rb_method_name(fn)}" : "#{rb_method_name(fn)}(#{args})"
              [mtdsig, true]
            end
          else # constant or method name only
            if DocGenerator.constants_xref_db.has_key?(constnm)
              ["#{DocGenerator.constants_xref_db[constnm]['mod']}::#{constnm}", true]
            elsif DocGenerator.constants_xref_db.has_key?(rb_constant_name(nm_str))
              ["Wx::#{rb_constant_name(nm_str)}", true]
            elsif DocGenerator.constants_xref_db.has_key?(rb_constant_name(nm_str, false))
              ["Wx::#{rb_constant_name(nm_str, false)}", true]
            elsif !_is_method?(nm_str, ref_scope)
              ["Wx::#{constnm}", true]
            else
              if ref_scope
                mtdnm = if ref_scope == nm_str
                          'initialize'
                        else
                          rb_method_name(nm_str)
                        end
                sep = _is_static_method?(ref_scope, nm_str) ? '.' : '#'
                constnm = rb_wx_name(ref_scope)
                if DocGenerator.constants_xref_db.has_key?(constnm)
                  ["#{DocGenerator.constants_xref_db[constnm]['mod']}::#{constnm}#{sep}#{mtdnm}", true]
                else
                  "Wx::#{constnm}#{sep}#{mtdnm}"
                end
              else
                mtdnm = rb_method_name(nm_str)
                [mtdnm, true]
              end
            end
          end
        else # scoped id
          itmnm = nmlist.shift.to_s
          mtd = nil
          args =  nil
          known = true
          if /(\w+)\s*\(([^\)]*)\)/ =~ itmnm
            mtd = $1
            args = _arglist_to_doc($2)
          end
          # transform the scope prefix
          if DocGenerator.constants_xref_db.has_key?(constnm)
            constnm = "#{DocGenerator.constants_xref_db[constnm]['mod']}::#{constnm}"
          elsif DocGenerator.constants_xref_db.has_key?(rb_constant_name(nm_str))
            cnm = rb_constant_name(nm_str)
            constnm = "#{DocGenerator.constants_xref_db[cnm]['mod']}::#{cnm}"
          elsif DocGenerator.constants_xref_db.has_key?(rb_constant_name(nm_str, false))
            cnm = rb_constant_name(nm_str, false)
            constnm = "#{DocGenerator.constants_xref_db[cnm]['mod']}::#{cnm}"
          elsif nm_str.start_with?('wx')
            known = false
            constnm = "Wx::#{constnm}"
          end
          # transform and append the element id
          if mtd.nil?
            if DocGenerator.constants_xref_db.has_key?(rb_wx_name(itmnm))
              itmnm = rb_wx_name(itmnm)
              # in case of enum constants the documented scope most likely omits the enum class
              # which we want for correct linking for wxRuby
              if DocGenerator.constants_xref_db[itmnm]['mod'].start_with?("#{constnm}::")
                ["#{DocGenerator.constants_xref_db[itmnm]['mod']}::#{rb_wx_name(itmnm)}", known]
              else
                ["#{constnm}::#{rb_wx_name(itmnm)}", known]
              end
            elsif !_is_method?(itmnm, nm_str)
              ["#{constnm}::#{rb_wx_name(itmnm)}", known]
            else
              sep = _is_static_method?(nm_str, itmnm) ? '.' : '#'
              ["#{constnm}#{sep}#{rb_method_name(itmnm)}", known]
            end
          elsif nm_str == mtd # ctor?
            [args.empty? ? "#{constnm}\#initialize" : "#{constnm}\#initialize(#{args})", known]
          else
            sep = _is_static_method?(nm_str, mtd) ? '.' : '#'
            [args.empty? ? "#{constnm}#{sep}#{rb_method_name(mtd)}" : "#{constnm}#{sep}#{rb_method_name(mtd)}(#{args})", known]
          end
        end
      end
      private :_ident_str_to_doc

      # transform all cross references
      def ref_to_doc(node)
        return node.text if no_idents?
        if @classdef
          ref = @classdef.crossref_table[node['refid']]
        end
        ref ||= {}
        return node.text if /\s/ =~ node.text # no crossref transforming if text contains whitespace; return plain text
        if no_ref?
          doc_id, _ = _ident_str_to_doc(node.text, ref[:scope])
          doc_id
        else
          doc_id, id_known = _ident_str_to_doc(node.text, ref[:scope])
          id_known ? _ident_to_ref(doc_id) : doc_id
        end
      end

      # transform all titles
      def title_to_doc(node)
        "## #{node.text}\n"
      end

      def heading_to_doc(node)
        lvl = 1+(node['level'] || '1').to_i
        txt = node_to_doc(node)
        event_section(/Events emitted by this class|Events using this class/i =~ txt)
        txt.strip!
        txt.empty? ? txt : "#{'#' * lvl} #{txt}"
      end

      # transform all itemizedlist
      def itemizedlist_to_doc(node)
        doc = node_to_doc(node)
        if event_list?
          # event emitter block ended
          event_list(false)
          event_section(false)
        end
        doc
      end

      # transform all listitem
      def listitem_to_doc(node)
        itm_text = node_to_doc(node)
        # fix possible unwanted leading spaces resulting in verbatim blocks
        itm_text = itm_text.split("\n").collect {|s|s.lstrip}.join("\n") if itm_text.index("\n")
        "\n- #{itm_text}"
      end

      def node_to_doc(xmlnode)
        xmlnode.children.inject('') do |docstr, node|
          node = preprocess_node(node)
          docstr << (node.is_a?(Nokogiri::XML::Node) ? self.__send__("#{node.name}_to_doc", node) : node.to_s)
        end
      end

      def get_event_override(evt)
        if ifspec.event_overrides.has_key?(@classdef.name)
          return ifspec.event_overrides[@classdef.name][evt] || ifspec.event_overrides[@classdef.name][evt.upcase]
        end
        nil
      end
      private :get_event_override

      def para_to_doc(node)
        para = node_to_doc(node)
        # loose specific notes paragraphs
        case para
        when /\A(\<(b)\>)?(wxPerl|\{Wx::Perl\}) Note:/, # wxPerl note
             /\A\s*Library:/,                 # Library note
             /\A\s*Include\s+file:/           # Include file note
          ''
        else
          para.sub!(/Include\s+file:\s+\\?#include\s+<[^>]+> */, '')
          if event_section?
            case para
            when /The following event handler macros redirect.*(\{.*})/
              event_ref = $1
              "The following event-handler methods redirect the events to member method or handler blocks for #{event_ref} events."
            when /\AEVT_[_A-Z0-9]+/
              if event_list? && /\A(EVT_[_A-Z0-9]+)\((.*,)\s+\w+\):(.*)/ =~ para
                evthnd_name = $1.downcase
                docstr = $3.lstrip
                if override_spec = get_event_override(evthnd_name)
                  evthnd_name, evt_type, evt_arity, evt_klass = override_spec
                  idarg = case evt_arity
                          when 0
                            ''
                          when 1
                            'id, '
                          when 2
                            'first_id, last_id, '
                          end
                  arglist = "#{idarg}meth = nil, &block"
                else
                  arglist = "#{$2} meth = nil, &block"
                end
                package.event_docs[evthnd_name] = [arglist, docstr.dup] # register for eventlist doc gen
                "{Wx::EvtHandler\##{evthnd_name}}(#{arglist}): #{docstr}"
              elsif event_list? && /\A(EVT_[_A-Z0-9]+)(\*)?\(\w+\):(.*)/ =~ para
                wildcard = ($2 == '*')
                evthnd_name = $1.downcase
                arglist = "meth = nil, &block"
                docstr = $3.lstrip
                package.event_docs[wildcard ? /\A#{evthnd_name}/ : evthnd_name] = [arglist, docstr.dup] # register for eventlist doc gen
                if wildcard
                  "{Wx::EvtHandler}#{evthnd_name}*(#{arglist}): #{docstr}"
                else
                  "{Wx::EvtHandler\##{evthnd_name}}(#{arglist}): #{docstr}"
                end
              else
                para
              end
            else
              para
            end
          else
            para
          end
        end
      end

      def preprocess_node(node)
        if @item_doc_ovr[:pre] && @item_doc_ovr[:pre][node.name.to_sym]
          @item_doc_ovr[:pre][node.name.to_sym].each do |ovr|
            if ovr[:replace]
              return ovr[:replace] if ovr[:pattern] =~ node.inner_html
            elsif ovr[:subst]
              if ovr[:global]
                node.inner_html = node.inner_html.gsub(ovr[:pattern], ovr[:subst])
              else
                node.inner_html = node.inner_html.sub(ovr[:pattern], ovr[:subst])
              end
            end
          end
        end
        node
      end
      private :preprocess_node

      def method_missing(mtd, *args, &block)
        if /\A\w+_to_doc\Z/ =~ mtd.to_s && args.size==1
          node_to_doc(*args)
        else
          super
        end
      end

      public

      def initialize(director)
        @director = director
        @doc_overrides = if File.exist?(doc_override_file =  File.join(__dir__, 'doc', underscore(name)+ '.yaml'))
                           if ::Psych::VERSION >= '3.1.0'
                            ::Psych.safe_load(File.read(doc_override_file), permitted_classes: [::Regexp, ::Symbol])
                           else
                             ::Psych.safe_load(File.read(doc_override_file), [::Regexp, ::Symbol])
                           end
                         else
                           {}
                         end
        @classdef = nil
        @see_list = []
      end

      attr_reader :director

      def for_class(clsdef, &block)
        prevcls = @classdef
        @classdef = clsdef
        begin
          block.call
        ensure
          @classdef = prevcls
        end
      end

      def to_doc(xmlnode_or_set, item: nil, desc: :brief)
        return '' unless xmlnode_or_set
        @see_list.clear
        @item_doc_ovr = if item
                          item_key = if item.is_a?(Extractor::BaseDef)
                                       if item.is_a?(Extractor::ClassDef) || @classdef.nil?
                                         item.name
                                       else
                                         "#{@classdef.name}.#{item.name}"
                                       end
                                     else
                                       @classdef ? "#{@classdef.name}.#{item}" : item.to_s
                                     end
                          item_key = item_key.to_sym
                          @doc_overrides[item_key] ? @doc_overrides[item_key][desc] || {} : {}
                        else
                          {}
                        end
        doc = if Nokogiri::XML::NodeSet === xmlnode_or_set
                xmlnode_or_set.inject('') do |s, n|
                  n = preprocess_node(n)
                  s << (n.is_a?(Nokogiri::XML::Node) ? node_to_doc(n) : n.to_s)
                end
              else
                xmlnode_or_set = preprocess_node(xmlnode_or_set)
                xmlnode_or_set.is_a?(Nokogiri::XML::Node) ? node_to_doc(xmlnode_or_set) : xmlnode_or_set.to_s
              end
        event_section(false)
        if @item_doc_ovr.has_key?(:post)
          @item_doc_ovr[:post].each do |ovr|
            if ovr[:global]
              doc.gsub!(ovr[:pattern], ovr[:subst])
            else
              doc.sub!(ovr[:pattern], ovr[:subst])
            end
          end
        end
        if @item_doc_ovr.has_key?(:replace)
          doc = @item_doc_ovr[:replace][:text]
        end
        doc.sub!(/\<b\>\{Wx::Perl.*/, '')
        doc.strip!
        # reduce triple(or more) newlines to max 2
        doc << "\n" # always end with a NL without following whitespace
        doc.gsub!(/\n *\n *\n+/, "\n\n")
        # add crossref tags
        @see_list.each { |s| doc << "@see #{s}\n" }
        doc
      end

      def constants_db
        DocGenerator.constants_db
      end

      def constants_xref_db
        DocGenerator.constants_xref_db
      end

      def type_to_doc(ctype_decl)
        return ctype_decl if ctype_decl == 'void'
        ctype = Typemap.strip_type_decl(ctype_decl)
        nmlist = ctype.split('::')
        nm_str = nmlist.shift.to_s
        constnm = rb_wx_name(nm_str)
        if nmlist.empty?  # unscoped id?
          if DocGenerator.constants_xref_db.has_key?(constnm)
            "#{DocGenerator.constants_xref_db[constnm]['mod']}::#{constnm}"
          elsif DocGenerator.constants_xref_db.has_key?(rb_constant_name(nm_str))
            "Wx::#{rb_constant_name(nm_str)}"
          elsif DocGenerator.constants_xref_db.has_key?(rb_constant_name(nm_str, false))
            "Wx::#{rb_constant_name(nm_str, false)}"
          else
            Typemap.wx_type_to_rb(ctype)
          end
        else
          itmnm = nmlist.shift.to_s
          if DocGenerator.constants_xref_db.has_key?(constnm)
            constnm = "#{DocGenerator.constants_xref_db[constnm]['mod']}::#{constnm}"
          elsif DocGenerator.constants_xref_db.has_key?(rb_constant_name(nm_str))
            cnm = rb_constant_name(nm_str)
            constnm = "#{DocGenerator.constants_xref_db[cnm]['mod']}::#{cnm}"
          elsif DocGenerator.constants_xref_db.has_key?(rb_constant_name(nm_str, false))
            cnm = rb_constant_name(nm_str, false)
            constnm = "#{DocGenerator.constants_xref_db[cnm]['mod']}::#{cnm}"
          elsif nm_str.start_with?('wx')
            constnm = "Wx::#{constnm}"
          end
          "#{constnm}::#{rb_wx_name(itmnm)}"
        end
      end

    end

    def run
      # run an analysis comparing inherited generated methods with this class's own generated methods
      InterfaceAnalyzer.check_interface_methods(@director, doc_gen: true)

      @xml_trans = DocGenerator::XMLTransformer.new(@director)
      Stream.transaction do
        fdoc = CodeStream.new(File.join(package.ruby_doc_path, underscore(name)+'.rb'))
        fdoc << <<~__HEREDOC
          # :stopdoc:
          # This file is automatically generated by the WXRuby3 documentation 
          # generator. Do not alter this file.
          # :startdoc:
        __HEREDOC
        # at least 2 newlines to make Yard skip/forget the header comment
        fdoc.puts
        fdoc.puts
        mod_indent = 0
        package.all_modules.each do |modnm|
          fdoc.iputs("module #{package.fullname}", mod_indent)
          fdoc.puts
          mod_indent += 1
        end
        fdoc.indent(mod_indent) do
          gen_constants_doc(fdoc)
          gen_functions_doc(fdoc) unless no_gen?(:functions)
          gen_class_doc(fdoc) unless no_gen?(:classes)
        end
        package.all_modules.each do |_|
          mod_indent -= 1
          fdoc.puts
          fdoc.iputs('end', mod_indent)
        end
      end
    end

    protected

    def to_feature_text(feat)
      if Config::AnyOf === feat
        feat.features.collect { |f| f.is_a?(::Array) ? f.join('&') : f }.join('|')
      else
        feat
      end
    end
    private :to_feature_text

    def gen_item_requirements(fdoc, item)
      if item.required_features_doc
        fdoc.doc.puts '@wxrb_require ' + item.required_features_doc.collect(&->(feat){ to_feature_text(feat) }).join(',')
      end
    end

    def has_class_requirements?
      !(ifspec.requirements.empty? && ifspec.package.required_features.empty?)
    end

    def get_class_requirements
      ifspec.requirements + ifspec.package.required_features.to_a
    end
    private :get_class_requirements

    def gen_class_requirements(fdoc)
      if has_class_requirements?
        fdoc.doc.puts '@wxrb_require ' + get_class_requirements.collect(&->(feat){ to_feature_text(feat) }).join(',')
      end
    end

    def get_constant_doc(const)
      @xml_trans.to_doc(const.brief_doc, item: const)
    end

    def gen_constant_value(val)
      if ::String === val && /\A(#<(.*)>|[\w:]+\.\w+\(.*\))\Z/ =~ val
        if $2
          valstr = $2
          if /\Awx/ =~ valstr
            valstr.sub(/\Awx/, '')
          else
            'nil'
          end
        else
          $1
        end
      else
        val.inspect
      end
    end

    def gen_constant_doc(fdoc, name, spec, doc)
      fdoc.doc.puts doc
      fdoc.puts "#{name} = #{gen_constant_value(spec['value'])}"
      fdoc.puts
    end

    def get_enum_doc(enumdef)
      doc = @xml_trans.to_doc(enumdef.brief_doc, item: enumdef)
      doc << "\n" if enumdef.detailed_doc
      doc << @xml_trans.to_doc(enumdef.detailed_doc, item: enumdef, desc: :detail) if enumdef.detailed_doc
      doc
    end

    def gen_enum_doc(fdoc, enumname, enumdef, enum_table)
      fdoc.doc.puts get_enum_doc(enumdef)
      gen_class_requirements(fdoc)
      fdoc.puts "class #{enumname} < Wx::Enum"
      fdoc.puts
      fdoc.indent do
        enumdef.items.each do |e|
          const_name = rb_wx_name(e.name)
          if enum_table.has_key?(const_name)
            gen_constant_doc(fdoc, const_name, enum_table[const_name], get_constant_doc(e))
          end
        end
      end
      fdoc.puts "end # #{enumname}"
      fdoc.puts
    end

    def gen_constants_doc(fdoc)
      xref_table = package.all_modules.reduce(DocGenerator.constants_db) { |db, mod| db[mod] }
      def_items.select {|itm| !itm.docs_ignored(Director::Package.full_docs?) }.each do |item|
        case item
        when Extractor::GlobalVarDef
          unless no_gen?(:variables)
            const_name = rb_constant_name(item.name)
            if xref_table.has_key?(const_name)
              gen_constant_doc(fdoc, const_name, xref_table[const_name], get_constant_doc(item))
            end
          end
        when Extractor::EnumDef
          unless no_gen?(:enums)
            if item.is_type
              enum_name = rb_wx_name(item.name)
              if xref_table.has_key?(enum_name)
                gen_enum_doc(fdoc, enum_name, item, xref_table[enum_name] || {})
              end
            else
              item.items.each do |e|
                const_name = rb_constant_name(e.name, false)
                if xref_table.has_key?(const_name)
                  gen_constant_doc(fdoc, const_name, xref_table[const_name], get_constant_doc(e))
                end
              end
            end
          end
        when Extractor::DefineDef
          unless no_gen?(:defines)
            if !item.is_macro? && item.value && !item.value.empty?
              const_name = rb_constant_name(item.name)
              if xref_table.has_key?(const_name)
                gen_constant_doc(fdoc, const_name, xref_table[const_name], get_constant_doc(item))
              end
            end
          end
        end
      end
    end

    def get_function_doc(func)
      func.rb_doc(@xml_trans, type_maps, Director::Package.full_docs?)
    end

    def gen_functions_doc(fdoc)
      def_items.select {|itm| !itm.docs_ignored(Director::Package.full_docs?) }.each do |item|
        if Extractor::FunctionDef === item
          get_method_doc(item).each_pair do |name, docs|
            if docs.size>1 # method with overloads?
              docs.each do |ovl, params, ovl_doc|
                fdoc.doc.puts "@overload #{name}(#{params})"
                fdoc.doc.indent { fdoc.doc.puts ovl_doc }
                fdoc.doc.indent { gen_item_requirements(fdoc, ovl) }
              end
              fdoc.puts "def #{name}(*args) end"
            else
              mtd, params, doc = docs.shift
              fdoc.doc.puts doc
              gen_item_requirements(fdoc, mtd)
              if params.empty?
                fdoc.puts "def #{name}; end"
              else
                fdoc.puts "def #{name}(#{params}) end"
              end
            end
            fdoc.puts
          end
        end
      end
    end

    def get_class_doc(cls)
      doc = @xml_trans.to_doc(cls.brief_doc, item: cls)
      doc << "\n" if cls.detailed_doc # force empty line (paragraph break) between summary and detail
      doc << @xml_trans.to_doc(cls.detailed_doc, item: cls, desc: :detail) if cls.detailed_doc
      doc
    end

    def get_method_doc(mtd)
      mtd.rb_doc(@xml_trans, type_maps, Director::Package.full_docs?)
    end

    def get_method_head(clsdef, mtdef)
      if mtdef.class_name == clsdef.name
        # get method head item
        clsdef.items.find { |m| Extractor::MethodDef === m && m.name == mtdef.name }
      else
        # check folded bases
        base = folded_bases(clsdef.name).find { |bc| bc == mtdef.class_name }
        base = def_classes.find { |c| base == c.name }
        base ? base.items.find { |m| Extractor::MethodDef === m && m.name == mtdef.name } : nil
      end
    end

    def gen_class_doc_members(fdoc, clsdef, cls_members, alias_methods)
      # generate method and member variable documentation
      mtd_done = ::Set.new
      cls_members.each do |cm|
        case cm
        when Extractor::MethodDef
          # overloads are flattened out by the Analyzer keeping only the non-ignored items
          # but for doc gen we need the head item (and only that, so keep track and skip the rest)
          unless cm.is_dtor || mtd_done.include?(cm.name)
            # get method head item
            mtd_head = get_method_head(clsdef, cm)
            get_method_doc(mtd_head).each_pair do |name, docs|
              if docs.size>1 # method with overloads?
                docs.each do |ovl, params, ovl_doc|
                  fdoc.doc.puts "@overload #{name}(#{params})"
                  fdoc.doc.indent { fdoc.doc.puts ovl_doc }
                  fdoc.doc.indent { gen_item_requirements(fdoc, ovl) }
                end
                fdoc.puts "def #{name}(*args) end"
                # in case all overloads were ignored there will be no SWIG generated alias
                no_gen_alias = docs.all? { |ovl, _, _| ovl.ignored }
              else
                mtd, params, doc = docs.shift
                fdoc.doc.puts doc
                gen_item_requirements(fdoc, mtd)
                if params.empty?
                  fdoc.puts "def #{name}; end"
                else
                  fdoc.puts "def #{name}(#{params}) end"
                end
                # in case the documented method was ignored there will be no SWIG generated alias
                no_gen_alias = mtd.ignored
              end
              # check for SWIG generated aliases (skip ignored method defs as these will not have SWIG generated aliases)
              if !no_gen_alias && alias_methods.has_key?(cm.name)
                fdoc.puts "alias_method :#{alias_methods[cm.name]}, :#{name}"
              else
                # check for aliases that will be available from WxRubyStyleAccessors at runtime
                # and document these as well
                alias_name = case name
                             when /\Aget_(\w+)/
                               $1
                             when /\Aset_(\w+)/
                               # only document alias if at least 1 method overload has a single required argument
                               if docs.any? { |ovl, _, _| ovl.parameter_count > 0 && ovl.required_param_count < 2 }
                                 "#{$1}="
                               else
                                 nil
                               end
                             when /\Ais_(\w+)/
                               "#{$1}?"
                             when /\A(has_|can_)/
                               "#{name}?"
                             else
                               nil
                             end
                # only consider alias if no other method matching alias_name exists as WxRubyStyleAccessors
                # aliases rely on method_missing being called
                if alias_name &&
                  !cls_members.any? { |m| Extractor::MethodDef === m && !m.ignored && m.rb_decl_name == alias_name }
                  fdoc.puts "alias_method :#{alias_name}, :#{name}"
                end
              end
              fdoc.puts
            end
            mtd_done << cm.name
          end
        when Extractor::MemberVarDef
          rd_doc, rd_decl, wr_doc, wr_decl = cm.rb_doc(@xml_trans, type_maps)
          rd_doc.each { |s| fdoc.doc.puts s }
          fdoc.puts rd_decl
          if wr_doc
            wr_doc.each { |s| fdoc.doc.puts s }
            fdoc.puts wr_decl
          end
          fdoc.puts
        end
      end
    end

    def gen_class_doc(fdoc)
      const_table = package.all_modules.reduce(DocGenerator.constants_db) { |db, mod| db[mod] }
      def_items.select {|itm| !itm.docs_ignored(Director::Package.full_docs?) && Extractor::ClassDef === itm && !is_folded_base?(itm.name) }.each do |item|
        if !item.is_template? || template_as_class?(item.name)
          @xml_trans.for_class(item) do
            intf_class_name = if (item.is_template? && template_as_class?(item.name))
                                template_class_name(item.name)
                              else
                                item.name
                              end
            clsnm = rb_wx_name(intf_class_name)
            xref_table = const_table[clsnm] || {}
            fdoc.doc.puts get_class_doc(item)
            if is_mixin?(item)
              fdoc.doc.puts "\n@note  In wxRuby this is a mixin module instead of a (base) class."
              gen_class_requirements(fdoc)
              fdoc.puts "module #{clsnm}"
            else
              fdoc.doc.puts "\n@note This class is <b>untracked</b> and should not be derived from nor instances extended!" unless is_tracked?(item)
              gen_class_requirements(fdoc)
              basecls = ifspec.classdef_name(base_class(item, doc: true))
              fdoc.puts "class #{clsnm} < #{basecls ? basecls.sub(/\Awx/, '') : '::Object'}"
            end
            fdoc.puts

            # mixin includes
            if included_mixins.has_key?(item.name)
              included_mixins[item.name].keys.each { |mod| fdoc.iputs "include #{mod}" }
              fdoc.puts
            end

            # collect possible aliases
            alias_methods = item.aliases
            folded_bases(item.name).each do |basename|
              alias_methods = def_item(basename).aliases.merge(alias_methods)
            end

            fdoc.indent do
              cls_members = InterfaceAnalyzer.class_interface_members_public(intf_class_name)
              # generate documentation for any enums
              cls_members.each do |member|
                case member
                when Extractor::EnumDef
                  unless member.is_type
                    member.items.each do |e|
                      const_name = rb_constant_name(e.name)
                      if xref_table.has_key?(const_name)
                        gen_constant_doc(fdoc, const_name, xref_table[const_name], get_constant_doc(e))
                      elsif xref_table.has_key?(e.name)
                        gen_constant_doc(fdoc, e.name, xref_table[e.name], get_constant_doc(e))
                      end
                    end
                  else
                    enum_name = rb_wx_name(member.name)
                    if xref_table.has_key?(enum_name)
                      gen_enum_doc(fdoc, enum_name, member, xref_table[enum_name] || {})
                    end
                  end
                end
              end if xref_table
              # generate method and member var documentation
              gen_class_doc_members(fdoc, item, cls_members, alias_methods)

              cls_members = InterfaceAnalyzer.class_interface_members_protected(intf_class_name)
              unless cls_members.empty?
                fdoc.puts
                fdoc.puts 'protected'
                fdoc.puts
                gen_class_doc_members(fdoc, item, cls_members, alias_methods)
              end
            end
            fdoc.puts "end # #{clsnm}"
            fdoc.puts
          end
        end
      end
    end

  end

end