#!ruby
#vim: set fileencoding:utf-8

# AUTHOR: dearblue <dearblue@users.sourceforge.co.jp>
# Only this program is usable as public domain software.

require "extlz4"
require "optparse"

PROGNAME = File.basename(__FILE__)
opt = OptionParser.new(<<-EOS, 8, "  ")
Usage: #{PROGNAME} [option]... [file]...

  This program is likely lz4-cli. Reinventing the wheel.
EOS

class << opt
  #
  # Define hidden switch.
  #
  def hide_on(opt, &block)
    top.short[opt.to_s[1]] ||= OptionParser::Switch::NoArgument.new(&block)
  end
end

mode = nil
verbose = 1
opt.separator("")
level = 1
opt.on("-1", "fastest (worst) compression (default)") { mode = :encode; level = 1 }
opt.separator("  -2 .. -8 set compression level")
opt.hide_on("-2") { mode = :encode; level = 2 }
opt.hide_on("-3") { mode = :encode; level = 3 }
opt.hide_on("-4") { mode = :encode; level = 4 }
opt.hide_on("-5") { mode = :encode; level = 5 }
opt.hide_on("-6") { mode = :encode; level = 6 }
opt.hide_on("-7") { mode = :encode; level = 7 }
opt.hide_on("-8") { mode = :encode; level = 8 }
opt.on("-9", "best (slowest) compression") { mode = :encode; level = 9 }
outstdout = false
opt.on("-c", "write to stdout, keep original files") { outstdout = true }
opt.on("-d", "uncompress files") { mode = :decode }
forceoverwrite = false
opt.on("-f", "force overwriting of output file") { forceoverwrite = true }
keepfile = false
opt.on("-k", "don't delete input files during operation") { keepfile = true }
opt.on("-q", "output no warnings") { verbose = 0 }
recursive = false
opt.on("-r", "recursively compress files in directories") { recursive = true }
opt.on("-t", "test compressed file") { mode = :test }
opt.on("-v", "increment verbosery level") { verbose += 1 }
outdir = nil
opt.on("-Cdir", "set output directory") { |dir| outdir = dir }
blocksize = 7
blockdep = false
blockchecksum = false
opt.separator("  -B#      set block size [4-7] (default: 7)") # merged to "-BX"
opt.separator("  -BD      set mode to block dependency (improve compression ratio)") # merged to "-BX"
opt.on("-BX", "enable block checksum (default: disabled)", %w[4 5 6 7 D X]) do |o|
  case o
  when ?4, ?5, ?6, ?7
    blocksize = o.ord - ?0.ord
  when ?D
    blockdep = true
  when ?X
    blockchecksum = true
  else
    0/0
  end
end
streamchecksum = true
opt.on("-Sx", "disable stream checksum (default: enabled)", %w(x)) { streamchecksum = false }
opt.on("-V", "display program version") {
  puts <<-EOS
#{PROGNAME}: extlz4-cli program version 0.1.1 (powered by #{RUBY_ENGINE}-#{RUBY_VERSION})
  EOS

  exit 0
}

opt.separator <<EOS

temporary special operation:
EOS
opt.on("--fix-extlz4-0.1-bug", "fix corrupted lz4 stream file for encoded by extlz4-0.1") { mode = :fix_extlz4_0_1_bug }

begin
  opt.parse!
rescue OptionParser::InvalidOption, OptionParser::InvalidArgument
  $stderr.puts <<-EOS
#{PROGNAME}: #{$!}
        enter ``#{PROGNAME} --help'' to print help.
  EOS
  exit 1
end

def file_operation(outdir, infile, defaultoutfile, outstdout, forceoverwrite, keepfile)
  case
  when outstdout
    outfile = $stdout.binmode
  when infile == defaultoutfile
    $stderr.puts "#{PROGNAME}: out file name is same as in file - #{infile}"
  else
    outfile = defaultoutfile
    outfile = File.join(outdir, File.basename(outfile)) if outdir
    if !forceoverwrite && File.exist?(outfile)
      $stderr.puts "#{PROGNAME}: file exists - #{outfile}. (with ``-f'' switch to overwrite)"
      return false
    end
  end

  yield(infile, outfile)

  if !outstdout
    t = File.mtime(infile)
    File.utime(t, t, outfile)
    File.unlink(infile) if !keepfile
  end

  true
end  

if ARGV.empty?
  if $stdout.tty? && !forceoverwrite && mode != :decode && mode != :test
    $stderr.puts <<-EOS
#{PROGNAME}: not written to terminal. use ``-f'' to force encode.
\tor enter ``#{PROGNAME} --help'' to print help.
    EOS
    exit 1
  end

  $stdin.binmode
  $stdout.binmode

  case mode
  when :encode, nil
    LZ4.encode_file($stdin, $stdout, level,
                    block_dependency: blockdep,
                    block_checksum: blockchecksum,
                    stream_checksum: streamchecksum)
  when :decode
    LZ4.decode_file($stdin, $stdout)
  when :test
    LZ4.test_file($stdin)
  else
    raise NotImplementedError
  end

  exit 0
else
  ARGV.each do |file|
    file = file.gsub(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
    begin
      case
      when mode == :decode || (mode.nil? && file =~ /\.lz4$/i)
        file_operation(outdir, file, file.sub(/\.lz4$/i, ""), outstdout, forceoverwrite, keepfile) do |infile, outfile|
          LZ4.decode_file(infile, outfile)
        end
      when mode == :encode || mode.nil?
        file_operation(outdir, file, file + ".lz4", outstdout, forceoverwrite, keepfile) do |infile, outfile|
          LZ4.encode_file(infile, outfile, level,
                          block_dependency: blockdep,
                          block_checksum: blockchecksum,
                          stream_checksum: streamchecksum)
        end
      when mode == :test
        LZ4.test_file(file)
      when mode == :fix_extlz4_0_1_bug
        outname = file.sub(/(?<=#{File::SEPARATOR})(?=[^#{File::SEPARATOR}]+$)|^(?=[^#{File::SEPARATOR}]+$)/, "fixed-")
        file_operation(outdir, file, outname, outstdout, forceoverwrite, true) do |infile, outfile|
          require "extlz4/fix-0.1bug.rb"
          if verbose > 0
            $stderr.puts "#{PROGNAME}: correcting lz4 file - #{infile} to #{outfile}"
          end
          if verbose > 1
            outinfo = ->(mesg, offset, total, *etc) do
              $stderr.puts "#{mesg} (at #{offset} of #{total})"
            end
          end
          LZ4.fix_extlz4_0_1_bug(infile, outfile, &outinfo)
        end
      else
        $stderr.puts "#{PROGNAME}: mode error - #{mode}"
      end
    rescue LZ4::Error #, Object
      $stderr.puts "#{PROGNAME}: #{file} - #$! (#{$!.class})"
    end
  end
end
