require 'pg'
require 'yaml'

Dir[File.join(File.dirname(__FILE__), 'lib', '*.rb')].each do |file|
  require_relative File.join('lib', File.basename(file))
end

class UdfHarness

  def initialize(only_udf = nil)
    @udfs = [
        UdfJsonArrays::UDFS,
        UdfMysqlCompat::UDFS,
        UdfTimeHelpers::UDFS,
        UdfStringUtils::UDFS,
        UdfNumberUtils::UDFS,
        UdfStats::UDFS,
    ].flatten.select { |u| only_udf.nil? or u[:name] == only_udf.to_sym }
  end

  def create_udfs
    @udfs.each { |udf| create_udf(udf) }
  end

  def drop_udfs
    @udfs.each { |udf| drop_udf(udf) }
  end

  def test_udfs
    create_udfs
    @udfs.each { |udf| test_udf(udf) }
  end

  def print_udfs
    @udfs.each { |udf| puts make_create_query(udf) }
  end

  private

  # --------------------------- Running Queries --------------------

  def get_env_or_panic!(var)
    val = YAML.load(File.open(File.join(File.dirname(__FILE__), 'config.yaml')))[var]
    val ||= ENV[var].to_s.strip
    if val == ''
      puts "Error: Missing value for #{var}"
      exit
    end
    val
  end

  def connect!
    @conn ||= PG.connect({
                             host:     get_env_or_panic!('UDF_CLUSTER_HOST'),
                             port:     get_env_or_panic!('UDF_CLUSTER_PORT'),
                             dbname:   get_env_or_panic!('UDF_CLUSTER_DB_NAME'),
                             user:     get_env_or_panic!('UDF_CLUSTER_USER'),
                             password: get_env_or_panic!('UDF_CLUSTER_PASSWORD'),
                         })
  end

  def query(str, print_errors = false)
    result = {rows: nil, error: nil}
    begin
      connect!.exec(str) do |r|
        result[:rows] = r.values
      end
    rescue => ex
      puts ex.message if print_errors
      result[:error] = ex.message
    end
    result
  end

  # --------------------------- Query Generation ------------------------------

  def make_drop_query(udf)
    "drop #{udf[:type]} #{udf[:name]}(#{udf[:params]}) cascade"
  end

  def make_create_query(udf)
    if udf[:type] == :function
      make_header(udf) + make_function_create_query(udf)
    elsif udf[:type] == :aggregate
      make_header(udf) + make_aggregate_query(udf)
    end
  end

  def make_header(udf)
    examples = make_examples(udf)
    desc     = udf[:description]
    desc     = "Aggregate helper: #{desc}" if udf[:name].to_s.start_with?('agg_')
    %~
      /*
      #{udf[:name].to_s.upcase}
      #{desc}

      Examples:
      #{examples.map { |e| '  ' + e.sub('?', udf[:name].to_s) }.join("\n#{' ' * 6}")}
      */~
  end

  def make_function_create_query(udf)
    %~
      create or replace function #{udf[:name]} (#{udf[:params]})
        returns #{udf[:return_type]}
        stable as $$
          #{udf[:body].split("\n").map { |r| r.sub(/^\s{2}/, '') }.join("\n").strip}
        $$ language plpythonu;
    ~
  end

  def make_aggregate_query(aggregate_udf)
    %~
      create aggregate #{aggregate_udf[:name]} (#{aggregate_udf[:params]})
      (
        initfunc = #{aggregate_udf[:init_function]},
        aggfunc = #{aggregate_udf[:agg_function]},
        finalizefunc = #{aggregate_udf[:finalize_function]}
      );
    ~
  end

  def make_examples(udf)
    examples = []
    udf[:tests].each do |test|
      next unless test[:example]
      expect = test[:expect]
      expect = "'#{expect}'" if expect.class == String
      if test[:query].present?
        examples << "#{test[:query]} --> #{expect}"
      elsif test[:rows].present?
        examples << "[#{test[:rows].join(", ")}] --> #{expect}"
      end
    end
    examples
  end

  # --------------------------------- Creating UDFs ----------------------------

  def create_udf(udf)
    puts "Making #{udf[:type]} #{udf[:name]}"
    drop_udf(udf, true)
    result = query(make_create_query(udf))
    puts "Error: #{result[:error]}" if result[:error].present?
  end

  def drop_udf(udf, silent = false)
    puts "Dropping #{udf[:type]} #{udf[:name]}" unless silent
    query(make_drop_query(udf))
  end

  # --------------------------------- Testing UDFs ----------------------------

  def test_udf(udf)
    print "Testing #{udf[:type]} #{udf[:name]}"
    test_function(udf)
    print "\n"
  end

  def test_function(udf)
    udf[:tests].each do |test|
      print compare_test_results(test[:query].sub("?", udf[:name].to_s), test)
    end
  end

  def test_aggregate(aggregate_udf)
    aggregate_udf[:tests].each do |test|
      union_rows = test[:rows].map do |r|
        casted_row = r
        casted_row = "'#{r}'::varchar" if r.class == String
        casted_row = "'#{r}'::float" if r.class == Float
        "select #{casted_row} as _test_c\n"
      end
      query = %~
        with _test_t as (#{union_rows.join(' union all ')})
        select #{aggregate_udf[:name]}(_test_c) from _test_t
      ~
      print compare_test_results(query, test)
    end
  end

  def compare_test_results(q, test)
    result = query(q)
    if result[:error].present?
      return "\nError on #{test}: #{result[:error]}\n"
    elsif !test[:skip]
      result_val = result[:rows][0][0]
      result_val = result_val.to_i if test[:expect].class == Fixnum
      result_val = result_val.to_f if test[:expect].class == Float
      if result_val != test[:expect]
        return "\nMismatch on #{test}: #{result_val || 'null'} != #{test[:expect] || 'null'}\n"
      end
    end
    "."
  end
end
