#Copyright 2010 Erik Tollerud
#
#   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.


"""
This module contains tools and utilities for computing ephemerides and physical
locations of solar system objects, as well as proper motion calculations for
extrasolar objects.

.. seealso::
        
    `Pyephem <http://rhodesmill.org/pyephem/>`_
        A Pythonic implementation of the 
        `xephem <http://www.clearskyinstitute.com/xephem/>`_ ephemerides 
        algorithms. 
        
    `Meeus, Jean H. "Astronomical Algorithms" ISBN 0943396352 <http://www.willbell.com/MATH/mc1.htm>`_ 
        An authoritative reference on coordinates, ephemerides, and related
        transforms in astronomy.
        
    `JPL Solar System Dynamics Group <http://ssd.jpl.nasa.gov/>`_
        The standard source for solar system dynamics and ephemerides.  Source 
        of DE200 and DE405 solar system models, and HORIZON ephemerides service.

.. todo:: Tutorials

Classes and Inheritance Structure
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. inheritance-diagram:: astropysics.coords.ephems
   :parts: 1
   
Module API
^^^^^^^^^^

"""

#TODO: JPL Ephemeris option

#useful references:
#*http://www.astro.rug.nl/software/kapteyn/index.html
#*"Astronomical Algorithms" by Jean Meeus 
#*"The IAU Resolutions on Astronomical Reference Systems,Time Scales, and Earth 
#  Rotation Models": http://aa.usno.navy.mil/publications/docs/Circular_179.pdf
from __future__ import division,with_statement

from ..constants import pi
import numpy as np

_twopi = 2*pi

try:
    #requires Python 2.6
    from abc import ABCMeta
    from abc import abstractmethod
    from abc import abstractproperty
    from collections import Sequence,MutableSequence
except ImportError: #support for earlier versions
    abstractmethod = lambda x:x
    abstractproperty = property
    ABCMeta = type
    class MutableSequence(object):
        __slots__=('__weakref__',) #support for weakrefs as necessary
    class Sequence(object):
        __slots__=('__weakref__',) #support for weakrefs as necessary
        
class EphemerisAccuracyWarning(Warning):
    """
    Class for warnings due to Ephemeris accuracy issues
    """

#<--------------------Define general classes----------------------------------->
    
class EphemerisObject(object):
    """
    This class is the superclass of objects that change sky coordinates over
    time. 
    
    **Subclassing**
    
    * Subclasses should implement the :meth:`_getCoordObj` abstract method.
      This should return a :class:`astropysics.coords.coordsys.CoordinateSystem`
      object of the coordinates for the current value of :attr:`jd`.
      
    * Subclasses may implement a :meth:`_jdhook` method to perform an action
      whenever the jd is changed.  It must have a signature f(oldjd,newjd). 
    
    * Subclasses may implement the :meth:`getVelocity` method to return
      instantaneous velocities for the coordinate at the current value of 
      :attr:`jd`.  If this is not implemented, calling it will raise a 
      :exc:`NotImplementedError`.
    
    """
    
    __metaclass__ = ABCMeta
    
    name = '' #put here so it ends up in autogenerated documentation
    'The name of the object.'
    
    def __init__(self,name,validjdrange=None):
        """
        :params name: The name of the object.
        :params validjdrange:  
            Sets the jd range over which this method is valid as (minjd,maxjd).
            Trying to get something outside will result in an
            :`exc`:EphemerisAccuracyWarning warning. `minjd` or `maxjd` can be
            None to indicate no bound.
            
        """
        from ..obstools import jd2000
        
        self._jd = jd2000
        self._jdhook(jd2000,jd2000)
        
        self.name = name
        self._setValidjdrange(validjdrange)
        
    def _getJd(self):
        return self._jd
    def _setJd(self,val):
        from operator import isSequenceType
        from ..obstools import calendar_to_jd
        from datetime import datetime
        
        if val == 'now':
            jd =  calendar_to_jd(datetime.utcnow(),tz=None)
        elif hasattr(val,'year') or isSequenceType(val):
            jd = calendar_to_jd(val)
        else:
            jd = val
        if self._validrange is not None:
            from warnings import warn
            if self._validrange[0] is not None and jd < self._validrange[0]:
                warn('JD {0} is below the valid range for this EphemerisObject'.format(jd),EphemerisAccuracyWarning)
            elif self._validrange[1] is not None and jd > self._validrange[1]:
                warn('JD {0} is above the valid range for this EphemerisObject'.format(jd),EphemerisAccuracyWarning)
        
        self._jdhook(self._jd,jd)
        self._jd = jd        
    jd = property(_getJd,_setJd,doc="""
    Julian Date at which to calculate the orbital elements. Can be set either as
    a scalar JD, 'now', :class:`datetime.datetime` object or a compatible tuple.
    """)
       
    def _jdhook(self,oldjd,newjd):
        """
        Override in subclasses to perform an action when the jd is changed
        (although before self._jd is updated).
        """
        pass

    
    
    @property
    def validjdrange(self):
        """
        The range of jds over which these ephemerides are valid. Returns a 
        2-tuple (minjd,maxjd), either of which can be None to indicate no bound.
        """
        return self._validrange
    
    def _setValidjdrange(self,val):
        """
        Sets the jd range over which this method is valid. Trying to get
        something outside will result in an `exc`:EphemerisAccuracyWarning:
        Intended for use in __init__.
        
        :param val: 
            The range as (minjd,maxjd), can be None to indicate no bound. If set
            to None, the result will be (None,None).
        
        """
        if val is None:
            self._validrange = (None,None)
        else:
            v1,v2 = val
            if v1 is None and v2 is None:
                self._validrange = (None,None)
            else:
                from operator import isSequenceType
                from ..obstools import calendar_to_jd
                from datetime import datetime
                
                vs = []
                for v in (v1,v2):
                    if v is None:
                        vs.append(None)
                    elif v == 'now':
                        vs.append(calendar_to_jd(datetime.utcnow(),tz=None))
                    elif hasattr(v,'year') or isSequenceType(v):
                        vs.append(calendar_to_jd(v))
                    else:
                        vs.append(v)
                self._validrange = tuple(vs)
                
    def __call__(self,jds=None,coordsys=None):
        """
        Computes the coordinates of this object at the specified time(s).
        
        :param jds: 
            A sequence of julian dates at which to compute the coordinates, a
            scalar JD, or None to use the :attr:`jd` attribute's current value.
        :param coordsys: 
            A :class:`astropysics.coords.coordsys.CooordinateSystem` class that
            specifies the type of the output coordinates, or None to use the
            default coordinate type.

        :returns: 
            A list of objects with the coordinates in the same order as `jds`,
            or a single object if `jds` is None or a scalar. Outputs are
            :class:`astropysics.coords.coordsys.CooordinateSystem` subclasses,
            and their type is either `coordsys` or the default type if
            `coordsys` is None.
        
        
        """
        single = False #return an object instead of a sequence of objects
        if jds is None:
            single = True
            res = (self._getCoordObj(),)
        else:
            if isinstance(jds,np.ndarray):
                if jds.shape == ():
                    single = True
                jds = jds.ravel()
            elif np.isscalar(jds):
                single = True
                jds = (jds,)
            
            jd0 = self.jd
            try:
                res = []
                for jd in jds:
                    self.jd = jd
                    res.append(self._getCoordObj())
            finally:
                self.jd = jd0
                
        
        if coordsys is not None:
            res = [c.convert(coordsys) for c in res]
        
        if single:
            return res[0]
        else:
            return res
                
    
    @abstractmethod
    def _getCoordObj(self):
        """
        Computes and returns the coordinate location of the object at the time
        given by the :attr:`jd` attribute's value.
        
        :returns: 
            The current coordinates for the object. The exact output coordinate
            system is not specified, other than that it must be a subclass of
            :class:`astropysics.coords.coordsys.CooordinateSystem`.
            
        """
        raise NotImplementedError
    
    def getVelocity(self):
        """
        Computes and returns the instantaneous velocity for this object at the
        time given by the :attr:`jd` attribute's value.
        
        :returns:
            The current velocity of this object. The exact type is not
            specified, but it should be something that can be added to the
            coordinates that are returned when the object is called.
        
        :raises NotImplementedError: 
            If velocities are not implemented for this class.
        """
        raise NotImplementedError
    
