# frozen_string_literal: true
##
# Base class for the RDoc code tree.
#
# We contain the common stuff for contexts (which are containers) and other
# elements (methods, attributes and so on)
#
# Here's the tree of the CodeObject subclasses:
#
# * RDoc::Context
#   * RDoc::TopLevel
#   * RDoc::ClassModule
#     * RDoc::AnonClass (never used so far)
#     * RDoc::NormalClass
#     * RDoc::NormalModule
#     * RDoc::SingleClass
# * RDoc::MethodAttr
#   * RDoc::Attr
#   * RDoc::AnyMethod
#     * RDoc::GhostMethod
#     * RDoc::MetaMethod
# * RDoc::Alias
# * RDoc::Constant
# * RDoc::Require
# * RDoc::Mixin
#   * RDoc::Include
#   * RDoc::Extend

class RDoc::CodeObject

  include RDoc::Text

  ##
  # Our comment

  attr_reader :comment

  ##
  # Do we document our children?

  attr_reader :document_children

  ##
  # Do we document ourselves?

  attr_reader :document_self

  ##
  # Are we done documenting (ie, did we come across a :enddoc:)?

  attr_reader :done_documenting

  ##
  # Which file this code object was defined in

  attr_reader :file

  ##
  # Force documentation of this CodeObject

  attr_reader :force_documentation

  ##
  # Line in #file where this CodeObject was defined

  attr_accessor :line

  ##
  # Hash of arbitrary metadata for this CodeObject

  attr_reader :metadata

  ##
  # Sets the parent CodeObject

  attr_writer :parent

  ##
  # Did we ever receive a +:nodoc:+ directive?

  attr_reader :received_nodoc

  ##
  # Set the section this CodeObject is in

  attr_writer :section

  ##
  # The RDoc::Store for this object.

  attr_reader :store

  ##
  # When mixed-in to a class, this points to the Context in which it was originally defined.

  attr_accessor :mixin_from

  ##
  # Creates a new CodeObject that will document itself and its children

  def initialize
    @metadata         = {}
    @comment          = ''
    @parent           = nil
    @parent_name      = nil # for loading
    @parent_class     = nil # for loading
    @section          = nil
    @section_title    = nil # for loading
    @file             = nil
    @full_name        = nil
    @store            = nil
    @track_visibility = true
    @mixin_from       = nil

    initialize_visibility
  end

  ##
  # Initializes state for visibility of this CodeObject and its children.

  def initialize_visibility # :nodoc:
    @document_children   = true
    @document_self       = true
    @done_documenting    = false
    @force_documentation = false
    @received_nodoc      = false
    @ignored             = false
    @suppressed          = false
    @track_visibility    = true
  end

  ##
  # Replaces our comment with +comment+, unless it is empty.

  def comment=(comment)
    @comment = case comment
               when NilClass               then ''
               when RDoc::Comment          then comment.normalize
               else
                 if comment and not comment.empty? then
                   normalize_comment comment
                 else
                   # HACK correct fix is to have #initialize create @comment
                   #      with the correct encoding
                   if String === @comment and @comment.empty? then
                     @comment = RDoc::Encoding.change_encoding @comment, comment.encoding
                   end
                   @comment
                 end
               end
  end

  ##
  # Should this CodeObject be displayed in output?
  #
  # A code object should be displayed if:
  #
  # * The item didn't have a nodoc or wasn't in a container that had nodoc
  # * The item wasn't ignored
  # * The item has documentation and was not suppressed

  def display?
    @document_self and not @ignored and
      (documented? or not @suppressed)
  end

  ##
  # Enables or disables documentation of this CodeObject's children unless it
  # has been turned off by :enddoc:

  def document_children=(document_children)
    return unless @track_visibility

    @document_children = document_children unless @done_documenting
  end

  ##
  # Enables or disables documentation of this CodeObject unless it has been
  # turned off by :enddoc:.  If the argument is +nil+ it means the
  # documentation is turned off by +:nodoc:+.

  def document_self=(document_self)
    return unless @track_visibility
    return if @done_documenting

    @document_self = document_self
    @received_nodoc = true if document_self.nil?
  end

  ##
  # Does this object have a comment with content or is #received_nodoc true?

  def documented?
    @received_nodoc or !@comment.empty?
  end

  ##
  # Turns documentation on/off, and turns on/off #document_self
  # and #document_children.
  #
  # Once documentation has been turned off (by +:enddoc:+),
  # the object will refuse to turn #document_self or
  # #document_children on, so +:doc:+ and +:start_doc:+ directives
  # will have no effect in the current file.

  def done_documenting=(value)
    return unless @track_visibility
    @done_documenting  = value
    @document_self     = !value
    @document_children = @document_self
  end

  ##
  # File name where this CodeObject was found.
  #
  # See also RDoc::Context#in_files

  def file_name
    return unless @file

    @file.absolute_name
  end

  ##
  # Force the documentation of this object unless documentation
  # has been turned off by :enddoc:
  #--
  # HACK untested, was assigning to an ivar

  def force_documentation=(value)
    @force_documentation = value unless @done_documenting
  end

  ##
  # Sets the full_name overriding any computed full name.
  #
  # Set to +nil+ to clear RDoc's cached value

  def full_name=(full_name)
    @full_name = full_name
  end

  ##
  # Use this to ignore a CodeObject and all its children until found again
  # (#record_location is called).  An ignored item will not be displayed in
  # documentation.
  #
  # See github issue #55
  #
  # The ignored status is temporary in order to allow implementation details
  # to be hidden.  At the end of processing a file RDoc allows all classes
  # and modules to add new documentation to previously created classes.
  #
  # If a class was ignored (via stopdoc) then reopened later with additional
  # documentation it should be displayed.  If a class was ignored and never
  # reopened it should not be displayed.  The ignore flag allows this to
  # occur.

  def ignore
    return unless @track_visibility

    @ignored = true

    stop_doc
  end

  ##
  # Has this class been ignored?
  #
  # See also #ignore

  def ignored?
    @ignored
  end

  ##
  # The options instance from the store this CodeObject is attached to, or a
  # default options instance if the CodeObject is not attached.
  #
  # This is used by Text#snippet

  def options
    @store&.options || RDoc::Options.new
  end

  ##
  # Our parent CodeObject.  The parent may be missing for classes loaded from
  # legacy RI data stores.

  def parent
    return @parent if @parent
    return nil unless @parent_name

    if @parent_class == RDoc::TopLevel then
      @parent = @store.add_file @parent_name
    else
      @parent = @store.find_class_or_module @parent_name

      return @parent if @parent

      begin
        @parent = @store.load_class @parent_name
      rescue RDoc::Store::MissingFileError
        nil
      end
    end
  end

  ##
  # Name of our parent

  def parent_name
    @parent ? @parent.full_name : '(unknown)'
  end

  ##
  # Records the RDoc::TopLevel (file) where this code object was defined

  def record_location(top_level)
    @ignored    = false
    @suppressed = false
    @file       = top_level
  end

  ##
  # The section this CodeObject is in.  Sections allow grouping of constants,
  # attributes and methods inside a class or module.

  def section
    return @section if @section

    @section = parent.add_section @section_title if parent
  end

  ##
  # Enable capture of documentation unless documentation has been
  # turned off by :enddoc:

  def start_doc
    return if @done_documenting

    @document_self = true
    @document_children = true
    @ignored    = false
    @suppressed = false
  end

  ##
  # Disable capture of documentation

  def stop_doc
    return unless @track_visibility

    @document_self = false
    @document_children = false
  end

  ##
  # Sets the +store+ that contains this CodeObject

  def store=(store)
    @store = store

    return unless @track_visibility

    if :nodoc == options.visibility then
      initialize_visibility
      @track_visibility = false
    end
  end

  ##
  # Use this to suppress a CodeObject and all its children until the next file
  # it is seen in or documentation is discovered.  A suppressed item with
  # documentation will be displayed while an ignored item with documentation
  # may not be displayed.

  def suppress
    return unless @track_visibility

    @suppressed = true

    stop_doc
  end

  ##
  # Has this class been suppressed?
  #
  # See also #suppress

  def suppressed?
    @suppressed
  end

end
