module Hanuman
  module Graphvizzer

    COL_1_WIDTH = 47

    class Item
      include Gorillib::Builder

      field :name,  Symbol
      field :label, String, :default => ->{ name }
      field :owner, Item

      def indent(adj=0)
        "  " * (depth + adj)
      end

      def depth
        owner.depth + 1
      end

      def quote(str)        str.to_s.include?('"') ? str : %Q{"#{str}"} ;  end
      def attrib(attr, val) "#{attr}=#{val}"    ; end
      def brace(str)        "#{indent}#{str} {" ; end
      def close_brace()     "#{indent}}"        ; end

      def line(str, attrs={}, term=';')
        if attrs.empty?
          attr_strs = ''
        else
          width = COL_1_WIDTH - indent.length
          str = "%-#{width}s" % str
          attr_strs = attrs.map{|attr, val| attrib(attr, val) }
          attr_strs = "\t[ #{attr_strs.join(", ")} ]"
        end
        [indent, str, attr_strs, term].join
      end
    end

    class Graph < Item
      field :items, Array, :default => []
      field :edges, Array, :default => []

      def to_s
        str = []
        str   << brace("subgraph #{quote("cluster_#{name}")}") ## subgraph "cluster_crust" {
        str   << line(attrib("  label", quote(label)))         ##   label="crust";
        items.each do |item|                                   ##
          str << item.to_s                                     ##   "cherry_pie.crust.small_bowl"        [ shape=Mrecord, label="{small\nbowl}" ];
        end                                                    ##   "cherry_pie.crust.flour"             [ shape=Mrecord, label="{flour}" ];
        edges.each do |edge|                                   ##
          str << edge.to_s                                     ##   "cherry_pie.crust.small_bowl"        -> "cherry_pie.crust.add_to_4";
        end                                                    ##   "cherry_pie.crust.flour"             -> "cherry_pie.crust.add_to_4";
        str << close_brace                                     ## }
        str.join("\n")
      end

      def graph(name, attrs={})
        obj = Graph.new(attrs.merge(:name => name, :owner => self))
        items << obj
        yield(obj) if block_given?
        obj
      end

      def node(name, attrs={})
        obj = Node.new(attrs.merge(:name => name, :owner => self))
        items << obj
        yield(obj) if block_given?
        obj
      end

      def edge(from, into, from_slot=nil, into_slot=nil)
        obj = Edge.new(
          :name => name, :owner => self,
          :from => from, :into => into,
          :from_slot => from_slot, :into_slot => into_slot)
        edges << obj
        yield(obj) if block_given?
        obj
      end
    end

    class Universe < Graph
      field :orient, Symbol, :doc => 'one of :TB, :BT, :LR, :RL', :default => :TB
      field :engine, Symbol, :default => :dot

      def to_s
        str = []
        str << brace("digraph #{name}")         ## digraph Wukong {
        str << line("  rankdir = #{orient}")    ##   rankdir = TB;
        items.each do |item|                    ##   subgraph "cluster_cherry_pie" {
          str << item.to_s                      ##     # ...
        end                                     ##   }
        str << close_brace                      ## }
        str.join("\n")
      end

      def depth() 0; end

      def save(path, type=nil)
        File.open "#{path}.dot", "w" do |f|
          f.puts self.to_s
        end
        system "#{engine} -T#{type} #{path}.dot > #{path}.#{type}" if type
      end
    end

    class Node < Item
      field :inslots,  Array, :default => []
      field :outslots, Array, :default => []
      field :shape, Symbol, :default => :Mrecord

      def to_s
        line(
          quote(name),                            ## "cherry_pie.crust.small_bowl"      [
          :shape => shape,                        ##   shape=Mrecord,
          :label => quote(structured_label),      ##   label="{{<in>sb}|small\nbowl}"
          )                                       ## ];
      end

      def abbreviate(word)
        word.to_s.
          gsub(/-\d+$/,'').   # remove a trailing index (-0...)
          split(/[\W_]+/).    # split into _ separated segment
          reject(&:empty?).map{|str| str[0] }.join
      end

      def inslots_str
        inslots.map{|slot|  "<#{slot}>#{abbreviate(slot)}"}.join("|")
      end

      def outslots_str
        outslots.map{|slot| "<_#{slot}>#{abbreviate(slot)}"}.join("|")
      end

      def label
        super.to_s.gsub(/_\d+$/, '').gsub(/[_\.]+/, "\\n")
      end

      def structured_label
        return label unless shape =~ /record/
        str = "{"
        str << "{"  << (inslots.empty?  ? "<i>"  : inslots_str) << "}|"
        str << label
        str << "|{" << (outslots.empty? ? "<_o>" : outslots_str) << "}"
        str << "}"
        str
      end
    end

    class Edge < Item
      field :from, Whatever
      field :into, Whatever

      def to_s
        width = COL_1_WIDTH - indent.length
        "#{indent}%-#{width}s\t-> %-s;" % [ from, into ] ## "cherry_pie.crust.small_bowl" -> "cherry_pie.crust.add_to_4";
      end
    end

  end
end