class ProperMotionObject(EphemerisObject):
    """
    An object with linear proper motion relative to a specified epoch.
    """
    
    def __init__(self,name,ra0,dec0,dra=0,ddec=0,epoch0=2000,distpc0=None,rv=0,
                      coordclass=None):
        """
        :param str name: Name of the object.
        :param float ra0: RA in degrees at the starting epoch.
        :param float dec0: Dec in degrees at the starting epoch.
        :param float dra: Proper motion in RA, arcsec/yr.
        :param float ddec: Proper motion in Dec, arcsec/yr.
        :param epoch0: Epoch for which `ra0`,`dec0`,and`distpc0` are valid.
        :param distpc0: Distance in pc at the starting epoch.
        :param float rv: Radial velocity in km/s
        :param coordclass: 
            The type of output coordinates. If None, defaults to
            :class:`~astropysics.coords.coordsys.ICRSCoordinates`.
        :type coordclass: 
            :class:`astropysics.coords.coordsys.EpochalLatLongCoordinates`
        """
        from ..obstools import epoch_to_jd
        from .coordsys import ICRSCoordinates
        
        self.ra0 = ra0
        self.dec0 = dec0
        self.dra = dra
        self.ddec = ddec
        self.epoch0 = epoch0
        self._jd0 = epoch_to_jd(epoch0)
        self.distpc0 = distpc0
        self.rv = rv
        
        if coordclass is None:
            self.coordclass = ICRSCoordinates
        else:
            self.coordclass = coordclass
        
        EphemerisObject.__init__(self,name)
        self.jd = self._jd0
        
    @property
    def ra(self):
        """
        RA at the current :attr:`jd` in degrees.
        """
        from math import degrees
        from ..constants import asecperrad
        
        return self.ra0 + degrees(self._dyr*self.dra/asecperrad)
    
    @property
    def dec(self):
        """
        Declination at the current :attr:`jd` in degrees.
        """
        from math import degrees
        from ..constants import asecperrad
        
        return self.dec0 + degrees(self._dyr*self.ddec/asecperrad)
    
    @property
    def distancepc(self):
        """
        Distance at the current :attr:`jd` in parsecs.
        """
        from ..constants import cmperpc,secperyr
        
        if self.distpc0 is None:
            return None
        else:
            return self.distpc0 + self._dyr*self.rv*secperyr*1e5/cmperpc
    
    def _jdhook(self,oldjd,newjd):
        self._dyr = (newjd - self._jd0)/365.25
    
    def _getCoordObj(self):
        from ..obstools import jd_to_epoch
        
        if self.distancepc is None:
            return self.coordclass(self.ra,self.dec,epoch=jd_to_epoch(self.jd))
        else:
            return self.coordclass(self.ra,self.dec,distancepc=self.distancepc,
                                           epoch=jd_to_epoch(self.jd))
            
    
