# -------------------------------------------------------------------------- #
# Copyright 2002-2012, OpenNebula Project Leads (OpenNebula.org)             #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #

require 'optparse'
require 'pp'

class String
    def unindent(spaces=nil)
        unless spaces
            m = self.match(/^(\s*)/)
            spaces = m[1].size
        end

        self.gsub!(/^ {#{spaces}}/, '')
    end
end

module CommandParser
    OPTIONS = [
        VERBOSE={
            :name  => "verbose",
            :short => "-v",
            :large => "--verbose",
            :description => "Verbose mode"
        },
        HELP={
            :name => "help",
            :short => "-h",
            :large => "--help",
            :description => "Show this message"
        },
        VERSION={
            :name => "version",
            :short => "-V",
            :large => "--version",
            :description => "Show version and copyright information",
        }
    ]

    class CmdParser
        attr_reader :options, :args

        def initialize(args=[], &block)
            @available_options = Array.new
            @commands = Hash.new
            @formats = Hash.new

            @main = nil

            @exit_code = nil

            @args = args
            @options = Hash.new

            define_default_formats

            instance_eval(&block)

            self.run
        end

        # Defines the usage information of the command
        # @param [String] str
        def usage(str)
            @usage = str
            @name ||= @usage.split(' ').first
        end

        # Defines the version the command
        # @param [String] str
        def version(str)
            @version = str
        end

        # Defines the additional information of the command
        # @param [String] str
        def description(str)
            @description = str
        end

        # Defines the name of the command
        # @param [String] str
        def name(str)
            @name = str
        end

        # Defines a block that will be used to parse the arguments
        # of the command. Formats defined using this method con be used
        # in the arguments section of the command method, when defining a new
        # action
        #
        # @param [Symbol] format name of the format
        # @param [String] description
        #
        # @yieldreturn [Array[Integer, String]] the block must return an Array
        #    containing the result (0:success, 1:failure) and the
        #    new value for the argument.
        def format(format, description, &block)
            @formats[format] = {
                :desc => description,
                :proc => block
            }
        end

        # Defines a global option for the command that will be used for all the
        # actions
        # @param [Hash, Array<Hash>] options the option to be included. An
        #   array of options can be also provided
        # @option options [String] :name
        # @option options [String] :short
        # @option options [String] :large
        # @option options [String] :description
        # @option options [Class] :format
        # @option options [Block] :proc The block receives the value of the
        #    option and the hash of options. The block must return an Array
        #    containing the result (0:success, 1:failure) and the
        #    new value for the argument or nil. More than one option can be
        #    specified in the block using the options hash. This hash will be
        #    available inside the command block.
        #
        # @example
        #       This example will define the following options:
        #           options[:type] = type
        #
        #       TYPE={
        #           :name => "type",
        #           :short => "-t type",
        #           :large => "--type type",
        #           :format => String,
        #           :description => "Type of the new Image"
        #       }
        #
        #       This example will define the following options:
        #           options[:check] = true
        #           options[:datastore] = id
        #
        #       DATASTORE = {
        #           :name   => "datastore",
        #           :short  => "-d id|name",
        #           :large  => "--datastore id|name" ,
        #           :description => "Selects the datastore",
        #           :format => String,
        #           :proc   => lambda { |o, options|
        #               options[:check] = true
        #               [0, OpenNebulaHelper.dname_to_id(o)]
        #           }
        #       }
        #
        def option(options)
            if options.instance_of?(Array)
                options.each { |o| @available_options << o }
            elsif options.instance_of?(Hash)
                @available_options << options
            end
        end

        # Defines the exit code to be returned by the command
        # @param [Integer] code
        def exit_code(code)
            @exit_code = code
        end

        def exit_with_code(code, output=nil)
            puts output if output
            exit code
        end

        # Defines a new action for the command, several actions can be defined
        # for a command. For example: create, delete, list.
        # The options and args variables can be used inside the block, and
        # they contain the parsedarguments and options.
        #
        # @param [Symbol] name Name of the action (i.e: :create, :list)
        # @param [String] desc Description of the action
        # @param [Array<Symbol, Array<Symbol, nil>>, Hash] args_format arguments
        #    or specific options for this actiion
        #    Note that the first argument of the command is the
        #    action and should not be defined using this parameter. The rest of
        #    the argument must be defined using this parameter.
        #    This parameter can use formats previously defined with the format
        #    method
        #    Options are specified using a hash :options => ... containing
        #    the hashes representing the options. The option method doc contains
        #    the hash that has to be used to specify an option
        # @yieldreturn [Integer, Array[Integer, String]] the block must
        #    return the exit_code and if a String is returned it will be printed
        #
        # @example
        #   Definining two arguments:
        #       $ onetest test a1 a2
        #
        #   CommandParser::CmdParser.new(ARGV) do
        #       description "Test"
        #       usage "onetest <command> <args> [options]"
        #       version "1.0"
        #
        #       options VERBOSE, HELP
        #
        #       command :test, "Test", :test1, :test2, :options => XML do
        #           puts options[:xml]
        #           puts options[:verbose]
        #           puts args[0]
        #           puts args[1]
        #           [0, "It works"]
        #       end
        #   end
        #
        #
        #   Defining optional arguments: test1 is mandatory, test2 optional
        #       $ onetest test a1 | $ onetest test a1 a2
        #
        #   CommandParser::CmdParser.new(ARGV) do
        #       description "Test"
        #       usage "onetest <command> <args> [options]"
        #       version "1.0"
        #
        #       options VERBOSE, HELP
        #
        #       command :test, "Test", :test1, [:test2, nil], :options => XML do
        #           puts options[:xml]
        #           puts options[:verbose]
        #           puts args[0]
        #           puts "It works"
        #           0
        #       end
        #   end
        #
        #
        #   Defining an argument with different formats:
        #       $ onetest test a1 a2 | $ onetest test a1 123
        #
        #   CommandParser::CmdParser.new(ARGV) do
        #       description "Test"
        #       usage "onetest <command> <args> [options]"
        #       version "1.0"
        #
        #       options VERBOSE, HELP
        #
        #       format :format1, "String to Integer" do
        #           [0, arg.to_i]
        #       end
        #
        #       command :test, "Test", :test1, [:format1, format2], :options => XML do
        #           puts options[:xml]
        #           puts options[:verbose]
        #           puts args[0]
        #           0
        #       end
        #   end
        #
        def command(name, desc, *args_format, &block)
            cmd = Hash.new
            cmd[:desc] = desc
            cmd[:arity] = 0
            cmd[:options] = []
            cmd[:args_format] = Array.new
            args_format.each {|args|
                if args.instance_of?(Array)
                    cmd[:arity]+=1 unless args.include?(nil)
                    cmd[:args_format] << args
                elsif args.instance_of?(Hash) && args[:options]
                    cmd[:options] << args[:options]
                else
                    cmd[:arity]+=1
                    cmd[:args_format] << [args]
                end
            }
            cmd[:proc] = block
            @commands[name.to_sym] = cmd
        end

        # Defines a new action for the command, several actions can be defined
        # for a command. For example: create, delete, list.
        # The options and args variables can be used inside the block, and
        # they contain the parsedarguments and options.
        #
        # @param [Array<Symbol, Array<Symbol, nil>>] args_format arguments
        #    or specific options for this actiion
        #    Note that the first argument of the command is the
        #    action and should not be defined using this parameter. The rest of
        #    the argument must be defined using this parameter.
        #    This parameter can use formats previously defined with the format
        #    method
        # @yieldreturn [Integer, Array[Integer, String]] the block must
        #    return the exit_code and if a String is returned it will be printed
        #
        # @example
        #   Definining two arguments:
        #       $ onetest a1 a2
        #
        #   CommandParser::CmdParser.new(ARGV) do
        #       description "Test"
        #       usage "onetest <args> [options]"
        #       version "1.0"
        #
        #       options XML, VERBOSE, HELP
        #
        #       main :test1, :test2 do
        #           puts options[:xml]
        #           puts options[:verbose]
        #           puts args[0]
        #           puts args[1]
        #           [0, "It works"]
        #       end
        #   end
        #
        #
        #   Defining optional arguments: test1 is mandatory, test2 optional
        #       $ onetest a1 | $ onetest a1 a2
        #
        #   CommandParser::CmdParser.new(ARGV) do
        #       description "Test"
        #       usage "onetest <args> [<options>]"
        #       version "1.0"
        #
        #       options XML, VERBOSE, HELP
        #
        #       main :test1, [:test2, nil] do
        #           puts options[:xml]
        #           puts options[:verbose]
        #           puts args[0]
        #           puts "It works"
        #           0
        #       end
        #   end
        #
        #
        #   Defining an argument with different formats:
        #       $ onetest a1 a2 | $ onetest a1 123
        #
        #   CommandParser::CmdParser.new(ARGV) do
        #       description "Test"
        #       usage "onetest <args> [<options>]"
        #       version "1.0"
        #
        #       options XML, VERBOSE, HELP
        #
        #       format :format1, "String to Integer" do
        #           [0, arg.to_i]
        #       end
        #
        #       main :test1, [:format1, :format2] do
        #           puts options[:xml]
        #           puts options[:verbose]
        #           puts args[0]
        #           puts args[1]
        #           0
        #       end
        #   end
        #
        def main(*args_format, &block)
            @main=Hash.new
            @main[:arity] = 0
            @main[:args_format] = Array.new
            args_format.collect {|args|
                if args.instance_of?(Array)
                    @main[:arity]+=1 unless args.include?(nil)
                    @main[:args_format] << args
                elsif args.instance_of?(Hash) && args[:options]
                    @available_options << args[:options]
                else
                    @main[:arity]+=1
                    @main[:args_format] << [args]
                end
            }

            @main[:proc] = block
        end

        # DEPRECATED, use format and options instead
        def set(e, *args, &block)
            case e
            when :option
                option(args[0])
            when :format
                format(args[0], args[1], &block)
            end
        end


        def run
            comm_name=""

            if @main
                comm_name = @name
                comm      = @main
            elsif
                if @args[0] && !@args[0].match(/^-/)
                    comm_name = @args.shift.to_sym
                    comm      = @commands[comm_name]
                end
            end

            if comm.nil?
                print_help
                exit -1
            end

            extra_options = comm[:options] if comm
            parse(extra_options)

            if comm
                check_args!(comm_name, comm[:arity], comm[:args_format])

                rc = comm[:proc].call
                if rc.instance_of?(Array)
                    puts rc[1]
                    exit rc.first
                else
                    exit(@exit_code || rc)
                end
            end
        end

        private

        def parse(extra_options)
            @cmdparse=OptionParser.new do |opts|
                merge = @available_options
                merge = @available_options + extra_options if extra_options
                merge.flatten.each do |e|
                    args = []
                    args << e[:short] if e[:short]
                    args << e[:large]
                    args << e[:format]
                    args << e[:description]

                    opts.on(*args) do |o|
                        if e[:proc]
                            rc = e[:proc].call(o, @options)
                            if rc.instance_of?(Array)
                                if rc[0] == 0
                                    options[e[:name].to_sym] = rc[1]
                                else
                                    puts rc[1]
                                    puts "option #{e[:name]}: Parsing error"
                                    exit -1
                                end
                            end
                        elsif e[:name]=="help"
                            print_help
                            exit
                        elsif e[:name]=="version"
                            puts @version
                            exit
                        else
                            @options[e[:name].to_sym]=o
                        end
                    end
                end
            end

            begin
                @cmdparse.parse!(@args)
            rescue => e
                puts e.message
                exit -1
            end
        end

        def check_args!(name, arity, args_format)
            if @args.length < arity
                print "Command #{name} requires "
                if arity>1
                    puts "#{args_format.length} parameters to run."
                else
                    puts "one parameter to run"
                end
                puts
                puts "Usage:"

                if @main
                    print "    #{@usage}\n"
                else
                    print "    #{name} "
                    print_command(@commands[name])
                end
                exit -1
            else
                id=0
                @args.collect!{|arg|
                    unless format=args_format[id]
                        args_str=args_format.collect{ |a|
                            if a.include?(nil)
                                "[#{a.compact.join("|")}]"
                            else
                                "<#{a.join("|")}>"
                            end
                        }.join(' ')

                        puts "Wrong number of arguments"
                        if args_str.empty?
                            puts "No argument is required"
                        else
                            puts "The arguments should be: #{args_str}"
                        end
                        exit -1
                    end

                    format = args_format[id]
                    argument = nil
                    error_msg = nil
                    format.each { |f|
                        if @formats[f]
                            format_hash = @formats[f]
                        elsif f.nil?
                            argument = nil
                            break
                        else
                            format_hash = @formats[:text]
                        end

                        rc = format_hash[:proc].call(arg)
                        if rc[0]==0
                            argument=rc[1]
                            break
                        else
                            error_msg=rc[1]
                            next
                        end
                    }

                    unless argument
                        puts error_msg if error_msg
                        puts "command #{name}: argument #{id} must be one of #{format.join(', ')}"
                        exit -1
                    end

                    id+=1
                    argument
                }
            end
        end

        ########################################################################
        # Printers
        ########################################################################

        def print_help
            if @usage
                puts "## SYNOPSIS"
                puts
                puts @usage
                puts
            end
            puts @description if @description
            puts
            print_options
            puts
            print_commands
            puts
            print_formatters
            puts
            if @version
                puts "## LICENSE"
                puts @version
            end
        end


        def print_options
            puts "## OPTIONS"

            shown_opts = Array.new
            opt_format = "#{' '*5}%-25s %s"
            @commands.each{ |key,value|
                value[:options].flatten.each { |o|
                    if shown_opts.include?(o[:name])
                        next
                    else
                        shown_opts << o[:name]

                        str = ""
                        str << o[:short].split(' ').first << ', ' if o[:short]
                        str << o[:large]

                        printf opt_format, str, o[:description]
                        puts
                    end
                }
            }

            @available_options.each{ |o|
                str = ""
                str << o[:short].split(' ').first << ', ' if o[:short]
                str << o[:large]

                printf opt_format, str, o[:description]
                puts
            }
        end

        def print_commands
            cmd_format5 =  "#{' '*3}%s"

            if @main
                print_command(@main)
            else
                puts "## COMMANDS"

                @commands.each{ |key,value|
                    printf cmd_format5, "* #{key} "

                    print_command(value)
                }
            end
        end

        def print_command(command)
            cmd_format10 =  "#{' '*8}%s"

            args_str=command[:args_format].collect{ |a|
                if a.include?(nil)
                    "[<#{a.compact.join("|")}>]"
                else
                    "<#{a.join("|")}>"
                end
            }.join(' ')
            printf "#{args_str}"
            puts

            command[:desc].split("\n").each { |l|
                printf cmd_format10, l
                puts
            } if command[:desc]

            if command[:options] && !command[:options].empty?
                opts_str=command[:options].flatten.collect{|o|
                    o[:name]
                }.join(', ')
                printf cmd_format10, "valid options: #{opts_str}"
                puts
            end
            puts
        end

        def print_formatters
            puts "## ARGUMENT FORMATS"

            cmd_format5 =  "#{' '*3}%s"
            cmd_format10 =  "#{' '*8}%s"
            @formats.each{ |key,value|
                printf cmd_format5, "* #{key}"
                puts

                value[:desc].split("\n").each { |l|
                    printf cmd_format10, l
                    puts
                }

                puts
            }
        end

        ########################################################################
        # Default Formatters for arguments
        ########################################################################
        def format_text(arg)
            arg.instance_of?(String) ? [0,arg] : [-1]
        end

        def format_int(arg)
            arg.match(/^\d+$/) ? [0,arg] : [-1]
        end

        def format_file(arg)
            File.file?(arg) ? [0,arg] : [-1]
        end

        REG_RANGE=/^(?:(?:\d+\.\.\d+|\d+),)*(?:\d+\.\.\d+|\d+)$/

        def format_range(arg)
            arg_s = arg.gsub(" ","").to_s
            return [-1] unless arg_s.match(REG_RANGE)

            ids = Array.new
            arg_s.split(',').each { |e|
                if e.match(/^\d+$/)
                    ids << e.to_i
                elsif m = e.match(/^(\d+)\.\.(\d+)$/)
                    ids += (m[1].to_i..m[2].to_i).to_a
                else
                    return [-1]
                end
            }

            return 0,ids.uniq
        end

        def define_default_formats
            format :file, "Path to a file" do |arg|
                format_file(arg)
            end

            format :range, "List of id's in the form 1,8..15" do |arg|
                format_range(arg)
            end

            format :text, "String" do |arg|
                format_text(arg)
            end
        end
    end
end


