# Allen Institute Software License - This software license is the 2-clause BSD
# license plus a third clause that prohibits redistribution for commercial
# purposes without further permission.
#
# Copyright 2015-2017. Allen Institute. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Redistributions for commercial purposes are not permitted without the
# Allen Institute's written permission.
# For purposes of this license, commercial purposes is the incorporation of the
# Allen Institute's software into anything for which you will charge fees or
# other compensation. Contact terms@alleninstitute.org for commercial licensing
# opportunities.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
from allensdk.config.manifest_builder import ManifestBuilder
from allensdk.api.cache import Cache
from allensdk.api.queries.reference_space_api import ReferenceSpaceApi
from allensdk.api.queries.ontologies_api import OntologiesApi
from allensdk.deprecated import deprecated

from .ontology import Ontology
from .structure_tree import StructureTree
from .reference_space import ReferenceSpace


class ReferenceSpaceCache(Cache):

    REFERENCE_SPACE_VERSION_KEY = 'REFERENCE_SPACE_VERSION'
    ANNOTATION_KEY = 'ANNOTATION'
    TEMPLATE_KEY = 'TEMPLATE'
    STRUCTURES_KEY = 'STRUCTURES'
    STRUCTURE_TREE_KEY = 'STRUCTURE_TREE'
    STRUCTURE_MASK_KEY = 'STRUCTURE_MASK'
    STRUCTURE_MESH_KEY = 'STRUCTURE_MESH'

    MANIFEST_VERSION = 1.2

    def __init__(self, 
                 resolution, 
                 reference_space_key,
                 **kwargs):

        if not 'version' in kwargs:
            kwargs['version'] = self.MANIFEST_VERSION

        if not 'base_uri' in kwargs:
            kwargs['base_uri'] = None

        super(ReferenceSpaceCache, self).__init__(**kwargs)

        self.resolution = resolution
        self.reference_space_key = reference_space_key        
        
        self.api = ReferenceSpaceApi(base_uri=kwargs['base_uri'])

        
    def get_annotation_volume(self, file_name=None):
        """
        Read the annotation volume.  Download it first if it doesn't exist.

        Parameters
        ----------

        file_name: string
            File name to store the annotation volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(
            file_name, self.ANNOTATION_KEY, self.reference_space_key, self.resolution)

        annotation, info = self.api.download_annotation_volume(
            self.reference_space_key,
            self.resolution,
            file_name, 
            strategy='lazy')

        return annotation, info


    def get_template_volume(self, file_name=None):
        """
        Read the template volume.  Download it first if it doesn't exist.

        Parameters
        ----------

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(
            file_name, self.TEMPLATE_KEY, self.resolution)

        template, info = self.api.download_template_volume(self.resolution, 
                                                           file_name, 
                                                           strategy='lazy')

        return template, info


    def get_structure_tree(self, file_name=None, structure_graph_id=1):
        """
        Read the list of adult mouse structures and return an StructureTree 
        instance.

        Parameters
        ----------

        file_name: string
            File name to save/read the structures table.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.
        structure_graph_id: int
            Build a tree using structure only from the identified structure graph.
        """
        
        file_name = self.get_cache_path(file_name, self.STRUCTURE_TREE_KEY)

        return OntologiesApi(self.api.api_url).get_structures_with_sets(
            strategy='lazy',
            path=file_name,
            pre=StructureTree.clean_structures,
            post=lambda x: StructureTree(StructureTree.clean_structures(x)), 
            structure_graph_ids=structure_graph_id,
            **Cache.cache_json())


    def get_reference_space(self, structure_file_name=None, 
                            annotation_file_name=None):
        """
        Build a ReferenceSpace from this cache's annotation volume and 
        structure tree. The ReferenceSpace does operations that relate brain 
        structures to spatial domains.
        
        Parameters
        ----------
        
        structure_file_name: string
            File name to save/read the structures table.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.
            
        annotation_file_name: string
            File name to store the annotation volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.
        
        """
        
        return ReferenceSpace(self.get_structure_tree(structure_file_name), 
                              self.get_annotation_volume(annotation_file_name)[0], 
                              [self.resolution] * 3)

    def get_structure_mask(self, structure_id, file_name=None, annotation_file_name=None):
        """
        Read a 3D numpy array shaped like the annotation volume that has non-zero values where
        voxels belong to a particular structure.  This will take care of identifying substructures.

        Notes
        -----
        This method downloads structure masks from the Allen Institute. To make your own locally, see 
        ReferenceSpace.many_structure_masks.
        
        Parameters
        ----------

        structure_id: int
            ID of a structure.

        file_name: string
            File name to store the structure mask.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        annotation_file_name: string
            File name to store the annotation volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.
        """
        structure_id = ReferenceSpaceCache.validate_structure_id(structure_id)

        file_name = self.get_cache_path(
            file_name, self.STRUCTURE_MASK_KEY, self.reference_space_key, 
            self.resolution, structure_id)

        return self.api.download_structure_mask(structure_id, 
                                                self.reference_space_key,
                                                self.resolution,
                                                file_name, 
                                                strategy='lazy')


    def get_structure_mesh(self, structure_id, file_name=None):
        """Obtain a 3D mesh specifying the surface of an annotated structure.
    
        Parameters
        -----------
        structure_id: int
            ID of a structure.
        file_name: string
            File name to store the structure mesh.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        Returns
        -------
        vertices : np.ndarray
            Dimensions are (nSamples, nCoordinates=3). Locations in the reference space
            of vertices
        vertex_normals : np.ndarray
            Dimensions are (nSample, nElements=3). Vectors normal to vertices.
        face_vertices : np.ndarray
            Dimensions are (sample, nVertices=3). References are given in indices 
            (0-indexed here, but 1-indexed in the file) of vertices that make up each face.
        face_normals : np.ndarray
            Dimensions are (sample, nNormals=3). References are given in indices 
            (0-indexed here, but 1-indexed in the file) of vertex normals that make up each face.

        Notes
        -----
        These meshes are meant for 3D visualization and as such have been smoothed. 
        If you are interested in performing quantative analyses, we recommend that you 
        use the structure masks instead.

        """
        structure_id = ReferenceSpaceCache.validate_structure_id(structure_id)

        file_name = self.get_cache_path(
            file_name, self.STRUCTURE_MESH_KEY, self.reference_space_key, structure_id)

        return self.api.download_structure_mesh(structure_id, 
                                                self.reference_space_key,
                                                file_name, 
                                                strategy='lazy')


    def add_manifest_paths(self, manifest_builder):
        """
        Construct a manifest for this Cache class and save it in a file.

        Parameters
        ----------

        file_name: string
            File location to save the manifest.

        """

        manifest_builder = super(ReferenceSpaceCache, self).add_manifest_paths(manifest_builder)
                                  
        manifest_builder.add_path(self.STRUCTURE_TREE_KEY,
                                  'structures.json',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.REFERENCE_SPACE_VERSION_KEY,
                                  '%s',
                                  parent_key='BASEDIR',
                                  typename='dir')

        manifest_builder.add_path(self.ANNOTATION_KEY,
                                  'annotation_%d.nrrd',
                                  parent_key=self.REFERENCE_SPACE_VERSION_KEY,
                                  typename='file')

        manifest_builder.add_path(self.TEMPLATE_KEY,
                                  'average_template_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.STRUCTURE_MASK_KEY,
                                  'structure_masks/resolution_%d/structure_%d.nrrd',
                                  parent_key=self.REFERENCE_SPACE_VERSION_KEY,
                                  typename='file')

        manifest_builder.add_path(self.STRUCTURE_MESH_KEY,
                                  'structure_meshes/structure_%d.obj',
                                  parent_key=self.REFERENCE_SPACE_VERSION_KEY,
                                  typename='file')

        return manifest_builder


       
 
    @classmethod
    def validate_structure_id(cls, structure_id):

        try:
            structure_id = int(structure_id)
        except ValueError as e:
            raise ValueError("Invalid structure_id (%s): could not convert to integer." % str(structure_id))

        return structure_id


    @classmethod
    def validate_structure_ids(cls, structure_ids):

        for ii, sid in enumerate(structure_ids):
            structure_ids[ii] = cls.validate_structure_id(sid)

        return structure_ids