class KeplerianObject(EphemerisObject):
    """
    An object with Keplerian orbital elements.
    
    The following orbital elements are available for the current value of
    :attr:`jd` as read-only properties:
    
    * :attr:`a` 
    * :attr:`e` 
    * :attr:`i` 
    * :attr:`Lan`
    * :attr:`L`
    * :attr:`Lp`
    * :attr:`M`
    * :attr:`ap`
    * :attr:`nu`
    
    Additional read only properties derived from the orbital elements include:
    
    * :attr:`P`
    * :attr:`d`
    * :attr:`dapo`
    * :attr:`dperi`
        
    """
    
    Etol = None #default set in constructor
    r""" Desired accuracy for iterative calculation of eccentric anamoly (or
    true anomaly) from mean anomaly. If None, default tolerance is used
    (1.5e-8), or if 0, an analytic approximation will be used (:math:`E \approx
    M + e (1 + e \cos M ) \sin M`). This approximation can be 10x faster to
    compute but fails for e close to 1.
    """
    
    def __init__(self,**kwargs):
        """
        Keywords should be the names of orbital elements. Orbital elements can
        be a callable f(T) that returns the orbital element, where T is the
        Julian century from J2000. Otherwise, it must be a sequence giving
        polynomial coefficients of T, specified in increasing power (i.e.
        constant term first).
        
        The orbital elements :attr:`a`,:attr:`e`, :attr:`i`, and :attr:`Lan`
        must be specified, as must either :attr:`L` and :attr:`Lp` or :attr:`ap`
        and :attr:`M`
        
        Three additional keywords can be supplied:
        
        :params outcoords: 
            The coordinate class that should be used as the output. Should be a
            :class:`astropysics.coords.coordsys.CartesianCoordinates` subclass,
            and the origin will be taken as the center of the Keplerian orbit.
            Defaults to
            :class:`astropysics.coords.coordsys.RectangularCoordinates`.
        :params outtransfunc:
            A function that maps the coordinates from the standard coordinate
            system (where the x-y plane is the plane of reference) to a system
            matching the type specified by `outcoords`. The function should have
            the signature f(x,y,z,jd) and return (xp,yp,zp). It can also be None
            to perform no trasnformation. Defaults to None.
        :params Etol:
            Tolerance for eccentric anomaly (see :attr:`Etol`). Defaults to
            None.
            
        Any other keywords will be passed into the constructor for
        :class:`EphemerisObject`.
            
        :except TypeError: If necessary orbital elements are missing.
            
        """
        from .coordsys import RectangularCoordinates
        
        
        self.outcoords = kwargs.pop('outcoords',RectangularCoordinates)
        self.outtransfunc = kwargs.pop('outtransfunc',None)
        self.Etol = kwargs.pop('Etol',23.43928)
        
        
        kwnms = ('a','e','i','Lan','L','Lp','ap','M')
        a,e,i,Lan,L,Lp,ap,M = [kwargs.pop(n,None) for n in kwnms]
        EphemerisObject.__init__(self,**kwargs)
        
        if a is None:
            raise TypeError('Need to provide orbital element `a`')
        if e is None:
            raise TypeError('Need to provide orbital element `e`')
        if i is None:
            raise TypeError('Need to provide orbital element `i`')
        if Lan is None:
            raise TypeError('Need to provide orbital element `Lan`')
        
        self._a = self._makeElement(a)
        self._e = self._makeElement(e)
        self._i = self._makeElement(i)
        self._Lan = self._makeElement(Lan)
        
        if L is not None and Lp is not None:
            if ap is not None or M is not None:
                raise TypeError('Cannot specify both `L`/`Lp` and `ap`/`M`')
            self._L = self._makeElement(L)
            self._Lp = self._makeElement(Lp)
            
        elif ap is not None and M is not None:
            if L is not None or Lp is not None:
                raise TypeError('Cannot specify both `L`/`Lp` and `ap`/`M`')
            self._ap = self._makeElement(ap)
            self._M = self._makeElement(M)
        
            
    def _jdhook(self,oldjd,newjd):
        from ..obstools import jd2000
        self._t = (newjd - jd2000)/36525.
   
    def _makeElement(self,val):
        from operator import isSequenceType
        
        if callable(val):
            return val
        elif isSequenceType(val):
            if len(val)==2:
                a,b = val
                return lambda T:a+b*T
            else:
                coeffs = tuple(reversed(val))
                return lambda T:np.polyval(coeffs,T)
        else:
            raise TypeError('invalid value for orbit element %s'%val)
    
    @property
    def a(self):
        """
        The semi-major axis.
        """
        return self._a(self._t)
        
    @property
    def e(self):
        """
        The eccentricity in radians.
        """
        return self._e(self._t)
    
    @property
    def i(self):
        """
        The orbital inclination in degrees.
        """
        return self._i(self._t)
        
    @property
    def Lan(self):
        """
        The longitude of the ascending node in degrees.
        """
        return self._Lan(self._t)
        
    @property
    def L(self):
        """
        The mean longitude in degrees.
        """
        if hasattr(self,'_L'):
            return self._L(self._t)
        else:
            return self.M + self.Lp
        
    @property
    def Lp(self):
        """
        The longitude of the pericenter in degrees.
        """
        if hasattr(self,'_Lp'):
            return self._Lp(self._t)
        else:
            return self.ap + self.Lan
        
    @property
    def M(self):
        """
        The mean anomaly in degrees.
        """
        if hasattr(self,'_M'):
            return self._M(self._t)
        elif hasattr(self,'_bcsf'): #special hidden correction used for 3000BCE-3000CE 
            from math import sin,cos
            
            b,c,s,f = self._bcsf
            T = self._t
            
            return self.L - self.Lp  + b*T*T + c*cos(f*T) + s*sin(f*T)
        
        else:
            return self.L - self.Lp
        
    @property
    def ap(self):
        """
        The argument of the pericenter in degrees.
        """
        if hasattr(self,'_ap'):
            return self._ap(self._t)
        else:
            return self.Lp - self.Lan
    
    @staticmethod
    def _keplerEq(E,M,e):
        r"""
        Kepler's equation :math:`-M + E - e \sin(E)` - used for finding E from M
        """
        from math import sin
        
        return E-M-e*sin(E)
        
    @property
    def E(self):
        """
        Eccentric anamoly in degrees - calculated from mean anamoly with
        accuracy given by :attr:`Etol`.
        """
        from math import radians,sin,cos,degrees,pi
        from scipy.optimize import fsolve
        
        M = radians((self.M + 180)%360 - 180)
        e = self.e #radians
        E0 = M + e*sin(M)*(1.0 + e*cos(M))
        
        if self.Etol==0:
            Er = E0
        elif self.Etol is None:
            Er = fsolve(KeplerianObject._keplerEq,E0,args=(M,e))[0]
        else:
            Er = fsolve(KeplerianObject._keplerEq,E0,args=(M,e),xtol=self.Etol)[0]
        
        return degrees(Er)%360
    
    @property
    def nu(self):
        r"""
        True anamoly in degrees (:math:`-180 < \nu < 180`) - calculated from
        eccentric anamoly with accuracy given by :attr:`Etol`.
        """
        from math import radians,sin,cos,atan2,sqrt,degrees
        
        E = radians(self.E)
        e = self.e
        
        xv = cos(E) - e
        yv = sqrt(1.0 - e*e) * sin(E)
        
        return degrees(atan2(yv,xv))
    
    @property
    def d(self):
        """
        Current distance from focus of attraction to object.
        """
        from math import radians,cos
        
        a = self.a
        e = self.e
        nu = radians(self.nu)
        
        return a*(1-e*e)/(1+e*cos(nu))
        
    @property
    def dperi(self):
        """
        Distance from focus of attraction at pericenter.
        """
        return self.a*(1 - self.e)
    
    @property
    def dapo(self):
        """
        Distance from focus of attraction at apocenter.
        """
        return self.a*(1 + self.e)
    
    @property
    def P(self):
        """
        Orbital period (if gravitational parameter GM=1).
        """
        return self.a**1.5
    
    
    def _getCoordObj(self):
        from ..obstools import jd_to_epoch
        from math import sin,cos,sqrt,radians
        
        #orbital plane coordinates
        a = self.a
        E = radians(self.E)
        e = self.e
        xp = a*(cos(E)-e)
        yp = a*sqrt(1-e*e)*sin(E)
        
        w = radians(self.ap)
        o = radians(self.Lan)
        i = radians(self.i)
        cw,sw = cos(w),sin(w)
        co,so = cos(o),sin(o)
        ci,si = cos(i),sin(i)
        
        x = (cw*co-sw*so*ci)*xp + (-sw*co - cw*so*ci)*yp
        y = (cw*so+sw*co*ci)*xp + (-sw*so + cw*co*ci)*yp
        z = (sw*si)*xp + (cw*si)*yp
        
        if self.outtransfunc:
            x,y,z = self.outtransfunc(x,y,z,self._jd)
        res = self.outcoords(x,y,z)              
        
        #adjust units to AU if the coordinate system has units
        if hasattr(res,'unit'):
            res.unit = None #convention is that None implies not to do conversions
            res.unit = 'au'
            
        #add epoch info if coordinates have an epoch
        if hasattr(res,'epoch'):
            res.epoch = jd_to_epoch(self.jd)
            
        return res
    
    def getPhase(self,viewobj='Earth',illumobj='Sun'):
        """
        Computes the phase of this object. The phase is computed as viwed from
        `viewobj` if illuminated by `illumobj`
        
        :param viewobj: 
            The object viewing this object. Either a string with the name of a
            solar system object (from the list in
            :func:`list_solar_system_objects`), or a :class:`EphemerisObject`
            object. If None, it is taken to be the coordinate origin.
        :param viewobj: 
            The object illuminating this object. Either a string with the name
            of a solar system object (from the list in
            :func:`list_solar_system_objects`), or a :class:`EphemerisObject`
            object. If None, it is taken to be the coordinate origin.
        
        :returns: 
            The phase of the object as a float where 0 is new and 1 is full. 
        """
        from math import sqrt
        
        c = self()
        coordcls = c.__class__
        x,y,z = c.x,c.y,c.z
        
        if illumobj is None:
            xi = yi = zi = 0
        else:
            if isinstance(illumobj,basestring):
                cillum = get_solar_system_ephems(illumobj,self.jd)
            else:
                cillum = illumobj(self.jd)
            if not isinstance(cillum,coordcls):
                cillum = cillum.convert(coordcls)
            xi,yi,zi = cillum.x,cillum.y,cillum.z
            
        if viewobj is None:
            xv = yv = zv = 0
        else:
            if isinstance(viewobj,basestring):
                cview = get_solar_system_ephems(viewobj,self.jd)
            else:
                cview = viewobj(self.jd)
            if not isinstance(cview,coordcls):
                cview = cview.convert(coordcls)
            xv,yv,zv = cview.x,cview.y,cview.z
        
        xR,yR,zR = x-xv,y-yv,z-zv #view -> self vector
        xs,ys,zs = xv-xi,yv-yi,zv-zi #illum -> view vector
        xr,yr,zr = x-xi,y-yi,z-zi #illum -> self vector
        
        r = sqrt(xr*xr+yr*yr+zr*zr)
        R = sqrt(xR*xR+yR*yR+zR*zR)
        s = sqrt(xs*xs+ys*ys+zs*zs)
        
        return (1+(r*r + R*R - s*s)/(2*r*R))/2
    
