#
# Copyright (C) 2013 - 2023 Oracle and/or its affiliates. All rights reserved.
#

from pypgx._utils.error_handling import java_handler
from pypgx._utils.error_messages import UNHASHABLE_TYPE
from pypgx._utils import conversion
from datetime import date, datetime, time
from pypgx.api._pgql_result_set import PgqlResultSet
from pypgx.api._pgx_graph import PgxGraph
from typing import Sequence, NoReturn, TYPE_CHECKING

if TYPE_CHECKING:
    # Don't import at runtime, to avoid circular imports.
    from pypgx.api._pgx_session import PgxSession


class PreparedStatement:
    """An object that represents a precompiled PGQL statement.

    A PGQL statement is precompiled and stored in a PreparedStatement object.
    This object can then be used to efficiently execute this statement multiple times.
    """

    _java_class = 'oracle.pgx.api.PgxPreparedStatement'

    def __init__(self, java_prepared_statement, session: "PgxSession") -> None:
        self.java_prepared_statement = java_prepared_statement
        self._session = session

    def execute_query(self) -> PgqlResultSet:
        """Blocking version of execute_query_async().
        Calls execute_query_async() and waits for the returned results to complete.
        Throws PgqlException, If the caller thread gets interrupted while waiting
        for completion or if any exception occurred during asynchronous execution.
        The actual exception will be nested.

        :return: The pgql result set.
        :rtype: PgqlResultSet
        """
        java_pgql_result_set = java_handler(self.java_prepared_statement.executeQuery, [])
        java_graph = java_handler(java_pgql_result_set.getGraph, [])
        graph = PgxGraph(self._session, java_graph)

        return PgqlResultSet(graph, java_pgql_result_set)

    def execute_query_async(self) -> PgqlResultSet:
        """Execute the PGQL query in this PgxPreparedStatement object and returns the
        PgqlResultSet object generated by the query.

        :returns: A PgqlResultSet object that contains the data produced by the query; never null.
        :rtype: PgqlResultSet
        """
        java_pgql_result_set = java_handler(self.java_prepared_statement.executeQueryAsync, [])
        java_graph_future = java_handler(java_pgql_result_set.getGraph, [])
        java_graph = java_handler(java_graph_future.get, [])
        graph = PgxGraph(self._session, java_graph)

        return PgqlResultSet(graph, java_pgql_result_set)

    def execute(self) -> bool:
        """Execute the PGQL statement in this PreparedStatement object, which may be any kind of
        PGQL statement.
        Throws PgqlException, If a server-side error occurs or this method is called
        on a closed Statement.

        :returns: A boolean to indicate the form of the first result: true in case of a SELECT
            query, false otherwise. In case of a SELECT query you must call the method getResultSet
            to retrieve the result.
        :rtype: bool
        """
        return java_handler(self.java_prepared_statement.execute, [])

    def execute_async(self) -> bool:
        """Execute the PGQL statement in this PreparedStatement object, which may be any kind of
        PGQL statement.
        In case of a SELECT query you must call the method getResultSet to retrieve the result.
        Throws PgqlException, If a server-side error occurs or this method is called on a
        closed Statement.

        :returns: A boolean to indicate the form of the first result: true in case of a SELECT
            query, false otherwise.
        :rtype: bool
        """
        java_bool_future = java_handler(self.java_prepared_statement.executeAsync, [])
        return java_handler(java_bool_future.get, [])

    def get_result_set(self) -> PgqlResultSet:
        """Retrieve the current result as a PgqlResultSet object.
        This method should be called only once per result.
        Throws PgqlException, If a server-side error occurs or this method is called on a
        closed Statement

        :returns: Current result as a ResultSet object or null if the query is not a SELECT query.
        :rtype: PgqlResultSet
        """
        java_pgql_result_set = java_handler(self.java_prepared_statement.getResultSet, [])
        java_graph = java_handler(java_pgql_result_set.getGraph, [])
        graph = PgxGraph(self._session, java_graph)

        return PgqlResultSet(graph, java_pgql_result_set)

    def get_result_set_async(self) -> PgqlResultSet:
        """Retrieve the current result as a PgqlResultSet object.
        This method should be called only once per result.
        Throws PgqlException, If a server-side error occurs or this method is called on a
        closed Statement

        :returns: Current result as a ResultSet object or null if the query is not a SELECT query.
        :type: PgqlResultSet
        """
        java_pgql_result_set = java_handler(self.java_prepared_statement.getResultSetAsync, [])
        java_graph_future = java_handler(java_pgql_result_set.getGraph, [])
        java_graph = java_handler(java_graph_future.get, [])
        graph = PgxGraph(self._session, java_graph)

        return PgqlResultSet(graph, java_pgql_result_set)

    def __hash__(self) -> NoReturn:
        raise TypeError(UNHASHABLE_TYPE.format(type_name=self.__class__))

    def close(self) -> None:
        """Release this Statement's resources. Calling the method `close` on a Statement object that
        is already closed has no effect.
        Note: When a Statement is closed, its associated ResultSet objects are also closed.
        """
        java_handler(self.java_prepared_statement.close, [])

    def set_array(self, parameter_index: int, x: Sequence) -> None:
        """Set the designated parameter to the given array value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: Sequence
        """
        x = conversion.to_java_list(x)
        java_handler(self.java_prepared_statement.setArray, [parameter_index, x])

    def set_boolean(self, parameter_index: int, x: bool) -> None:
        """Set the designated parameter to the given boolean value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: bool
        """
        java_handler(self.java_prepared_statement.setBoolean, [parameter_index, x])

    def set_date(self, parameter_index: int, x: date) -> None:
        """Set the designated parameter to the given date value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: date
        """
        java_local_date = conversion.to_java_local_date(x)
        java_handler(self.java_prepared_statement.setDate, [parameter_index, java_local_date])

    def set_double(self, parameter_index: int, x: float) -> None:
        """Set the designated parameter to the given double value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: float
        """
        java_handler(self.java_prepared_statement.setDouble, [parameter_index, x])

    def set_int(self, parameter_index: int, x: int) -> None:
        """Set the designated parameter to the given int value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: int
        """
        java_handler(self.java_prepared_statement.setInt, [parameter_index, x])

    def set_long(self, parameter_index: int, x: int) -> None:
        """Set the designated parameter to the given long value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: int
        """
        java_handler(self.java_prepared_statement.setLong, [parameter_index, x])

    def set_string(self, parameter_index: int, x: str) -> None:
        """Set the designated parameter to the given string value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: str
        """
        java_handler(self.java_prepared_statement.setString, [parameter_index, x])

    def set_time(self, parameter_index: int, x: time) -> None:
        """Set the designated parameter to the given time value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: time
        """
        java_local_time = conversion.to_java_local_time(x)
        java_handler(self.java_prepared_statement.setTime, [parameter_index, java_local_time])

    def set_time_with_timezone(self, parameter_index: int, x: time) -> None:
        """Set the designated parameter to the given time with timezone value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: time
        """
        java_time = conversion.to_java_time_with_timezone(x)
        java_handler(self.java_prepared_statement.setTimeWithTimezone, [parameter_index, java_time])

    def set_timestamp(self, parameter_index: int, x: datetime) -> None:
        """Set the designated parameter to the given timestamp value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: datetime
        """
        java_timestamp = conversion.to_java_timestamp(x)
        java_handler(self.java_prepared_statement.setTimestamp, [parameter_index, java_timestamp])

    def set_timestamp_with_timezone(self, parameter_index: int, x: datetime) -> None:
        """Set the designated parameter to the given timestamp with timezone value.

        :param parameter_index: The first parameter is 1, the second is 2, ...
        :type parameter_index: int
        :param x: The parameter value.
        :type x: datetime
        """
        java_timestamp_with_tz = conversion.to_java_timestamp_with_timezone(x)
        java_handler(
            self.java_prepared_statement.setTimestampWithTimezone,
            [parameter_index, java_timestamp_with_tz],
        )
