module TreasureData
  class FileReader

    class MessagePackParsingReader
      def initialize(io, error, opts)
        require 'msgpack'
        @io = io
        @error = error
        @u = MessagePack::Unpacker.new(@io)
      end

      def next
        @u.next
      end
    end

    class LineReader
      def initialize(io, error, opts)
        if encoding = opts[:encoding]
          io.external_encoding = encoding
        end
        #@delimiter = opts[:line_delimiter_expr] || /\r?\n/
        @io = io
        @error = error
      end

      def next_row
        @io.readline($/).chomp
      end
    end

    class DelimiterParser
      def initialize(reader, error, opts)
        @reader = reader
        @delimiter_expr = opts[:delimiter_expr]
        @null_expr = opts[:null_expr]
        # TODO
        #@escape_char = opts[:escape_char]
        #@quote_char = opts[:quote_char]
      end

      def next
        row = @reader.next_row
        array = row.split(@delimiter_expr)
        array.map! {|x|
          @null_expr =~ x ? nil : x
        }
      end
    end

    class JSONParser
      def initialize(reader, error, opts)
        @reader = reader
        @error = error
      end

      def next
        while true
          line = @reader.next_row
          begin
            return JSON.parse(line)
          rescue
            @error.call("invalid json format: #{$!}", line)
            next
          end
        end
      end
    end

    #class ApacheParser
    #  REGEXP = /^([^ ]*) [^ ]* ([^ ]*) \[([^\]]*)\] "(\S+)(?: +([^ ]*) +\S*)?" ([^ ]*) ([^ ]*)(?: "([^\"]*)" "([^\"]*)")?$/
    #
    #  def initialize(reader, error, opts)
    #    @reader = reader
    #  end
    #
    #  def next
    #    while true
    #      m = REGEXP.match(@reader.next_row)
    #      if m
    #        h = {
    #          'host' => m[1],
    #          'user' => m[2],
    #          'time' => m[3],
    #          'method' => m[4],
    #          'path' => m[5],
    #          'code' => m[6],
    #          'size' => m[7].to_i,
    #          'referer' => m[8],
    #          'agent' => m[9],
    #        }
    #        return h
    #      end
    #    end
    #  end
    #end

    class AutoTypeConvertParserFilter
      def initialize(parser, error)
        @parser = parser
      end

      def next
        array = @parser.next
        array.map! {|s|
          # nil.to_i == 0 != nil.to_s
          i = s.to_i
          i.to_s == s ? i : s
        }
      end
    end

    class HashBuilder
      def initialize(parser, error, columns)
        @parser = parser
        @columns = columns
      end

      def next
        array = @parser.next
        # FIXME error handling
        Hash[@columns.zip(array)]
      end
    end

    class TimeParserFilter
      def initialize(parser, error, opts)
        require 'time'
        @parser = parser
        @error = error
        @time_column = opts[:time_column]
        unless @time_column
          raise '-t, --time-column NAME option is required'
        end
        @time_format = opts[:time_format]
      end

      def next
        while true
          row = @parser.next
          tval = row[@time_column]

          unless tval
            @error.call("time column '#{@time_column}' is missing", row)
            next
          end

          begin
            if tf = @time_format
              row['time'] = parse_time(tval, tf).to_i
            elsif tval.is_a?(Integer)
                row['time'] = tval
            else
              row['time'] = Time.parse(tval).to_i
            end
            return row

          rescue
            @error.call("invalid time format '#{tval}': #{$!}", row)
            next
          end
        end
      end

      if Time.respond_to?(:strptime)
        def parse_time(value, format)
          Time.strptime(value, format)
        end
      else
        def parse_time(value, format)
          Time.parse(DateTime.strptime(value, format).to_s)
        end
      end
    end

    def initialize
      @format = "text"
      @default_opts = {
        :delimiter_expr => /\t|,/,
        #:line_delimiter_expr => /\r?\n/,
        :null_expr => /\A(?:\\N|\-|)\z/,
        #:quote_char => "\"",
      }
      @opts = {}
      @parser_class = nil
    end

    attr_reader :default_opts, :opts
    attr_accessor :parser_class

    def init_optparse(op)
      op.on('-f', '--format NAME', "source file format") {|s|
        set_format_template(s)
      }
      op.on('-h', '--columns NAME,NAME,...', 'column names') {|s|
        @opts[:column_names] = s.split(',')
      }
      op.on('-H', '--column-header', 'first line includes column names', TrueClass) {|b|
        @opts[:column_header] = b
      }
      op.on('-d', '--delimiter REGEX', "delimiter between columns (default: #{@default_opts[:delimiter_expr].inspect[1..-2]}") {|s|
        @opts[:delimiter_expr] = Regexp.new(s)
      }
      #op.on('-D', '--line-delimiter REGEX', "delimiter between rows (default: #{@default_opts[:line_delimiter_expr].inspect[1..-2]})") {|s|
      #  @opts[:line_delimiter_expr] = Regexp.new(s)
      #}
      op.on('-N', '--null REGEX', "null expression (default: #{@default_opts[:null_expr].inspect[1..-2]}") {|s|
        @opts[:null_expr] = Regexp.new(s)
      }
      # TODO
      #op.on('-E', '--escape CHAR', "escape character (default: no escape character)") {|s|
      #  @opts[:escape_char] = s
      #}
      #op.on('-Q', '--quote CHAR', "quote character (default: #{@default_opts[:quote_char]}") {|s|
      #  @opts[:quote_char] = s
      #}
      op.on('-S', '--all-string', 'disable automatic type conversion', TrueClass) {|b|
        @opts[:all_string] = b
      }
      op.on('-t', '--time-column NAME', 'name of the time column (default: auto detect)') {|s|
        @opts[:time_column] = s
      }
      op.on('-T', '--time-format FORMAT', 'strftime(3) format of the time column') {|s|
        @opts[:time_format] = s
      }
      op.on('-e', '--encoding NAME', "text encoding") {|s|
        @opts[:encoding] = s
      }
      op.on('-C', '--compress NAME', 'compression format name [plain, gzip] (default: auto detect)') {|s|
        @opts[:compress] = s
      }
    end

    def set_format_template(name)
      case name
      when 'csv'
        @format = 'text'
        @opts[:delimiter_expr] = /,/
      when 'tsv'
        @format = 'text'
        @opts[:delimiter_expr] = /\t/
      #when 'apache'
      #  @format = 'apache'
      #  @opts[:column_names] = ['host', 'user', 'time', 'method', 'path', 'code', 'size', 'referer', 'agent']
      #  @opts[:null_expr] = /\A(?:\-|)\z/
      #  @opts[:time_column] = 'time'
      #  @opts[:time_format] = '%d/%b/%Y:%H:%M:%S %z'
      when 'msgpack'
        @format = 'msgpack'
      when 'json'
        @format = 'json'
      else
        raise "Unknown format: #{name}"
      end
    end

    def compose_factory
      opts = @default_opts.merge(@opts)
      case @format
      when 'text'
        Proc.new {|io,error|
          reader = LineReader.new(io, error, opts)
          parser = DelimiterParser.new(reader, error, opts)
          if opts[:column_header]
            column_names = parser.next
          elsif opts[:column_names]
            column_names = opts[:column_names]
          else
            raise "--column-header or --columns option is required"
          end
          unless opts[:all_string]
            parser = AutoTypeConvertParserFilter.new(parser, error)
          end
          parser = HashBuilder.new(parser, error, column_names)
          parser = TimeParserFilter.new(parser, error, opts)
        }

      #when 'apache'

      when 'json'
        Proc.new {|io,error|
          reader = LineReader.new(io, error, opts)
          parser = JSONParser.new(reader, error, opts)
          if opts[:column_header]
            column_names = parser.next
          elsif opts[:column_names]
            column_names = opts[:column_names]
          end
          if column_names
            parser = HashBuilder.new(parser, error, column_names)
          end
          parser = TimeParserFilter.new(parser, error, opts)
        }

      when 'msgpack'
        Proc.new {|io,error|
          parser = MessagePackParsingReader.new(io, error, opts)
          if opts[:column_header]
            column_names = parser.next
          elsif opts[:column_names]
            column_names = opts[:column_names]
          end
          if column_names
            parser = HashBuilder.new(parser, error, column_names)
          end
          parser = TimeParserFilter.new(parser, error, opts)
        }
      end
    end

    def parse(io, error, &block)
      factory = compose_factory
      parser = factory.call(io, error)
      begin
        while record = parser.next
          block.call(record)
        end
      rescue EOFError
      end
    end

  end
end