def get_solar_system_ephems(objname,jds=None,coordsys=None):
    """
    Retrieves an :class:`EphemerisObject` object or computes the coordinates for
    a solar system object.  
    
    :param str objname: 
        The (case-sensitive) name of the object (see
        :func:`list_solar_system_objects` for valid names)
    :param jds:  
        The jds at which to compute the coordinates or None to return
        :class:`EphemerisObject` instances.
    :param coordsys: 
        Specifies the coordinate system class of the returned ephemerides. See
        :meth:`EphemerisObject.__call__` for the details. Ignored if `jds` is
        None.
    
    :returns: 
        A subclass of :class:`EphemerisObject` if `jds` is None, or the
        appropriate coordinate object (or objects) as detailed in
        :meth:`EphemerisObject.__call__` if `jds` is not None.
    
    """
    from inspect import isclass
    from copy import copy
    
    #entrys in _ss_ephems may be classes or objects, so do the appropriate action.
    eobj = _ss_ephems[objname]
    if jds is None:
        if isclass(eobj):
            return eobj()
        else:
            return copy(eobj)
    else:
        if isclass(eobj):
            return eobj()(jds,coordsys)
        else:
            return eobj(jds,coordsys)
    
def list_solar_system_objects():
    """
    Returns a list of objects that can be returned by
    :func:`get_solar_system_object`.
    """
    return _ss_ephems.keys()

