lib/south/migrations.ex

defmodule Xmeta.South.Migrations do
  @data_path "./priv/repo/migrations.exs"
  @migrate_path "./priv/repo/migrations"

  def get_data_path() do
    conf = Application.get_env(:ecto_south, :path)
    if conf, do: conf[:data_path] || @data_path, else: @data_path
  end

  def get_migrate_path() do
    conf = Application.get_env(:ecto_south, :path)
    if conf, do: conf[:migrate_path] || @migrate_path, else: @migrate_path
  end

  def mods(), do: find_xmd_modules()
  def old_data() do
    try do
      Code.eval_file(get_data_path())
      Xmeta.South.Migrations.Data.show()
    catch
      _, _ -> false
    end
  end

  def can_check?() do
      migration_count = length(File.ls!(@migrate_path))
      migration_count = if migration_count > 0, do: migration_count-1, else: 0
      repo_status = Xmeta.South.Ecto.migrations_status([]) |> List.first
      if migration_count > repo_status.count_up do
        false
      else
        true
      end
  end

  def run() do
    if can_check?() do
      old = old_data()
      new = meta_mod_all(mods())
      if length(new) == 0 do
        Mix.shell.info "Not search any ecto schema \n"
      end
      unless old do
        diff(new, %{})
        init_migrations_file()
      else
        if old == new do
          Mix.shell.info "No changes detected \n"
        else
          Mix.shell.info "Has changes, checking....\n"
          diff(new, old)
          # init_migrations_file()
        end
      end
    else
      file_name = find_last_migrations_file()
      Mix.shell.info("need to migrate your new migration file: #{file_name}")
    end
  end

  def meta_mod(mod) do
    mod_types = Enum.reduce(
      mod.__schema__(:fields),
      %{},
      fn (f, acc) ->
        Map.put(acc, f, mod.__schema__(:type, f))
      end
    )
    types = Enum.reduce(
      mod.__schema__(:associations),
      mod_types,
      fn (ref, acc) ->
        ass = mod.__schema__(:association, ref)
        if ass do
          type = if ass.cardinality != :many do
            String.to_atom "ref__#{ass.related.__schema__(:source)}"
          else
            String.to_atom "m2m__#{ass.related.__schema__(:source)}__#{ass.join_through}"
          end
          Map.put acc, ass.field, type
        else
          acc
        end
      end
    )
    %{
      types: types,
      primary_key: mod.__schema__(:primary_key)
    }
  end

  def meta_mod_all(mods) do
    Enum.reduce(
      mods,
      [],
      fn (m, acc) ->
        acc ++ [{String.to_atom(m.__schema__(:source)), meta_mod(m)}]
      end
    )
  end

  def diff(new, old) do
    changes = for {k, v} <- new do
      unless old[k] == v do
        unless old[k], do: create_table(k, v), else: check_field(k, v, old[k])
      end
    end
    changes_drop = for {k, _} <- old do
      unless new[k], do: drop_table(k)
    end
    change_data = Enum.filter(changes ++ changes_drop, fn x -> x != nil end)
    show_change(change_data)

    content = Xmeta.South.Migrate.Template.get(change_data)
    file_name = "/#{string_time_now()}_south.exs"
    File.write(get_migrate_path() <> file_name, content)
  end

  def string_time_now() do
    d = DateTime.utc_now
    month = if d.month < 10, do: "0#{d.month}", else: d.month
    day = if d.day < 10, do: "0#{d.day}", else: d.day
    hour = if d.hour < 10, do: "0#{d.hour}", else: d.hour
    minute = if d.minute < 10, do: "0#{d.minute}", else: d.minute
    second = if d.second < 10, do: "0#{d.second}", else: d.second
    "#{d.year}#{month}#{day}#{hour}#{minute}#{second}"
  end

  # change = %{action: atom, table: atom, schema: [%{action: atom, filed: atom, type: atom }]}
  def create_table(table, data) do
    change_field = for {k, v} <- Enum.to_list(data[:types]) do
      %{action: :add, filed: k, type: v}
    end
    %{table: table, action: :create, schema: change_field}
  end

  def drop_table(table), do: %{table: table, action: :drop}

  def check_field(table, new_data, old_data) do
    new = new_data[:types]
    old = old_data[:types]
    change_add = Enum.reduce(
      Enum.to_list(new),
      [],
      fn ({f, t}, acc) ->
        unless old[f], do: acc ++ [%{action: :add, filed: f, type: t}], else: acc
      end
    )
    change_delete = Enum.reduce(
      Enum.to_list(old),
      [],
      fn ({f, _}, acc) ->
        unless new[f], do: acc ++ [%{action: :remove, filed: f}], else: acc
      end
    )
    %{table: table, action: :alter, schema: change_add ++ change_delete}
  end

  def show_change(data) do
    for i <- data do
      Mix.shell.info "table: #{i.table}, action: #{i.action}"
      if i[:schema] do
        for s <- i.schema do
          if s[:type] do
            Mix.shell.info "    #{s.action}: #{s.filed}, :#{s.type}"
          else
            Mix.shell.info "    #{s.action}: #{s.filed}"
          end
        end
      end
      IO.puts ""
    end
  end

  def init_migrations_file() do
    content = mods()
              |> meta_mod_all()
              |> Xmeta.South.Migrations.Template.get
    File.write(get_data_path(), content)
  end

  def find_last_migrations_file() do
    migration_num = Enum.reduce(File.ls!(@migrate_path), 0, fn(file_name,num) ->
        if String.contains?(file_name, "_south") do
          new_num = file_name |> String.split("_") |> List.first |> String.to_integer
          if new_num > num, do: new_num, else: num
        else
          num
        end
      end)
    "#{migration_num}_south.exs"
  end

  def find_xmd_modules() do
      {_, keys} = Mix.Project.config()[:app] |> :application.get_all_key()
      modules = keys[:modules]
      Enum.reduce modules, [], fn(mod,acc) ->  
        module_name = to_string(mod)
        if String.contains?(module_name, "Xmd") do
          acc ++ [mod]
        else
          acc
        end
      end
  end
end