#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#

import threading

import grpc

from ._exceptions import ImplError, RemoteError


# BaseRemote():
#
# Provides the basic functionality required to set up remote
# interaction via GRPC. In particular, this will set up a
# grpc.insecure_channel, or a grpc.secure_channel, based on the given
# spec.
#
# Customization for the particular protocol is expected to be
# performed in children.
#
class BaseRemote:
    key_name = None

    def __init__(self, spec):
        self.spec = spec
        self._initialized = False
        self._lock = threading.Lock()

    ####################################################
    #                 Dunder methods                   #
    ####################################################
    def __enter__(self):
        return self

    def __exit__(self, _exc_type, _exc_value, traceback):
        return False

    def __str__(self):
        if self.spec:
            return self.spec.url
        else:
            return "(default remote)"

    ####################################################
    #                   Remote API                     #
    ####################################################

    # init():
    #
    # Initialize the given remote. This function must be called before
    # any communication is performed, since such will otherwise fail.
    #
    def init(self):
        with self._lock:
            if self._initialized:
                return

            self._configure_protocols()
            self._initialized = True

    # check():
    #
    # Check if the remote is functional and has all the required
    # capabilities. This should be used somewhat like an assertion,
    # expecting a RemoteError.
    #
    # Note that this method runs the calls on a separate process, so
    # that we can use grpc calls even if we are on the main process.
    #
    # Raises:
    #     RemoteError: If the grpc call fails.
    #
    def check(self):
        try:
            self.init()
            self._check()
        except grpc.RpcError as e:
            # str(e) is too verbose for errors reported to the user
            raise RemoteError("{}: {}".format(e.code().name, e.details()))

    ####################################################
    #                Abstract methods                  #
    ####################################################

    # _check():
    #
    # Check if this remote provides everything required for the
    # particular kind of remote. This is expected to be called as part
    # of check(), and must be called in a non-main process.
    #
    # Raises:
    #    RemoteError: when the remote isn't compatible or another error happened.
    #
    def _check(self):
        pass

    # _configure_protocols():
    #
    # An abstract method to configure remote-specific protocols. This
    # is *not* done as super().init() because we want to be able to
    # set self._initialized *after* initialization completes in the
    # parent class.
    #
    # This method should *never* be called outside of init().
    #
    def _configure_protocols(self):
        raise ImplError("An implementation of a Remote must configure its protocols.")