_ss_ephems = {}
def set_solar_system_ephem_method(meth=None):
    """
    Sets the type of ephemerides to use.  Must be 'keplerian' for now.
    """
    global _ss_ephems
    
    if meth is None:
        meth = 'keplerian'
    if meth=='keplerian':
        _ss_ephems = _keplerian_ephems()
    else:
        raise ValueError('Solar System ephemerides method %s not available'%s)

    #Add in Simon 94 Moon and SOFA earth pv if needed
    if 'Moon' not in _ss_ephems:
        _ss_ephems['Moon'] = Moon
    if 'Earth' not in _ss_ephems:
        _ss_ephems['Earth'] = Earth
        

#<--------------Moon location from Simon 94------------------------------------>

def _ecl_to_icrs(x,y,z,jd):
    """
    tilt from ecliptic to ICRS orientation
    """
    #from math import sin,cos,radians
    #obliquity = radians(23.43928)
    #seps = sin(obliquity)
    #ceps = cos(obliquity)
    seps = 0.39777697800876388
    ceps = 0.91748213920828747
    
    yp = ceps*y - seps*z
    zp = seps*y + ceps*z
    
    return x,yp,zp

class Moon(KeplerianObject):
    """
    A :class:`KeplerianObject` for Earth's moon based on the model of Simon et
    al. 1994. The osculating elements for the "1992 values" are used to compute
    the location. The output coordinates are
    :class:`astropysics.coords.coordsys.RectangularGCRSCoordinates`.
    """
    def __init__(self):
        from math import degrees
        from .coordsys import RectangularGCRSCoordinates
        from ..obstools import calendar_to_jd
        from ..constants import aupercm,asecperrad
        auperkm = aupercm*1e5
        degperasec = degrees(1./asecperrad)
        
        jd4000b = calendar_to_jd((-3999,1,1))
        jd8000 = calendar_to_jd((8000,1,1))
        kw = {'name':'Moon','validjdrange':(jd4000b,jd8000),
                  'outcoords':RectangularGCRSCoordinates, #origin already geocentric
                  'outtransfunc':_ecl_to_icrs}
        
        kw['a'] = (383397.7725*auperkm,.0040*auperkm) #Simon values are in km
        kw['e'] = (.055545526,-1.6e-8)
        kw['i'] = (5.15668983,
             -.00008*degperasec,
              .02966*degperasec,
             -.000042*degperasec,
             -.00000013*degperasec)
        kw['Lp'] = (83.35324312,
               14643420.2669*degperasec,
              -38.2702*degperasec,
              -.045047*degperasec,
               .00021301*degperasec) 
        kw['Lan'] = (125.04455501,
               -6967919.3631*degperasec,
                6.3602*degperasec,
                .007625*degperasec,
                .00003586*degperasec)
        kw['L'] = (218.31664563,
              1732559343.48470*degperasec,
             -6.3910*degperasec,
              .006588*degperasec,
             -.00003169*degperasec)
        
        KeplerianObject.__init__(self,**kw)
        
    def getPhase(self,viewobj=None,illumobj=None):
        """
        Computes the phase of the Moon. This is computed as viewed from the
        Earth and illuminated by the Sun if `viewobj` and `illumobj` are None -
        otherwise, see :meth:`KeplerianObject.getPhase` for the meaning of the
        parameters.
        
        :returns: A float where 1 is full and 0 is new.
        
        """
        if viewobj is None and illumobj is None:
            from math import sqrt
            
            c = self()
            xg,yg,zg = c.x,c.y,c.z #relative to earth
            xe,ye,ze = earth_pos_vel(self.jd,False)[0] #heliocentric
            xm = xe + xg
            ym = ye + yg
            zm = ze + zg
            
            r = sqrt(xm*xm+ym*ym+zm*zm)
            R = sqrt(xg*xg+yg*yg+zg*zg)
            s = sqrt(xe*xe+ye*ye+ze*ze)
            
            return (1+(r*r + R*R - s*s)/(2*r*R))/2
        else:
            return KeplerianObject.getPhase(self,viewobj,illumobj)
        
        
