#!/usr/bin/env ruby
#
# x5j

require 'rubygems'
require 'cgi'
require 'json'
require 'yard'

include YARD

  class YARD_LD

    def initialize(file_path)
      Registry.load(file_path) #D - ???
    end

    def fmt_json(options={})
    #
    # Format an YARD Registry subset into a JSON(-LD) string.
    #
    # @param  options     [Hash<String>]  hash of options
    # @return             [String]        JSON(-LD)
    #
    # The incoming options hash may contain several values:
    #
    #   :filter   filtering specification for descendent nodes
    #             (eg, 'ba+em', 'ns+method')
    #
    #   :levels   levels of descendents to display
    #
    #   :project  name of the project being analyzed
    #
    #   :start    YARD "path" to start (top) node

      @keys     = {}        # keys seen in this formatting run

      @options                = options.dup
      @options[:filter]     ||= 'ba+em'     # allow all descendents
      @options[:levels]     ||= 999999      # to any number of levels
      @options[:project]    ||= 'unknown'   # placeholder project name
      @options[:start]      ||= ''          # starting at the top
      @options[:types]        = types_want( @options[:filter] )
      @options[:y_ld_ver]     = 'y_ld-0.1'  # early days...

      top_node  = Registry.at( @options[:start] )
      out       = fmt_tree(top_node)
    end

    private

    def fmt_context
    #
    # Format the JSON-LD context header.
    #
    # @param  project     [String]    project name
    # @return             [String]    JSON

      iri_base  = 'http://sparqly.cfcl.com'
      format    = '%-30s  %s'
      yard_ver  = YARD::VERSION

      tmp = <<-EOT
"type":             "@type",
"y_ld":             "#{ iri_base }/#{      @options[:y_ld_ver] }/",
"y_me":             "#{ iri_base }/y_me/#{ @options[:project]  }/",
"yard":             "#{ iri_base }/yard/#{ yard_ver }/",

"y_ld:children":    { "@type":  "@id" },
"y_ld:namespace":   { "@type":  "@id" },
"y_ld:superclass":  { "@type":  "@id" },

"yard:path":        "@id",\n
      EOT

      keys  = @keys.keys.sort.reject {|key| key =~ /:/ }

      list  = keys.map do |key|
        key_str = "\"#{ key }\":"
        val_str = "\"yard:#{ key }\""
        format % [ key_str, val_str ]
      end

      tmp   += list.join(",\n")

      out = <<-EOT
