
'''
.. currentmodule:: clifford.cga
========================================
cga (:mod:`clifford.cga`)
========================================

Object Oriented Conformal Geometric Algebra. 

Examples
-----------
>>> from clifford import Cl
>>> from clifford.cga import CGA
>>> g3,blades  = Cl(3)
>>> g3c = CGA(g3)
>>> C = g3c.round(3)             # create random sphere 
>>> T = g3c.translation(e1+e2)   # create translation
>>> C_ = T(C)                    # translate the sphere 
>>> C_.center                    # compute center of sphere 

The CGA 
========

.. autosummary::
    :toctree: generated/

    CGA
    

Objects
================

.. autosummary::
    :toctree: generated/

    Flat
    Round

Operators
================

.. autosummary::
    :toctree: generated/

    Rotation
    Dilation
    Translation
    Transversion

Meta-Class
===========

.. autosummary::
    :toctree: generated/

    CGAThing

'''

from functools import reduce
from . import conformalize, op,gp, MultiVector,Cl
from numpy import zeros,e,log
from numpy.random import rand
import math
    

class CGAThing(object):
    '''
    base class for cga objects and operators. 
    
    maps versor product to `__call__`. 
    '''
    def __call__(self, other):
        if isinstance(other, MultiVector):
            if other.grades() ==[1]:
                null= self.cga.null_vector(other)
                return self.mv*null*~self.mv
            return self.mv*other*~self.mv
        else:
            klass = other.__class__
            return klass(self.cga, self.mv*other.mv*~self.mv)
    
    def inverted(self):
        '''
        inverted version of this thing. 
        
        self -> ep*self*ep
        
        where ep is the positive added basis vector 
        '''
        return self.cga.ep*self.mv* self.cga.ep
    
    def involuted(self):
        '''
        inverted version of this thing. 
        
        self -> E0*self*E0
        
        where E0 is the added minkowski bivector 
        '''
        return self.cga.E0*self.mv* self.cga.E0
        
## Objects  
class Flat(CGAThing):
    '''
        A line, plane, or hyperplane. 
        
        Typically constructed as method of existing cga, like `cga.flat()`
        
        multivector is accessable by `mv` property
        
        Parameters 
        -----------
        cga : `CGA`
            the cga object
        
        args : [int, Multivector,Multivectors]    
            * if nothing supplied, generate a flat of highest dimension 
            * int: dimension of flat  (2=line, 3=plane, etc)
            * Multivector : can be 
              * existing Multivector representing the Flat
              * vectors on the flat
              
        Examples
        ----------  
        
        >>> cga.flat()               # from None 
        >>> cga.flat(2)              # from dim of space
        >>> cga.flat(e1,e2)          # from points
        >>> cga.flat(cga.flat().mv)  # from existing multivector   
        '''
    # could inherent some generic CGAObject class
    def __init__(self,cga, *args):
        
        self.cga = cga
        self.layout = cga.layout # note: self.layout is the cga layout
        self.einf = self.cga.einf # we use this alot
        
        if len(args) == 0:    
            # generate random highest dimension flat
            nulls = [self.cga.null_vector() for k in range(self.layout.dims-2)]
            self.mv = reduce(op,nulls + [self.einf]) 
        
        
        elif len(args) == 1:
            # from existing multivector
            if isinstance(args[0], MultiVector):
                self.mv = args[0]
            
            # generate random flat  for given  dimension
            elif isinstance(args[0], int):
                dim = args[0]
                points = [self.cga.base_vector() for k in range(dim+1)]
                points = list(map(self.cga.up, points))
                self.mv = reduce(op,points + [self.einf])
            
            
        # from vectors on flat         
        else:
            nulls = map(self.cga.null_vector,args)
            if self.einf not in nulls:
                nulls = list(nulls)+[self.einf]
                
            self.mv = reduce(op,nulls) 
        
        self.mv = self.mv.normal()
    
    def __repr__(self):
        return '%i-Flat'%self.dim
                    