#<--------------Earth location and velocity, based on SOFA epv00--------------->
# SIMPLIFIED SOLUTION from the planetary theory VSOP2000
# X. Moisson, P. Bretagnon, 2001, 
# Celes. Mechanics & Dyn. Astron., 80, 3/4, 205-213


        
def _load_earth_series(datafn='earth_series.tab'):
    """
    Load series terms from VSOP2000 simplified solution
    """
    from ..utils.io import get_package_data
    from numpy import array,matrix
    from math import sqrt
    
    lines = [l for l in get_package_data(datafn).split('\n') if not l.startswith('#') if not l=='']
    
    lst = None
    lsts = {}
    for l in lines:
        if l.endswith(':'):
            #new variable
            lsts[l[:-1]] = lst = []
        else:
            ls = l.split(',')
            if ls[-1]=='':
                lst.extend(ls[:-1])
            else:
                lst.extend(ls)
                
                
    res = {}
    
    #first add all matricies
    for k,v in lsts.items():
        if k.endswith('mat'):
            mat = matrix(v,dtype=float)
            res[k] = mat.reshape(sqrt(mat.size),sqrt(mat.size))
    
    #now construct all the x,y,z combination series'
    coeffnms = set([k[:-1] for k in lsts.keys() if not k.endswith('mat')])
        
    #bad any coeff sets where x,y, and z don't match so that the missing entries are 0
    for cnm in coeffnms:
        xs = lsts[cnm+'x']
        ys = lsts[cnm+'y']
        zs = lsts[cnm+'z']
        mx = max(max(len(xs),len(ys)),len(zs))
        
        if len(xs)<mx:
            for i in range(mx-len(xs)):
                xs.append(0)
        if len(ys)<mx:
            for i in range(mx-len(ys)):
                ys.append(0)
        if len(zs)<mx:
            for i in range(mx-len(zs)):
                zs.append(0)
                
        res[cnm+'coeffs'] = array([xs,ys,zs],dtype=float)
    
    return res
    
_earth_series_coeffs = _load_earth_series()

def _compute_earth_series(t,coeffs0,coeffs1,coeffs2):
    """
    Internal function to computes Earth location/velocity components from series
    coefficients.
    
    :param t:  T = JD - JD_J2000
    :param coeffs0: constant term
    :param coeffs1: T^1 term
    :param coeffs2: T^2 term
    
    :returns: pos,vel
    """
    
    #T^0 terms
    acs = coeffs0[:,0::3]
    bcs = coeffs0[:,1::3]
    ccs = coeffs0[:,2::3]
    ps = bcs + ccs*t
    pos = np.sum(acs*np.cos(ps),axis=1)
    vel = np.sum(-acs*ccs*np.sin(ps),axis=1)
    
    #T^1 terms
    acs = coeffs1[:,0::3]
    bcs = coeffs1[:,1::3]
    ccs = coeffs1[:,2::3]
    cts = ccs*t
    ps = bcs + cts
    cps = np.cos(ps)
    pos += np.sum(acs*t*cps,axis=1)
    vel += np.sum(acs*(cps - cts*np.sin(ps)),axis=1)
    
    #T^2 terms
    acs = coeffs2[:,0::3]
    bcs = coeffs2[:,1::3]
    ccs = coeffs2[:,2::3]
    cts = ccs*t
    ps = bcs + cts
    cps = np.cos(ps)
    pos += np.sum(acs*cps*t*t,axis=1)
    vel += np.sum(acs*t*(2.0*cps - cts*np.sin(ps)),axis=1)
    
    return pos,vel

