import enum
import re
from abc import ABC
from typing import List, Optional, Union

from attr import Factory, attrs

from .utils import quote


class StringEnum(enum.Enum):
    def __str__(self):
        return self.name


class OnConflict(StringEnum):
    ROLLBACK = enum.auto()
    ABORT = enum.auto()
    FAIL = enum.auto()
    IGNORE = enum.auto()
    REPLACE = enum.auto()

    def __str__(self):
        return self.name


class OnDelete(StringEnum):
    SET_NULL = enum.auto()
    SET_DEFAULT = enum.auto()
    CASCADE = enum.auto()
    RESTRICT = enum.auto()
    NO_ACTION = enum.auto()

    def __str__(self):
        if self == OnDelete.SET_NULL:
            return "SET NULL"
        elif self == OnDelete.SET_DEFAULT:
            return "SET DEFAULT"
        elif self == OnDelete.NO_ACTION:
            return "NO ACTION"
        else:
            return super().__str__()


OnUpdate = OnDelete


class ForeignKeyMatch(StringEnum):
    SIMPLE = enum.auto()
    FULL = enum.auto()
    PARTIAL = enum.auto()


class CollatingSequence(StringEnum):
    BINARY = enum.auto()
    NOCASE = enum.auto()
    RTRIM = enum.auto()


class GeneratedColumnStorage(StringEnum):
    VIRTUAL = enum.auto()
    STORED = enum.auto()


class DefaultValue(StringEnum):
    CURRENT_TIME = enum.auto()
    CURRENT_TIMESTAMP = enum.auto()
    CURRENT_DATE = enum.auto()


class Node(ABC):
    def accept(self, visitor):
        """
        Accept a visitor implementing the visitor pattern.

        For example::

            class RenameVisitor:
                def __init__(self, old_name, new_name):
                    self.old_name = old_name
                    self.new_name = new_name

                def rename(self, node):
                    return node.accept(self)

                def visit_column(self, node):
                    if node.name == self.old_name:
                        return Column(self.new_name, node.definition)
                    else:
                        return node

                def visit_default(self, node):
                    return node

        The implementation uses introspection to avoid having to define ``accept``
        methods on every ``Node`` subclass and every ``visit_XYZ`` method on the visitor
        class.
        """
        name = snake_case(self.__class__.__name__)
        visit_method = getattr(visitor, "visit_" + name, None)
        if visit_method is not None:
            return visit_method(self)
        else:
            default_visit_method = getattr(visitor, "visit_default", None)
            if default_visit_method is not None:
                return default_visit_method(self)
            else:
                return None

    def as_string(self, *, p: bool) -> str:
        raise NotImplementedError

    def __str__(self) -> str:
        return self.as_string(p=True)


class BaseConstraint(Node):
    pass


class Expression(Node):
    pass


@attrs(auto_attribs=True)
class CreateTableStatement(Node):
    """
    A SQL ``CREATE TABLE`` statement.
    """

    name: Union[str, "TableName"]
    columns: List["Column"]
    constraints: List[BaseConstraint] = Factory(list)
    as_select: Optional[Node] = None
    temporary: bool = False
    without_rowid: bool = False
    if_not_exists: bool = False

    def as_string(self, *, p: bool) -> str:
        builder = ["CREATE "]
        if self.temporary:
            builder.append("TEMPORARY ")
        builder.append("TABLE ")
        if self.if_not_exists:
            builder.append("IF NOT EXISTS ")

        if isinstance(self.name, TableName):
            builder.append(str(self.name))
        else:
            builder.append(quote(self.name))

        if self.as_select:
            builder.append(" AS ")
            builder.append(str(self.as_select))
            return "".join(builder)

        builder.append("(\n")
        for i, column in enumerate(self.columns):
            builder.append(str(column))
            if i < len(self.columns) - 1 or self.constraints:
                builder.append(",")
            builder.append("\n")

        for i, constraint in enumerate(self.constraints):
            builder.append(str(constraint))
            if i < len(self.constraints) - 1:
                builder.append(",")
            builder.append("\n")

        builder.append(")")
        if self.without_rowid:
            builder.append(" WITHOUT ROWID")

        return "".join(builder)


@attrs(auto_attribs=True)
class SelectStatement(Node):
    """
    A SQL ``SELECT`` statement.
    """

    columns: List[Expression]

    def as_string(self, *, p: bool) -> str:
        raise NotImplementedError


@attrs(auto_attribs=True)
class Column(Node):
    name: str
    definition: Optional["ColumnDefinition"]

    def as_string(self, *, p: bool) -> str:
        if self.definition is None:
            return quote(self.name)
        else:
            definition = self.definition.as_string(p=False)
            return f"{quote(self.name)} {definition}"


@attrs(auto_attribs=True)
class ColumnType(Node):
    name: str
    args: List[int]

    def as_string(self, *, p: bool) -> str:
        joined_args = ", ".join(map(str, self.args))
        return f"{quote(self.name)}({joined_args})"


@attrs(auto_attribs=True)
class ColumnDefinition(Node):
    type: Optional[Union[ColumnType, str]] = None
    default: Optional[Expression] = None
    constraints: List[BaseConstraint] = Factory(list)

    def as_string(self, *, p: bool) -> str:
        builder = []
        if self.type is not None:
            builder.append(" ")
            builder.append(str(self.type))

        for constraint in self.constraints:
            builder.append(" ")
            builder.append(str(constraint))

        if self.default is not None:
            builder.append(" DEFAULT ")
            builder.append(str(self.default))

        return "".join(builder)


@attrs(auto_attribs=True)
class CheckConstraint(BaseConstraint):
    expr: Expression

    def as_string(self, *, p: bool) -> str:
        e = self.expr.as_string(p=False)
        return f"CHECK({e})"