class Round(CGAThing):
    '''
    A point pair, circle, sphere or hyper-sphere.
    
    Typically constructed as method of existing cga, like `cga.round()`
    
    multivector is accessable by `mv` property
    
    Parameters 
    -----------
    cga : `CGA`
        the cga object
    
    args : [int, Multivector,Multivectors]    
        * if nothing supplied, generate a round of highest dimension 
        * int: dimension of flat  (2=point pair, 3=circle, etc)
        * Multivector : can be 
          * existing Multivector representing the round
          * vectors on the round
          
    Examples
    ----------  
    
    >>> cga.round()               # from None 
    >>> cga.round(2)              # from dim of space
    >>> cga.round(e1,e2,-e1)      # from points
    >>> cga.round(cga.flat().mv)  # from existing multivector   
    '''
    # could inherent some generic CGAObject class
    def __init__(self,cga, *args):
        # me
        self.cga = cga
        self.layout = cga.layout # note: self.layout is the cga layout
        
        
        if len(args) == 0:    
            # generate random highest dimension round
            nulls = [self.cga.null_vector() for k in range(self.layout.dims-1)]
            self.mv = reduce(op,nulls) 
        
        
        elif len(args) == 1:
            # from existing multivector
            if isinstance(args[0], MultiVector):
                self.mv = args[0]
            
            # generate random round  for given  dimension
            elif isinstance(args[0], int):
                dim = args[0]
                points = [self.cga.base_vector() for k in range(dim+2)]
                points = map(self.cga.up, points)
                self.mv = reduce(op,points)
        
            # from center, radius tuple
            else:
                if len(args[0]) == 2:
                    center,radius = args[0]
                    center = self.cga.null_vector(center)
                    dual_round = (center - .5*radius**2*self.cga.einf)
                    self.mv = dual_round.normal().dual()
                       
        # from vectors on round         
        else:
            nulls = map(self.cga.null_vector,args)
            self.mv = reduce(op,nulls) 
        
        self.mv = self.mv.normal()
        
    def from_center_radius(self,center, radius):
        '''
        construct a round from center/radius
        '''
        center = self.cga.null_vector(center)
        self.mv = (center - .5*radius**2*self.cga.einf).normal().dual()
        return self
    
    def __repr__(self):
        names = {4:'Sphere',3:'Circle',2:'Point Pair',1:'Point'}
        if self.dim <=4:
            return names[self.dim]
        else:
            return '%i-Round'%self.dim
    
    @property
    def dim(self):
        '''
        dimension of this round
        '''
        return self.mv.grades()[0]
    
    @property
    def center(self):
        '''
        center of this round, as a null vector 
        '''
        return self.mv * self.cga.einf * self.mv
    
    @property
    def center_down(self):
        '''
        center of this round, as a down-projected  vector (in I_base)
        
        (but still in cga's layout)
        '''
        return self.cga.down(self.center)
    
    @property
    def radius(self):
        '''
        radius of the round (a float)
        '''
        dual_sphere = self.dual 
        dual_sphere / (-dual_sphere | self.cga.einf)
        return math.sqrt(abs(dual_sphere * dual_sphere))

    @property 
    def dual(self):
        '''
        self.mv* self.layout.I
        '''
        return self.mv* self.layout.I
            
## Operators
class Translation(CGAThing):
    '''
    A Translation
    
    Can be constructed from a vector in base space or a null
    vector, or nothing. 
    
    Parameters 
    -------------
    args : [none, `clifford.Multivector`]
        if none, a random translation will be generated
        several types of Multivectors can be used:
            * base vector - vector in base space 
            * null vector 
            * existing translation rotor  
        
    Examples
    ----------
    >>> T = cga.translation()       # from None 
    >>> T = cga.translation(e1+e2)  # from base vector  
    >>> T = cga.translation(cga.up(e1+e2)) # from null vector 
    >>> T = cga.translation(T.mv)  # from existing translation rotor 
    '''
    def __init__(self,cga, *args):
        self.cga = cga
        self.layout = cga.layout
        
        if len(args) ==0:    
            # generate generator!
            mv = 1 - self.cga.base_vector()*self.cga.einf/2.
        
        elif len(args)==1:
            arg = args[0]
            if isinstance(arg, MultiVector):
                if arg.grades()==[1]:
                    # we have vector 
                    mv = 1 - self.cga.straight_up(arg)*self.cga.einf/2.     
                if arg.grades()==[0,2]:
                    # we have ro tor  
                    ## TODO ensure its a translation
                    mv = args[0]
        else:
            raise ValueError('bad input')
            
        self.mv = mv
    
    def __repr__(self):
        return 'Translation'
  
class Dilation(CGAThing):
    '''
    A global dilation
    
    Parameters 
    -------------
    args : [none, number]
        if none, a random dilation will be generated
        if a number, dilation of given amount
        
    Examples
    ----------
    >>> D = cga.dilation()          # from  none 
    >>> D = cga.dilation(.4)        # from number
    '''
    def __init__(self,cga, *args):
        self.cga = cga
        self.layout = cga.layout
        
        if len(args) ==0:    
            # generate a dilation 
            args= [rand()]
        
        if len(args)==1:
            arg = args[0]
            if isinstance(arg, MultiVector):
                if arg.grades()==[0,2]:
                    # we have a rotor 
                    mv = arg
                if arg.grades == [0]:
                    arg = float(arg)
            
            if arg<0:
                raise(ValueError('dilation should be positive'))
            
            mv = e**((-log(arg)/2.)*(self.cga.E0)) 
            
                
        else:
            raise ValueError('bad input')
            
        self.mv = mv
    
    def __repr__(self):
        return 'Dilation'