class Earth(EphemerisObject):
    """
    Earth position (and velocity) relative to solar system barycenter. Adapted
    from SOFA fits generated from DE405 solar system model. Outputs
    :class:`astropysics.coords.coordsys.RectangularICRSCoordinates` objects.
    """
    
    
    def __init__(self):
        from ..obstools import calendar_to_jd
        
        jd1900 = calendar_to_jd((1900,1,1))
        jd2100 = calendar_to_jd((2100,1,1))
        EphemerisObject.__init__(self,'Earth',(jd1900,jd2100))
        
    def _getCoordObj(self):
        from .coordsys import RectangularICRSCoordinates
        from ..obstools import jd_to_epoch
        
        x,y,z = earth_pos_vel(self.jd,True)[0]
        return RectangularICRSCoordinates(x=x,y=y,z=z,epoch=jd_to_epoch(self.jd))
    
    def getVelocity(self,jd=None,kms=True):
        """
        Computes and returns the velociy of the Earth relative to the solar
        system barycenter.
        
        :params jd: 
            The julian date at which to compute the velocity, or None to use the
            :attr:`jd` attribute.
        :params bool kms: 
            If True, velocities are returned in km/s, otherwise AU/yr.
            
        :returns: vx,vy,vz in km/s if `kms` is True, otherwise AU/yr.
            
        """
        return earth_pos_vel(self.jd if jd is None else jd,True,kms)[1]

def earth_pos_vel(jd,barycentric=False,kms=True):
    """
    Computes the earth's position and velocity at a given julian date. 
    
    Output coordinates are aligned to GCRS/ICRS so that +z is along the
    spherical GCRS pole and +x points down the spherical GCRS origin.
    
    Adapted from SOFA function epv00.c from fits to DE405, valid from ~
    1900-2100. 
    
    :param jd: The julian date for the positions and velocities.
    :param bool barycentric: 
        If True, the output positions and velocities are relative to the solar
        system barycenter. Otherwise, positions and velocities are heliocentric.
    :param bool kms: If True, velocity outputs are in km/s, otherwise AU/yr.
    
    :returns: 
        2 3-tuples (x,y,z),(vx,vy,vz) where x,y, and z are GCRS-aligned
        positions in AU, and vx,vy, and vz are velocities in km/s if `kms` is
        True, or AU/yr.
        
    
    """
    from ..obstools import jd2000
    from ..constants import aupercm,secperyr
    from warnings import warn
    
    coeffsd = _earth_series_coeffs
    
    t = (jd-jd2000)/365.25 #Julian years since 2000.0 reference
    
    if t > 100 or t < -100:
        warn('JD {0} is not in range 1900-2100 CE for Earth position'.format(jd),EphemerisAccuracyWarning)
        
    pos,vel = _compute_earth_series(t,coeffsd['h0coeffs'],coeffsd['h1coeffs'],coeffsd['h2coeffs'])
    
    if barycentric:
        poff,voff = _compute_earth_series(t,coeffsd['b0coeffs'],coeffsd['b1coeffs'],coeffsd['b2coeffs'])
        pos += poff
        vel += voff
    
    #this rotates the analytic model from the series to DE405/BCRS
    #same as rotating by -23d26'21.4091" about x then 0.0475" about z        
    pos = (coeffsd['ec2bcrsmat']*pos.reshape(3,1)).A.ravel()
    vel = (coeffsd['ec2bcrsmat']*vel.reshape(3,1)).A.ravel()
    
    if kms:
        #AU/yr*(   km/AU  *  yr/sec ) = km/sec
        vel *= (1e-5/aupercm/secperyr)
        
    return pos,vel
    
#<---------------Approximate Keplerian major planet ephemerides---------------->
def _load_jpl_orb_elems(datafn):
    from ..utils.io import get_package_data
    
    s = get_package_data(datafn)
    data = []
    names = []
        
    for l in s.split('\n'):
        ls = l.strip()
        
        if not (ls=='' or ls.startswith('#')):
            lss = ls.split()
            if len(lss)>6:
                names.append(' '.join(lss[:-6]))
                data.append(lss[-6:])
            elif len(lss)<=5:
                names.append(lss[0])
                dat = lss[1:]
                while len(dat)<4:
                    dat.append(0)
                data.append(dat)
            else:
                data.append(lss[-6:])
    arrs = np.split(np.array(data,dtype=float),len(names))
    return dict(zip(names,arrs))
    
            
    return namearrs
            
_shortelems = _load_jpl_orb_elems('ss_elems_1.dat') #array elements are 2 x 6
_longelems  = _load_jpl_orb_elems('ss_elems_2.dat') #array elements are 2 x 6
_longelemsb = _load_jpl_orb_elems('ss_elems_2b.dat') #array elements are 1 x 4

def _ecl_to_gcrs(x,y,z,jd):
    from math import sin,cos,radians
    
    #tilt to ICRS orientation
    #sine = sin(radians(23.43928))
    sine = 0.39777697800876388
    #cose = cos(radians(23.43928))
    cose = 0.917482139208
    
    xp = x
    yp = cose*y - sine*z
    zp = sine*y + cose*z
    #xp,yp,zp = _ecl_icrs(x,y,z,jd)
    
    #Now offset to earth coordinates
    (xe,ye,ze),(vxe,vye,vze) = earth_pos_vel(jd,barycentric=True)
    
    return xp-xe,yp-ye,zp-ze

