class Ridgepole::Delta
  SCRIPT_NAME = '<Schema>'

  def initialize(delta, options = {})
    @delta = delta
    @options = options
  end

  def migrate(options = {})
    if log_file = @options[:log_file]
      result = ActiveRecord::Migration.record_time do
        migrate0(options)
      end

      open(log_file, 'wb') {|f| f.puts JSON.pretty_generate(result) }
    else
      migrate0(options)
    end
  end

  def script
    buf = StringIO.new

    (@delta[:add] || {}).each do |table_name, attrs|
      append_create_table(table_name, attrs, buf)
    end

    (@delta[:rename] || {}).each do |table_name, attrs|
      append_rename_table(table_name, attrs, buf)
    end

    (@delta[:change] || {}).each do |table_name, attrs|
      append_change(table_name, attrs, buf)
    end

    (@delta[:delete] || {}).each do |table_name, attrs|
      append_drop_table(table_name, attrs, buf)
    end

    buf.string.strip
  end

  def differ?
    not script.empty?
  end

  private

  def migrate0(options = {})
    if options[:noop]
      disable_logging_orig = ActiveRecord::Migration.disable_logging

      begin
        ActiveRecord::Migration.disable_logging = true
        buf = StringIO.new

        callback = proc do |sql, name|
          buf.puts sql if sql =~ /\A(CREATE|ALTER)\b/i
        end

        Ridgepole::ExecuteExpander.without_operation(callback) do
          eval_script(script)
        end

        buf.string.strip
      ensure
        ActiveRecord::Migration.disable_logging = disable_logging_orig
      end
    else
      eval_script(script)
    end
  end

  def eval_script(script)
    begin
      ActiveRecord::Schema.new.instance_eval(script, SCRIPT_NAME, 1)
    rescue => e
      raise_exception(script, e)
    end
  end

  def raise_exception(script, org)
    lines = script.each_line
    digit_number = (lines.count + 1).to_s.length
    err_num = detect_error_line(org)

    errmsg = lines.with_index.map {|l, i|
      line_num = i + 1
      prefix = (line_num == err_num) ? '* ' : '  '
      "#{prefix}%*d: #{l}" % [digit_number, line_num]
    }

    if err_num > 0
      from = err_num - 6
      from = 0 if from < 0
      to = err_num + 4
      errmsg = errmsg.slice(from..to)
    end

    e = RuntimeError.new(org.message + "\n" + errmsg.join)
    e.set_backtrace(org.backtrace)
    raise e
  end

  def detect_error_line(e)
    rgx = /\A#{Regexp.escape(SCRIPT_NAME)}:(\d+):/
    line = e.backtrace.find {|i| i =~ rgx }

    if line and (m = rgx.match(line))
      m[1].to_i
    else
      0
    end
  end

  def append_create_table(table_name, attrs, buf)
    options = attrs[:options] || {}
    options[:options] ||= @options[:table_options] if @options[:table_options]
    definition = attrs[:definition] || {}
    indices = attrs[:indices] || {}

    buf.puts(<<-EOS)
create_table(#{table_name.inspect}, #{options.inspect}) do |t|
    EOS

    definition.each do |column_name, column_attrs|
      column_type = column_attrs.fetch(:type)
      column_options = column_attrs[:options] || {}

      buf.puts(<<-EOS)
  t.#{column_type}(#{column_name.inspect}, #{column_options.inspect})
      EOS
    end

    buf.puts(<<-EOS)
end
    EOS

    indices.each do |index_name, index_attrs|
      append_add_index(table_name, index_name, index_attrs, buf)
    end

    buf.puts
  end

  def append_rename_table(to_table_name, from_table_name, buf)
    buf.puts(<<-EOS)
rename_table(#{from_table_name.inspect}, #{to_table_name.inspect})
    EOS

    buf.puts
  end

  def append_drop_table(table_name, attrs, buf)
    buf.puts(<<-EOS)
drop_table(#{table_name.inspect})
    EOS

    buf.puts
  end

  def append_change(table_name, attrs, buf)
    append_change_definition(table_name, attrs[:definition] || {}, buf)
    append_change_indices(table_name, attrs[:indices] || {}, buf)
    buf.puts
  end

  def append_change_definition(table_name, delta, buf)
    (delta[:add] || {}).each do |column_name, attrs|
      append_add_column(table_name, column_name, attrs, buf)
    end

    (delta[:rename] || {}).each do |column_name, attrs|
      append_rename_column(table_name, column_name, attrs, buf)
    end

    (delta[:change] || {}).each do |column_name, attrs|
      append_change_column(table_name, column_name, attrs, buf)
    end

    (delta[:delete] || {}).each do |column_name, attrs|
      append_remove_column(table_name, column_name, attrs, buf)
    end
  end

  def append_add_column(table_name, column_name, attrs, buf)
    type = attrs.fetch(:type)
    options = attrs[:options] || {}

    buf.puts(<<-EOS)
add_column(#{table_name.inspect}, #{column_name.inspect}, #{type.inspect}, #{options.inspect})
    EOS
  end

  def append_rename_column(table_name, to_column_name, from_column_name, buf)
    buf.puts(<<-EOS)
rename_column(#{table_name.inspect}, #{from_column_name.inspect}, #{to_column_name.inspect})
    EOS
  end

  def append_change_column(table_name, column_name, attrs, buf)
    type = attrs.fetch(:type)
    options = attrs[:options] || {}

    buf.puts(<<-EOS)
change_column(#{table_name.inspect}, #{column_name.inspect}, #{type.inspect}, #{options.inspect})
    EOS
  end

  def append_remove_column(table_name, column_name, attrs, buf)
    buf.puts(<<-EOS)
remove_column(#{table_name.inspect}, #{column_name.inspect})
    EOS
  end

  def append_change_indices(table_name, delta, buf)
    (delta[:delete] || {}).each do |index_name, attrs|
      append_remove_index(table_name, index_name, attrs, buf)
    end

    (delta[:add] || {}).each do |index_name, attrs|
      append_add_index(table_name, index_name, attrs, buf)
    end
  end

  def append_add_index(table_name, index_name, attrs, buf)
    column_name = attrs.fetch(:column_name)
    options = attrs[:options] || {}

    buf.puts(<<-EOS)
add_index(#{table_name.inspect}, #{column_name.inspect}, #{options.inspect})
    EOS
  end

  def append_remove_index(table_name, index_name, attrs, buf)
    column_name = attrs.fetch(:column_name)
    options = attrs[:options] || {}
    target = options[:name] ? {:name => options[:name]} : column_name

    buf.puts(<<-EOS)
remove_index(#{table_name.inspect}, #{target.inspect})
    EOS
  end
end