class Rotation(CGAThing):
    '''
    A Rotation
    
    Can be constructed from a generator, rotor, or none
    
    Parameters 
    -------------
    args : [none, `clifford.Multivector`]
        if none, a random translation will be generated
        several types of Multivectors can be used:
            * bivector   - interpreted as the generator
            * existing translation rotor  
        
    Examples
    ----------
    >>> R = cga.rotation()          # from None
    >>> R = cga.rotation(e12+e23)   # from bivector 
    >>> R = cga.rotation(R.mv)   # from bivector 
    '''
    def __init__(self,cga, *args):
        self.cga = cga
        self.layout = cga.layout
        
        if len(args) ==0:    
            # generate a rotation
            
            U = self.layout.randomMV()(2)
            U = self.cga.I_base.project(U)
            self.mv = e**(U)
        
        
        elif len(args)==1:
            arg = args[0]
            if isinstance(arg, MultiVector):
                if arg.grades()==[0,2]:
                    # we have a rotor 
                    self.mv = arg
                
                elif arg.grades() == [2]:
                    # we have a bivector, make sure its in base space
                    if arg^self.cga.I_base != 0:
                        arg = self.cga.I_base.project(arg)
                    self.mv = e**(arg)
                else:
                    # multivector has improper grade
                    raise ValueError('bad input')
            else:
                # arg isnt a multivector
                raise ValueError('bad input')            
                
        else:
            # more than 1 arg
            raise ValueError('bad input')
                
    def __repr__(self):
        return 'Rotation'    

class Transversion(Translation):
    '''
    A Transversion
    
    A transversion is a combination of an inversion-translation-inversion,
    or in other words an inverted translation operator. This inherits
    from `Translation`
    
    Can be constructed from a vector in base space or a null
    vector, or nothing.  
    
    Parameters 
    -------------
    args : [none, `clifford.Multivector`]
        if none, a random transversion will be generated
        several types of Multivectors can be used:
            * base vector - vector in base space 
            * null vector 
            * existing transversion rotor  
        
    Examples
    ----------
    >>> K = cga.transversion()       # from None 
    >>> K = cga.transversion(e1+e2)  # from base vector  
    >>> K = cga.transversion(cga.up(e1+e2)) # from null vector 
    >>> K = cga.transversion(T.mv)  # from existing translation rotor 
    '''
    def __init__(self,cga, *args):
        self.mv = Translation(cga, *args).inverted()
        
    def __repr__(self):
        return 'Transversion'    
       
         
class CGA(object):
    '''
    Conformal Geometric Algebra 
    
    conformalizes the layout_orig,  and provides several
    methods and for objects/operators
    
    Parameters 
    -----------
    layout_orig: [`clifford.Layout`, int]
        a layout for the *base* geometric algebra which is conformalized
        if given as an int, then generates a euclidean space of given 
        dimension
        
    Examples 
    ----------
    >>> from clifford import Cl
    >>> from clifford.cga import CGA
    >>> g3,blades  = Cl(3)
    >>> g3c = CGA(g3)
    >>> g3c = CGA(3)
    
    '''
    def __init__(self, layout_orig):
        if isinstance( layout_orig,int):
            layout_orig,blades = Cl(layout_orig)
        self.layout_orig = layout_orig
        self.layout, self.blades, stuff = conformalize(layout_orig)
        self.__dict__.update(stuff)
    
    ## Objects
    def base_vector(self):
        '''
        random vector in the lower(original) space
        '''
        return self.I_base.project(self.layout.randomV())
    
    def null_vector(self,x=None):
        '''
        generates random null vector if x is None, or
        returns a null vector from base vector x, if  x^self.I_base ==0 
        returns x, 
        
        a null vector will lay on the horisphere
        '''
        if x is None:
            return self.up(self.base_vector())
        else:
            if x^self.I_base ==0:
                return self.up(x)
            return x
    
    def round(self, *args):
        '''
        see :class:`Round`
        '''
        return Round(self,*args) 
    
    def flat(self, *args):
        '''
        see :class:`Flat`
        '''
        return Flat(self,*args) 
    ##  Operators
    def translation(self, *args):
        '''
        see :class:`Translation`
        '''
        return Translation(self, *args)
        
    def transversion(self, *args):
        '''
        see :class:`Transversion`
        '''
        return Transversion(self, *args)

    def dilation(self,*args):
        '''
        see :class:`Dilation`
        '''
        return Dilation(self,*args)
    
    def rotation(self,*args):
        '''
        see :class:`Rotation`
        '''
        return Rotation(self,*args)
    
    ##  methods
    def straight_up(self, x):
        '''
        place a vector from layout_orig into this CGA, without up()
        '''
        return self.I_base.project(self.up(x))
        
