#
# These class represents the management of Traceable object
#
# TraceableSet
# Traceable
#

require 'rubygems'
require 'nokogiri'
#require 'amatch'
require 'diffy'
#require 'ruby-debug'

# this class represents a set of traceables
class TraceableSet


  public

  #
  # Initialize the traceable set
  #
  # @return [type] [description]
  def initialize()
    # the traces
    @traces={}

    # the list of supporters
    # supporters for foo 0 @@supported_by["foo"]
    @supported_by={}


    # define the sort order policy
    # it is the same for all slots
    @sortOrder=[]

  end

  #
  # [add add a traceable to the Traceable set]
  # @param  traceable [Traceable] The traceable which shall be added
  #    * first
  #    * second
  #
  # @return [type] [description]
  #
  #
  def add(traceable)
    #TOOD: check traceable
    #TODO: check if append can be optimized

    @traces[traceable.id] = [@traces[traceable.id], traceable].flatten.compact

    traceable.contributes_to.each do |t|
      @supported_by[t] = [@supported_by[t], traceable.id].flatten.compact
    end
  end


  #
  # expose added traceables found as a result of comparing two TraceableSets
  # @param  reference_set [TraceableSet] The set of traceables used as reference
  # @param  category  [Symbol] Restrict the comparison to a particlar category
  #
  # @return [Array] the ids of the added traces (list of trace_id which are not in @referece_set)
  def added_trace_ids(reference_set, category=nil)
    self.all_trace_ids(category) - reference_set.all_trace_ids(category)
  end


  #
  # expose changed traceables
  # @param  reference_set [TraceableSet] the set of traceables used as reference
  # @param  category [Symbol] Restrict the operation to traceables of this category.
  #
  # @return [Array] List of trace_id which changed not in reference_set
  def changed_trace_ids(reference_set, category=nil)
    candidates=self.all_trace_ids(category) & reference_set.all_trace_ids(category)
    candidates.map{|candidate|
      self[candidate].get_diff(reference_set[candidate])
    }.compact
  end

  #
  # expose *unchanged* traceables
  # @param  reference_set [TraceableSet] the set of traceables used as reference
  # @param  category [Symbol] Restrict the operation to traceables of this category.
  #
  # @return [Array] List of trace_id which unchanged
  def unchanged_trace_ids(reference_set, category=nil)
    candidates=self.all_trace_ids(category) & reference_set.all_trace_ids(category)
    candidates.select{|candidate|
      self[candidate].get_diff(reference_set[candidate]).nil?
    }.compact
  end


  #
  # expose *deleted* traceables
  # @param  reference_set [TraceableSet] the set of traceables used as reference
  # @param  category [Symbol] Restrict the operation to traceables of this category.
  #
  # @return [Array] List of trace_id which are deleted (not in current set)
  def deleted_trace_ids(reference_set, category=nil)
    reference_set.all_trace_ids(category) - self.all_trace_ids(category)
  end


  # export the trace as graphml for yed
  # @return - the requirements tree in graphml
  def to_graphml
    f = File.open("#{File.dirname(__FILE__)}/../../resources/requirementsSynopsis.graphml")
    doc = Nokogiri::XML(f)
    f.close

    graph=doc.xpath("//xmlns:graph").first

    # generate all nodes
    self.all_traces(nil).each{|theTrace|
      n_node = Nokogiri::XML::Node.new "node", doc
      n_node["id"] = theTrace.id
      n_data = Nokogiri::XML::Node.new "data", doc
      n_data["key"]= "d6"
      n_ShapeNode = Nokogiri::XML::Node.new "y:ShapeNode", doc
      n_NodeLabel = Nokogiri::XML::Node.new "y:NodeLabel", doc
      n_NodeLabel.content = "[#{theTrace.id}] #{theTrace.header_orig}"
      n_ShapeNode << n_NodeLabel
      n_data << n_ShapeNode
      n_node << n_data
      graph << n_node

      theTrace.contributes_to.each{|up|
        n_edge=Nokogiri::XML::Node.new "edge", doc
        n_edge["source" ] = theTrace.id
        n_edge["target" ] = up
        n_edge["id"     ] = "#{up}_#{theTrace.id}"
        graph << n_edge
      }
    }
    xp(doc).to_xml
  end

  # this delivers an array ids of all Traceables
  # @param [Symbol] selected_category the category of the deisred Traceables
  #                 if nil is given, then all Traceables are returned
  # @return [Array of String] an array of the registered Traceables
  #                              of the selectedCategory
  def all_trace_ids(selected_category = nil)
    @traces.keys.select{|x|
      y = @traces[x].first
      selected_category.nil? or y.category == selected_category
    }.sort
  end

  #this delivers an array of all traces


  #
  # return an array of all traces of a given category
  # @param  selected_category [Symbol] the category of traceables to return
  #
  # @return [Array of Traceable] The array of traceables
  def all_traces(selected_category = nil)
    all_trace_ids(selected_category).map{|t| @traces[t].first}
  end


  #
  # return an array of all traces
  #
  # @return [Array of Traceable] array of all traces
  def all_traces_as_arrays
    @traces
  end

  # this returns a particular trace
  # in case of duplicates, it delivers the first one
  # @param id [String] the id of the requested traceable
  def [] (id)
    if @traces.has_key?(id)
      @traces[id].first
    else
      nil
    end
  end

  # this lists duplicate traces
  # @return [Array of String] the list of the id of duplicate Traces
  def duplicate_ids()
    @traces.select{|id, traceables| traceables.length > 1}.map{|id, traceable| id}.sort
  end

  # this lists duplicate traces
  # @return [Array of Traceable] the list duplicate Traces.
  def duplicate_traces()
    @traces.select{|id, traceables| traceables.length > 1}.map{|id, traceable| traceable}.sort
  end


  # this serializes a particular slot for caching
  # @param file [String] name of the cachefile
  def dump_to_marshal(file)
    File.open(file, "wb"){|f|
      Marshal.dump(self, f)
    }
  end

  # this loads cached information into a particular slot
  # @param file [String] name of the cachefile
  def  self.load_from_marshal(file)
    a=nil
    File.open(file, "rb"){|f| a=Marshal.load(f)}
    a
  end

  # this merges a TraceableSet
  # @return [Treaceable] the current traceable set
  def merge(set)
    set.all_traces_as_arrays.values.flatten.each{|t| self.add(t)}
  end

  # this retunrs traces marked as supported but not being defined
  # @return [Array of String] the list of the id of undefined Traces
  #         traces which are marked as uptraces but do not exist.
  def undefined_ids
    @supported_by.keys.select{|t| not @traces.has_key?(t)}.sort
  end

  #
  # returns the list of all uptraces in the current TraceableSet. Note that
  # this is a hash of strings.
  #
  #    myset.uptrace_ids[rs_foo_001]  # yield id of traces referring to rs_foo_001
  #
  # @return [Hash of Array of String] the Hash of the uptrace ids.
  def uptrace_ids
    @supported_by
  end

  # this adjusts the sortOrder
  # @param sort_order [Array of String ]  is an array of strings
  # if a traceId starts with such a string
  # it is placed according to the sequence
  # in the array. Otherwise it is sorted at the end
  def sort_order= (sort_order)
    @sort_order=sort_order
  end

  # this determines the sort order index of a trace
  # required behavior needs to be set in advance by the method sortOrder=
  # @param trace_id [String] the id of a Traceable for which
  #                 the sort order index shall be coumputed.
  # @return [String] the sort key of the given id.
  def trace_order_index(trace_id)
    global=@sort_order.index{|x| trace_id.start_with? x} ||
      (@sort_order.length+1)

    # add the {index} of the trace to
    orderId = [global.to_s.rjust(5,"0"),trace_id].join("_")
    orderId
  end

  # this delivers a string of traceables which can be used to compare
  # traces with a
  #


  #
  # return a string all traces sorted which can be saved as a file
  # and subsequently be used by a textual diff to determine changed
  # traces.
  #
  # @return [type] [description]
  def to_compareEntries
    all_traces.sort.map{|t| "\n\n[#{t.id}]\n#{t.as_oneline}" }.join("\n")
  end



  #############################

  private

  #
  # this is used to beautify an nokigiri document
  # @param [Nokogiri::XML::Document] doc - the document
  # @return [Nokogiri::XML::Document] the beautified document
  def xp(doc)
    xsl =<<-XSL
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" encoding="UTF-8" indent="yes"/>
    <xsl:strip-space elements="*"/>
    <xsl:template match="/">
    <xsl:copy-of select="."/>
    </xsl:template>
    </xsl:stylesheet>
    XSL


    xslt = Nokogiri::XSLT(xsl)
    out  = xslt.transform(doc)

    out
  end


