#!/usr/bin/env ruby

require 'bluecloth'

require 'tidy'
require 'diff/lcs'
require 'diff/lcs/callbacks'
require 'spec/lib/constants'


### Expectation matcher classes/functions
module BlueCloth::Matchers

	### Matcher for comparing output of a BlueCloth-generated HTML fragment against a known-good
	### string.
	class TransformMatcher

		### Create a new matcher for the given +html+
		def initialize( html )
			@html = html
		end

		### Strip tab indentation from the expected HTML output.
		def without_indentation
			if indent = @html[/\A\t+/]
				indent.gsub!( /\A\n/m, '' )
				@html.gsub!( /^#{indent}/m, '' )
			end

			return self
		end


		### Returns true if the HTML generated by the given +bluecloth+ object matches the 
		### expected HTML, comparing only the salient document structures.
		def matches?( bluecloth )
			@bluecloth = bluecloth
			@output_html = bluecloth.to_html.gsub( /\n\n\n/, "\n\n" )
			return @output_html.strip == @html.strip
		end

		### Build a failure message for the matching case.
		def failure_message
			if self.should_output_html?
				patch = self.make_html_patch( @html, @output_html )
				return %{
					<p>Expected the generated html:<br />
					   <pre><code>#@output_html</code></pre>
					to be the same as:<br />
					   <pre><code>#@html</code></pre>
					</p>
					<p>Diffs:</p>
					#{patch}
				}
			else
				patch = self.make_patch( @html, @output_html )
				return ("Expected the generated html:\n\n  %p\n\nto be the same as:\n\n" +
					"  %p\n\nDiffs:\n\n%s") % [ @output_html, @html, patch ]
			end
		end

		### Build a failure message for the non-matching case.
		def negative_failure_message
			return "Expected the generated html:\n\n  %p\n\nnot to be the same as:\n\n  %p\n\n" %
				[ @output_html, @html ]
		end


		### Returns true if it appears HTML output should be used instead of plain-text. This
		### will be true if running from TextMate or if the HTML_LOGGING environment variable
		### is set.
		def should_output_html?
			return false
			# return ENV['HTML_LOGGING'] ||
			# 	(ENV['TM_FILENAME'] && ENV['TM_FILENAME'] =~ /_spec\.rb/)
		end


		### Compute a patch between the given +expected+ output and the +actual+ output
		### and return it as a string.
		def make_patch( expected, actual )
			diffs = Diff::LCS.sdiff( expected.split("\n"), actual.split("\n"),
				Diff::LCS::ContextDiffCallbacks )

			maxcol = diffs.flatten.
				collect {|d| [d.old_element.to_s.length, d.new_element.to_s.length ] }.
				flatten.max || 0
			maxcol += 4

			patch = "              %#{maxcol}s | %s\n" % [ "Expected", "Actual" ]
			patch << diffs.collect do |changeset|
				changeset.collect do |change|
					"%s [%03d, %03d]: %#{maxcol}s | %-#{maxcol}s" % [
						change.action,
						change.old_position,
						change.new_position,
						change.old_element.inspect,
						change.new_element.inspect,
					]
				end.join("\n")
			end.join("\n---\n")
		end

		### Compute a patch similar to #make_patch, but output HTML instead of plain text.
		def make_html_patch( expected, actual )
			diffs = Diff::LCS.sdiff( expected.split("\n"), actual.split("\n"),
				Diff::LCS::ContextDiffCallbacks )

			patch = %{
				<table>
					<caption>Diffs</caption>
					<thead>
						<tr><th>Op</th><th>Pos</th><th>Expected</th><th>Actual</th></tr>
					</thead>
					<tbody>
				}
			patch << diffs.collect do |changeset|
				changeset.collect do |change|
					"<tr><td>%s</td><td>[%03d, %03d]</td><td>%s</td><td>%s</td></tr>" % [
						change.action,
						change.old_position,
						change.new_position,
						change.old_element.inspect,
						change.new_element.inspect,
					]
				end.join("\n")
			end.join( "</tbody><tbody>" )
			patch << %{</tbody></table>\n}
		end

	end

	### Variant of the regular TransformMatcher that normalizes the two strings using the 'tidy'
	### library before comparing.
	class TidyTransformMatcher < TransformMatcher

		TIDY_OPTIONS = {}

		### Set the matcher's expected output to a tidied version of the input +html+.
		def initialize( html )
			@tidy = Tidy.open( TIDY_OPTIONS )
			@html = @tidy.clean( html )
		end


		### Returns true if the HTML generated by the given +bluecloth+ object matches the 
		### expected HTML after normalizing them both with 'tidy'.
		def matches?( bluecloth )
			@bluecloth = bluecloth
			@output_html = @tidy.clean( bluecloth.to_html )
			return @output_html == @html
		end

	end


	class TransformRegexpMatcher

		### Create a new matcher for the given +regexp+
		def initialize( regexp )
			@regexp = regexp
		end

		### Returns true if the regexp associated with this matcher matches the output generated
		### by the specified +bluecloth+ object.
		def matches?( bluecloth )
			@bluecloth = bluecloth
			@output_html = bluecloth.to_html
			return @output_html =~ @regexp
		end

		### Build a failure message for the matching case.
		def failure_message
			return "Expected the generated html:\n\n   %pto match the regexp:\n\n%p\n\n" %
				[ @output_html, @regexp ]
		end


		### Build a failure message for the negative matching case.
		def negative_failure_message
			return "Expected the generated html:\n\n   %pnot to match the regexp:\n\n%p\n\n" %
				[ @output_html, @regexp ]
		end
	end


	### Create a new BlueCloth object out of the given +string+ and +options+ and
	### return it.
	def the_markdown( string, *options )
		return BlueCloth.new( string, *options )
	end


	### Strip indentation from the given +string+, create a new BlueCloth object 
	### out of the result and any +options+, and return it.
	def the_indented_markdown( string, *options )
		if indent = string[/\A\t+/]
			indent.gsub!( /\A\n/m, '' )
			$stderr.puts "Source indent is: %p" % [ indent ] if $DEBUG
			string.gsub!( /^#{indent}/m, '' )
		end

		return BlueCloth.new( string, *options )
	end


	### Generate a matcher that expects to equal the given +html+.
	def be_transformed_into( html )
		return BlueCloth::Matchers::TransformMatcher.new( html )
	end

	### Generate a matcher that expects to match a normalized version of the specified +html+.
	def be_transformed_into_normalized_html( html )
		return BlueCloth::Matchers::TidyTransformMatcher.new( html )
	end

	### Generate a matcher that expects to match the given +regexp+.
	def be_transformed_into_html_matching( regexp )
		return BlueCloth::Matchers::TransformMatcher.new( regexp )
	end

end # module BlueCloth::Matchers