def _keplerian_ephems():
    """
    Generates dictionary with Keplerian ephemerides for the solar system from
    JPL Solar System Dynamics group Approximate positions
    (http://ssd.jpl.nasa.gov/?planet_pos).
    """
    from ..obstools import calendar_to_jd
    from .coordsys import RectangularGCRSCoordinates
    
    d = {}
    
    jd1800 = calendar_to_jd((1800,1,1))
    jd2050 = calendar_to_jd((2050,1,1))
    jd3000ce = calendar_to_jd((3000,1,1))
    jd3000bce = calendar_to_jd((-2999,1,1))
    
    for n,oes in _shortelems.iteritems():
        kw = {'name':n,'validjdrange':(jd1800,jd2050),
              'outcoords':RectangularGCRSCoordinates,
              'outtransfunc':_ecl_to_gcrs}
        for oen,oe in zip(('a','e','i','L','Lp','Lan'),oes.T):
            kw[oen] = oe
        d[n] = KeplerianObject(**kw)
        
    for n,oes in _longelems.iteritems():
        n = n+'-long'
        kw = {'name':n,'validjdrange':(jd3000bce,jd3000ce),
              'outcoords':RectangularGCRSCoordinates,
              'outtransfunc':_ecl_to_gcrs}
        for oen,oe in zip(('a','e','i','L','Lp','Lan'),oes.T):
            kw[oen] = oe
        d[n] = obj = KeplerianObject(**kw)
        if n in _longelemsb:
            obj._bcsf = _longelemsb[n][0]
    
    return d

    
    
#<----Lunisolar/Solar system fundamental arguments, mostly used in coordsys---->
#from 2003 IERS Conventions via adaptations of SOFA 

_mean_anomaly_of_moon_poly = np.poly1d([-0.00024470,
                                        0.051635,
                                        31.8792,
                                        1717915923.2178,
                                        485868.249036])
def _mean_anomaly_of_moon(T):
    """
    From SOFA (2010) - IERS 2003 conventions
    
    :param T: Julian centuries from 2000.0 in TDB (~TT for this function)
    :type T: float or array-like
    
    :returns: Mean anomaly of the moon  in radians
    """
    from ..constants import asecperrad
    return np.fmod(_mean_anomaly_of_moon_poly(T)/asecperrad,_twopi)

_mean_anomaly_of_sun_poly = np.poly1d([-0.00001149,
                                       0.000136,
                                       -0.5532,
                                       129596581.0481,
                                       1287104.793048])
def _mean_anomaly_of_sun(T):
    """
    From SOFA (2010) - IERS 2003 conventions
    
    :param T: Julian centuries from 2000.0 in TDB (~TT for this function)
    :type T: float or array-like
    
    :returns: Mean anomaly of the sun  in radians
    """
    from ..constants import asecperrad
    return np.fmod(_mean_anomaly_of_sun_poly(T)/asecperrad,_twopi)
   
_mean_long_of_moon_minus_ascnode_poly = np.poly1d([0.00000417,
                                              -0.001037,
                                              -12.7512,
                                              1739527262.8478,
                                              335779.526232])
def _mean_long_of_moon_minus_ascnode(T):
    """
    From SOFA (2010) - IERS 2003 conventions
    
    :param T: Julian centuries from 2000.0 in TDB (~TT for this function)
    :type T: float or array-like
    
    :returns: Mean logitude of the Moon minus the ascending node in radians
    
    """
    from ..constants import asecperrad
    return np.fmod(_mean_long_of_moon_minus_ascnode_poly(T)/asecperrad,_twopi)

_mean_elongation_of_moon_from_sun_poly = np.poly1d([-0.00003169,
                                                    0.006593,
                                                    -6.3706,
                                                    1602961601.2090,
                                                    1072260.703692])
def _mean_elongation_of_moon_from_sun(T):
    """
    From SOFA (2010) - IERS 2003 conventions
    
    :param T: Julian centuries from 2000.0 in TDB (~TT for this function)
    :type T: float or array-like
    
    :returns: Mean elongation of the Moon from the Sun in radians
    """
    from ..constants import asecperrad
    return np.fmod(_mean_elongation_of_moon_from_sun_poly(T)/asecperrad,_twopi)

_mean_long_ascnode_moon_poly = np.poly1d([-0.00005939,
                                          0.007702,
                                          7.4722,
                                          -6962890.5431,
                                          450160.398036])
def _mean_long_asc_node_moon(T):
    """
    From SOFA (2010) - IERS 2003 conventions
    
    :param T: Julian centuries from 2000.0 in TDB (~TT for this function)
    :type T: float or array-like
    
    :returns: Mean longitude of the Moon's ascending node in radians 
    """
    from ..constants import asecperrad
    return np.fmod(_mean_long_ascnode_moon_poly(T)/asecperrad,_twopi)
   
def _long_venus(T):
    """
    From SOFA (2010) - IERS 2003 conventions
    
    :param T: Julian centuries from 2000.0 in TDB (~TT for this function)
    :type T: float or array-like
    
    :returns: Mean longitude of Venus in radians
    """
    return np.fmod(3.176146697 + 1021.3285546211*T,_twopi)
   
def _long_earth(T):
    """
    From SOFA (2010) - IERS 2003 conventions
    
    :param T: Julian centuries from 2000.0 in TDB (~TT for this function)
    :type T: float or array-like
    
    :returns: Mean longitude of Earth in radians
    """
    return np.fmod(1.753470314 + 628.3075849991*T,_twopi)

def _long_prec(T):
    """
    From SOFA (2010) - IERS 2003 conventions
    
    :param T: Julian centuries from 2000.0 in TDB (~TT for this function)
    :type T: float or array-like
    
    :returns: General accumulated precession in longitude in radians
    """
    return (0.024381750 + 0.00000538691*T)*T


#Set to solar system ephemerides to default
set_solar_system_ephem_method()

