# -*- coding: utf-8 -*-

from ..support.fluent import Fluent


class Blueprint(object):
    def __init__(self, table):
        """
        :param table: The table to operate on
        :type table: str
        """
        self._table = table
        self._columns = []
        self._commands = []
        self.engine = None
        self.charset = None
        self.collation = None

    def build(self, connection, grammar):
        """
        Execute the blueprint against the database.

        :param connection: The connection to use
        :type connection: orator.connections.Connection

        :param grammar: The grammar to user
        :type grammar: orator.query.grammars.QueryGrammar
        """
        for statement in self.to_sql(connection, grammar):
            connection.statement(statement)

    def to_sql(self, connection, grammar):
        """
        Get the raw SQL statements for the blueprint.

        :param connection: The connection to use
        :type connection: orator.connections.Connection

        :param grammar: The grammar to user
        :type grammar: orator.schema.grammars.SchemaGrammar

        :rtype: list
        """
        self._add_implied_commands()

        statements = []

        for command in self._commands:
            method = "compile_%s" % command.name

            if hasattr(grammar, method):
                sql = getattr(grammar, method)(self, command, connection)
                if sql is not None:
                    if isinstance(sql, list):
                        statements += sql
                    else:
                        statements.append(sql)

        return statements

    def _add_implied_commands(self):
        """
        Add the commands that are implied by the blueprint.
        """
        if len(self.get_added_columns()) and not self._creating():
            self._commands.insert(0, self._create_command("add"))

        if len(self.get_changed_columns()) and not self._creating():
            self._commands.insert(0, self._create_command("change"))

        return self._add_fluent_indexes()

    def _add_fluent_indexes(self):
        """
        Add the index commands fluently specified on columns:
        """
        for column in self._columns:
            for index in ["primary", "unique", "index"]:
                column_index = column.get(index)

                if column_index is True:
                    getattr(self, index)(column.name)

                    break
                elif column_index:
                    getattr(self, index)(column.name, column_index)

                    break

    def _creating(self):
        """
        Determine if the blueprint has a create command.

        :rtype: bool
        """
        for command in self._commands:
            if command.name == "create":
                return True

        return False

    def create(self):
        """
        Indicates that the table needs to be created.

        :rtype: Fluent
        """
        return self._add_command("create")

    def drop(self):
        """
        Indicates that the table needs to be dropped.

        :rtype: Fluent
        """
        self._add_command("drop")

        return self

    def drop_if_exists(self):
        """
        Indicates that the table should be dropped if it exists.

        :rtype: Fluent
        """
        return self._add_command("drop_if_exists")

    def drop_column(self, *columns):
        """
        Indicates that the given columns should be dropped.

        :param columns: The columns to drop
        :type columns: tuple

        :rtype: Fluent
        """
        columns = list(columns)

        return self._add_command("drop_column", columns=columns)

    def rename_column(self, from_, to):
        """
        Indicates that the given columns should be renamed.

        :param from_: The original column name
        :type from_: str
        :param to: The new name of the column
        :type to: str

        :rtype: Fluent
        """
        return self._add_command("rename_column", **{"from_": from_, "to": to})

    def drop_primary(self, index=None):
        """
        Indicate that the given primary key should be dropped.

        :param index: The index
        :type index: str

        :rtype: dict
        """
        return self._drop_index_command("drop_primary", "primary", index)

    def drop_unique(self, index):
        """
        Indicate that the given unique key should be dropped.

        :param index: The index
        :type index: str

        :rtype: Fluent
        """
        return self._drop_index_command("drop_unique", "unique", index)

    def drop_index(self, index):
        """
        Indicate that the given index should be dropped.

        :param index: The index
        :type index: str

        :rtype: Fluent
        """
        return self._drop_index_command("drop_index", "index", index)

    def drop_foreign(self, index):
        """
        Indicate that the given foreign key should be dropped.

        :param index: The index
        :type index: str

        :rtype: dict
        """
        return self._drop_index_command("drop_foreign", "foreign", index)

    def drop_timestamps(self):
        """
        Indicate that the timestamp columns should be dropped.

        :rtype: Fluent
        """
        return self.drop_column("created_at", "updated_at")

    def drop_soft_deletes(self):
        """
        Indicate that the soft delete column should be dropped

        :rtype: Fluent
        """
        return self.drop_column("deleted_at")

    def rename(self, to):
        """
        Rename the table to a given name

        :param to: The new table name
        :type to: str

        :rtype: Fluent
        """
        return self._add_command("rename", to=to)

    def primary(self, columns, name=None):
        """
        Specify the primary key(s) for the table

        :param columns: The primary key(s) columns
        :type columns: str or list

        :param name: The name of the primary key
        :type name: str

        :rtype: Fluent
        """
        return self._index_command("primary", columns, name)

    def unique(self, columns, name=None):
        """
        Specify a unique index on the table

        :param columns: The primary key(s) columns
        :type columns: str or list

        :param name: The name of the primary key
        :type name: str

        :rtype: Fluent
        """
        return self._index_command("unique", columns, name)

    def index(self, columns, name=None):
        """
        Specify an index on the table

        :param columns: The primary key(s) columns
        :type columns: str or list

        :param name: The name of the primary key
        :type name: str

        :rtype: Fluent
        """
        return self._index_command("index", columns, name)

    def foreign(self, columns, name=None):
        """
        Specify an foreign key on the table

        :param columns: The foreign key(s) columns
        :type columns: str or list

        :param name: The name of the foreign key
        :type name: str

        :rtype: Fluent
        """
        return self._index_command("foreign", columns, name)

    def increments(self, column):
        """
        Create a new auto-incrementing integer column on the table.

        :param column: The auto-incrementing column
        :type column: str

        :rtype: Fluent
        """
        return self.unsigned_integer(column, True)

    def big_increments(self, column):
        """
        Create a new auto-incrementing big integer column on the table.

        :param column: The auto-incrementing column
        :type column: str

        :rtype: Fluent
        """
        return self.unsigned_big_integer(column, True)

    def char(self, column, length=255):
        """
        Create a new char column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("char", column, length=length)

    def string(self, column, length=255):
        """
        Create a new string column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("string", column, length=length)

    def text(self, column):
        """
        Create a new text column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("text", column)

    def medium_text(self, column):
        """
        Create a new medium text column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("medium_text", column)

    def long_text(self, column):
        """
        Create a new long text column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("long_text", column)

    def integer(self, column, auto_increment=False, unsigned=False):
        """
        Create a new integer column on the table.

        :param column: The column
        :type column: str

        :type auto_increment: bool

        :type unsigned: bool

        :rtype: Fluent
        """
        return self._add_column(
            "integer", column, auto_increment=auto_increment, unsigned=unsigned
        )

    def big_integer(self, column, auto_increment=False, unsigned=False):
        """
        Create a new big integer column on the table.

        :param column: The column
        :type column: str

        :type auto_increment: bool

        :type unsigned: bool

        :rtype: Fluent
        """
        return self._add_column(
            "big_integer", column, auto_increment=auto_increment, unsigned=unsigned
        )

    def medium_integer(self, column, auto_increment=False, unsigned=False):
        """
        Create a new medium integer column on the table.

        :param column: The column
        :type column: str

        :type auto_increment: bool

        :type unsigned: bool

        :rtype: Fluent
        """
        return self._add_column(
            "medium_integer", column, auto_increment=auto_increment, unsigned=unsigned
        )

    def tiny_integer(self, column, auto_increment=False, unsigned=False):
        """
        Create a new tiny integer column on the table.

        :param column: The column
        :type column: str

        :type auto_increment: bool

        :type unsigned: bool

        :rtype: Fluent
        """
        return self._add_column(
            "tiny_integer", column, auto_increment=auto_increment, unsigned=unsigned
        )

    def small_integer(self, column, auto_increment=False, unsigned=False):
        """
        Create a new small integer column on the table.

        :param column: The column
        :type column: str

        :type auto_increment: bool

        :type unsigned: bool

        :rtype: Fluent
        """
        return self._add_column(
            "small_integer", column, auto_increment=auto_increment, unsigned=unsigned
        )

    def unsigned_integer(self, column, auto_increment=False):
        """
        Create a new unsigned integer column on the table.

        :param column: The column
        :type column: str

        :type auto_increment: bool

        :rtype: Fluent
        """
        return self.integer(column, auto_increment, True)

    def unsigned_big_integer(self, column, auto_increment=False):
        """
        Create a new unsigned big integer column on the table.

        :param column: The column
        :type column: str

        :type auto_increment: bool

        :rtype: Fluent
        """
        return self.big_integer(column, auto_increment, True)

    def float(self, column, total=8, places=2):
        """
        Create a new float column on the table.

        :param column: The column
        :type column: str

        :type total: int

        :type places: 2

        :rtype: Fluent
        """
        return self._add_column("float", column, total=total, places=places)

    def double(self, column, total=None, places=None):
        """
        Create a new double column on the table.

        :param column: The column
        :type column: str

        :type total: int

        :type places: 2

        :rtype: Fluent
        """
        return self._add_column("double", column, total=total, places=places)

    def decimal(self, column, total=8, places=2):
        """
        Create a new decimal column on the table.

        :param column: The column
        :type column: str

        :type total: int

        :type places: 2

        :rtype: Fluent
        """
        return self._add_column("decimal", column, total=total, places=places)

    def boolean(self, column):
        """
        Create a new decimal column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("boolean", column)

    def enum(self, column, allowed):
        """
        Create a new enum column on the table.
        
        :param column: The column
        :type column: str
        
        :type allowed: list
        
        :rtype: Fluent
        """
        return self._add_column("enum", column, allowed=allowed)

    def json(self, column):
        """
        Create a new json column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("json", column)

    def date(self, column):
        """
        Create a new date column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("date", column)

    def datetime(self, column, with_time_zone=False):
        """
        Create a new datetime column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("datetime", column, with_time_zone=with_time_zone)

    def time(self, column, with_time_zone=False):
        """
        Create a new time column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("time", column, with_time_zone=with_time_zone)

    def timestamp(self, column, with_time_zone=False):
        """
        Create a new timestamp column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("timestamp", column, with_time_zone=with_time_zone)

    def nullable_timestamps(self):
        """
        Create nullable creation and update timestamps to the table.

        :rtype: Fluent
        """
        self.timestamp("created_at").nullable()
        self.timestamp("updated_at").nullable()

    def timestamps(self, use_current=True, with_time_zone=False):
        """
        Create creation and update timestamps to the table.

        :rtype: Fluent
        """
        if use_current:
            self.timestamp("created_at", with_time_zone=with_time_zone).use_current()
            self.timestamp("updated_at", with_time_zone=with_time_zone).use_current()
        else:
            self.timestamp("created_at", with_time_zone=with_time_zone)
            self.timestamp("updated_at", with_time_zone=with_time_zone)

    def soft_deletes(self, with_time_zone=False):
        """
        Add a "deleted at" timestamp to the table.

        :rtype: Fluent
        """
        return self.timestamp("deleted_at", with_time_zone=with_time_zone).nullable()

    def binary(self, column):
        """
        Create a new binary column on the table.

        :param column: The column
        :type column: str

        :rtype: Fluent
        """
        return self._add_column("binary", column)

    def morphs(self, name, index_name=None):
        """
        Add the proper columns for a polymorphic table.

        :type name: str

        :type index_name: str
        """
        self.unsigned_integer("%s_id" % name)
        self.string("%s_type" % name)
        self.index(["%s_id" % name, "%s_type" % name], index_name)

    def _drop_index_command(self, command, type, index):
        """
        Create a new drop index command on the blueprint.

        :param command: The command
        :type command: str

        :param type: The index type
        :type type: str

        :param index: The index name
        :type index: str

        :rtype: Fluent
        """
        columns = []

        if isinstance(index, list):
            columns = index

            index = self._create_index_name(type, columns)

        return self._index_command(command, columns, index)

    def _index_command(self, type, columns, index):
        """
        Add a new index command to the blueprint.

        :param type: The index type
        :type type: str

        :param columns: The index columns
        :type columns: list or str

        :param index: The index name
        :type index: str

        :rtype: Fluent
        """
        if not isinstance(columns, list):
            columns = [columns]

        if not index:
            index = self._create_index_name(type, columns)

        return self._add_command(type, index=index, columns=columns)

    def _create_index_name(self, type, columns):
        if not isinstance(columns, list):
            columns = [columns]

        index = "%s_%s_%s" % (
            self._table,
            "_".join([str(column) for column in columns]),
            type,
        )

        return index.lower().replace("-", "_").replace(".", "_")

    def _add_column(self, type, name, **parameters):
        """
        Add a new column to the blueprint.

        :param type: The column type
        :type type: str

        :param name: The column name
        :type name: str

        :param parameters: The column parameters
        :type parameters: dict

        :rtype: Fluent
        """
        parameters.update({"type": type, "name": name})

        column = Fluent(**parameters)
        self._columns.append(column)

        return column

    def _remove_column(self, name):
        """
        Removes a column from the blueprint.

        :param name: The column name
        :type name: str

        :rtype: Blueprint
        """
        self._columns = filter(lambda c: c.name != name, self._columns)

        return self

    def _add_command(self, name, **parameters):
        """
        Add a new command to the blueprint.

        :param name: The command name
        :type name: str

        :param parameters: The command parameters
        :type parameters: dict

        :rtype: Fluent
        """
        command = self._create_command(name, **parameters)
        self._commands.append(command)

        return command

    def _create_command(self, name, **parameters):
        """
        Create a new command.

        :param name: The command name
        :type name: str

        :param parameters: The command parameters
        :type parameters: dict

        :rtype: Fluent
        """
        parameters.update({"name": name})

        return Fluent(**parameters)

    def get_table(self):
        return self._table

    def get_columns(self):
        return self._columns

    def get_commands(self):
        return self._commands

    def get_added_columns(self):
        return list(filter(lambda column: not column.get("change"), self._columns))

    def get_changed_columns(self):
        return list(filter(lambda column: column.get("change"), self._columns))
