# Copyright 2016 The Johns Hopkins University Applied Physics Laboratory
#
# 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 six
from abc import ABCMeta
from six.moves import configparser
import os

CONFIG_FILE ='~/.intern/intern.cfg'


@six.add_metaclass(ABCMeta)
class Remote(object):
    """Base class for communicating with remote data stores.

    Attributes:
        _volume (intern.service.Service): Class that communicates with the volume service.
        _metadata (intern.service.Service): Class that communicates with the metadata service.
        _project (intern.service.Service): Class that communicates with the project service.
        _object (intern.service.Service): Class that communicates with the object service.
    """

    def __init__(self, cfg_file_or_dict=None):
        """Constructor.

        Loads credentials in order from user provided dictionary > user provided file > default file > environment vars

        Args:
            cfg_file_or_dict (optional[string|dict]): Path to config file in INI format or a dict of config parameters.
        """

        # Service Objects
        self._volume = None
        self._metadata = None
        self._project = None
        self._object = None

        # Configuration data loaded from file or passed directly to the constructor
        # Is available for children Remote classes to use as needed
        self._config = None

        # Tokens for Services
        self._token_project = None
        self._token_metadata = None
        self._token_volume = None
        self._token_object = None

        if cfg_file_or_dict is None:
            # Default to the config file in the user directory if no config file was provided
            cfg_file_or_dict = os.path.expanduser(CONFIG_FILE)

        if isinstance(cfg_file_or_dict, dict) is True:
            # A config dictionary was provided directly. Keep things consistent by creating an INI string.
            cfg_str = "[Default]\n"
            for key in cfg_file_or_dict:
                cfg_str = "{}{} = {}\n".format(cfg_str, key, cfg_file_or_dict[key])
            self._config = self.load_config_file(six.StringIO(cfg_str))

        else:
            # A file path was provided by the user
            if os.path.isfile(os.path.expanduser(cfg_file_or_dict)):
                with open(os.path.expanduser(cfg_file_or_dict), 'r') as cfg_file_handle:
                    self._config = self.load_config_file(cfg_file_handle)
            else:
                # Provided file or default file do not exist.  Try loading from env variables
                if "INTERN_PROTOCOL" in os.environ and "INTERN_HOST" in os.environ and "INTERN_TOKEN" in os.environ:
                    cfg_str = "[Default]\n"
                    cfg_str = "{}{} = {}\n".format(cfg_str, "protocol", os.environ['INTERN_PROTOCOL'])
                    cfg_str = "{}{} = {}\n".format(cfg_str, "host", os.environ['INTERN_HOST'])
                    cfg_str = "{}{} = {}\n".format(cfg_str, "token", os.environ['INTERN_TOKEN'])

                    self._config = self.load_config_file(six.StringIO(cfg_str))
                else:
                    raise IOError("Configuration file not found: {}. Please provide credential file or set environment variables".format(cfg_file_or_dict))

    def load_config_file(self, config_handle):
        """Load config data for the Remote.

        Args:
            config_handle (io.StringIO): Config data encoded in a string.

        Returns:
            (configparser.ConfigParser)
        """
        cfg_parser = configparser.ConfigParser()
        cfg_parser.readfp(config_handle)
        return cfg_parser

    @property
    def volume_service(self):
        return self._volume

    @property
    def project_service(self):
        return self._project

    @property
    def metadata_service(self):
        return self._metadata

    @property
    def object_service(self):
        return self._object

    def list_project(self, **kwargs):
        """Perform list operation on the project.

        What this does is highly dependent on project's data model.

        Args:
            (**kwargs): Args are implementation dependent.

        Returns:
            (list)
        """
        return self._project.list(**kwargs)

    def get_cutout(self, resource, resolution, x_range, y_range, z_range, time_range=None, id_list=[], **kwargs):
        """Get a cutout from the volume service.

        Args:
            resource (intern.resource.Resource): Resource compatible with cutout operations.
            resolution (int): 0 indicates native resolution.
            x_range (list[int]): x range such as [10, 20] which means x>=10 and x<20.
            y_range (list[int]): y range such as [10, 20] which means y>=10 and y<20.
            z_range (list[int]): z range such as [10, 20] which means z>=10 and z<20.
            time_range (optional [list[int]]): time range such as [30, 40] which means t>=30 and t<40.
            id_list (optional [list]): list of object ids to filter the cutout by.

        Returns:
            (): Return type depends on volume service's implementation.

        Raises:
            RuntimeError when given invalid resource.
            Other exceptions may be raised depending on the volume service's implementation.
        """

        if not resource.valid_volume():
            raise RuntimeError('Resource incompatible with the volume service.')
        return self._volume.get_cutout(
            resource, resolution, x_range, y_range, z_range, time_range, id_list, **kwargs)

    def create_cutout(self, resource, resolution, x_range, y_range, z_range, data, time_range=None):
        """Upload a cutout to the volume service.

        Args:
            resource (intern.resource.Resource): Resource compatible with cutout operations.
            resolution (int): 0 indicates native resolution.
            x_range (list[int]): x range such as [10, 20] which means x>=10 and x<20.
            y_range (list[int]): y range such as [10, 20] which means y>=10 and y<20.
            z_range (list[int]): z range such as [10, 20] which means z>=10 and z<20.
            data (object): Type depends on implementation.
            time_range (optional [list[int]]): time range such as [30, 40] which means t>=30 and t<40.

        Returns:
            (): Return type depends on volume service's implementation.

        Raises:
            RuntimeError when given invalid resource.
            Other exceptions may be raised depending on the volume service's implementation.
        """
        if not resource.valid_volume():
            raise RuntimeError('Resource incompatible with the volume service.')
        return self._volume.create_cutout(
            resource, resolution, x_range, y_range, z_range, data, time_range)

    def reserve_ids(self, resource, num_ids):
        """Reserve a block of unique, sequential ids for annotations.

        Args:
            resource (intern.resource.Resource): Resource compatible with annotation operations.
            num_ids (int): Number of ids to reserve.

        Returns:
            (int): First id reserved.

        """
        if not resource.valid_volume():
            raise RuntimeError('Resource incompatible with the volume service.')
        return self._volume.reserve_ids(resource, num_ids)

    def get_bounding_box(self, resource, resolution, id, bb_type='loose'):
        """Get bounding box containing object specified by id.

        Currently only supports 'loose' bounding boxes.  The bounding box
        returned is cuboid aligned.

        Args:
            resource (intern.resource.Resource): Resource compatible with annotation operations.
            resolution (int): 0 indicates native resolution.
            id (int): Id of object of interest.
            bb_type (optional[string]): Defaults to 'loose'.

        Returns:
            (dict): {'x_range': [0, 10], 'y_range': [0, 10], 'z_range': [0, 10], 't_range': [0, 10]}
        """
        if not resource.valid_volume():
            raise RuntimeError('Resource incompatible with the volume service.')

        if bb_type != 'loose' and bb_type != 'tight':
            raise RuntimeError("bb_type must be either 'loose' or 'tight'.")

        return self._volume.get_bounding_box(resource, resolution, id, bb_type)

    def get_ids_in_region(
            self, resource, resolution,
            x_range, y_range, z_range, time_range=[0, 1]):
        """Get all ids in the region defined by x_range, y_range, z_range.

        Args:
            resource (intern.resource.Resource): Resource compatible with annotation operations.
            resolution (int): 0 indicates native resolution.
            x_range (list[int]): x range such as [10, 20] which means x>=10 and x<20.
            y_range (list[int]): y range such as [10, 20] which means y>=10 and y<20.
            z_range (list[int]): z range such as [10, 20] which means z>=10 and z<20.
            time_range (optional [list[int]]): time range such as [30, 40] which means t>=30 and t<40.  Defaults to [0, 1].

        Returns:
            (list[int]): Example: [1, 2, 25].

        Raises:
            requests.HTTPError
            TypeError: if resource is not an annotation channel.
        """
        return self._volume.get_ids_in_region(
            resource, resolution, x_range, y_range, z_range, time_range)
