# -*- ruby encoding: utf-8 -*-

require 'optparse'
require 'ostruct'
require 'diff/lcs/hunk'

# == ldiff Usage
#   ldiff [options] oldfile newfile
#
# -c::                            Displays a context diff with 3 lines of context.
# -C [LINES], --context [LINES]:: Displays a context diff with LINES lines of context. Default 3 lines.
# -u::                            Displays a unified diff with 3 lines of context.
# -U [LINES], --unified [LINES]:: Displays a unified diff with LINES lines of context. Default 3 lines.
# -e::                            Creates an 'ed' script to change oldfile to newfile.
# -f::                            Creates an 'ed' script to change oldfile to newfile in reverse order.
# -a, --text::                    Treats the files as text and compares them line-by-line, even if they do not seem to be text.
# --binary::                      Treats the files as binary.
# -q, --brief::                   Reports only whether or not the files differ, not the details.
# --help::                        Shows the command-line help.
# --version::                     Shows the version of Diff::LCS.
#
# By default, runs produces an "old-style" diff, with output like UNIX diff.
#
# == Copyright
# Copyright &copy; 2004 Austin Ziegler
#
#   Part of Diff::LCS <http://rubyforge.org/projects/ruwiki/>
#   Austin Ziegler <diff-lcs@halostatue.ca>
#
# This program is free software. It may be redistributed and/or modified under
# the terms of the GPL version 2 (or later), the Perl Artistic licence, or the
# Ruby licence.
module Diff::LCS::Ldiff
  BANNER = <<-COPYRIGHT
ldiff #{Diff::LCS::VERSION}
  Copyright 2004-2013 Austin Ziegler

  Part of Diff::LCS.
  http://rubyforge.org/projects/ruwiki/

  Austin Ziegler <diff-lcs@halostatue.ca>

  This program is free software. It may be redistributed and/or modified under
  the terms of the GPL version 2 (or later), the Perl Artistic licence, or the
  MIT licence.
              COPYRIGHT
end

class << Diff::LCS::Ldiff
  attr_reader :format, :lines #:nodoc:
  attr_reader :file_old, :file_new #:nodoc:
  attr_reader :data_old, :data_new #:nodoc:

  def run(args, input = $stdin, output = $stdout, error = $stderr) #:nodoc:
    @binary = nil

    args.options do |o|
      o.banner = "Usage: #{File.basename($0)} [options] oldfile newfile"
      o.separator ""
      o.on('-c', '-C', '--context [LINES]', Numeric, 'Displays a context diff with LINES lines', 'of context. Default 3 lines.') do |ctx|
        @format = :context
        @lines  = ctx || 3
      end
      o.on('-u', '-U', '--unified [LINES]', Numeric, 'Displays a unified diff with LINES lines', 'of context. Default 3 lines.') do |ctx|
        @format = :unified
        @lines  = ctx || 3
      end
      o.on('-e', 'Creates an \'ed\' script to change', 'oldfile to newfile.') do |ctx|
        @format = :ed
      end
      o.on('-f', 'Creates an \'ed\' script to change', 'oldfile to newfile in reverse order.') do |ctx|
        @format = :reverse_ed
      end
      o.on('-a', '--text', 'Treat the files as text and compare them', 'line-by-line, even if they do not seem', 'to be text.') do |txt|
        @binary = false
      end
      o.on('--binary', 'Treats the files as binary.') do |bin|
        @binary = true
      end
      o.on('-q', '--brief', 'Report only whether or not the files', 'differ, not the details.') do |ctx|
        @format = :report
      end
      o.on_tail('--help', 'Shows this text.') do
        error << o
        return 0
      end
      o.on_tail('--version', 'Shows the version of Diff::LCS.') do
        error << BANNER
        return 0
      end
      o.on_tail ""
      o.on_tail 'By default, runs produces an "old-style" diff, with output like UNIX diff.'
      o.parse!
    end

    unless args.size == 2
      error << args.options
      return 127
    end

    # Defaults are for old-style diff
    @format ||= :old
    @lines  ||= 0

    file_old, file_new = *ARGV

    case @format
    when :context
      char_old = '*' * 3
      char_new = '-' * 3
    when :unified
      char_old = '-' * 3
      char_new = '+' * 3
    end

    # After we've read up to a certain point in each file, the number of
    # items we've read from each file will differ by FLD (could be 0).
    file_length_difference = 0

    if @binary.nil? or @binary
      data_old = IO::read(file_old)
      data_new = IO::read(file_new)

      # Test binary status
      if @binary.nil?
        old_txt = data_old[0...4096].scan(/\0/).empty?
        new_txt = data_new[0...4096].scan(/\0/).empty?
        @binary = (not old_txt) or (not new_txt)
        old_txt = new_txt = nil
      end

      unless @binary
        data_old = data_old.split($/).map { |e| e.chomp }
        data_new = data_new.split($/).map { |e| e.chomp }
      end
    else
      data_old = IO::readlines(file_old).map { |e| e.chomp }
      data_new = IO::readlines(file_new).map { |e| e.chomp }
    end

    # diff yields lots of pieces, each of which is basically a Block object
    if @binary
      diffs = (data_old == data_new)
    else
      diffs = Diff::LCS.diff(data_old, data_new)
      diffs = nil if diffs.empty?
    end

    return 0 unless diffs

    if (@format == :report) and diffs
      output << "Files #{file_old} and #{file_new} differ\n"
      return 1
    end

    if (@format == :unified) or (@format == :context)
      ft = File.stat(file_old).mtime.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z')
      puts "#{char_old} #{file_old}\t#{ft}"
      ft = File.stat(file_new).mtime.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z')
      puts "#{char_new} #{file_new}\t#{ft}"
    end

    # Loop over hunks. If a hunk overlaps with the last hunk, join them.
    # Otherwise, print out the old one.
    oldhunk = hunk = nil

    if @format == :ed
      real_output = output
      output = []
    end

    diffs.each do |piece|
      begin
        hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, @lines,
                                   file_length_difference)
        file_length_difference = hunk.file_length_difference

        next unless oldhunk
        next if (@lines > 0) and hunk.merge(oldhunk)

        output << oldhunk.diff(@format) << "\n"
      ensure
        oldhunk = hunk
      end
    end

    output << oldhunk.diff(@format) << "\n"

    if @format == :ed
      output.reverse_each { |e| real_output << e.diff(:ed_finish) }
    end

    return 1
  end
end
