require 'rubygems'
require 'yaml'
require 'active_record'


module YamlDb
	def self.dump(filename)
		disable_logger
		YamlDb::Dump.dump(File.new(filename, "w"))
		reenable_logger
	end

	def self.load(filename)
		disable_logger
		YamlDb::Load.load(File.new(filename, "r"))
		reenable_logger
	end

	def self.disable_logger
		@@old_logger = ActiveRecord::Base.logger
		ActiveRecord::Base.logger = nil
	end

	def self.reenable_logger
		ActiveRecord::Base.logger = @@old_logger
	end
end


module YamlDb::Utils
	def self.chunk_records(records)
		yaml = [ records ].to_yaml
		yaml.sub!("--- \n", "")
		yaml.sub!('- - -', '  - -')
		yaml
	end

	def self.unhash(hash, keys)
		keys.map { |key| hash[key] }
	end

	def self.unhash_records(records, keys)
		records.each_with_index do |record, index|
			records[index] = unhash(record, keys)	
		end
		
		records
	end

	def self.convert_booleans(records, columns)
		records.each do |record|
			columns.each do |column|
				next if is_boolean(record[column])
				record[column] = (record[column] == 't' or record[column] == '1')
			end
		end
		records
	end

	def self.boolean_columns(table)
		columns = ActiveRecord::Base.connection.columns(table).reject { |c| c.type != :boolean }
		columns.map { |c| c.name }
	end

	def self.is_boolean(value)
		value.kind_of?(TrueClass) or value.kind_of?(FalseClass)
	end

	def self.quote_table(table)
		ActiveRecord::Base.connection.quote_table_name(table)
	end
end


module YamlDb::Dump
	def self.dump(io)
		tables.each do |table|
			dump_table(io, table)
		end
	end

	def self.tables
		ActiveRecord::Base.connection.tables.reject { |table| ['schema_info', 'schema_migrations'].include?(table) }
	end

	def self.dump_table(io, table)
		return if table_record_count(table).zero?

		dump_table_columns(io, table)
		dump_table_records(io, table)
	end

	def self.dump_table_columns(io, table)
		io.write("\n")
		io.write({ table => { 'columns' => table_column_names(table) } }.to_yaml)
	end

	def self.dump_table_records(io, table)
		table_record_header(io)	
	
		column_names = table_column_names(table)

		each_table_page(table) do |records|
			rows = YamlDb::Utils.unhash_records(records, column_names)
			io.write(YamlDb::Utils.chunk_records(records))
		end
	end

	def self.table_record_header(io)
		io.write("  records: \n")
	end

	def self.table_column_names(table)
		ActiveRecord::Base.connection.columns(table).map { |c| c.name }
	end

	def self.each_table_page(table, records_per_page=1000)
		total_count = table_record_count(table)
		pages = (total_count.to_f / records_per_page).ceil - 1
		id = table_column_names(table).first
		boolean_columns = YamlDb::Utils.boolean_columns(table)
		quoted_table_name = YamlDb::Utils.quote_table(table)
		
		(0..pages).to_a.each do |page|
			sql = ActiveRecord::Base.connection.add_limit_offset!("SELECT * FROM #{quoted_table_name} ORDER BY #{id}",
				:limit => records_per_page, :offset => records_per_page * page
			)
			records = ActiveRecord::Base.connection.select_all(sql)
			records = YamlDb::Utils.convert_booleans(records, boolean_columns)
			yield records
		end
	end

	def self.table_record_count(table)
		ActiveRecord::Base.connection.select_one("SELECT COUNT(*) FROM #{YamlDb::Utils.quote_table(table)}").values.first.to_i
	end
end


module YamlDb::Load
	def self.load(io)
		ActiveRecord::Base.connection.transaction do
			YAML.load_documents(io) do |ydoc|
				ydoc.keys.each do |table_name|
					next if ydoc[table_name].nil?
					load_table(table_name, ydoc[table_name])
				end
			end
		end
	end

	def self.truncate_table(table)
		begin
			ActiveRecord::Base.connection.execute("TRUNCATE #{YamlDb::Utils.quote_table(table)}")
		rescue Exception
			ActiveRecord::Base.connection.execute("DELETE FROM #{YamlDb::Utils.quote_table(table)}")
		end
	end

	def self.load_table(table, data)
		column_names = data['columns']
		truncate_table(table)
		load_records(table, column_names, data['records'])
		reset_pk_sequence!(table)
	end

	def self.load_records(table, column_names, records)
		quoted_column_names = column_names.map { |column| ActiveRecord::Base.connection.quote_column_name(column) }.join(',')
		quoted_table_name = YamlDb::Utils.quote_table(table)
		records.each do |record|
			ActiveRecord::Base.connection.execute("INSERT INTO #{quoted_table_name} (#{quoted_column_names}) VALUES (#{record.map { |r| ActiveRecord::Base.connection.quote(r) }.join(',')})")
		end
	end

	def self.reset_pk_sequence!(table_name)
		if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
			ActiveRecord::Base.connection.reset_pk_sequence!(table_name)
		end
	end
end
