#!/usr/bin/env ruby

# msqlm.rb: written by itacchi (DATE Ken) 2000-
# That's sample program for Ruby/mSQL.
# msqlm.rb is a `msql' clone.

require 'getopts'
require 'tempfile'
require 'msql'


class OptionError < StandardError; end
class InputError  < StandardError; end

class MsqlResult

  def to_table
    str = ""
    separator = ' +'
    length = Hash.new

    each_field do |field|
      name = field.name
      case field.type
      when MsqlField::REAL_TYPE
        length[name] = name.size
        length[name] = 12 if length[name] < 12
      when MsqlField::INT_TYPE,   MsqlField::UINT_TYPE,
           MsqlField::MONEY_TYPE, MsqlField::TIME_TYPE
        length[name] = name.size
        length[name] =  8 if length[name] <  8
      when MsqlField::DATE_TYPE
        length[name] = name.size
        length[name] = 11 if length[name] < 11
      when MsqlField::CHAR_TYPE, MsqlField::TEXT_TYPE
        length[name] = name.size > field.length ? name.size : field.length
      end
      separator += '-' * length[name] + '-+'
    end

    separator +=  "\n"
    str = separator
    field_seek 0
    str += ' |'

    each_field do |field|
      name = field.name
      str += " #{name}"
      str += ' ' * (length[name] - name.size)
      str += '|'
    end

    str += "\n" + separator
    field_seek 0

    each_row do |row|
      str += ' |'
      each_field_with_index do |field, i|
        name = field.name
        if row[i]
          str += " #{row[i]}" + ' ' * (length[name] - row[i].size)
        else
          str += " NULL" + ' ' * (length[name] - 4)
        end
        str += '|'
      end
      str += "\n"
      field_seek 0
    end

    str += separator + "\n\n"
  end

end



class MsqlMonitor

  def initialize(host, database)
    @client = Msql::connect(host)
    @client.select_db(database)
    @message      = ""
    @query_string = ""
  end
  attr_reader :message, :query_string


  def add_query(str)
    @query_string += str
  end


  def clear_query
    @query_string = ""
  end


  def edit_query
    tmp = Tempfile::new('msql')

    editor = ENV['VISUAL'] or ENV['EDITOR']
    if not editor
      case RUBY_PLATFORM
      when /win32/
        editor = "edit"
      when /cygwin/
        editor = "vim"
      else
        editor = "vi"
      end
    end

    tmp.puts @query_string
    tmp.close

    system "#{editor} #{tmp.path}"

    tmp.open
    @query_string = tmp.read
    tmp.close(true)
  end


  def handle_query
    if @query_string.size == 0
      raise MsqlError, "No query specified !!\n"
    end

    begin
      res = @client.query @query_string
    rescue MsqlError
      return @message = "ERROR: #{$!}\n\n"
    end

    @message = "\nQuery OK.  #{res} row(s) modified or retrieved.\n\n"

    result = @client.store_result

    @message += result.to_table if result
  end

end



class MsqlMonitorView

  def initialize(host, database)
    @monitor = MsqlMonitor.new(host, database)
  end


  def run
    is_in_string = false
    prompt       = true
    new_query    = false

    print "Welcome to the miniSQL monitor.  Type \\h for help.\n\n"
    print "\nmSQL > "

    STDOUT.flush
    while line = STDIN.gets
      while line.size > 0
        if is_in_string

          case line               # in literal (such as 'LITERAL')
          when /\A.*\'/           # go out
            @monitor.add_query($& + ' ')
            is_in_string = false
          else
            @monitor.add_query line
            break
          end

        else

          case line
          when /\A#/              # skip comment
            break

          when /\A\n/
            if prompt
              @monitor.add_query $&
              print "    -> "
            end
            prompt = true

          when /\A\s+/            # skip space

          when /\A\\h/
            help
            @monitor.clear_query
            print "\nmSQL > "
            prompt    = false
            new_query = true

          when /\A\\g/
            @monitor.handle_query
            print @monitor.message
            print "\nmSQL > "
            prompt    = false
            new_query = true

          when /\A\\e/
            @monitor.edit_query
            print "Query buffer\n"
            print "------------\n"
            print "#{@monitor.query_string}\n[continue]\n"
            prompt = false

          when /\A\\q/
            print "\n\nBye!\n\n"
            return

          when /\A\\p/
            print "\nQuery buffer\n"
            print  "------------\n"
            print  "#{@monitor.query_string}\n[continue]\n"
            print  "    -> "
            prompt = false

          when /\A\\./
            print "\n\nUnknown command.\n\n"
            print "\nmSQL > "
            prompt    = false
            new_query = true

          when /\A\'.*\'/         # literal
            if new_query
              @monitor.clear_query
              new_query = false
            end
            @monitor.add_query($& + ' ')

          when /\A\'[^\']*\Z/     # into literal
            is_in_string = true
            if new_query
              @monitor.clear_query
              new_query = false
            end
            @monitor.add_query $&

          when /\A[\w;,'\(\)=_<>\*]+/
            if new_query
              @monitor.clear_query
              new_query = false
            end
            prompt = true
            @monitor.add_query($&+' ')

          when /\A.*/
            raise InputError,"ERROR: illegal charactor \"#{$&}\"\n\n"
          end

        end
        line = $'
      end

    end

    print "\nBye!\n\n"
  end


  private
  def help
    print "\n\nMiniSQL Help!\n\n"
    print "The following commands are available :- \n\n"
    print "\t\\q Quit\n"
    print "\t\\g Go (Send query to database)\n"
    print "\t\\e Edit (Edit previous query)\n"
    print "\t\\p Print (Print the query buffer)\n"
  end

end



# print usage
def usage
  STDERR.print "\n\nUsage : msql [-f conf_file] [-h host] database\n\n"
end


# --- main process

# parse option
opts = getopts(nil, "h:", "f:")

# parse database name
database = ARGV.shift


host  = $OPT_h
conf  = $OPT_f


begin

  print "\n"

  # illegal option or not specified database
  raise OptionError unless opts and database


  # too many args...
  raise OptionError if ARGV.size != 0


  # when option -f exists, load specified config file
  if conf
    Msql::load_config_file(conf)
  end


  # create mSQL interactive monitor instance
  msqlm = MsqlMonitorView.new(host, database)

  # monitor run
  msqlm.run

rescue OptionError
  usage
end
