#!/usr/bin/env ruby
#
# x4o

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(node_path, options={})
      @keys = {}
      out   = fmt_tree(Registry.at(node_path), options)
    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

    private

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

      fmt = '%-30s  %s'

      tmp = <<-EOT
"type":                         "@type",
"y_ld":                         "http://sparqly.cfcl.com/y_ld/",
"yard":                         "http://sparqly.cfcl.com/yard/",

"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 }\""
        fmt % [ 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) }

      when 'files'
        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

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

      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)

      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 * 3 + item_line }
      end

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

    def fmt_tree(node, options={}, 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
      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, options, level+1)   # Recurse!
        end
      end

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

      if (level == 1)
        out_str   = <<-EOT
{
#{ fmt_context.chomp },\n
    "@graph": [
#{ out_str }
    ]
}
        EOT
      end

      out_str
    end

    def initialize(file_path)
      Registry.load(file_path) #D - ???
    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 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

  end

  def main
    file_path   = 'yard/.yardoc'
    yld         = YARD_LD.new(file_path)

    node_path   = ''
    node_path   = 'YARD'
    node_types  = yld.types_want('ns+method') #D

    options = {
      :types  =>  node_types,
      :levels =>  2
    }

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

    out = yld.fmt_json(node_path, options)
    puts '', out #T
  end

  main