@attrs(auto_attribs=True)
class NamedConstraint(BaseConstraint):
    name: str
    constraint: BaseConstraint

    def as_string(self, *, p: bool) -> str:
        return f"CONSTRAINT {quote(self.name)} {self.constraint}"


@attrs(auto_attribs=True)
class NotNullConstraint(BaseConstraint):
    on_conflict: Optional[OnConflict] = None

    def as_string(self, *, p: bool) -> str:
        if self.on_conflict is not None:
            return f"NOT NULL {self.on_conflict}"
        else:
            return "NOT NULL"


@attrs(auto_attribs=True)
class PrimaryKeyConstraint(BaseConstraint):
    ascending: Optional[bool] = None
    on_conflict: Optional[OnConflict] = None
    autoincrement: bool = False

    def as_string(self, *, p: bool) -> str:
        builder = ["PRIMARY KEY"]
        if self.ascending is not None:
            if self.ascending:
                builder.append(" ASC")
            else:
                builder.append(" DESC")

        if self.on_conflict is not None:
            builder.append(" ")
            builder.append(str(self.on_conflict))

        if self.autoincrement:
            builder.append(" AUTOINCREMENT")

        return "".join(builder)


@attrs(auto_attribs=True)
class CollateConstraint(BaseConstraint):
    sequence: CollatingSequence

    def as_string(self, *, p: bool) -> str:
        return f"COLLATE {self.sequence}"


@attrs(auto_attribs=True)
class ForeignKeyConstraint(BaseConstraint):
    columns: List[str]
    foreign_table: str
    foreign_columns: List[str]
    on_delete: Optional[OnDelete] = None
    on_update: Optional[OnUpdate] = None
    match: Optional[ForeignKeyMatch] = None
    deferrable: Optional[bool] = None
    initially_deferred: Optional[bool] = None

    def as_string(self, *, p: bool) -> str:
        builder = []
        if self.columns:
            builder.append("FOREIGN KEY (")
            builder.append(", ".join(map(str, self.columns)))
            builder.append(" ")

        builder.append("REFERENCES ")
        builder.append(self.foreign_table)
        if self.foreign_columns:
            builder.append("(")
            builder.append(", ".join(map(str, self.foreign_columns)))
            builder.append(")")

        if self.on_delete:
            builder.append(f" ON DELETE {self.on_delete}")

        if self.on_update:
            builder.append(f" ON UPDATE {self.on_update}")

        if self.match:
            builder.append(f" MATCH {self.match}")

        if self.deferrable is not None:
            builder.append(" ")
            if not self.deferrable:
                builder.append("NOT")
            builder.append(" DEFERRABLE")

            if self.initially_deferred is not None:
                builder.append(" INITIALLY ")
                builder.append("DEFERRED" if self.initially_deferred else "IMMEDIATE")

        return "".join(builder)


@attrs(auto_attribs=True)
class UniqueConstraint(BaseConstraint):
    on_conflict: Optional[OnConflict] = None

    def as_string(self, *, p: bool) -> str:
        if self.on_conflict is not None:
            return f"UNIQUE {self.on_conflict}"
        else:
            return "UNIQUE"


@attrs(auto_attribs=True)
class GeneratedColumnConstraint(BaseConstraint):
    expression: Expression
    storage: Optional[GeneratedColumnStorage] = None

    def as_string(self, *, p: bool) -> str:
        e = self.expression.as_string(p=False)
        storage_string = "" if self.storage is None else " " + str(self.storage)
        return f"GENERATED ALWAYS AS ({e}){storage_string}"


@attrs(auto_attribs=True)
class Infix(Expression):
    operator: str
    left: Expression
    right: Expression

    def as_string(self, *, p: bool) -> str:
        left = self.left.as_string(p=True)
        right = self.right.as_string(p=True)
        core = f"{left} {self.operator} {right}"
        return f"({core})" if p else core


@attrs(auto_attribs=True)
class ExpressionList(Expression):
    values: List[Expression]

    def as_string(self, *, p: bool) -> str:
        return "(" + ", ".join(v.as_string(p=False) for v in self.values) + ")"


@attrs(auto_attribs=True)
class Identifier(Expression):
    value: str

    def as_string(self, *, p: bool) -> str:
        return quote(self.value)


@attrs(auto_attribs=True)
class String(Expression):
    value: str

    def as_string(self, *, p: bool) -> str:
        escaped = self.value.replace("'", "''")
        return f"'{escaped}'"


@attrs(auto_attribs=True)
class Blob(Expression):
    value: bytes

    def as_string(self, *, p: bool) -> str:
        return "X'" + "".join(f"{hex(b)[2:]:0>2}" for b in self.value) + "'"


@attrs(auto_attribs=True)
class Integer(Expression):
    value: int

    def as_string(self, *, p: bool) -> str:
        return str(self.value)


@attrs(auto_attribs=True)
class Null(Expression):
    def as_string(self, *, p: bool) -> str:
        return "NULL"


@attrs(auto_attribs=True)
class Boolean(Expression):
    value: bool

    def as_string(self, *, p: bool) -> str:
        return "TRUE" if self.value else "FALSE"


@attrs(auto_attribs=True)
class TableName(Node):
    schema_name: str
    table_name: str

    def as_string(self, *, p: bool) -> str:
        return f"{quote(self.schema_name)}.{quote(self.table_name)}"


def snake_case(s: str) -> str:
    def snake_case_replacer(match: re.Match) -> str:
        text = match.group(0)
        return text[0] + "_" + text[1]

    name = re.sub(r"[a-z][A-Z]", snake_case_replacer, s)
    return name.lower()
