#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

###
### gr8 -- a great command-line utility powered by Ruby
###
### $Release: 0.1.1 $
### $Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $
### $License: MIT License $
###

require "optparse"


def fu
  #; [!ktccp] returns FileUtils class object.
  require "fileutils" unless defined?(FileUtils)
  FileUtils
end


class String

  def q
    #; [!ejo5y] quotes string with single-quoation.
    #; [!ageyj] escapes single-quotation characters.
    "'%s'" % self.gsub(/'/) { "\\'" }
  end

  def qq
    #; [!wwvll] quotes string with double-quotation.
    #; [!rc66j] escapes double-quotation characters.
    '"%s"' % self.gsub(/"/) { '\\"' }
  end

end


class Object

  ## [Experimental]
  ## With Object#_, you can use '_' character as each value
  ## in block argument of map() or select().
  ## For example, 'map{|s|s+".bkp"}' can be 'map{_+".bkp"}'.
  ## See 'transform()', 'map()' or 'select()' for details.
  def _
    #; [!wvemx] returns self object.
    self
  end

end


module Enumerable

  def transform(&block)
    #; [!peitw] similar to map() or collect(), make each item as self in block.
    collect {|x| x.instance_exec(x, &block) }
  end
  alias xf transform

  alias __map map
  def map(&block)
    #; [!zfmcx] each item is available as self in block of map().
    __map {|x| x.instance_exec(x, &block) }
  end

  alias __select select
  def select(&block)
    #; [!41hap] each item is available as self in block of select().
    __select {|x| x.instance_exec(x, &block) }
  end

  def sum
    #; [!9izc1] returns sum of numbers.
    inject(0, :+)
  end

  def sum_i
    #; [!01ehd] returns sum of integers, converting values into integer.
    inject(0) {|t, x| t + x.to_i }
  end

  def sum_f
    #; [!kplnt] returns sum of floats, converting values into float.
    inject(0.0) {|t, x| t + x.to_f }
  end

  def avg
    #; [!pvi8h] returnns average of numbers.
    #; [!poidi] returns nil when no numbers.
    i = 0
    sum = inject(0) {|t, n| i += 1; t + n }
    i == 0 ? nil : sum.to_f / i
  end

  def avg_i
    #; [!btiat] returns average of numbers, converting values into integer.
    #; [!892q9] returns nil when no numbers.
    i = 0
    sum = inject(0) {|t, x| i += 1; t + x.to_i }
    i == 0 ? nil : sum.to_f / i
  end

  def avg_f
    #; [!oqpmc] returns average of numbers, converting values into float.
    #; [!9bckq] returns nil when no numbers.
    i = 0
    sum = inject(0) {|t, x| i += 1; t + x.to_f }
    i == 0 ? nil : sum.to_f / i
  end

  def xsplit(pat=nil)
    #; [!1pz77] splits each lines with pattern.
    #; [!wte7b] if block given, use its result as index.
    if block_given?
      idx = yield
      collect {|s| s.split(pat)[idx] }
    else
      collect {|s| s.split(pat) }
    end
  end

  def sed(pat, str=nil, &block)
    #; [!c7m34] replaces all patterns found in each line with str or block.
    if block_given?
      collect {|s| s.sub(pat, &block) }
    else
      collect {|s| s.sub(pat, str) }
    end
  end

  def gsed(pat, str=nil, &block)
    #; [!9lzjv] replaces first pattern found in each line with str or block.
    if block_given?
      collect {|s| s.gsub(pat, &block) }
    else
      collect {|s| s.gsub(pat, str) }
    end
  end

  def paths(&block)
    #; [!t55ce] collects Pathname objects when block argument is not passed.
    #; [!yjkm5] yields Pathname objects when block argument is passed.
    #; [!4kppy] self is Patname object in block argument.
    require "pathname" unless defined?(Pathname)
    if block_given?
      collect {|s| x = Pathname(s); x.instance_exec(x, &block) }
    else
      collect {|s| Pathname(s) }
    end
  end

  def edit(verbose=true, encoding='utf-8', &block)
    edit_i(nil, verbose, encoding, &block)
  end

  def edit_i(suffix, verbose=true, encoding='utf-8', &block)
    require "fileutils" unless defined?(FileUtils)
    arity = block.arity
    collect {|fpath|
      fpath.strip!
      msg = nil
      if File.file?(fpath)
        #; [!lpncu] creates backup file with suffix spedified.
        if suffix && ! suffix.empty?
          bkup_fpath = "#{fpath}#{suffix}"
          FileUtils.mv(fpath, bkup_fpath)
          FileUtils.cp(bkup_fpath, fpath)
        end
        #; [!ur9mj] opens file with utf-8 encoding.
        File.open(fpath, 'r+', encoding: encoding) do |f|
          s = f.read()
          s1 = s + ""
          s1.object_id != s.object_id  or raise "** assertion failed"
          #; [!qqegl] file content and file path are passed to block argument.
          #; [!d8dxv] make content as self in block argument.
          s2 = s.instance_exec(s, fpath, &block)
          #; [!9g7re] edit file when content changed.
          if s1 != s2
            f.rewind()
            f.truncate(0)
            f.write(s2.to_s)
            msg = "Edit: '#{fpath}'" if verbose
          #; [!exzkz] don't edit file when content not changed.
          else
            msg = "NotChanged: '#{fpath}'" if verbose
          end
        end
      else
        #; [!k9d31] skips if file not exist.
        #; [!6m49n] skips if file is not a file.
        if ! File.exist?(fpath)
          msg = "Skip: '#{fpath}' does not exist."
        else
          msg = "Skip: '#{fpath}' is not a file."
        end
      end
      msg
    }.reject {|x| x.nil? }
  end

  ## experimentals

  def move_to(verbose=true, &block)   #:experimental:
    __copy_or_move_to("Move", verbose, false, false, "move_to", &block)
  end

  def move_to!(verbose=true, &block)   #:experimental:
    __copy_or_move_to("Move", verbose, true, false, "move_to!", &block)
  end

  def mkdir_and_move_to(verbose=true, &block)   #:experimental:
    __copy_or_move_to("Move", verbose, false, true, "mkdir_and_move_to", &block)
  end

  def mkdir_and_move_to!(verbose=true, &block)   #:experimental:
    __copy_or_move_to("Move", verbose, true, true, "mkdir_and_move_to!", &block)
  end

  def copy_to(verbose=true, &block)   #:experimental:
    __copy_or_move_to("Copy", verbose, false, false, "copy_to", &block)
  end

  def copy_to!(verbose=true, &block)   #:experimental:
    __copy_or_move_to("Copy", verbose, true, false, "copy_to!", &block)
  end

  def mkdir_and_copy_to(verbose=true, &block)   #:experimental:
    __copy_or_move_to("Copy", verbose, false, true, "mkdir_and_copy_to", &block)
  end

  def mkdir_and_copy_to!(verbose=true, &block)   #:experimental:
    __copy_or_move_to("Copy", verbose, true, true, "mkdir_and_copy_to!", &block)
  end

  def rename_as(verbose=true, &block)   #:experimental:
    __copy_or_rename_as("Rename", verbose, false, false, "rename_as", &block)
  end

  def rename_as!(verbose=true, &block)   #:experimental:
    __copy_or_rename_as("Rename", verbose, true, false, "rename_as!", &block)
  end

  def mkdir_and_rename_as(verbose=true, &block)   #:experimental:
    __copy_or_rename_as("Rename", verbose, false, true, "mkdir_and_rename_as", &block)
  end

  def mkdir_and_rename_as!(verbose=true, &block)   #:experimental:
    __copy_or_rename_as("Rename", verbose, true, true, "mkdir_and_rename_as!", &block)
  end

  def copy_as(verbose=true, &block)   #:experimental:
    __copy_or_rename_as("Copy", verbose, false, false, "copy_as", &block)
  end

  def copy_as!(verbose=true, &block)   #:experimental:
    __copy_or_rename_as("Copy", verbose, true, false, "copy_as!", &block)
  end

  def mkdir_and_copy_as(verbose=true, &block)   #:experimental:
    __copy_or_rename_as("Copy", verbose, false, true, "mkdir_and_copy_as", &block)
  end

  def mkdir_and_copy_as!(verbose=true, &block)   #:experimental:
    __copy_or_rename_as("Copy", verbose, true, true, "mkdir_and_copy_as!", &block)
  end

  private

  def __copy_or_move_to(action, verbose_p, overwrite_p, mkdir_p, meth, &block)
    #; [!n0ubo] block argument is required.
    #; [!40se5] block argument is required.
    #; [!k74dw] block argument is required.
    #; [!z9yus] block argument is required.
    block  or
      raise ArgumentError.new("#{meth}(): block argument required.")
    require "fileutils" unless defined?(FileUtils)
    existing = nil
    collect {|fpath|
      #; [!qqzqz] trims target file name.
      fpath.strip!
      #; [!nnud9] destination directory name is derived from target file name.
      dirpath = fpath.instance_exec(fpath, &block)
      #; [!ey3e4] if target directory name is nil or empty, skip moving file.
      if ! dirpath || dirpath.empty?
        msg = "Skip: target directory name is nil or empty (file: '#{fpath}')"
      #; [!i5jt6] if destination directory exists, move file to it.
      elsif dirpath == existing || File.directory?(dirpath)
        msg = nil
      #; [!azqgk] if there is a file that name is same as desination directory, skip.
      elsif File.exist?(dirpath)
        msg = "Skip: directory '#{dirpath}' not a directory"
      #; [!b9d4m] if destination directory doesn't exist, creates it.
      elsif mkdir_p
        FileUtils.mkdir_p(dirpath)
        msg = nil
      #; [!rqu5q] if destinatio directory doesn't exist, skip.
      else
        msg = "Skip: directory '#{dirpath}' not exist"
      end
      #
      if msg.nil?
        new_fpath = File.join(dirpath, File.basename(fpath))
        #; [!0gq9h] if destination file already exist, skip.
        exist_p = File.exist?(new_fpath)
        if exist_p && ! overwrite_p
          msg = "Skip: destination file '#{new_fpath}' already exist."
        #; [!ebdqh] overwrite destination file even if it exists.
        else
          #; [!fa5y0] copy files or directories into destination directory.
          if action == "Copy"
            FileUtils.cp_r(fpath, dirpath)
          #; [!d9vxl] move files or directories into destination directory.
          elsif action == "Move"
            FileUtils.mv(fpath, dirpath)
          else
            raise "** unreachable"
          end
          existing = dirpath
          #; [!n7a1q] prints target file and destination directory when verbose mode.
          #; [!itsh0] use 'Move!' instead of 'Move' when overwriting existing file.
          msg = "#{action}#{exist_p ? '!' : ''}: '#{fpath}' => '#{dirpath}'" if verbose_p
        end
      end
      msg
    }.reject {|s| s.nil? }
  end

  def __copy_or_rename_as(action, verbose_p, overwrite_p, mkdir_p, meth, &block)
    #; [!ignfm] block argument is required.
    block  or
      raise ArgumentError.new("#{meth}(): block argument required.")
    require "fileutils" unless defined?(FileUtils)
    existing = nil
    collect {|fpath|
      #; [!qqzqz] trims target file name.
      fpath.strip!
      #; [!nnud9] destination file name is derived from source file name.
      new_fpath = fpath.instance_exec(fpath, &block)
      #
      overwritten = false
      #; [!dkejf] if target directory name is nil or empty, skips renaming file.
      if ! new_fpath || new_fpath.empty?
        msg = "Skip: target file name is nil or empty (file: '#{fpath}')"
      #
      elsif File.exist?(new_fpath)
        #; [!1yzjd] if target file or directory already exists, removes it before renaming file.
        if overwrite_p
          FileUtils.rm_rf(new_fpath)
          overwritten = true
          msg = nil
        #; [!8ap57] if target file or directory already exists, skips renaming files.
        else
          msg = "Skip: target file '#{new_fpath}' already exists."
        end
      #
      else
        #; [!qhlc8] if directory of target file already exists, renames file.
        dirpath = File.dirname(new_fpath)
        if existing == dirpath || File.directory?(dirpath)
          existing = dirpath
          msg = nil
        #; [!sh2ti] if directory of target file not exist, creates it.
        elsif mkdir_p
          FileUtils.mkdir_p(dirpath)
          existing = dirpath
          msg = nil
        #; [!gg9w1] if directory of target file not exist, skips renaming files.
        else
          msg = "Skip: directory of target file '#{new_fpath}' not exist."
        end
      end
      if msg.nil?
        #; [!0txp4] copy files or directories.
        if action == "Copy"
          FileUtils.cp_r(fpath, new_fpath)
        #; [!xi8u5] rename files or directories.
        elsif action == "Rename"
          FileUtils.move(fpath, new_fpath)
        else
          raise "** unreachable"
        end
        #; [!vt24y] prints source and destination file path when verbose mode.
        #; [!gd9j9] use 'Rename!' instead of 'Rename' when overwriting existing file.
        #; [!8warb] use 'Copy!' instead of 'Copy' when overwriting exsiting file.
        msg = "#{action}#{overwritten ? '!': ''}: '#{fpath}' => '#{new_fpath}'" if verbose_p
      end
      msg
    }.reject {|s| s.nil? }
  end

end


class Enumerator::Lazy

  alias __map map
  def map(&block)
    #; [!drgky] each item is available as self in block of map().
    __map {|x| x.instance_exec(x, &block) }
  end

  alias __select select
  def select(&block)
    #; [!uhqz2] each item is available as self in block of map().
    __select {|x| x.instance_exec(x, &block) }
  end

end


module Gr8

  VERSION = "$Release: 0.1.1 $".split()[1]

  WEBSITE_URL = "https://kwatch.github.io/gr8/"

  HELP = <<"END"
%{script} -- great command-line utility powered by Ruby

Usage:
  %{script} [options] ruby-code

Options:
  -h, --help             : print help
      --doc              : open document with browser
  -v, --version          : print version
  -r lib[,lib2,...]      : require libraries
  -F[regexp]             : separate each line into fields
  -C N                   : select column (1-origin)

Example:
  $ cat data
  Haruhi   100
  Mikuru    80
  Yuki     120
  $ cat data | %{script} 'map{|s| s.split()[1]}'      ## self == $stdin.lazy
  100
  80
  120
  $ cat data | %{script} 'map{split()[1].to_i}.sum'   ## map{self} == map{|s| s}
  300
  $ cat data | %{script} 'map{split[1]}.sum_i'        ## .sum_i == map(&:to_i).sum
  300
  $ cat data | %{script} -C2 'sum_i'                  ## -C2 == map{split[1]}
  300

See #{WEBSITE_URL} for details and examples.

END


  class EnumWrapper
    include Enumerable

    def initialize(enum, separator=nil, column=nil)
      @_base      = enum
      @_separator = separator
      @_column    = column
    end

    def each
      #; [!hloy1] splits each line into array.
      #; [!c22km] chomps each lin before splitting.
      #; [!m411f] selects column when column number specified.
      sep = @_separator
      col = @_column
      if @_column
        index = @_column - 1
        @_base.each {|s| s.chomp!; yield s.split(sep)[index] }
      else
        @_base.each {|s| s.chomp!; yield s.split(sep) }
      end
    end

  end


  class App

    def run(*args)
      #
      begin
        opts = parse_options(args)
      rescue ::OptionParser::ParseError => ex
        #$stderr.puts "ERROR (#{script_name()}): #{ex.args.is_a?(Array) ? ex.args.join(' ') : ex.args}: #{ex.reason}"
        $stderr.puts "ERROR (#{script_name()}): #{ex.message}"
        return 1
      end
      #
      output = handle_opts(opts)
      if output
        puts output
        return 0
      end
      #
      errmsg = validate_args(args)
      if errmsg
        $stderr.puts "ERROR (#{script_name()}): #{errmsg}"
        return 1
      end
      #; [!8hk3g] option '-F': separates each line into array.
      #; [!vnwu6] option '-C': select colum.
      if opts[:separator] || opts[:column]
        sep = opts[:separator]
        stdin = EnumWrapper.new($stdin, sep == true ? nil : sep, opts[:column]).lazy
      else
        stdin = $stdin.lazy
        define_singleton_methods_on(stdin)
      end
      #; [!r69d6] executes ruby code with $stdin.lazy as self.
      code = args[0]
      filename = "<#{script_name()}>"
      val = stdin.instance_eval(code, filename)
      #; [!hsvnd] prints nothing when result is nil.
      #; [!eiaa6] prints each item when result is Enumerable.
      #; [!6pfay] prints value when result is not nil nor Enumerable.
      case val
      when nil        ; nil
      when Enumerable ; val.each {|x| puts x }
      else            ; puts val
      end
      #; [!h5wln] returns 0 as status code when executed successfully.
      return 0
    end

    def main(argv=ARGV)
      #; [!w9kb8] exit with status code 0 when executed successfully.
      #; [!nbag1] exit with status code 1 when execution failed.
      args = argv.dup
      status = run(*args)
      exit status
    end

    private

    def script_name
      @script_name ||= File.basename($0)
    end

    def parse_options(args)
      #; [!5efp5] returns Hash object containing command-line options.
      opts = {}
      parser = OptionParser.new
      parser.on("-h", "--help")     {|v| opts[:help]    = true }
      parser.on(      "--doc")      {|v| opts[:doc]     = true }
      parser.on("-v", "--version")  {|v| opts[:version] = true }
      parser.on("-r lib[,lib2,..]") {|v| opts[:require] = v }
      parser.on("-F[sep]") do |v|
        #; [!jt4y5] option '-F': separator is omissible.
        #; [!jo4gm] option '-F': error when invalid regular expression.
        begin
          opts[:separator] = v.nil? ? true : Regexp.new(v)
        rescue RegexpError
          raise invalid_argument_error(v, "invalid regular expression")
        end
      end
      #parser.on("-C N", Integer) {|v| opts[:column] = v }
      parser.on("-C N") do |v|
        #; [!7ruq0] option '-C': argument should be an integer.
        #; [!6x3dp] option '-C': argument should be >= 1.
        begin
          opts[:column] = Integer(v)
        rescue ArgumentError => ex
          raise invalid_argument_error(v, "integer expected")
        end
        if opts[:column] <= 0
          raise invalid_argument_error(v, "column number should be >= 1")
        end
      end
      #; [!wdzss] modifies args.
      parser.parse!(args)
      return opts
    end

    def invalid_argument_error(optarg, reason)
      err = OptionParser::InvalidArgument.new(optarg)
      err.reason = reason
      err
    end

    def handle_opts(opts)
      #; [!33bj3] option '-h', '--help': prints help message.
      if opts[:help]
        return HELP % {script: script_name()}
      end
      #; [!7dvjg] option '--doc': opens website with browser.
      if opts[:doc]
        cmd = command_to_open_website()
        system cmd
        return cmd
      end
      #; [!2tfh5] option '-v', '--version': prints version string.
      if opts[:version]
        return VERSION
      end
      #; [!1s7wm] option '-r': requires libraries.
      if opts[:require]
        opts[:require].split(/,/).each do |libname|
          libname.strip!
          require libname
        end
      end
      nil
    end

    def command_to_open_website()
      url = WEBSITE_URL
      case RUBY_PLATFORM
      when /darwin/     ; "open #{url}"
      when /linux/      ; "xdg-open #{url}"
      when /bsd/        ; "xdg-open #{url}"  # really?
      when /win|mingw/i ; "start #{url}"
      else              ; "open #{url}"   # or error?
      end
    end

    def validate_args(args)
      #; [!7wqyh] prints error when no argument.
      #; [!bwiqv] prints error when too many argument.
      if args.length == 0
        return "argument required."
      elsif args.length > 1
        return "too many arguments."
      end
      nil
    end

    def define_singleton_methods_on(stdin)
      (class << stdin; self; end).class_eval do
        #; [!zcxh1] removes '\n' from each line automatically.
        alias __each_orig each
        def each
          __each_orig {|s| s.chomp!; yield s }
        end
        alias __each_new each
        #; [!i7npb] $1, $2, ... are available in grep() block argument.
        #; [!vkt64] lines are chomped automatically in grep() if block is not given.
        def grep(pattern, &block)
          if pattern.is_a?(Regexp) && block
            (class << self; self; end).class_eval { alias each __each_orig }
          end
          super(pattern, &block)
        end
      end
    end

  end


end


#if __FILE__ == $0
  Gr8::App.new.main() unless $DONT_RUN_GR8_APP
#end