from __future__ import unicode_literals

from collections import OrderedDict as od
from functools import partial

from .statements import Statements
from .util import differences

THINGS = [
    "schemas",
    "enums",
    "sequences",
    "constraints",
    "functions",
    "views",
    "indexes",
    "extensions",
    "privileges",
    "collations",
    "rlspolicies",
    "triggers",
]
PK = "PRIMARY KEY"


def statements_for_changes(
    things_from,
    things_target,
    creations_only=False,
    drops_only=False,
    modifications=True,
    dependency_ordering=False,
    add_dependents_for_modified=False,
):
    added, removed, modified, unmodified = differences(things_from, things_target)

    return statements_from_differences(
        added=added,
        removed=removed,
        modified=modified,
        replaceable=None,
        creations_only=creations_only,
        drops_only=drops_only,
        modifications=modifications,
        dependency_ordering=dependency_ordering,
        old=things_from,
    )


def statements_from_differences(
    added,
    removed,
    modified,
    replaceable=None,
    creations_only=False,
    drops_only=False,
    modifications=True,
    dependency_ordering=False,
    old=None,
):
    replaceable = replaceable or set()
    statements = Statements()
    if not creations_only:
        pending_drops = set(removed)
        if modifications:
            pending_drops |= set(modified) - replaceable
    else:
        pending_drops = set()
    if not drops_only:
        pending_creations = set(added)
        if modifications:
            pending_creations |= set(modified)
    else:
        pending_creations = set()

    def has_remaining_dependents(v, pending_drops):
        if not dependency_ordering:
            return False

        return bool(set(v.dependents) & pending_drops)

    def has_uncreated_dependencies(v, pending_creations):
        if not dependency_ordering:
            return False

        return bool(set(v.dependent_on) & pending_creations)

    while True:
        before = pending_drops | pending_creations
        if not creations_only:
            for k, v in removed.items():
                if not has_remaining_dependents(v, pending_drops):
                    if k in pending_drops:
                        statements.append(old[k].drop_statement)
                        pending_drops.remove(k)
        if not drops_only:
            for k, v in added.items():
                if not has_uncreated_dependencies(v, pending_creations):
                    if k in pending_creations:
                        statements.append(v.create_statement)
                        pending_creations.remove(k)
        if modifications:
            for k, v in modified.items():
                if not creations_only:
                    if not has_remaining_dependents(v, pending_drops):
                        if k in pending_drops:
                            statements.append(old[k].drop_statement)
                            pending_drops.remove(k)
                if not drops_only:
                    if not has_uncreated_dependencies(v, pending_creations):
                        if k in pending_creations:
                            statements.append(v.create_statement)
                            pending_creations.remove(k)
        after = pending_drops | pending_creations
        if not after:
            break

        elif (
            after == before
        ):  # this should never happen because there shouldn't be circular dependencies
            raise ValueError("cannot resolve dependencies")  # pragma: no cover

    return statements


def get_enum_modifications(tables_from, tables_target, enums_from, enums_target):
    _, _, e_modified, _ = differences(enums_from, enums_target)
    _, _, t_modified, _ = differences(tables_from, tables_target)
    pre = Statements()
    recreate = Statements()
    post = Statements()
    enums_to_change = e_modified
    for t, v in t_modified.items():
        t_before = tables_from[t]
        _, _, c_modified, _ = differences(t_before.columns, v.columns)
        for k, c in c_modified.items():
            before = t_before.columns[k]
            if (
                c.is_enum == before.is_enum
                and c.dbtypestr == before.dbtypestr
                and c.enum != before.enum
            ):
                pre.append(before.change_enum_to_string_statement(t))
                post.append(before.change_string_to_enum_statement(t))
    for e in enums_to_change.values():
        recreate.append(e.drop_statement)
        recreate.append(e.create_statement)
    return pre + recreate + post


