require 'rdf/n3/patches/graph_properties'

module RDF::N3
  ##
  # A Turtle serialiser in Ruby
  #
  # Note that the natural interface is to write a whole graph at a time.
  # Writing statements or Triples will create a graph to add them to
  # and then serialize the graph.
  #
  # @example Obtaining a Turtle writer class
  #   RDF::Writer.for(:n3)         #=> RDF::N3::Writer
  #   RDF::Writer.for("etc/test.n3")
  #   RDF::Writer.for("etc/test.ttl")
  #   RDF::Writer.for(:file_name      => "etc/test.n3")
  #   RDF::Writer.for(:file_name      => "etc/test.ttl")
  #   RDF::Writer.for(:file_extension => "n3")
  #   RDF::Writer.for(:file_extension => "ttl")
  #   RDF::Writer.for(:content_type   => "text/n3")
  #   RDF::Writer.for(:content_type   => "text/turtle")
  #
  # @example Serializing RDF graph into an Turtle file
  #   RDF::N3::Writer.open("etc/test.n3") do |writer|
  #     writer << graph
  #   end
  #
  # @example Serializing RDF statements into an Turtle file
  #   RDF::N3::Writer.open("etc/test.n3") do |writer|
  #     graph.each_statement do |statement|
  #       writer << statement
  #     end
  #   end
  #
  # @example Serializing RDF statements into an Turtle string
  #   RDF::N3::Writer.buffer do |writer|
  #     graph.each_statement do |statement|
  #       writer << statement
  #     end
  #   end
  #
  # @author [Gregg Kellogg](http://kellogg-assoc.com/)
  class Writer < RDF::Writer
    format RDF::N3::Format

    SUBJECT = 0
    VERB = 1
    OBJECT = 2

    attr_accessor :graph, :base_uri

    ##
    # Initializes the Turtle writer instance.
    #
    # Opitons:
    # max_depth:: Maximum depth for recursively defining resources, defaults to 3
    # base_uri:: Base URI of graph, used to shorting URI references
    # default_namespace:: URI to use as default namespace
    #
    # @param  [IO, File]               output
    # @param  [Hash{Symbol => Object}] options
    #   @option options [Integer]       :max_depth      (nil)
    #   @option options [String, #to_s] :base_uri (nil)
    #   @option options [String, #to_s] :lang   (nil)
    #   @option options [Array]         :attributes   (nil)
    #   @option options [String]        :default_namespace
    # @yield  [writer]
    # @yieldparam [RDF::Writer] writer
    def initialize(output = $stdout, options = {}, &block)
      @graph = RDF::Graph.new
      @stream = output
      super
    end

    ##
    # @param  [Graph] graph
    # @return [void]
    def insert_graph(graph)
      @graph = graph
    end

    ##
    # @param  [Statement] statement
    # @return [void]
    def insert_statement(statement)
      @graph << statement
    end

    ##
    # Stores the RDF/XML representation of a triple.
    #
    # @param  [RDF::Resource] subject
    # @param  [RDF::URI]      predicate
    # @param  [RDF::Value]    object
    # @return [void]
    # @see    #write_epilogue
    def insert_triple(subject, predicate, object)
      @graph << RDF::Statement.new(subject, predicate, object)
    end

    ##
    # Outputs the N3 representation of all stored triples.
    #
    # @return [void]
    # @see    #write_triple
    def write_epilogue
      @max_depth = @options[:max_depth] || 3
      @base = @options[:base_uri]
      @debug = @options[:debug]
      @default_namespace = @options[:default_namespace]

      self.reset

      add_debug "\nserialize: graph: #{@graph.size}"

      add_namespace("", @default_namespace) if @default_namespace

      preprocess
      start_document

      order_subjects.each do |subject|
        #puts "subj: #{subject.inspect}"
        unless is_done?(subject)
          statement(subject)
        end
      end
    end
    
    protected
    def start_document
      @started = true
      
      write("#{indent}@base <#{@base}> .\n") if @base
      
      add_debug("start_document: #{@namespaces.inspect}")
      @namespaces.keys.sort.each do |prefix|
        write("#{indent}@prefix #{prefix}: <#{@namespaces[prefix]}> .\n")
      end
    end
    
    def end_document; end
    
    # Checks if l is a valid RDF list, i.e. no nodes have other properties.
    def is_valid_list(l)
      props = @graph.properties(l)
      #puts "is_valid_list: #{props.inspect}" if $DEBUG
      return false unless props.has_key?(RDF.first.to_s) || l == RDF.nil
      while l && l != RDF.nil do
        #puts "is_valid_list(length): #{props.length}" if $DEBUG
        return false unless props.has_key?(RDF.first.to_s) && props.has_key?(RDF.rest.to_s)
        n = props[RDF.rest.to_s]
        #puts "is_valid_list(n): #{n.inspect}" if $DEBUG
        return false unless n.is_a?(Array) && n.length == 1
        l = n.first
        props = @graph.properties(l)
      end
      #puts "is_valid_list: valid" if $DEBUG
      true
    end
    
    def do_list(l)
      puts "do_list: #{l.inspect}" if $DEBUG
      position = SUBJECT
      while l do
        p = @graph.properties(l)
        item = p.fetch(RDF.first.to_s, []).first
        if item
          path(item, position)
          subject_done(l)
          position = OBJECT
        end
        l = p.fetch(RDF.rest.to_s, []).first
      end
    end
    
    def p_list(node, position)
      return false if !is_valid_list(node)
      #puts "p_list: #{node.inspect}, #{position}" if $DEBUG

      write(position == SUBJECT ? "(" : " (")
      @depth += 2
      do_list(node)
      @depth -= 2
      write(')')
    end
    
    def p_squared?(node, position)
      node.is_a?(RDF::Node) &&
        !@serialized.has_key?(node) &&
        ref_count(node) <= 1
    end
    
    def p_squared(node, position)
      return false unless p_squared?(node, position)

      #puts "p_squared: #{node.inspect}, #{position}" if $DEBUG
      subject_done(node)
      write(position == SUBJECT ? '[' : ' [')
      @depth += 2
      predicate_list(node)
      @depth -= 2
      write(']')
      
      true
    end
    
    def p_default(node, position)
      #puts "p_default: #{node.inspect}, #{position}" if $DEBUG
      l = (position == SUBJECT ? "" : " ") + label(node)
      write(l)
    end
    
    def path(node, position)
      puts "path: #{node.inspect}, pos: #{position}, []: #{is_valid_list(node)}, p2?: #{p_squared?(node, position)}, rc: #{ref_count(node)}" if $DEBUG
      raise RDF::WriterError, "Cannot serialize node '#{node}'" unless p_list(node, position) || p_squared(node, position) || p_default(node, position)
    end
    
    def verb(node)
      puts "verb: #{node.inspect}" if $DEBUG
      if node == RDF.type
        write(" a")
      else
        path(node, VERB)
      end
    end
    
    def object_list(objects)
      puts "object_list: #{objects.inspect}" if $DEBUG
      return if objects.empty?

      objects.each_with_index do |obj, i|
        write(",\n#{indent(2)}") if i > 0
        path(obj, OBJECT)
      end
    end
    
    def predicate_list(subject)
      properties = @graph.properties(subject)
      prop_list = sort_properties(properties) - [RDF.first.to_s, RDF.rest.to_s]
      puts "predicate_list: #{prop_list.inspect}" if $DEBUG
      return if prop_list.empty?

      prop_list.each_with_index do |prop, i|
        write(";\n#{indent(2)}") if i > 0
        verb(RDF::URI.new(prop))
        object_list(properties[prop])
      end
    end
    
    def s_squared?(subject)
      ref_count(subject) == 0 && subject.is_a?(RDF::Node) && !is_valid_list(subject)
    end
    
    def s_squared(subject)
      return false unless s_squared?(subject)
      
      write("\n#{indent} [")
      @depth += 1
      predicate_list(subject)
      @depth -= 1
      write("] .")
      true
    end
    
    def s_default(subject)
      write("\n#{indent}")
      path(subject, SUBJECT)
      predicate_list(subject)
      write(" .")
      true
    end
    
    def relativize(uri)
      uri = uri.to_s
      @base ? uri.sub(/^#{@base}/, "") : uri
    end

    def statement(subject)
      puts "statement: #{subject.inspect}, s2?: #{s_squared(subject)}" if $DEBUG
      subject_done(subject)
      s_squared(subject) || s_default(subject)
    end
    
    MAX_DEPTH = 10
    INDENT_STRING = " "
    
    def top_classes; [RDF::RDFS.Class]; end
    def predicate_order; [RDF.type, RDF::RDFS.label, RDF::DC.title]; end
    
    def is_done?(subject)
      @serialized.include?(subject)
    end
    
    # Mark a subject as done.
    def subject_done(subject)
      @serialized[subject] = true
    end
    
    def order_subjects
      seen = {}
      subjects = []
      
      top_classes.each do |class_uri|
        graph.query(:predicate => RDF.type, :object => class_uri).map {|st| st.subject}.sort.uniq.each do |subject|
          #add_debug "order_subjects: #{subject.inspect}"
          subjects << subject
          seen[subject] = @top_levels[subject] = true
        end
      end
      
      # Sort subjects by resources over bnodes, ref_counts and the subject URI itself
      recursable = @subjects.keys.
        select {|s| !seen.include?(s)}.
        map {|r| [r.is_a?(RDF::Node) ? 1 : 0, ref_count(r), r]}.
        sort
      
      subjects += recursable.map{|r| r.last}
    end
    
    def preprocess
      @graph.each {|statement| preprocess_statement(statement)}
    end
    
    def preprocess_statement(statement)
      #add_debug "preprocess: #{statement.inspect}"
      references = ref_count(statement.object) + 1
      @references[statement.object] = references
      @subjects[statement.subject] = true
      
      # Pre-fetch qnames, to fill namespaces
      get_qname(statement.subject)
      get_qname(statement.predicate)
      get_qname(statement.object)

      @references[statement.predicate] = ref_count(statement.predicate) + 1
    end
    
    # Return the number of times this node has been referenced in the object position
    def ref_count(node)
      @references.fetch(node, 0)
    end

    # Return a QName for the URI, or nil. Adds namespace of QName to defined namespaces
    def get_qname(uri)
      if uri.is_a?(RDF::URI)
        md = uri.to_s.match(/^#{@base}(.*)$/) if @base
        return "<#{md[1]}>" if md

        # Duplicate logic from URI#qname to remember namespace assigned

        if uri.qname
          return ":#{uri.qname.last}" if uri.vocab == @default_namespace
          add_namespace(uri.qname.first, uri.vocab)
          add_debug "get_qname(uri.qname): #{uri.qname.join(':')}"
          return uri.qname.join(":") 
        end
        
        # No vocabulary assigned, find one from cache of created namespace URIs
        @namespaces.each_pair do |prefix, vocab|
          if uri.to_s.index(vocab.to_s) == 0
            uri.vocab = vocab
            local_name = uri.to_s[(vocab.to_s.length)..-1]
            if vocab == @default_namespace
              add_debug "get_qname(ns): :#{local_name}"
              return ":#{local_name}"
            else
              add_debug "get_qname(ns): #{prefix}:#{local_name}"
              return "#{prefix}:#{local_name}"
            end
          end
        end
        
        nil
      end
    end
    
    def label(node)
      get_qname(node) || (node.uri? ? "<#{node}>" : node.to_s)
    end

    def add_namespace(prefix, ns)
      return if @namespaces.has_key?(prefix.to_s)
      add_debug "add_namespace: '#{prefix}', <#{ns}>"
      @namespaces[prefix.to_s] = ns.to_s
    end

    def reset
      @depth = 0
      @lists = {}
      @namespaces = {}
      @references = {}
      @serialized = {}
      @subjects = {}
      @top_levels = {}
      @shortNames = {}
      @started = false
    end

    # Take a hash from predicate uris to lists of values.
    # Sort the lists of values.  Return a sorted list of properties.
    def sort_properties(properties)
      properties.keys.each do |k|
        properties[k] = properties[k].sort do |a, b|
          a_li = a.is_a?(RDF::URI) && a.qname && a.qname.last =~ /^_\d+$/ ? a.to_i : a.to_s
          b_li = b.is_a?(RDF::URI) && b.qname && b.qname.last =~ /^_\d+$/ ? b.to_i : b.to_s
          
          a_li <=> b_li
        end
      end
      
      # Make sorted list of properties
      prop_list = []
      
      predicate_order.each do |prop|
        next unless properties[prop]
        prop_list << prop.to_s
      end
      
      properties.keys.sort.each do |prop|
        next if prop_list.include?(prop.to_s)
        prop_list << prop.to_s
      end
      
      add_debug "sort_properties: #{prop_list.to_sentence}"
      prop_list
    end

    # Add debug event to debug array, if specified
    #
    # @param [String] message::
    def add_debug(message)
      @debug << message if @debug.is_a?(Array)
    end

    # Returns indent string multiplied by the depth
    def indent(modifier = 0)
      INDENT_STRING * (@depth + modifier)
    end

    # Write text
    def write(text)
      @stream.write(text)
    end
  end
end