#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
Graph analysis module
---------------------
Module provides basic Graph classes.
"""
__author__    = 'Christoph Kirst <christoph.kirst.ck@gmail.com>'
__license__   = 'GPLv3 - GNU General Pulic License v3 (see LICENSE)'
__copyright__ = 'Copyright © 2020 by Christoph Kirst'
__webpage__   = 'http://idisco.info'
__download__  = 'http://www.github.com/ChristophKirst/ClearMap2'
import numpy as np
###############################################################################
### Base graph class
###############################################################################
[docs]
class Graph(object):
  """Abstract base Graph class.
  
  All graph interfaces should inherit from the class.
  """
  
  def __init__(self, name = None, n_vertices = None, edges = None, directed = None):
    
    if name is not None:
      self.name = name;
    
    if directed is not None:
      self.directed = directed;
    else:
      self.directed = False;
    
    if n_vertices is not None:
      self.add_vertex(n_vertices=n_vertices);
    
    if edges is not None:
      self.add_edge(edges);
  
  
  @property 
  def name(self):
    if hasattr(self, '_name'):
      return self._name;
    else:
      return type(self).__name__;
  @name.setter
  def name(self, value):
    self._name = str(value);
  
  @property
  def directed(self):
    return self._directed;
  
  @directed.setter
  def directed(self, value):
    self._directed = bool(value);
  
  ### Vertices
   
  @property
  def n_vertices(self):
    """Number of vertices in the graph."""
    return None;
  
  @n_vertices.setter
  def n_vertices(self, value):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)
  
[docs]
  def vertex(self, index):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  
  @property
  def vertices(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)  
  
  
[docs]
  def add_vertex(self, n_vertices = None, index = None, vertex = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
        
[docs]
  def remove_vertex(self, index = None, vertex = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  
  
[docs]
  def vertex_property(self, name, index = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
      
[docs]
  def vertex_properties(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  
[docs]
  def add_vertex_property(self, name, source, dtype = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)     
  
[docs]
  def set_vertex_property(self, name, source):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
[docs]
  def remove_vertex_property(self, name):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
[docs]
  def vertex_degrees(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)     
[docs]
  def vertex_degree(self, index):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
[docs]
  def vertex_out_degrees(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)     
[docs]
  def vertex_out_degree(self, index):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
[docs]
  def vertex_in_degrees(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)     
[docs]
  def vertex_in_degree(self, index):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  
  
  ### Edges 
  
  @property
  def n_edges(self):
    """Number of edges in the graph."""
    return None;
  
  @n_edges.setter
  def n_edges(self, value):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)  
  
[docs]
  def edge(self, edge):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  
  @property
  def edges(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)
    
[docs]
  def edge_connectivity(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)     
    
[docs]
  def add_edge(self, edge):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)   
  
[docs]
  def remove_edge(self, edge):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)     
  
    
[docs]
  def edge_property(self, name):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
      
[docs]
  def edge_properties(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  
[docs]
  def add_edge_property(self, name, source, dtype = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)     
  
[docs]
  def set_edge_property(self, name, source):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
[docs]
  def remove_edge_property(self, name):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  ### IO
  
[docs]
  def save(self, filename):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)        
    
[docs]
  def load(self, filename):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)      
  def __str__(self):
    try:
      name = self.name;
      name = '%s' % name if name is not None else '';
    except:
      name ='';
      
    try:
      vertices = self.n_vertices;
      vertices = '[%d·]' % vertices if vertices is not None else '';
    except:
      vertices = '';
    
    try:
      edges = self.n_edges;
      edges = '[%d-]' % edges if edges is not None else '';
    except:
      edges = '';
      
    return name + vertices + edges;
  
  def __repr__(self):
    return self.__str__(); 
###############################################################################
### Graphs with spatial geometry
###############################################################################    
[docs]
class GeometricGraph(Graph):
  """Base class for graphs whose vertices are embedded in an Eucledian space."""
  
  def __init__(self, name = None, n_vertices = None, edges = None, directed = False,
                     vertex_coordinates = None, vertex_radii = None,
                     edge_coordinates = None, edge_radii = None, edge_geometries = None, shape = None):
    super(GeometricGraph, self).__init__(name=name, n_vertices=n_vertices, edges=edges, directed=directed);
    
    if vertex_coordinates is not None:
      self.vertex_coordinates = vertex_coordinates;
    
    if vertex_radii is not None:
      self.vertex_radii = vertex_radii;
    
    if edge_coordinates is not None:
      self.edge_coordinates = edge_coordinates;
    
    if edge_radii is not None:
      self.edge_radii = edge_radii;
    
    if edge_geometries is not None:
      self.edge_geometries = edge_geometries;
    
    self.shape = shape;
  
  
  @property  
  def shape(self):
    """The shape of the underlying space in which the graph is embedded."""
    return self._shape;
  
  @shape.setter
  def shape(self, value):
    self._shape = value;
  
  @property
  def ndim(self):
    return len(self.shape);
  
    
  ### Vertices
  
[docs]
  def vertex_coordinates(self, axis = None, vertex = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)            
  
[docs]
  def set_vertex_coordinates(self, coordinates, axis = None, vertex = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)    
[docs]
  def vertex_radii(self, vertex = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)       
[docs]
  def set_vertex_radii(self, radius, vertex = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)   
  
  ### Edges
  
[docs]
  def edge_coordinates(self, edge = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)            
    
[docs]
  def set_edge_coordinates(self, coordinates, axis = None, edge = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)  
  
  
[docs]
  def edge_radii(self, edge = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)            
  
[docs]
  def set_edge_radii(self, edge = None):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)   
  
[docs]
  def edge_coordinates_from_vertex_coordinates(self):    
    coords = self.vertex_coordinates();
    i,j = self.edge_connectivity().T;
    return (0.5 * (coords[i] + coords[j])); 
  
[docs]
  def edge_radii_from_vertex_radii(self):
    r = self.vertex_radii();
    i,j = self.edge_connectivity().T;
    return (0.5 * (r[i] + r[j])); 
  
#  def set_edge_radii_from_vertex_radii(self):
#    r = self.vertex_radii();
#    i,j = self.edge_connectivity().T;
#    self.set_edge_radii(0.5 * (r[i] + r[j]));
[docs]
  def edge_vectors(self, normalize = False):
    xyz = self.vertex_coordinates();
    i,j = self.edge_connectiivty().T;
    v = xyz[i] - xyz[j];
    if normalize:
      v = (v.T / np.linalg.norm(v, axis = 1)).T;
    return v; 
  
  
  # Edge geometries
  
  @property
  def has_edge_geometry(self):
    return False; 
    
  @property
  def edge_geometry_type(self):
    return None;
  
[docs]
  def edge_geometry(self, *args, **kwargs):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)  
  
[docs]
  def set_edge_geometry(self, *args, **kwargs):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name)  
    
[docs]
  def reduce_edge_geometry(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
 
[docs]
  def expand_edge_geometry(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
    
  ### Functionality
  
  #def from_skeleton(self, skeleton):
  #  raise NotImplementedError('Not implemented in graph class %s!' % self.name)
  
  #def skeleton(self, sink = None, dtype = bool):
  #  raise NotImplementedError('Not implemented in graph class %s!' % self.name);
  
  ### Functionality
  def __str__(self):
    s = super(GeometricGraph, self).__str__();
    
    try:
      shape = self.shape;
      shape = '%r' % shape if shape is not None else '';
    except:
      shape = '';
      
    try:
      edge_geometry_type = self.edge_geometry_type;
      if edge_geometry_type is None:
        edge_geometry_type = ''
      elif edge_geometry_type == 'graph':
        edge_geometry_type = '|G|' 
      else:
        edge_geometry_type = '|E|'
    except:
      edge_geometry_type = '';
      
    return s + shape + edge_geometry_type; 
        
[docs]
class AnnotatedGraph(GeometricGraph):
  """Base class for graphs whose vertices are embedded in an Eucledian space and have an annotation."""
  
  def __init__(self, name = None, n_vertices = None, edges = None, directed = False,
                     vertex_coordinates = None, vertex_radii = None,
                     edge_coordinates = None, edge_radii = None, edge_geometries = None, shape = None,
                     vertex_labels = None, edge_labels = None, annotation = None):
    
    super(AnnotatedGraph, self).__init__(name=name, n_vertices=n_vertices, edges=edges, directed=directed,
                                         vertex_coordinates=vertex_coordinates, vertex_radii=vertex_radii,
                                         edge_coordinates=edge_coordinates, edge_radii=edge_radii, edge_geometries=edge_geometries, shape=shape);
    if vertex_labels is not None:
      self.vertex_labels = vertex_labels;
    
    if edge_labels is not None:
      self.edge_labels = edge_labels;
    
    self.annotation = annotation;
  
  @property
  def annotation(self):
    return self._annotation;
  
  @annotation.setter
  def annotation(self, value):
    self._annotation = value;
  
  
  ### Vertices
  
[docs]
  def vertex_annotation(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  
[docs]
  def set_vertex_annotation(self, value):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
 
  ### Edges
[docs]
  def edge_annotation(self):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
  
[docs]
  def set_edge_annotation(self, value):
    raise NotImplementedError('Not implemented in graph class %s!' % self.name) 
 
  
  def __str__(self):
    s = super(AnnotatedGraph, self).__str__();
    
    try:
      annotation = self.annotation;
      annotation = '{{%r}}' % annotation if annotation is not None else '';
    except:
      annotation = '';
    return s + annotation; 
 
  
[docs]
def load(filename):
  return Graph().load(filename); 
[docs]
def save(filename, graph):
  graph.save(filename); 
###############################################################################
### Tests
###############################################################################
def _test():
  import ClearMap.Analysis.Graphs.Graph as gr
  reload(gr)
  
  g = gr.Graph();
  print(g)
  
  g = gr.GeometricGraph();
  print(g)
  g = gr.AnnotatedGraph(annotation='test.npy')
  print(g)