def get_table_changes(tables_from, tables_target, enums_from, enums_target):
    added, removed, modified, _ = differences(tables_from, tables_target)

    statements = Statements()
    for t, v in removed.items():
        statements.append(v.drop_statement)
    for t, v in added.items():
        statements.append(v.create_statement)
    statements += get_enum_modifications(
        tables_from, tables_target, enums_from, enums_target
    )

    for t, v in modified.items():
        before = tables_from[t]

        # drop/recreate tables which have changed from partitioned to non-partitioned
        if v.is_partitioned != before.is_partitioned:
            statements.append(v.drop_statement)
            statements.append(v.create_statement)
            continue

        # attach/detach tables with changed parent tables
        if v.parent_table != before.parent_table:
            statements += v.attach_detach_statements(before)

    for t, v in modified.items():
        before = tables_from[t]

        if not v.is_alterable:
            continue

        c_added, c_removed, c_modified, _ = differences(before.columns, v.columns)
        for k, c in c_removed.items():
            alter = v.alter_table_statement(c.drop_column_clause)
            statements.append(alter)
        for k, c in c_added.items():
            alter = v.alter_table_statement(c.add_column_clause)
            statements.append(alter)
        for k, c in c_modified.items():
            statements += c.alter_table_statements(before.columns[k], t)

        if v.rowsecurity != before.rowsecurity:
            rls_alter = v.alter_rls_statement
            statements += [rls_alter]
    return statements


def get_selectable_changes(
    selectables_from,
    selectables_target,
    enums_from,
    enums_target,
    add_dependents_for_modified=True,
):
    tables_from = od((k, v) for k, v in selectables_from.items() if v.is_table)
    tables_target = od((k, v) for k, v in selectables_target.items() if v.is_table)

    other_from = od((k, v) for k, v in selectables_from.items() if not v.is_table)
    other_target = od((k, v) for k, v in selectables_target.items() if not v.is_table)

    added_tables, removed_tables, modified_tables, unmodified_tables = differences(
        tables_from, tables_target
    )
    added_other, removed_other, modified_other, unmodified_other = differences(
        other_from, other_target
    )

    changed_all = {}
    changed_all.update(modified_tables)
    changed_all.update(modified_other)
    modified_all = dict(changed_all)
    changed_all.update(removed_tables)
    changed_all.update(removed_other)

    replaceable = set()
    not_replaceable = set()

    if add_dependents_for_modified:
        for k, m in changed_all.items():
            old = selectables_from[k]

            if k in modified_all and m.can_replace(old):
                if not m.is_table:
                    replaceable.add(k)
                continue

            for d in m.dependents_all:
                if d in unmodified_other:
                    dd = unmodified_other.pop(d)
                    modified_other[d] = dd
                not_replaceable.add(d)
        modified_other = od(sorted(modified_other.items()))

    replaceable -= not_replaceable
    statements = Statements()

    def functions(d):
        return {k: v for k, v in d.items() if v.relationtype == "f"}

    statements += statements_from_differences(
        added_other,
        removed_other,
        modified_other,
        replaceable=replaceable,
        drops_only=True,
        dependency_ordering=True,
        old=selectables_from,
    )

    statements += get_table_changes(
        tables_from, tables_target, enums_from, enums_target
    )

    if any([functions(added_other), functions(modified_other)]):
        statements += ["set check_function_bodies = off;"]

    statements += statements_from_differences(
        added_other,
        removed_other,
        modified_other,
        replaceable=replaceable,
        creations_only=True,
        dependency_ordering=True,
        old=selectables_from,
    )
    return statements


class Changes(object):
    def __init__(self, i_from, i_target):
        self.i_from = i_from
        self.i_target = i_target

    def __getattr__(self, name):
        if name == "non_pk_constraints":
            a = self.i_from.constraints.items()
            b = self.i_target.constraints.items()
            a_od = od((k, v) for k, v in a if v.constraint_type != PK)
            b_od = od((k, v) for k, v in b if v.constraint_type != PK)
            return partial(statements_for_changes, a_od, b_od)

        elif name == "pk_constraints":
            a = self.i_from.constraints.items()
            b = self.i_target.constraints.items()
            a_od = od((k, v) for k, v in a if v.constraint_type == PK)
            b_od = od((k, v) for k, v in b if v.constraint_type == PK)
            return partial(statements_for_changes, a_od, b_od)

        elif name == "selectables":
            return partial(
                get_selectable_changes,
                od(sorted(self.i_from.selectables.items())),
                od(sorted(self.i_target.selectables.items())),
                self.i_from.enums,
                self.i_target.enums,
            )

        elif name in THINGS:
            return partial(
                statements_for_changes,
                getattr(self.i_from, name),
                getattr(self.i_target, name),
            )

        else:
            raise AttributeError(name)