"@context": {
#{ tmp.gsub(/^/,     '    ') }
}
      EOT

      out.gsub(/^/,     '    ').
          gsub(/^\s+$/, ''    )
    end

    def fmt_item(item_name, item_val)
    #
    # Format an item into a JSON string.
    #
    # @param  item_name   [String]    name of item
    # @param  item_val    [String]    value of item
    # @return             [String]    JSON

      def key(name)
        @keys[name] = true

        '"' + name + '":'
      end

      fmt = '%-30s  %s,'

      case item_name
      when 'children'
        out_val = item_val
        uri_val = item_val.map {|i| CGI.escape(i.path) } if json_ld?

      when 'files'
        if json_ld?
          out_val = item_val.map do |i|
            { 'file_path' => i[0],
              'file_line' => i[1] }
          end

          uri_val = item_val.map do |i|
            { 'file_path' => CGI.escape(i[0]),
              'file_line' => i[1] }
          end
        else
          out_val = item_val
        end

      when 'path'
        out_val = item_val
        uri_val = CGI.escape(out_val) if json_ld?

      when 'parameters'
        out_val = item_val.map do |i|
          { 'parm_name' => i[0],
            'parm_type' => i[1] }
        end

      when 'superclass'
        out_val = item_val.path
        uri_val = CGI.escape(out_val) if json_ld?

      else
        out_val = item_val
      end

      out_json    = out_val.to_json
      out_strs    = [ fmt % [ key(item_name), out_json ] ]

      if uri_val
        uri_json    = uri_val.to_json
        out_strs   <<   fmt % [ key('y_ld:' + item_name), uri_json ]
      end

      out_strs
    end

    def fmt_node(node_hash)
    #
    # Format node_hash into a JSON string.
    #
    # @param  node_hash   [Hash<String>]    input tags and values
    # @return             [Array<String>]   list of JSON strings

      indent      = ' ' * 4
      tmp_list    = [ ]
      skip_keys   = [ ]
      skip_vals   = [ false, nil, [], {} ]
      todo_keys   = node_hash.keys.sort

      todo_keys.each do |todo_key|
        todo_val    = node_hash[todo_key]

        next if skip_keys.include?(todo_key)
        next if skip_vals.include?(todo_val)

        item_lines = fmt_item(todo_key, todo_val)

        item_lines.each {|item_line| tmp_list << indent * 2 + item_line }
      end

      node_key = node_hash['path']
      node_key = 'y_me:' + CGI.escape(node_key) if json_ld?

      tmp_list.sort!
      tmp_list[-1].chop!
      out_list = [ indent + "\"#{ node_key }\": {" ] +
                 tmp_list +
                 [ indent + '}' ]
      out_list.join("\n")
    end

    def fmt_tree(node, level=1)
    #
    # Format a tree (ie, a node and its children) as JSON.
    #
    # @param  node        [Object]          input node object
    # @param  level       [Fixnum]          recursion level
    # @return             [Array<String>]   list of JSON strings

      levels      = @options[:levels] || 1

      ivar_names  = node.instance_variables
      node_hash   = {}

      ivar_names.each do |ivar_name|
        key   = json_key(ivar_name)
        val   = node.instance_variable_get(ivar_name)
        node_hash[key]  = val
      end

      node_hash['y_ld:level'] = level if json_ld?
      node_hash['type']       = node.type
      out_list                = [ fmt_node(node_hash) ]

      if node.respond_to?(:children)      # Traverse children.
        children = node.children.sort_by {|child| child.to_s }

        children.each do |child|
          have    = types_have(child)
          want    = @options[:types]
          match   = have & want

          if false #T
            puts 'have:   ' + have.inspect
            puts 'want:   ' + want.inspect
            puts 'match:  ' + match.inspect
          end

          next if match.empty?
          next if (level >= levels)

          out_list << fmt_tree(child, level+1)   # Recurse!
        end
      end

      out_str   = out_list.join(",\n\n")

      if (level == 1)
        if json_ld?
          out_str   = "{\n#{ fmt_context.chomp },\n\n#{ out_str }\n}\n"
        else
          out_str   =                            "{\n#{ out_str }\n}\n"
        end
      end

      out_str
    end

    def initialize(file_path)
      Registry.load(file_path)
    end

    def json_key(ivar_name)
    #
    # Convert an instance variable name for use as a JSON key.
    #
    # @param  ivar_name   [String]   instance variable name
    # @return             [String]   JSON key

      ivar_name[1 .. -1]
    end

    def json_ld?
      @options[:format] == 'json_ld'
    end

    def types_have(node)
    #
    # Generate a list of types that this node has or inherits.
    #
    # @param  node        [Object]          input node
    # @return             [Array<Symbol>]   list of types

      types   = node.class.ancestors.
        map    {|klass| klass.to_s                                          }.
        select {|ks| ks =~ /^YARD::CodeObjects::\w+$/                       }.
        map    {|ks| ks.sub(/^.*::(\w+?)(Object)?$/, '\1').downcase.to_sym  }
    end

    def types_want(type_str)
    #
    # Expand type_str into a list of type symbols.
    #
    # @param  type_str    [String]          input type string
    # @return             [Array<Symbol>]   list type symbols

      raw_types = type_str.split(/\+/)      # eg, 'ns+method'

      out_types = raw_types.map do |raw_type|
        case raw_type
        when 'ba';  :base               # Base
        when 'cv';  :classvariable      # | ClassVariableObject
        when 'co';  :constant           # | ConstantObject
        when 'ef';  :extrafile          # | ExtraFileObject
        when 'ma';  :macro              # | MacroObject
        when 'me';  :method             # | MethodObject
        when 'ns';  :namespace          # | NamespaceObject
        when 'cl';  :class              # | | ClassObject
        when 'mo';  :module             # | | ModuleObject
        when 'ro';  :root               # | | RootObject
        when 'em';  :extendedmethod     # ExtendedMethodObject
        else; raw_type.to_sym
        end
      end

      out_types
    end

  end

  def main

    formats     = %w[ json  json_ld ]
    inp_path    = 'yard/.yardoc'
    yld         = YARD_LD.new(inp_path)

    global      = { :project  => 'YARD-0.8.1' }

    data_sets   = {
      :full => { }, # Start at the top of the Registry; show all descendents.

      :part => {    # Start at "YARD"; show 2 levels of namespaces/methods.
        :filter   => 'ns+me',
        :levels   =>  2,
        :start    => 'YARD'
      }
    }

    data_sets.each do |ds_key, ds_val|
      options   = ds_val.merge(global)

      formats.each do |format|

        options[:format]  = format
        out_path          = "out.#{ ds_key }.#{ format }"

        if true #T
          puts
          puts 'options:   ' + options.inspect
          puts 'out_path:  ' + out_path.inspect
        end

        out   = yld.fmt_json(options)

        open(out_path, 'w') {|f| f.puts out }
      end
    end
  end

  main