end




class Traceable
  include Comparable


  # String: The trace-Id
  attr_accessor :id
  # string: the alternative Id, used e.g. for the constraint number
  attr_accessor :alternative_id
  # String: The header in plain text
  attr_accessor :header_plain
  # String: The header in original format
  attr_accessor :header_orig
  # String: The body in plain text
  attr_accessor :body_plain
  # String: he body in original format
  attr_accessor :body_orig
  # Array of Strings: The uplink as an array of Trace-ids
  attr_accessor :contributes_to
  # String: the Traceable in its original format
  attr_accessor :trace_orig
  # String: origin of the entry
  attr_accessor :origin
  # String: category of the entry
  attr_accessor :category
  # String: info on the entry
  attr_accessor :info


  def initialize()
    @id = ""
    @alternative_id = ""
    @header_orig = ""
    @body_plain = ""
    @body_orig = ""
    @contributes_to = []
    @trace_orig = ""
    @category = ""
    @info = ""
  end

  # define the comparison to makeit really comaprable
  # @param [Traceable] other the other traceable for comparison.
  def <=> (other)
    @id <=> other.id
  end

  def get_diff(other)
    newval = self.get_comparison_string
    oldval = other.get_comparison_string

    #todo: get it back as soon as amatch is available
    similarity = "n/a"
    #similarity=newval.levenshtein_similar(oldval).to_s[0..6]

    if newval == oldval
      result = nil
    else
      diff_as_html= "<pre>#{other.trace_orig}</pre><hr/><pre>#{self.trace_orig}</pre>"#Diffy::Diff.new(other.trace_orig, self.trace_orig).to_s(:text)
      rawDiff = Diffy::Diff.new(self.trace_orig, other.trace_orig)
      diff_as_html=rawDiff.to_s(:html)

      result = [self.id, similarity, diff_as_html]
      diff_as_html=nil
    end
    result
  end


  def get_comparison_string
    "#{header_orig};#{body_orig};#{contributes_to.sort}".gsub(/\s+/," ")
  end

  def as_oneline
    trace_orig.gsub(/\s+/, " ")
  end


end