# -*- coding: utf-8 -*-

# TODO: convert some methods to numeric attributes.

from decimal import Decimal, getcontext

getcontext().prec = 3


class Telescope(object):
    """
    Simple class that represents user's abstract telescope.

    """

    __count = 0

    def __init__(self, aperture, focal_length, name=None, brand=None, model=None, **kwargs):
        """
        Creates instance of the abstract telescope with minimal required params.
        At least focal_length and aperture params must be present.
        Name can be generated automatically in form 'Telescope #n' where n = number of created telescopes.
        `eyepieces` attribute is created for holding eyepieces for defined telescope.
        `additional_info` attribute can be used for holding user-defined data, for example mount or viewfinder type etc.

        :param focal_length: focal length of the telescope in millimeters
        :type focal_length: int
        :param aperture: aperture (diameter) of the telescope in millimeters
        :type aperture: int
        :param name: name of the telescope
        :type name: str
        :param brand: brand (merchant) of the telescope
        :type brand: str
        :param model: model name of the telescope
        :type model: str
        """

        Telescope.__count += 1
        if not name:
            name = 'Telescope #{}'.format(Telescope.__count)
        self.name = name
        self.brand = brand
        self.model = model
        self.focal_length = Decimal(focal_length)
        self.aperture = Decimal(aperture)
        self.eyepieces = {}
        self.additional_info = {}
        for key, value in kwargs.iteritems():
            self.additional_info[key] = value

    def __repr__(self):
        return ('{0}({1}, {2}, {3}, {4}, {5})'.format(self.__class__.__name__, self.name, self.brand, self.model,
                                                      self.focal_length, self.aperture))

    def __str__(self):
        return 'class: {0}, name: {1}, brand: {2}, model: {3}, focal length: {4}, aperture: {5}'.format(
            self.__class__.__name__, self.name, self.brand, self.model,
            self.focal_length, self.aperture)

    def add_eyepiece(self, focal_length, name=None, brand=None, afov=None):
        """
        Method that adds eyepiece representation into `self.eyepieces` attribute  for further calculations.
        If `name` param is not passed, it will generate default name for the eyepiece in form 'Eyepiece #n`,
        where n = number of eyepieces + 1.

        :param focal_length: focal length of the eyepiece im millimeters
        :type focal_length: int
        :param name: name of the eyepiece, used as key for dict with eyepiece representation
        :type name: str
        :param brand: brand of the eyepiece
        :type brand: str
        :param afov: field of view of the eyepiece in degrees
        :type afov: int
        :Example:

            >>> import pprint
            >>> myscope = Telescope(name='My Scope', model='Super', brand='MegaScope', aperture=100, focal_length=1000)
            >>> myscope.add_eyepiece(focal_length=25, name='MyOcular', brand='SuperBrand', afov=50)
            >>> pprint.pprint(myscope.eyepieces)
            {'MyOcular': {'afov': 50, 'brand': 'SuperBrand', 'focal_length': 25}}
            >>> myscope.add_eyepiece(focal_length=10, brand='Custom', afov=50)
            >>> myscope.add_eyepiece(focal_length=20, brand='Custom', afov=50)
            >>> pprint.pprint(myscope.eyepieces)
            {'Eyepiece #2': {'afov': 50, 'brand': 'Custom', 'focal_length': 10},
             'Eyepiece #3': {'afov': 50, 'brand': 'Custom', 'focal_length': 20},
             'MyOcular': {'afov': 50, 'brand': 'SuperBrand', 'focal_length': 25}}
        """
        if not name:
            name = 'Eyepiece #{}'.format(len(self.eyepieces) + 1)
        self.eyepieces[name] = {'focal_length': focal_length, 'brand': brand, 'afov': afov}

    def get_dawes_limit(self, numeric=False):
        """
        Method that calculates theoretical Dawes limit for telescope.

        :param numeric: if set to True, result will be returned as numeric value
        :type numeric: bool
        :return: string or Decimal value in arc seconds
        :rtype: str or Decimal
        :Example:

            >>> myscope = Telescope(name='My Scope', model='Super', brand='MegaScope', aperture=100, focal_length=1000)
            >>> print(myscope.get_dawes_limit())
            1.16"
            >>> print(myscope.get_dawes_limit(numeric=True))
            1.16
        """
        resolution = Decimal(116) / self.aperture
        if numeric:
            return resolution
        return '{}"'.format(resolution)

    def get_rayleigh_criterion(self, numeric=False):
        """
        Method that calculates theoretical Rayleigh criterion for telescope.

        :param numeric: if set to True, result will be returned as numeric value
        :type numeric: bool
        :return: string or Decimal value in arc seconds
        :rtype: str or Decimal
        :Example:
            >>> myscope = Telescope(name='My Scope', model='Super', brand='MegaScope', aperture=100, focal_length=1000)
            >>> print(myscope.get_rayleigh_criterion())
            1.38"
            >>> print(myscope.get_rayleigh_criterion(numeric=True))
            1.38
        """
        resolution = Decimal(138) / self.aperture
        if numeric:
            return resolution
        return '{}"'.format(resolution)

    def get_exit_pupil(self, eyepiece, numeric=False):
        """
        Method that calculates exit pupil for combination of telescope and eyepiece.
        Eyepiece must be added via `add_eyepiece` method.

        :param eyepiece: name of the eyepiece from `self.eyepieces` attribute
        :type eyepiece: str
        :param numeric: if set to True, result will be returned as numeric value
        :type numeric: bool
        :return: string or Decimal with exit pupil value in millimeters
        :rtype: str or Decimal
        :Example:

            >>> myscope = Telescope(name='My Scope', model='Super', brand='MegaScope', aperture=100, focal_length=1000)
            >>> myscope.add_eyepiece(focal_length=25, name='MyOcular', afov=50)
            >>> print(myscope.get_exit_pupil(eyepiece='MyOcular'))
            exit pupil: 2.5mm
            >>> print(myscope.get_exit_pupil(eyepiece='MyOcular', numeric=True))
            2.5
        """
        exit_pupil = self.aperture / self.get_eyepiece_magnification(eyepiece, numeric=True)
        if numeric:
            return exit_pupil
        return 'exit pupil: {}mm'.format(exit_pupil)

    def get_eyepiece_magnification(self, eyepiece, numeric=False):
        """
        Method that calculates magnification of the telescope combined with eyepiece.
        Eyepiece must be added via `add_eyepiece` method.

        :param eyepiece: name of the eyepiece from `self.eyepieces` attribute
        :type eyepiece: str
        :param numeric: if set to True, result will be returned as numeric value
        :type numeric: bool
        :return: string or Decimal with magnification value
        :rtype: str or Decimal
        :Example:

            >>> myscope = Telescope(name='My Scope', model='Super', brand='MegaScope', aperture=100, focal_length=1000)
            >>> myscope.add_eyepiece(focal_length=10, name='MyOcular')
            >>> print(myscope.get_eyepiece_magnification(eyepiece='MyOcular'))
            100X
            >>> print(myscope.get_eyepiece_magnification(eyepiece='MyOcular', numeric=True))
            100
        """
        eyepiece_magnification = Decimal(self.focal_length / self.eyepieces[eyepiece]['focal_length'])
        if numeric:
            return eyepiece_magnification
        return '{}X'.format(eyepiece_magnification)

    def get_field_of_view(self, eyepiece, numeric=False):
        """
        Method that calculates true field of view for combination of telescope and eyepiece.
        Eyepiece must be added via `add_eyepiece` method.

        :param eyepiece: name of the eyepiece from `self.eyepieces` attribute
        :type eyepiece: str
        :param numeric: if set to True, result will be returned as numeric value
        :type numeric: bool
        :return: string or Decimal with field of view value in degrees
        :rtype: str or Decimal
        :Example:

            >>> myscope = Telescope(name='My Scope', model='Super', brand='MegaScope', aperture=100, focal_length=1000)
            >>> myscope.add_eyepiece(focal_length=25, name='MyOcular', afov=50)
            >>> print(myscope.get_field_of_view(eyepiece='MyOcular'))
            FOV: 1.25
            >>> print(myscope.get_field_of_view(eyepiece='MyOcular', numeric=True))
            1.25
        """
        magnification = self.get_eyepiece_magnification(eyepiece, numeric=True)
        fov = self.eyepieces[eyepiece]['afov'] / magnification
        if numeric:
            return fov
        return 'FOV: {}'.format(fov)

    def get_focal_ratio(self, numeric=False):
        """
        Method that calculates focal ratio of the telescope.

        :param numeric: if set to True, result will be returned as numeric value
        :type numeric: bool
        :return: string or Decimal with f-number value
        :rtype: str or Decimal
        :Example:

            >>> myscope = Telescope(name='My Scope', model='Super', brand='MegaScope', aperture=100, focal_length=1000)
            >>> print(myscope.get_focal_ratio())
            f/10
            >>> print(myscope.get_focal_ratio(numeric=True))
            10
        """
        foc_ratio = self.focal_length / self.aperture
        if numeric:
            return foc_ratio
        return 'f/{}'.format(foc_ratio)

    def get_max_magnification(self, numeric=False):
        """
        Method that calculates telescope's theoretical highest useful magnification.

        :param numeric: if set to True, result will be returned as numeric value
        :type numeric: bool
        :return: string or Decimal with maximum magnification value
        :rtype: str or Decimal
        :Example:

            >>> myscope = Telescope(name='My Scope', model='Super', brand='MegaScope', aperture=100, focal_length=1000)
            >>> print(myscope.get_max_magnification())
            200X
            >>> print(myscope.get_max_magnification(numeric=True))
            200
        """
        max_magnification = self.aperture * 2
        if numeric:
            return max_magnification
        return '{}X'.format(max_magnification)

    def get_info(self):
        """
        Method that calculates common specifications for the defined telescope.

        :return: calculated common specifications
        :rtype: dict
        :Example:

            >>> import pprint
            >>> myscope = Telescope( aperture=100, focal_length=1000, name='My Scope', model='Super',\
                brand='MegaScope', mount='EQ2', viewfinder='Red Dot')
            >>> myscope.add_eyepiece(focal_length=25, name='MyOcular', brand='SuperBrand', afov=50)
            >>> pprint.pprint(myscope.get_info())
            {'additional info': {'mount': 'EQ2', 'viewfinder': 'Red Dot'},
             'angular resolution (Dawes)': '1.16"',
             'angular resolution (Rayleigh)': '1.38"',
             'aperture': '100mm',
             'brand': 'My Scope',
             'eyepieces': {'MyOcular': {'afov': 50,
                                        'brand': 'SuperBrand',
                                        'focal_length': 25}},
             'focal length': '1000mm',
             'focal ratio': 'f/10',
             'max magnification': '200X',
             'name': 'My Scope'}
        """
        info = {
            'name': self.name,
            'brand': self.name,
            'focal length': '{}mm'.format(self.focal_length),
            'aperture': '{}mm'.format(self.aperture),
            'max magnification': self.get_max_magnification(),
            'focal ratio': self.get_focal_ratio(),
            'angular resolution (Dawes)': self.get_dawes_limit(),
            'angular resolution (Rayleigh)': self.get_rayleigh_criterion(),
            'eyepieces': self.eyepieces,
            'additional info': self.additional_info
        }
        return info
