from pdfmargin import PDFMargin
from pdfcursor import PDFCursor
from zlib import compress

class PDFPage(object):

    """ Defines the structure of an individual page.
        Margins are set by default. If you want to change
        them, it should be done through the Document object,
        or before any content is written to the page.

    """

    def __init__(self, orientation="P", layout="letter", margin=None):
        # Additional layout sizes may be added to this dictionary.
        # Width then height, in pixels, in portrait orientation.
        self.layout_dict = {'a3': (841.89, 1190.55),
                            'a4': (595.28, 841.89),
                            'a5': (420.94, 595.28),
                            'letter': (612, 792),
                            'legal': (612, 1008),
                            '11x17': (792, 1224)
                            }

        self.set_page_size(layout)

        # "P" or "L"
        self.orientation = orientation

        # Each page has a cursor.
        self.cursor = PDFCursor()

        # Initialize the Page Margin.
        self.margin = margin
        self.set_orientation(orientation)
        self.set_margins(margin)

        self.orientation_change = False
        self.buffer = ""

    # Page may be retrieved and manipulated using these:
    def set_orientation(self, orientation="P"):
        self.orientation = orientation.lower()
        if self.orientation == 'p' or self.orientation == 'portrait':
            self.size = self.page_size
        elif self.orientation == 'l' or self.orientation == 'landscape':
            self.size = (self.page_size[1], self.page_size[0])
        else:
            raise Exception('Incorrect orientation: ', self.orientation)
        self._set_dimensions()
        self._set_bounds()

    def set_page_size(self, layout):
        """ Valid choices: 'a3, 'a4', 'a5', 'letter', 'legal', '11x17'.

        """
        self.layout = layout.lower()
        if self.layout in self.layout_dict:
            self.page_size = self.layout_dict[self.layout]
        else:
            dimensions = self.layout.split('x')
            if len(dimensions) == 2:
                self.page_size = (float(dimensions[0]) * 72, float(dimensions[1]) * 72)
            else:
                raise IndexError("Page is two dimensions, given: %s" % len(dimensions))

    def set_margins(self, margin=None):
        if margin is None:
            self.margin = PDFMargin()
        elif isinstance(margin, PDFMargin):
            self.margin = margin
        else:
            raise Exception("Invalid Margin object")
        self._set_dimensions()
        self._set_bounds()

    def get_margins(self):
        return self.margin

    # Private methods for building pages
    def _compress(self):
        """ Uses zlib to compress page buffers. Compression
            option is enabled through PDFLite object's
            setCompression method.

        """
        self.buffer = compress(self.buffer)

    def _set_index(self, value):
        self.index = value

    def _set_dimensions(self):
        self.width = self.size[0]
        self.height = self.size[1]

    def _change_orientation(self):
        if self.orientation_change is False:
            self.orientation_change = True
        else:
            self.orientation_change = False

        self.size = (self.size[1], self.size[0])
        self._set_dimensions()
        self._set_bounds()

    def _set_cursor(self, cursor):
        self.cursor = cursor
        self._set_bounds()

    def _set_bounds(self):
        if self.margin is None:
            xmin = 0
            xmax = self.size[0]
            ymin = 0
            ymax = self.size[1]
        else:
            xmin = 0 + self.margin.left
            xmax = self.size[0] - self.margin.right
            ymin = 0 + self.margin.top
            ymax = self.size[1] - self.margin.bottom

        self.cursor.set_bounds(xmin, ymin, xmax, ymax, self.size[1])

    def _add_newline(self, font, number=1, double_spacing=None):
        if double_spacing is not None:
            self.cursor.y_plus((font.line_size * number * double_spacing))
        self.cursor.y_plus((font.line_size * number))
        self.cursor.x_reset()

    def _add_indent(self, font, number=4):
        self.cursor.x_plus(number * font._string_width(' '))


from pdfrow import PDFRow
from pdfcolumn import PDFColumn

class PDFTable(object):
    def __init__(self, session, page, rows, cols, cursor, def_font):
        self.session = session
        self.page = page
        self.font = def_font
        self.number_of_rows = rows
        self.number_of_columns = cols

        self.cursor = cursor
        self.text_cursor = self.cursor.copy()
        self.border_cursor = self.cursor.copy()

        self._initiate_cells()

    def _initiate_cells(self):
        self.rows = []
        self.columns = []

        for x in range(self.number_of_columns):
            self.columns.append(PDFColumn(parent=self))

        for x in range(self.number_of_rows):
            self.rows.append(PDFRow(self, x, self.number_of_columns,
                                    self.text_cursor, self.border_cursor))

        for x in range(self.number_of_rows):
            for y in range(self.number_of_columns):
                self.columns[y].cells.append(self.rows[x].cells[y])

    # Public Methods, called to create table
    def write(self, row, col, text, format=None):
        cell = self.rows[row][col]
        cell._set_text(text)
        if format is not None:
            cell._set_format(format)

    def write_row(self, row, col_start, data, format):
        i = 0
        for c in range(col_start, col_start + len(data)):
            self.write(row, c, data[i], format)
            i += 1

    def write_column(self, row_start, col, data, format):
        i = 0
        for r in range(row_start, row_start + len(data)):
            self.write(r, col, data[i], format)
            i += 1

    def set_format(self, row, col, format):
        cell = self.rows[row][col]
        cell._set_format(format)

    def set_format_row(self, row, col_start, format):
        for c in range(col_start, self.number_of_columns):
            self.set_format(row, c, format)

    def set_format_column(self, row_start, col, format):
        for r in range(row_start, self.number_of_rows):
            self.set_format(r, col, format)

    def set_column_width(self, column, value):
        self.columns[column]._set_max_width(value)

    def set_row_height(self, row, value):
        self.rows[row]._set_max_height(value)

    # Private methods to build table
    def _draw(self):
        """ Don't use this, use document.draw_table """
        self._compile()
        self.rows[0]._advance_first_row()
        self._set_borders()
        self._draw_fill()
        self._draw_borders()
        self._draw_text()
        self._set_final_cursor()

    def _draw_text(self):
        for i in range(len(self.rows)):
            self.rows[i]._draw_text()
            self.text_cursor.x_reset()
            if (i + 1) < len(self.rows):
                self.text_cursor.y_plus(self.rows[i + 1].max_height)

    def _set_borders(self):
        for i in range(len(self.rows)):
            self.rows[i]._set_borders()
            self.border_cursor.x_reset()
            if (i + 1) < len(self.rows):
                self.border_cursor.y_plus(self.rows[i].max_height)

    def _draw_borders(self):
        for i in range(len(self.rows)):
            self.rows[i]._draw_borders()

    def _draw_fill(self):
        for i in range(len(self.rows)):
            self.rows[i]._draw_fill()

    def _set_final_cursor(self):
        if self.text_cursor > self.border_cursor:
            self.cursor = self.text_cursor
        else:
            self.cursor = self.border_cursor

    def _compile(self):
        for row in self.rows:
            for cell in row:
                cell._compile()

        for col in self.columns:
            col._finish()

        for row in self.rows:
            row._finish()
            for cell in row:
                cell._finish()


"""

To create a new tag called 'user_agent = curl' and associate it with a log called
'someset/somelog',

.. code-block:: python

    from logentries_api import Tags, Hooks, Labels, LogSets

    label = Labels().create('user_agent = curl')
    log = LogSets().get('someset/somelog')
    tag = Tags().create(label['sn'])
    hook = Hooks().create(
        name=label['title'],
        regexes=['user_agent = /curl\/[\d.]*/'],
        tag_id=tag['id'],
        logs=[log['key']]
    )

"""
from enum import Enum
import random

from logentries_api.base import Resource, ApiActions, ApiUri

class Colors(Enum):
    """
    These are a few preselected hex colors you can use for labels.

    Selected from the Ambition Style Guide
    """
    GREEN = '18ab7e'
    DARK_PURPLE = '374259'
    PURPLE = '554973'
    GRAY = 'b5bdc4'
    BLUE = '278abe'
    YELLOW = 'f9d94f'
    ORANGE = 'f26c36'
    RED = 'e61e56'

def random_color():
    """
    Create a random hex color

    :rtype: str
    """
    r = lambda: random.randint(0, 255)
    color = ('%02X%02X%02X' % (r(), r(), r()))
    return color

def dict_is_subset(d1, d2):
    return all((k in d2 and d2[k] == v) for k, v in d1.items())

class LimitRanges(Enum):
    HOUR = 'hour'
    DAY = 'day'

class RateRanges(Enum):
    HOUR = 'hour'
    DAY = 'day'

class Labels(Resource):
    """
    A class for dealing with labels

    Labels are just text and a color. We'll associate tags with them later
    """

    def create(self, name, description=None, color=None):
        """
        Creates a new label and returns the response

        :param name: The label name
        :type name: str

        :param description: An optional description for the label. The name is
            used if no description is provided.
        :type description: str

        :param color: The hex color for the label (ex: 'ff0000' for red). If no
            color is provided, a random one will be assigned.
        :type color: str

        :returns: The response of your post
        :rtype: dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        data = {
            'name': name,
            'title': name,
            'description': description or name,
            'appearance': {
                'color': color or random_color()
            }
        }
        # Yes, it's confusing. the `/tags/` endpoint is used for labels
        return self._post(
            request=ApiActions.CREATE.value,
            uri=ApiUri.TAGS.value,
            params=data
        )

    def list(self):
        """
        Get all current labels

        :return: The Logentries API response
        :rtype: list of dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        return self._post(
            request='list',
            uri=ApiUri.TAGS.value,
        ).get('tags')

    def get(self, name):
        """
        Get labels by name

        :param name: The label name, it must be an exact match.
        :type name: str

        :return: A list of matching labels. An empty list is returned if there are
            not any matches
        :rtype: list of dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        labels = self.list()
        return [
            label
            for label
            in labels
            if name == label.get('name')
        ]

    def update(self, label):
        """
        Update a Label

        :param label: The data to update. Must include keys:

            * id (str)
            * appearance (dict)
            * description (str)
            * name (str)
            * title (str)
        :type label: dict

        Example:

        .. code-block:: python

            Labels().update(
                label={
                    'id': 'd9d4596e-49e4-4135-b3b3-847f9e7c1f43',
                    'appearance': {'color': '278abe'},
                    'name': 'My Sandbox',
                    'description': 'My Sandbox',
                    'title': 'My Sandbox',
                }
            )

        :return:
        :rtype: dict
        """
        data = {
            'id': label['id'],
            'name': label['name'],
            'appearance': label['appearance'],
            'description': label['description'],
            'title': label['title'],
        }
        return self._post(
            request=ApiActions.UPDATE.value,
            uri=ApiUri.TAGS.value,
            params=data
        )

    def delete(self, id):
        """
        Delete the specified label

        :param id: the label's ID
        :type id: str

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        return self._post(
            request=ApiActions.DELETE.value,
            uri=ApiUri.TAGS.value,
            params={'id': id}
        )

class Tags(Resource):
    """
    A class for dealing with tags

    Tags have one or more labels.
    """

    def create(self, label_id):
        """
        Create a new tag

        :param label_id: The Label ID (the 'sn' key of the create label response)
        :type label_id: str

        :returns: The response of your post
        :rtype: dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        data = {
            'type': 'tagit',
            'rate_count': 0,
            'rate_range': 'day',
            'limit_count': 0,
            'limit_range': 'day',
            'schedule': [],
            'enabled': True,
            'args': {
                'sn': label_id,
                'tag_sn': label_id
            }
        }
        # Yes, it's confusing. the `/actions/` endpoint is used for tags, while
        # the /tags/ endpoint is used for labels.
        return self._post(
            request=ApiActions.CREATE.value,
            uri=ApiUri.ACTIONS.value,
            params=data
        )

    def list(self):
        """
        Get all current tags

        :return: All tags
        :rtype: list of dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        return list(
            filter(
                lambda x: x.get('type') == 'tagit',  # pragma: no cover
                self._post(
                    request=ApiActions.LIST.value,
                    uri=ApiUri.ACTIONS.value,
                ).get('actions')
            )
        )

    def get(self, label_sn):
        """
        Get tags by a label's sn key

        :param label_sn: A corresponding label's ``sn`` key.
        :type label_sn: str or int

        :return: A list of matching tags. An empty list is returned if there are
            not any matches
        :rtype: list of dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        tags = self.list()
        return [
            tag
            for tag
            in tags
            if str(label_sn) in tag.get('args', {}).values()
        ]

    def delete(self, id):
        """
        Delete the specified tag

        :param id: the tag's ID
        :type id: str

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        return self._post(
            request=ApiActions.DELETE.value,
            uri=ApiUri.ACTIONS.value,
            params={'id': id}
        )

class Hooks(Resource):
    """
    A class for dealing with hooks

    Hooks assign tags based on matching regexes to appropriate logs
    """
    def create(self, name, regexes, tag_ids, logs=None):
        """
        Create a hook

        :param name: The hook's name (should be the same as the tag)
        :type name: str

        :param regexes: The list of regular expressions that Logentries expects.
            Ex: `['user_agent = /curl\/[\d.]*/']` Would match where the
            user-agent is curl.
        :type regexes: list of str

        :param tag_id: The ids of the tags to associate the hook with.
            (The 'id' key of the create tag response)
        :type tag_id: list of str

        :param logs: The logs to add the hook to. Comes from the 'key'
            key in the log dict.
        :type logs: list of str

        :returns: The response of your post
        :rtype: dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        data = {
            'name': name,
            'triggers': regexes,
            'sources': logs or [],
            'groups': [],
            'actions': tag_ids
        }
        return self._post(
            request=ApiActions.CREATE.value,
            uri=ApiUri.HOOKS.value,
            params=data
        )

    def list(self):
        """
        Get all current hooks

        :return: All hooks
        :rtype: list of dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        return self._post(
            request=ApiActions.LIST.value,
            uri=ApiUri.HOOKS.value,
        ).get('hooks')

    def get(self, name_or_tag_id):
        """
        Get hooks by name or tag_id.
        :param name_or_tag_id: The hook's name or associated tag['id']
        :type name_or_tag_id: str

        :return: A list of matching tags. An empty list is returned if there are
            not any matches
        :rtype: list of dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        hooks = self.list()

        return [
            hook
            for hook
            in hooks
            if name_or_tag_id in hook.get('actions')
            or name_or_tag_id == hook.get('name')
        ]

    def update(self, hook):
        """
        Update a hook

        :param hook: The data to update. Must include keys:

            * id (str)
            * name (str)
            * triggers (list of str)
            * sources (list of str)
            * groups (list of str)
            * actions (list of str)
        :type hook: dict

        Example:

        .. code-block:: python

            Hooks().update(
                hook={
                    'id': 'd9d4596e-49e4-4135-b3b3-847f9e7c1f43',
                    'name': 'My Sandbox',
                    'triggers': [
                        'host = you.example.com'
                    ],
                    'sources': [
                        '4d42c719-4005-4929-aa4a-994da4b95040'
                    ],
                    'groups': [],
                    'actions': [
                        '9f6adf69-37b9-4a4b-88fb-c3fc4c781a11',
                        'ddc36d71-33cb-4f4f-be1b-8591814b1946'
                    ],
                }
            )

        :return:
        :rtype: dict
        """
        data = {
            'id': hook['id'],
            'name': hook['name'],
            'triggers': hook['triggers'],
            'sources': hook['sources'],
            'groups': hook['groups'],
            'actions': hook['actions'],
        }
        return self._post(
            request=ApiActions.UPDATE.value,
            uri=ApiUri.HOOKS.value,
            params=data
        )

    def delete(self, id):
        """
        Delete the specified hook

        :param id: the hook's ID
        :type id: str

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        return self._post(
            request=ApiActions.DELETE.value,
            uri=ApiUri.HOOKS.value,
            params={'id': id}
        )

class Alerts(Resource):
    """
    A class for dealing with alerts
    """

    def create(self,
               alert_config,
               occurrence_frequency_count=None,
               occurrence_frequency_unit=None,
               alert_frequency_count=None,
               alert_frequency_unit=None):
        """
        Create a new alert

        :param alert_config: A list of AlertConfig classes (Ex:
            ``[EmailAlertConfig('me@mydomain.com')]``)
        :type alert_config: list of
            :class:`PagerDutyAlertConfig<logentries_api.alerts.PagerDutyAlertConfig>`,
            :class:`WebHookAlertConfig<logentries_api.alerts.WebHookAlertConfig>`,
            :class:`EmailAlertConfig<logentries_api.alerts.EmailAlertConfig>`,
            :class:`SlackAlertConfig<logentries_api.alerts.SlackAlertConfig>`, or
            :class:`HipChatAlertConfig<logentries_api.alerts.HipChatAlertConfig>`

        :param occurrence_frequency_count: How many times per
            ``alert_frequency_unit`` for a match before issuing an alert.
            Defaults to 1
        :type occurrence_frequency_count: int

        :param occurrence_frequency_unit: The time period to monitor for sending
            an alert. Must be 'day', or 'hour'. Defaults to 'hour'
        :type occurrence_frequency_unit: str

        :param alert_frequency_count: How many times per
            ``alert_frequency_unit`` to issue an alert. Defaults to 1
        :type alert_frequency_count: int

        :param alert_frequency_unit: How often to regulate sending alerts.
            Must be 'day', or 'hour'. Defaults to 'hour'
        :type alert_frequency_unit: str

        :returns: The response of your post
        :rtype: dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        data = {
            'rate_count': occurrence_frequency_count or 1,
            'rate_range': occurrence_frequency_unit or 'hour',
            'limit_count': alert_frequency_count or 1,
            'limit_range': alert_frequency_unit or 'hour',
            'schedule': [],
            'enabled': True,
        }
        data.update(alert_config.args())

        # Yes, it's confusing. the `/actions/` endpoint is used for alerts, while
        # the /tags/ endpoint is used for labels.
        return self._post(
            request=ApiActions.CREATE.value,
            uri=ApiUri.ACTIONS.value,
            params=data
        )

    def list(self):
        """
        Get all current alerts

        :return: All alerts
        :rtype: list of dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """

        return list(
            filter(
                lambda x: x.get('type') != 'tagit',  # pragma: no cover
                self._post(
                    request=ApiActions.LIST.value,
                    uri=ApiUri.ACTIONS.value,
                ).get('actions')
            )
        )

    def get(self, alert_type, alert_args=None):
        """
        Get alerts that match the alert type and args.

        :param alert_type: The type of the alert. Must be one of 'pagerduty',
            'mailto', 'webhook', 'slack', or 'hipchat'
        :type alert_type: str

        :param alert_args: The args for the alert. The provided args must be a
            subset of the actual alert args. If no args are provided, all
            alerts matching the ``alert_type`` are returned. For example:
            ``.get('mailto', alert_args={'direct': 'me@mydomain.com'})`` or
            ``.get('slack', {'url': 'https://hooks.slack.com/services...'})``

        :return: A list of matching alerts. An empty list is returned if there
            are not any matches
        :rtype: list of dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        alert_args = alert_args or {}

        alerts = self.list()
        return [
            alert
            for alert
            in alerts
            if alert.get('type') == alert_type
            and dict_is_subset(alert_args, alert.get('args'))
        ]

    def update(self, alert):
        """
        Update an alert

        :param alert: The data to update. Must include keys:

            * id (str)
            * rate_count (int)
            * rate_range (str): 'day' or 'hour'
            * limit_count (int)
            * limit_range (str): 'day' or 'hour'
            * type (str)
            * schedule (list)
            * args (dict)
        :type alert: dict

        Example:

        .. code-block:: python

            Alert().update(
                alert={
                    'id': 'd9d4596e-49e4-4135-b3b3-847f9e7c1f43',
                    'args': {'direct': 'you@example.com'},
                    'rate_count': 1,
                    'rate_range': 'hour',
                    'limit_count': 1,
                    'limit_range': 'hour',
                    'schedule': [],
                    'enabled': True,
                    'type': 'mailto',
                }
            )

        :return:
        :rtype: dict
        """
        data = {
            'id': alert['id'],
            'args': alert['args'],
            'rate_count': alert['rate_count'],
            'rate_range': alert['rate_range'],
            'limit_count': alert['limit_count'],
            'limit_range': alert['limit_range'],
            'schedule': alert['schedule'],
            'enabled': alert['enabled'],
            'type': alert['type'],
        }

        return self._post(
            request=ApiActions.UPDATE.value,
            uri=ApiUri.ACTIONS.value,
            params=data
        )

    def delete(self, id):
        """
        Delete the specified alert

        :param id: the alert's ID
        :type id: str

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        return self._post(
            request=ApiActions.DELETE.value,
            uri=ApiUri.ACTIONS.value,
            params={'id': id}
        )


"""
A Sphinx extension for writing SageMath-style documentation

This extension sets up:
- the styling theme
- intersphinx: enables crosslinks to the Python and SageMath documentation
- extlinks: short hand roles for wikipedia, trac, arxiv, ...
"""

sage_doc_url = "http://doc.sagemath.org/html/en/"
sage_documents = [
    "a_tour_of_sage", "constructions", "developer", "faq",
    "installation", "prep", "reference", "thematic_tutorials",
    "tutorial"
]
sage_modules = [
    "algebras", "databases", "game_theory", "logic", "monoids", "quadratic_forms",
    "semirings", "arithgroup", "data_structures", "graphs", "manifolds",
    "notebook", "quat_algebras", "stats", "asymptotic",
    "diophantine_approximation", "groups", "matrices", "number_fields",
    "quivers", "structure", "calculus", "discrete_geometry", "hecke", "matroids",
    "numerical", "references", "tensor", "categories", "doctest",
    "history_and_license", "misc", "padics", "repl", "tensor_free_modules",
    "coding", "dynamics", "homology", "modabvar", "parallel",
    "riemannian_geometry", "coercion", "finance", "hyperbolic_geometry",
    "modfrm", "plot3d", "rings", "combinat", "finite_rings", "interfaces",
    "modfrm_hecketriangle", "plotting", "rings_numerical", "constants",
    "function_fields", "knots", "modmisc", "polynomial_rings", "rings_standard",
    "cryptography", "functions", "lfunctions", "modsym", "power_series", "sat",
    "curves", "games", "libs", "modules", "probability", "schemes",
]

import os
import sys
pythonversion = sys.version.split(' ')[0]

def setup(app):
    """
    Initialize this Sphinx extension
    """
    app.setup_extension('sphinx.ext.todo')
    app.setup_extension('sphinx.ext.mathjax')

    app.setup_extension("sphinx.ext.intersphinx")
    app.config.intersphinx_mapping.update({
        'https://docs.python.org/': None
        })
    app.config.intersphinx_mapping.update({
        sage_doc_url + doc + "/": None
        for doc in sage_documents
        })
    app.config.intersphinx_mapping.update({
        sage_doc_url + "reference/" + module: None
        for module in sage_modules
        })

    app.setup_extension("sphinx.ext.extlinks")
    app.config.extlinks.update({
        'python': ('https://docs.python.org/release/'+pythonversion+'/%s', ''),
        # Sage trac ticket shortcuts. For example, :trac:`7549` .
        'trac': ('https://trac.sagemath.org/%s', 'trac ticket #'),
        'wikipedia': ('https://en.wikipedia.org/wiki/%s', 'Wikipedia article '),
        'arxiv': ('http://arxiv.org/abs/%s', 'Arxiv '),
        'oeis': ('https://oeis.org/%s', 'OEIS sequence '),
        'doi': ('https://dx.doi.org/%s', 'doi:'),
        'pari': ('http://pari.math.u-bordeaux.fr/dochtml/help/%s', 'pari:'),
        'mathscinet': ('http://www.ams.org/mathscinet-getitem?mr=%s', 'MathSciNet ')
        })

    app.config.html_theme = 'sage'

def themes_path():
    """
    Retrieve the location of the themes directory from the location of this package

    This is taken from Sphinx's theme documentation
    """
    package_dir = os.path.abspath(os.path.dirname(__file__))
    return os.path.join(package_dir, 'themes')


from enum import Enum
import json

import os
import requests
from logentries_api.exceptions import ConfigurationException, ServerException

class ApiUri(Enum):
    TAGS = 'tags'
    ACTIONS = 'actions'
    HOOKS = 'hooks'

class ApiActions(Enum):
    REGISTER = 'register'
    LIST = 'list'
    CREATE = 'create'
    DELETE = 'delete'
    UPDATE = 'update'

class Resource(object):
    """
    A base class for API resources
    """

    def __init__(self, account_key=None):
        """
        :type account_key: str
        :param account_key: The API key. If no key is passed, the environment
            variable LOGENTRIES_ACCOUNT_KEY is used.
        :raises: If the account_key parameter is not present, and no environment
            variable is present, a
            :class:`ConfigurationException <logentries_api.exceptions.ConfigurationException>`
            is raised.
        """
        self.account_key = account_key or os.environ.get('LOGENTRIES_ACCOUNT_KEY')

        if not self.account_key:
            raise ConfigurationException('LOGENTRIES_ACCOUNT_KEY not present in environment!')

    @property
    def headers(self):
        return {
            'Content-type': 'application/json',
        }

    def _post(self, request, uri, params=None):
        """
        A wrapper for posting things.

        :param request: The request type. Must be one of the
            :class:`ApiActions<logentries_api.base.ApiActions>`
        :type request: str

        :param uri: The API endpoint to hit. Must be one of
            :class:`ApiUri<logentries_api.base.ApiUri>`
        :type uri: str

        :param params: A dictionary of supplemental kw args
        :type params: dict

        :returns: The response of your post
        :rtype: dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        request_data = {
            'acl': self.account_key,
            'account': self.account_key,
            'request': request,
        }

        request_data.update(params or {})

        response = requests.post(
            url='https://api.logentries.com/v2/{}'.format(uri),
            headers=self.headers,
            data=json.dumps(request_data)
        )

        if not response.ok:
            raise ServerException(
                '{}: {}'.format(response.status_code, response.text))
        return response.json()


from logentries_api.base import Resource
from logentries_api.exceptions import ServerException
import requests

class LogSets(Resource):
    """
    A base class for host-based resources (Old API)
    """

    @property
    def base_url(self):
        return 'https://api.logentries.com/{}/hosts/'.format(self.account_key)

    def list(self):
        """
        Get all log sets

        :return: Returns a dictionary where the key is the hostname or log set,
            and the value is a list of the log keys
        :rtype: dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        response = requests.get(self.base_url)

        if not response.ok:
            raise ServerException(
                '{}: {}'.format(response.status_code, response.text))

        return {
            host.get('name'): [
                log.get('key')
                for log
                in host.get('logs')]
            for host
            in response.json().get('list')
        }

    def get(self, log_set):
        """
        Get a specific log or log set

        :param log_set: The log set or log to get. Ex: `.get(log_set='app')` or
            `.get(log_set='app/log')`
        :type log_set: str

        :returns: The response of your log set or log
        :rtype: dict

        :raises: This will raise a
            :class:`ServerException<logentries_api.exceptions.ServerException>`
            if there is an error from Logentries
        """
        response = requests.get(self.base_url + log_set.rstrip('/'))

        if not response.ok:
            raise ServerException(
                '{}: {}'.format(response.status_code, response.text))
        return response.json()


# model - chess game model

# Copyright (C) 2016 DrEstebon

# This file is part of Papageorge.
#
# Papageorge is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Papageorge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Papageorge If not, see <http://www.gnu.org/licenses/>.

if __name__ == '__main__':
    import sys, os
    here = os.path.dirname(os.path.abspath(__file__))
    sys.path.append(os.path.abspath(os.path.join(here, '../')))

from papageorge.notation import San, CoordsMove
import papageorge.config as config

from time import time
import re

BB_MASK = 0xFFFFFFFFFFFFFFFF

class BB(object):
    def __init__(self, state):
        # if isinstance(state, GameState):
        if True:
            self.piece = { x:0 for x in 'RNBQKPrnbqkp' }
            for i, x in enumerate(state):
                if x != '-':
                    self.piece[x] |= 1<<i
            self.cocc = [0,0]
            for x in self.piece.keys():
                self.cocc[x.isupper()] |= self.piece[x]
            self.occ = self.cocc[0] | self.cocc[1]
        # else:
            # self.cocc = list(state.cocc)
            # self.occ = state.occ
            # self.piece = dict(state.piece)

    def pieces_but(self, x):
        r = 0
        for p in 'RNBQKPrnbqkp'.replace(x,''):
            r |= self.piece[p]
        return r

def print_rank(x, mark=[]):
    if len(mark):
        s = '{:08b}'.format(x)[::-1]
        for i in mark:
            s = s[0:i]+'*'+s[i+1::]
        print(s)
    else:
        print('{:08b}'.format(x)[::-1])

def printBB(x, mark=[]):
    for i in range(56, -1, -8):
        if not len(mark):
            print_rank((x>>i)&255)
        else:
            marks = []
            for j in mark:
                if j in range(i, i+8):
                    marks.append(j-i)
            print_rank((x>>i)&255, mark=marks)

def pos2sq(pos):
    return (pos%8, pos//8)

def pos2pos(pos):
    return ord(pos[0])-97+((int(pos[1])-1)<<3)

def postopos(pos):
    if isinstance(pos, tuple):
        return chr(97+pos[0])+str(pos[1]+1)
    else:
        return chr(pos%8+97)+str(pos//8+1)

def rank_mask(x):
    if x >= 0 and x < 8:
        return 0xFF<<(8*x)
    else:
        return 0x0

def file_mask(x):
    if x >= 0 and x < 8:
        return 0x0101010101010101<<x
    else:
        return 0x0

# RANK
def rank(x, sq):
    return x>>(sq-sq%8)&255

def pos_in_rank(sq):
    return sq%8

def rank_posmap(pos, sq):
    return pos+sq-sq%8

rank_domain = ( pos_in_rank, rank, rank_posmap )

# FILE
#sq' = ((sq >> 3) | (sq << 3)) & 63;
def flip(x):
   k1 = 0x5500550055005500
   k2 = 0x3333000033330000
   k4 = 0x0f0f0f0f00000000
   t  = k4 & (x ^ (x << 28))
   x ^=       t ^ (t >> 28)
   t  = k2 & (x ^ (x << 14))
   x ^=       t ^ (t >> 14)
   t  = k1 & (x ^ (x <<  7))
   x ^=       t ^ (t >>  7)
   return x

def file(x, sq):
    return flip(x)>>((sq%8)<<3)&255

def pos_in_file(sq):
    return sq//8

def file_posmap(pos, sq):
    return (pos<<3)+sq%8

file_domain = ( pos_in_file, file, file_posmap )

# MAIN DIAGONAL
def rotR(x, s):
    return ((x >> s) | (x << (64-s)))&BB_MASK

def rot45clk(x):
   x ^= 0xAAAAAAAAAAAAAAAA & (x ^ rotR(x,  8))
   x ^= 0xCCCCCCCCCCCCCCCC & (x ^ rotR(x, 16))
   x ^= 0xF0F0F0F0F0F0F0F0 & (x ^ rotR(x, 32))
   return x;

def mdiag_idx(sq):
    return (sq%8-sq//8)&0xF

def mdiag(x, sq):
    i = mdiag_idx(sq)
    shift = ((-i)&7)*8+i*(i<8)
    mask = 255&((-256)>>(8-abs(8-i)))
    return ((rot45clk(x) >> shift) | mask)&255

def pos_in_mdiag(sq):
    return min(sq%8, sq//8)

def mdiag_posmap(pos, sq):
    i = mdiag_idx(sq)
    shift = ((-i)&7)*8+i*(i<8)
    sq = shift+pos
    return (sq + 8*((sq&7))) & 63

mdiag_domain = ( pos_in_mdiag, mdiag, mdiag_posmap )

# ANTI DIAGONAL
def rot45aclk(x):
   x ^= 0x5555555555555555 & (x ^ rotR(x,  8))
   x ^= 0x3333333333333333 & (x ^ rotR(x, 16))
   x ^= 0x0f0f0f0f0f0f0f0f & (x ^ rotR(x, 32))
   return x

def adiag_idx(sq):
    return (sq%8+sq//8)^0x7

def adiag(x, sq):
    i = adiag_idx(sq)
    shift = ((-i)&7)*8+(8-i%8)*(i>8)
    mask = 255&((-256)>>(8-abs(8-i)))
    return ((rot45aclk(x) >> shift) | mask)&255

def pos_in_adiag(sq):
    return min(sq%8,7-(sq//8))

def adiag_posmap(pos, sq):
    i = adiag_idx(sq)
    shift = ((-i)&7)*8+(8-i%8)*(i>8)
    sq = shift+pos
    return (sq + 8*((sq&7)^7)) & 63

adiag_domain =  ( pos_in_adiag, adiag, adiag_posmap )

import array
# Reach for sliding pieces, given occupancy bitboard
reach = []
for i in range(8):
    l = array.array('B', (0,)*256)
    for j in range(256):
        l[j] = 1<<i
        if i<7:
            m = 1<<(i+1)
            while m&255 and not m&j:
                l[j] |= m
                m <<= 1
        if i>0:
            m = 1<<(i-1)
            while m&255 and not m&j:
                l[j] |= m
                m >>= 1
    reach.append(l)

# ray between two squares in a rank
ray = []
for i in range(8):
    l = array.array('B', (0,)*8)
    for j in range(8):
        if i!=j:
            m = 1<<((j if j<i else i)+1)
            t = 1<<(j if i<j else i)
            while m&255 and not m&t:
                l[j] |= m
                m <<= 1
    ray.append(l)

def knight_attacks(sq):
    r = sq//8
    a = 1<<sq
    x  = rank_mask(r+2)&(a<<17|a<<15)
    x |= rank_mask(r+1)&(a<<10|a<<6)
    x |= rank_mask(r-1)&(a>>10|a>>6)
    x |= rank_mask(r-2)&(a>>17|a>>15)
    return x

def king_attacks(sq):
    r = sq//8
    a = 1<<sq
    x  = rank_mask(r+1)&(a<<7|a<<8|a<<9)
    x |= rank_mask(r)  &(a<<1|a>>1)
    x |= rank_mask(r-1)&(a>>7|a>>8|a>>9)
    return x

def pawn_attacks(sq, side):
    r = sq//8
    a = 1<<sq
    if side:
        x = rank_mask(r-1)&(a>>7|a>>8|a>>9)
        x |= (r==3)*(a>>16)
    else:
        x = rank_mask(r+1)&(a<<7|a<<8|a<<9)
        x |= (r==4)*(a<<16)
    return x

def find_attacker_slider(dest_list, occ_bb, piece_bb, target_bb, pos,
                             domain):
    """ Find a slider attacker

    Parameters
    ----------
    dest_list : list
        To store the results.
    occ_bb : int, bitboard
        Occupancy bitboard.
    piece_bb : int, bitboard
        Bitboard with the position of the attacker piece.
    target_bb : int, bitboard
        Occupancy bitboard without any of the sliders in question.
    pos : int
        Target position.
    pos_map : function
        Mapping between a board position and its position in a single
        rotated/translated rank produced with domain_trans.
    domain_trans : function
        Transformation from a rank/file/diagonal/anti-diagonal containing pos
        to a single rank
    pos_inv_map : function
        Inverse of pos_map
    """
    pos_map, domain_trans, pos_inv_map = domain
    r = reach[pos_map(pos)][domain_trans(target_bb, pos)]
    m = r & domain_trans(piece_bb, pos)
    while m:
        r = m&-m
        rpos = r.bit_length()-1
        if not (ray[rpos][pos_map(pos)] & domain_trans(occ_bb, pos)):
            dest_list.append(pos_inv_map(rpos, pos))
        m ^= r

def find_attacker_jumper(dest_list, piece_bb, pos, jumper_attacks):
    m = piece_bb & jumper_attacks(pos)
    while m:
        r = m&-m
        rpos = r.bit_length()-1
        dest_list.append(rpos)
        m ^= r

def find_attacker_pawn(dest_list, occ_bb, piece_bb, pos, side):
    m = piece_bb & pawn_attacks(pos, side)
    while m:
        r = m&-m
        rpos = r.bit_length()-1
        if rpos%8 == pos%8:
            M = rank_mask(pos//8)
            M |= rank_mask(rpos//8+(1 if side else -1))
            if not (M & file_mask(pos%8) & occ_bb):
                dest_list.append(rpos)
        else:
            dest_list.append(rpos)
        m ^= r

def is_pinned(bb, orig, dest, side):
    kbb = bb.piece['K'] if side else bb.piece['k']
    kpos = kbb.bit_length()-1
    if  kpos//8 == orig//8 != dest//8:
        pos_map, domain_trans, pos_inv_map = rank_domain
        attkrs = 'QR'
    elif kpos%8 == orig%8 != dest%8:
        pos_map, domain_trans, pos_inv_map = file_domain
        attkrs = 'QR'
    elif adiag_idx(kpos) == adiag_idx(orig) != adiag_idx(dest):
        pos_map, domain_trans, pos_inv_map = adiag_domain
        attkrs = 'QB'
    elif mdiag_idx(kpos) == mdiag_idx(orig) != mdiag_idx(dest):
        pos_map, domain_trans, pos_inv_map = mdiag_domain
        attkrs = 'QB'
    else:
        return False

    if ray[pos_map(kpos)][pos_map(orig)] & domain_trans(bb.occ, orig):
        return False

    attkrs = attkrs.lower() if side else attkrs
    occ = bb.occ^kbb

    for a in attkrs:
        r = reach[pos_map(orig)][domain_trans(occ^bb.piece[a], orig)]
        m = r & domain_trans(bb.piece[a], orig)
        while m:
            r = m&-m
            rpos = r.bit_length()-1
            if not (ray[rpos][pos_map(orig)] & domain_trans(bb.occ, orig)):
                return pos_inv_map(rpos, orig)
            m ^= r
    return False

def find_attacker(bitboard, piece, pos,
                  attacker_file=None, attacker_rank=None, res=None):
    if res == None:
        res = list()
    for p in piece:
        bbp = bitboard.piece[p]

        if attacker_file != None:
            bbp &= file_mask(attacker_file)
        if attacker_rank != None:
            bbp &= rank_mask(attacker_rank)

        bbt = bitboard.occ^bbp

        kind = p.lower()

        if kind in 'rq':
            find_attacker_slider(res, bitboard.occ, bbp, bbt, pos, rank_domain)
            find_attacker_slider(res, bitboard.occ, bbp, bbt, pos, file_domain)
        if kind in 'bq':
            find_attacker_slider(res, bitboard.occ, bbp, bbt, pos, mdiag_domain)
            find_attacker_slider(res, bitboard.occ, bbp, bbt, pos, adiag_domain)
        elif kind == 'n':
            find_attacker_jumper(res, bbp, pos, knight_attacks)
        elif kind == 'k':
            find_attacker_jumper(res, bbp, pos, king_attacks)
        elif kind == 'p':
            find_attacker_pawn(res, bitboard.occ, bbp, pos, p.isupper())

    if kind != 'k':
        for x in res:
            if is_pinned(bitboard, x, pos, p.isupper()):
                res.remove(x)

    if len(res) == 1:
        return res[0]
    return None

def make(txt, sq_from, sq_to):
    a = txt[0:sq_from] + '-' + txt[sq_from+1::]
    return a[0:sq_to] + txt[sq_from] + a[sq_to+1::]

def put(txt, sq, piece):
    return txt[0:sq] + piece + txt[sq+1::]

_DIGIT_RULE = re.compile('^(.*)(\d)(.*)$')
_ES_RULE = re.compile('^([^-]*)(-+)(.*)$')

class GameState(str):
    cmove = move = 'none'
    names = ['', '']
    game_number = relation = itime = iinc = time = wtime = btime = 0
    halfmove = -1
    turn = True
    castling = 0b1111
    enpassant = -1
    prev = None
    comment = None

    # Castling Mask         Turn short?
    _CM = (( 0b0001 ,     # 0    0
             0b0010 ),    # 0    1
           ( 0b0100 ,     # 1    0
             0b1000 ))    # 1    1
    # Castling attacker range
    _CAR = (( (58,61),    # 0    0
              (60,63) ),  # 0    1
            ( (2,5),      # 1    0
              (4,7)  ))   # 1    1
    # Castling path range
    _CPR = (( (57,60),    # 0    0
              (61,63) ),  # 0    1
            ( (1,4),      # 1    0
              (5,7)  ))   # 1    1
    # Castling piece squares
    _CPS = ((((56, 59), (60, 58)),
             ((63, 61), (60, 62))),
            (((0, 3), (4, 2)),
             ((7, 5), (4, 6))))

    # piece values:
    _PV = {'q':9, 'r':5, 'b':3, 'n':3, 'p':1 , '-':0, 'k':0}

    def __new__(cls,
     value='RNBQKBNRPPPPPPPP--------------------------------pppppppprnbqkbnr'):
        fen = False
        if isinstance(value, GameState):
            return value.copy()
        elif '/' in value.split()[0]:
            fen = value.split()
            board = fen[0].split('/')
            for i in range(8):
                m = _DIGIT_RULE.match(board[i])
                while m:
                    board[i] = m.group(1) + int(m.group(2))*'-' + m.group(3)
                    m = _DIGIT_RULE.match(board[i])
            value = ''.join(board[::-1])
        ver = next((x for x in value if x not in 'RNBQKPrnbqkp-'), False)
        if not (len(value) != 64 or ver):
            self = super(GameState, cls).__new__(cls, value)
        else:
            self = super(GameState, cls).__new__(cls, 64*'-')
        self.strength = [0,0]
        self.figures = list()
        for i, x in enumerate(self):
            self.strength[x.isupper()] += self._PV[x.lower()]
            if x is not '-':
                self.figures.append(((i%8,i//8), x))
        self.next = list()
        if fen:
            self.turn = fen[1] == 'w'
            self.castling = 0
            for i, l in enumerate('qkQK'):
                self.castling += (l in fen[2])*(1<<i)
            self.enpassant = -1 if fen[3] == '-' else ord(fen[3][0])-97
            self.halfmove = (int(fen[5])-1)*2 + (not self.turn) -1
        return self

    def copy(self):
        s = GameState(str(self))
        for pn in ['game_number', 'turn', 'names', 'relation', 'itime', 'iinc',
                   'wtime', 'btime', 'halfmove', 'cmove', 'move', 'castling']:
            setattr(s, pn, getattr(self, pn))
        return s

    def piece_in(self, pos, kind=False):
        if isinstance(pos, tuple):
            square = self[pos[1]*8+pos[0]]
        else:
            square = self[pos]
        return (square.lower() if kind else square) if square != '-' else False

    def empty_range(self, r):
        return not bool(next((self[x] for x in r if self[x] != '-'), False))

    def __sub__(self, y):
        return [ i for i in range(64) if self[i] != y[i] ]

    def __eq__(self, y):
        if not y:
            return False
        else:
            return (super().__eq__(y) and self.halfmove == y.halfmove and
                    self.cmove == y.cmove)

    def make_cleanup(self, txt, cmove, move):
        s = GameState(txt)
        s.game_number = self.game_number
        s.turn        = not self.turn
        s.names       = self.names
        s.relation    = self.relation
        s.itime       = self.itime
        s.iinc        = self.iinc
        s.wtime       = self.wtime
        s.btime       = self.btime
        s.halfmove    = self.halfmove + 1
        s.cmove       = cmove
        s.move        = move
        s.prev        = self
        self.next.append(s) # <-- it is done by history.update
        # Block castling
        if 'O-O' in move:
            s.castling = (self.castling &
                    ~(self._CM[self.turn][0]|self._CM[self.turn][1]))
        elif cmove[0] == 'R' and (cmove[1:3] in ['a1', 'a8']):
            s.castling = (self.castling & ~self._CM[self.turn][0])
        elif cmove[0] == 'R' and (cmove[1:3] in ['h1', 'h8']):
            s.castling = (self.castling & ~self._CM[self.turn][1])
        else:
            s.castling = self.castling
        # en passant
        if (cmove[0] == 'P' and
                int(cmove[3]) == (2 if self.turn else 7)  and
                int(cmove[6]) == (4 if self.turn else 5)):
            s.enpassant = ord(cmove[2])-97
        else:
            s.enpassant = -1
        s.time = time()
        return s

    def make(self, move):
        if isinstance(move, list):
            m = CoordsMove(self, move)
        else:
            m = San(move)

        bb = BB(self)

        # Castling
        if m.castling_short or m.castling_long:
            L = []
            for s in range(*self._CAR[self.turn][bool(m.castling_short)]):
                find_attacker(bb, 'prnbqk' if self.turn else 'PRNBQK', s, res=L)
            if ((self._CM[self.turn][bool(m.castling_short)] & self.castling)
                and self.empty_range(range(
                           *self._CPR[self.turn][bool(m.castling_short)])) and
                    len(L) == 0):
                moves = self._CPS[self.turn][bool(m.castling_short)]
                a = make(self, *moves[0])
                b = make(a, *moves[1])
                return self.make_cleanup(b,
                        'o-o' if m.castling_short else 'o-o-o',
                        'O-O' if m.castling_short else 'O-O-O')
            else:
                return False

        piece = m.piece.upper() if self.turn else m.piece.lower()
        sq_to = pos2pos(m.sq_to)
        attacker_file = ord(m.file_from)-97 if m.file_from else None
        attacker_rank = int(m.rank_from)-1  if m.rank_from else None

        sq_from = find_attacker(bb, piece, sq_to, attacker_file, attacker_rank)

        # Attacker not found or attacking my self
        if (not isinstance(sq_from, int) or not (self.turn == self[sq_from].isupper()) or
                (self[sq_to] != '-' and (self.turn == self[sq_to].isupper()))):
            return False

        # En passant
        if m.piece == 'P' and abs((sq_from%8)-(sq_to%8)) == 1 and self[sq_to] == '-':
            if sq_to%8==self.enpassant and sq_to//8==(5 if self.turn else 2):
                a = put(self, sq_to + (-8 if self.turn else 8), '-')
                b = make(a, sq_from, sq_to)
                return self.make_cleanup(b,
                        m.piece+'/'+postopos(sq_from)+'-'+postopos(sq_to),
                        m)
            else:
                # no se puede en passant
                return False

        b = make(self, sq_from, sq_to)

        # Promotion
        if m.piece == 'P' and sq_to//8 in [0,7]:
            if m.promotion:
                b = put(b, sq_to,
                     m.promotion.upper() if self.turn else m.promotion.lower())
            else:
                b = put(b, sq_to, 'Q' if self.turn else 'q')
                m.promotion = 'Q'

        # check for checks
        L = []
        find_attacker(BB(b), 'prnbqk' if self.turn else 'PRNBQK',
                      b.index('K' if self.turn else 'k'), res=L)

        if len(L):
            return False
        else:
            cmove = m.piece+'/'+postopos(sq_from)+'-'+postopos(sq_to)+(
                    ('='+m.promotion) if m.promotion else '')
            return self.make_cleanup(b, cmove, m)

    def fen(self):
        fen = list()
        board = list()
        for i in range(8):
            board.append(self[8*i:8*i+8])
            m = _ES_RULE.match(board[i])
            while m:
                board[i] = m.group(1) + str(len(m.group(2))) + m.group(3)
                m = _ES_RULE.match(board[i])
        fen.append('/'.join(board[::-1]))
        fen.append('w' if self.turn else 'b')
        castling = ''
        for i, l in enumerate('qkQK'):
            castling = bool((1<<i)&self.castling)*l + castling
        fen.append(castling)
        if self.enpassant > 0:
            fen.append(chr(97+self.enpassant) + ('6' if self.turn else '3'))
        else:
            fen.append('-')
        fen.append('-')
        fen.append(str((self.halfmove if self.halfmove > 0 else 0)//2+1))
        return ' '.join(fen)

class Style12(GameState):
    def __new__(cls, value):
        svalue = value.split()
        self = super(Style12, cls).__new__(cls, ''.join(svalue[8:0:-1]))
        self.turn = svalue[9] == 'W'
        self.halfmove  = (int(svalue[26])-1)*2 + (not self.turn) - 1
        self.cmove = svalue[27]
        self.move = svalue[29]
        self.castling = int(''.join(svalue[11:15]),2)
        self.enpassant = int(svalue[10])
        self.wtime = int(svalue[24])
        self.btime = int(svalue[25])
        self.time = time()
        # not really neded
        self.style12 = value
        self.game_number = int(svalue[16])
        self.names = svalue[17:19][::-1]
        self.relation = int(svalue[19])
        self.itime = svalue[20]
        self.iinc  = svalue[21]
        return self

def contiguous(src, dst):
    if dst.halfmove-src.halfmove != 1 or dst.cmove == 'none':
        return False
    D = set(dst - src)
    # Canstling
    if dst.cmove == 'o-o':
        x = 0 if src.turn else 56
        if (D == {4+x, 5+x, 6+x, 7+x} and
                src.piece_in(4+x, True) == 'k' and
                src.piece_in(7+x, True) == 'r'):
            return True
        else:
            return False
    if dst.cmove == 'o-o-o':
        x = 0 if src.turn else 56
        if (D == {0+x, 2+x, 3+x, 4+x} and
                src.piece_in(4+x, True) == 'k' and
                src.piece_in(0+x, True) == 'r'):
            return True
        else:
            return False
    p, m = dst.cmove.split('/')
    s, d = m.split('-')
    s = pos2pos(s)
    d = pos2pos(d)
    DD = {s,d}^D
    # En passant
    if len(DD) == 1:
        dp = DD.pop()
        if (p.lower() == 'p' and
                dp//8 == (4 if src.turn else 3) and
                src.piece_in(s, True) == 'p' and
                src.piece_in(dp, True) == 'p' and
                not src.piece_in(d)):
            return True
        else:
            return False
    # promotion
    if ('=' in dst.cmove and src.piece_in(s, True) == 'p' and
            dst.piece_in(d, True) == dst.cmove.split('=')[1].lower()):
        return True
    if (len(DD) or src.piece_in(s, True) != p.lower()):
        return False
    else:
        return True

class StateDirectory(dict):
    def __missing__(self, key):
        self[key] = list()
        return self[key]

    def back_sorted(self):
        for i in sorted(self.keys(), reverse=True):
            for x in self[i]:
                yield x

def rewind(state):
    while state.prev:
        state = state.prev
    return state

class GameHistory(list):
    def __new__(cls, value=None):
        self = super(GameHistory, cls).__new__(cls, value)
        self._directory = StateDirectory()
        self._not_connected = list()
        if isinstance(value, list):
            for x in value:
                self._directory[x.halfmove].append(x)
        return self

    def extend(self, l):
        for x in l:
            self.update_reg(x)
        super().extend(l)

    def append(self, x):
        self.update_reg(x)
        super().append(x)

    def insert(self, idx, x):
        self.update_reg(x)
        super().insert(idx, x)

    def update_reg(self, state):
        s = next((x for x in self._directory[state.halfmove] if state == x), None)
        while s:
            self._directory[state.halfmove].remove(s)
            s = next((x for x in self._directory[state.halfmove] if state == x), None)
        self._directory[state.halfmove].append(state)

    def remove_move(self, x):
        y = x.prev
        if y:
            y.next.remove(x)
            x.prev = None
        else:
            return
        L = list()
        while x:
            self._directory[x.halfmove].remove(x)
            L.extend(x.next)
            x.next.clear()
            x.prev = None
            x = L.pop() if L else None
        return self.set_mainline(y)

    def remove_variation(self, x):
        while x.prev and len(x.prev.next)<2:
            x = x.prev
        return self.remove_move(x)

    def set_main_variation(self, x):
        y = x
        while x.prev:
            l = x.prev.next
            l.insert(0, l.pop(l.index(x)))
            x = x.prev
        return self.set_mainline(y)

    def remove_variations(self):
        x = self.parent_node
        while x:
            for y in x.next[1::]:
                self.remove_move(y)
            x = x.next[0] if x.next else None

    def update(self, state):
        inreg = False
        # is there an identical state we would want to replace?
        s = next((x for x in self._directory[state.halfmove] if state == x), None)
        if s:
            state.next = list(s.next)
            for x in s.next:
                x.prev = state
            if s.prev:
                state.prev = s.prev
                idx = s.prev.next.index(s)
                s.prev.next.remove(s)
                s.prev.next.insert(idx, state)
            self._directory[s.halfmove].remove(s)
            if s in self:
                idx = self.index(s)
                self.remove(s)
                self.insert(idx, state)
                inreg = True

        # is the new state contiguous to any existing state?
        s = next((x for x in self._directory[state.halfmove-1]
                        if contiguous(x, state)), None)
        if s and not state.prev:
            s.next.append(state)
            state.prev = s
            if self and s == self[-1]:
                self.append(state)
                inreg = True

        if not self or state not in self and state.prev == self[-1]:
            self.append(state)
            inreg = True

        # is the any existing state without prev contiguous to the new state?
        s = next((x for x in self._directory[state.halfmove+1]
                        if contiguous(state, x) and x.prev == None), None)
        if s:
            state.next.append(s)
            s.prev = state
            if s in self._not_connected:
                self._not_connected.remove(s)

        if not inreg:
            self.update_reg(state)

        if not state.prev:
            self._not_connected.append(state)
            return False
        else:
            return True

    def set_mainline(self, x):
        if x is None:
            return False
        sr = list()
        l = list()
        y = x
        while x and x not in self:
            l.insert(0, x)
            x = x.prev
        if x in self:
            rl = self[self.index(x)+1::]
            for x in rl:
                self.remove(x)
            self.extend(l)
            return (y, l, rl)
        self.clear()
        if x not in self and l:
            self.extend(l)
        else:
            self.append(x)
        return False

    def marked(self):
        if len(self) > 1:
            l = self[-1] - self[-2]
            return l if len(l)<=4 else []
        else:
            return []

    @property
    def parent_node(self):
        return rewind(self[0])

    def go_deeper(self, lines, state, line):
        if not len(state.next):
            lines.append(' '.join(line+['{} {}'.format(state.halfmove,state.move)]))
        else:
            for x in state.next:
                self.go_deeper(lines, x, line+['{} {}'.format(state.halfmove,state.move)])

    def get_lines(self):
        lines = []
        self.go_deeper(lines,rewind(self[0]),[])
        # ncl = []
        # for x in self._not_connected:
            # self.go_deeper(ncl,x,[])
        return lines


#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
:py:mod:`transit.py` - Transit models
-------------------------------------

A :py:mod:`ctypes` wrapper around a generalized C implementation of the
`Mandel and Agol (2002) <http://adsabs.harvard.edu/abs/2002ApJ...580L.171M>`_
transit model

.. warning:: The longitude of pericenter `w` may be defined differently here than in other transit codes; \
             watch out for a possible offset of :math:`\pi` from what you're used to.

.. todo::
   - Add nonlinear limb darkening
   - Add secondary eclipses

'''

from __future__ import division, print_function, absolute_import, unicode_literals
import ctypes
import numpy as np
import os
from numpy.ctypeslib import ndpointer, as_ctypes

# Define errors
_ERR_NONE             =   0                                                           # We're good!
_ERR_NOT_IMPLEMENTED  =   1                                                           # Function/option not yet implemented
_ERR_MAX_PTS          =   2                                                           # Maximum number of points exceeded in transit. Increase settings.maxpts.
_ERR_KEPLER           =   3                                                           # Error in the Kepler solver; probably didn't converge
_ERR_NO_TRANSIT       =   4                                                           # The planet doesn't transit the star
_ERR_BAD_ECC          =   5                                                           # Bad value for eccentricity
_ERR_RC               =   6                                                           # Error in rc() function
_ERR_RJ               =   7                                                           # Error in rj() function
_ERR_RF               =   8                                                           # Error in rf() function
_ERR_RADIUS           =   9                                                           # Bad input radius
_ERR_EXP_PTS          =   10                                                          # The number of exposure points cannot be odd
_ERR_NOT_COMPUTED     =   11                                                          # User attempted to bin before computing
_ERR_STAR_CROSS       =   12                                                          # Star-crossing orbit
_ERR_PER              =   13                                                          # Bad period
_ERR_RHOS_ARS         =   14                                                          # Must specify either rhos or aRs!
_ERR_RHOS             =   15                                                          # Bad rhos
_ERR_ECC_W            =   16                                                          # Bad eccentricity/omega
_ERR_LD               =   17                                                          # Bad limb darkening coeffs
_ERR_T0               =   18                                                          # Bad t0

# Define models
QUADRATIC  =              0
KIPPING    =              1
NONLINEAR  =              2
RIEMANN    =              5
TRAPEZOID  =              6
SMARTINT   =              7
SLOWINT    =              8
MDFAST     =              9
NEWTON     =              10

# Cadences
KEPLONGEXP =              (1765.5/86400.)
KEPLONGCAD =              (1800./86400.)
KEPSHRTEXP =              (58.89/86400.)
KEPSHRTCAD =              (60./86400.)

# Array IDs
_ARR_FLUX    =             0
_ARR_BFLX    =             1
_ARR_M       =             2
_ARR_E       =             3
_ARR_F       =             4
_ARR_R       =             5
_ARR_X       =             6
_ARR_Y       =             7
_ARR_Z       =             8
_ARR_B       =             9

# Other
MAXTRANSITS =             500
TRANSITSARR =             ctypes.c_double * MAXTRANSITS
G           =             6.672e-8
DAYSEC      =             86400.

class TRANSIT(ctypes.Structure):
      '''
      The class containing all the transiting planet parameters

      '''

      _fields_ = [("bcirc", ctypes.c_double),
                  ("rhos", ctypes.c_double),
                  ("MpMs", ctypes.c_double),
                  ("esw", ctypes.c_double),
                  ("ecw", ctypes.c_double),
                  ("per", ctypes.c_double),
                  ("RpRs", ctypes.c_double),
                  ("t0", ctypes.c_double),
                  ("ecc", ctypes.c_double),
                  ("w", ctypes.c_double),
                  ("aRs", ctypes.c_double),
                  ("ntrans", ctypes.c_int),
                  ("_tN", TRANSITSARR)]

      def __init__(self, **kwargs):
        self._tN_p = []
        self._tN = TRANSITSARR(*self._tN_p)
        self.ntrans = len(self._tN_p)
        self.update(**kwargs)

      def update(self, **kwargs):
        '''

        '''

        self.MpMs = kwargs.pop('MpMs', 0.)
        self.per = kwargs.pop('per', 10.)
        self.RpRs = kwargs.pop('RpRs', 0.1)

        self.bcirc = kwargs.pop('bcirc', 0.)
        b = kwargs.pop('b', None)                                                     # User may specify either ``b`` or ``bcirc``
        if b is not None:
          self.bcirc = b

        self.rhos = kwargs.pop('rhos', 1.4)                                           # User may specify either ``rhos`` or ``aRs``
        self.aRs = kwargs.pop('aRs', np.nan)
        if not np.isnan(self.aRs):
          self.rhos = np.nan

        self.ecc = kwargs.pop('ecc', 0.)                                              # User may specify (``esw`` and ``ecw``) or (``ecc`` and ``w``)
        self.w = kwargs.pop('w', 0.)
        self.esw = kwargs.pop('esw', np.nan)
        self.ecw = kwargs.pop('ecw', np.nan)
        if (not np.isnan(self.esw)) and (not np.isnan(self.ecw)):
          self.ecc = np.nan
          self.w = np.nan

        self.t0 = kwargs.pop('t0', 0.)                                                # User may specify either ``t0`` or ``times``
        tN = kwargs.pop('times', None)
        if tN is not None:
          self.t0 = np.nan
          self._tN_p = tN                                                             # The transit times. NOTE: Must be sorted!
          self._tN = TRANSITSARR(*self._tN_p)
          self.ntrans = len(self._tN_p)                                               # Number of transits; only used if tN is set (i.e., for TTVs)

      @property
      def times(self):
        return self._tN_p                                                             # The python-friendly list/array of transit times

      @times.setter
      def times(self, value):
        self._tN_p = value
        self.ntrans = len(self._tN_p)
        self._tN = TRANSITSARR(*self._tN_p)                                           # The pointer version that gets passed to C

      @property
      def duration(self):
        '''
        The approximate transit duration for the general case of an eccentric orbit

        '''
        ecc = self.ecc if not np.isnan(self.ecc) else np.sqrt(self.ecw**2 + self.esw**2)
        esw = self.esw if not np.isnan(self.esw) else ecc * np.sin(self.w)
        aRs = ((G * self.rhos * (1. + self.MpMs) *
              (self.per * DAYSEC)**2.) / (3. * np.pi))**(1./3.)
        inc = np.arccos(self.bcirc/aRs)
        becc = self.bcirc * (1 - ecc**2)/(1 - esw)
        tdur = self.per / 2. / np.pi * np.arcsin(((1. + self.RpRs)**2 -
               becc**2)**0.5 / (np.sin(inc) * aRs))
        tdur *= np.sqrt(1. - ecc**2.)/(1. - esw)
        return tdur

class LIMBDARK(ctypes.Structure):
      '''
      The class containing the limb darkening parameters

      '''

      _fields_ = [("ldmodel", ctypes.c_int),
                  ("u1", ctypes.c_double),
                  ("u2", ctypes.c_double),
                  ("q1", ctypes.c_double),
                  ("q2", ctypes.c_double),
                  ("c1", ctypes.c_double),
                  ("c2", ctypes.c_double),
                  ("c3", ctypes.c_double),
                  ("c4", ctypes.c_double)]

      def __init__(self, **kwargs):
        self.update(**kwargs)

      def update(self, **kwargs):
        '''

        '''

        self.ldmodel = kwargs.pop('ldmodel', QUADRATIC)
        self.u1 = kwargs.pop('u1', 0.40)                                              # ~ The sun seen by Kepler (http://arxiv.org/pdf/0912.2274.pdf)
        self.u2 = kwargs.pop('u2', 0.26)
        self.q1 = kwargs.pop('q1', np.nan)
        self.q2 = kwargs.pop('q2', np.nan)
        self.c1 = kwargs.pop('c1', np.nan)
        self.c2 = kwargs.pop('c2', np.nan)
        self.c3 = kwargs.pop('c3', np.nan)
        self.c4 = kwargs.pop('c4', np.nan)

        # If other coeffs are set, clear the defaults
        if (not np.isnan(self.q1)) or (not np.isnan(self.c1)):
          self.u1 = np.nan
          self.u2 = np.nan

class ARRAYS(ctypes.Structure):
      '''
      The class that stores the input and output arrays

      '''

      _fields_ = [("nstart", ctypes.c_int),
                  ("nend", ctypes.c_int),
                  ("ipts", ctypes.c_int),
                  ("_calloc", ctypes.c_int),
                  ("_balloc", ctypes.c_int),
                  ("_ialloc", ctypes.c_int),
                  ("_time", ctypes.POINTER(ctypes.c_double)),
                  ("_flux", ctypes.POINTER(ctypes.c_double)),
                  ("_bflx", ctypes.POINTER(ctypes.c_double)),
                  ("_M", ctypes.POINTER(ctypes.c_double)),
                  ("_E", ctypes.POINTER(ctypes.c_double)),
                  ("_f", ctypes.POINTER(ctypes.c_double)),
                  ("_r", ctypes.POINTER(ctypes.c_double)),
                  ("_x", ctypes.POINTER(ctypes.c_double)),
                  ("_y", ctypes.POINTER(ctypes.c_double)),
                  ("_z", ctypes.POINTER(ctypes.c_double)),
                  ("_b", ctypes.POINTER(ctypes.c_double)),
                  ("_iarr", ctypes.POINTER(ctypes.c_double))]

      def __init__(self, **kwargs):
        self.nstart = 0
        self.nend = 0
        self.ipts = 0
        self._calloc = 0
        self._balloc = 0
        self._ialloc = 0

      @property
      def time(self):
        return np.array([self._time[i] for i in range(self.nstart, self.nend)])

      @property
      def flux(self):
        return np.array([self._flux[i] for i in range(self.nstart, self.nend)])

      @property
      def bflx(self):
        return np.array([self._bflx[i] for i in range(self.nstart, self.nend)])

      @property
      def M(self):
        return np.array([self._M[i] for i in range(self.nstart, self.nend)])

      @property
      def E(self):
        return np.array([self._E[i] for i in range(self.nstart, self.nend)])

      @property
      def f(self):
        return np.array([self._f[i] for i in range(self.nstart, self.nend)])

      @property
      def r(self):
        return np.array([self._r[i] for i in range(self.nstart, self.nend)])

      @property
      def x(self):
        return np.array([self._x[i] for i in range(self.nstart, self.nend)])

      @property
      def y(self):
        return np.array([self._y[i] for i in range(self.nstart, self.nend)])

      @property
      def z(self):
        return np.array([self._z[i] for i in range(self.nstart, self.nend)])

      @property
      def b(self):
        return np.array([self._b[i] for i in range(self.nstart, self.nend)])

      @property
      def iarr(self):
        return np.array([self._iarr[i] for i in range(self.ipts)])

class SETTINGS(ctypes.Structure):
      '''
      The class that contains the light curve settings

      '''
      _fields_ = [("exptime", ctypes.c_double),
                  ("keptol", ctypes.c_double),
                  ("fullorbit", ctypes.c_int),
                  ("maxpts", ctypes.c_int),
                  ("exppts", ctypes.c_int),
                  ("binmethod", ctypes.c_int),
                  ("intmethod", ctypes.c_int),
                  ("maxkepiter", ctypes.c_int),
                  ("computed", ctypes.c_int),
                  ("binned", ctypes.c_int),
                  ("kepsolver", ctypes.c_int)]

      def __init__(self, **kwargs):
        self.exptime = KEPLONGEXP
        self.fullorbit = 0
        self.maxpts = 10000
        self.exppts = 50
        self.binmethod = RIEMANN
        self.intmethod = SMARTINT
        self.keptol = 1.e-15
        self.maxkepiter = 100
        self.kepsolver = NEWTON
        self.update(**kwargs)

      def update(self, **kwargs):
        '''

        '''

        self.exptime = kwargs.pop('exptime', self.exptime)                         # Long cadence integration time
        self.fullorbit = 1 if kwargs.pop('fullorbit', self.fullorbit) else 0          # Compute full orbit or just the transits (default)
        self.maxpts = kwargs.pop('maxpts', self.maxpts)                               # Maximum number of points in arrays (for mem. allocation)
        self.exppts = kwargs.pop('exppts', self.exppts)                               # Average flux over this many points for binning
        self.binmethod = kwargs.pop('binmethod', self.binmethod)                      # How to integrate when binning?
        self.intmethod = kwargs.pop('intmethod', self.intmethod)                      # Integration method
        self.keptol = kwargs.pop('keptol', self.keptol)                               # Kepler solver tolerance
        self.maxkepiter = kwargs.pop('maxkepiter', self.maxkepiter)                   # Maximum number of iterations in Kepler solver
        self.kepsolver = kwargs.pop('kepsolver', self.kepsolver)                      # Newton solver or fast M&D solver?
        self.computed = 0
        self.binned = 0

# Load the C library
try:
  lib = ctypes.CDLL(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'transitlib.so'))
except:
  raise Exception("Can't find `transitlib.so`; please run `make` to compile `pysyzygy`.")

# Declare the C functions; user should access these through the Transit() class below
_Compute = lib.Compute
_Compute.restype = ctypes.c_int
_Compute.argtypes = [ctypes.POINTER(TRANSIT), ctypes.POINTER(LIMBDARK),
                    ctypes.POINTER(SETTINGS), ctypes.POINTER(ARRAYS)]

_Bin = lib.Bin
_Bin.restype = ctypes.c_int
_Bin.argtypes = [ctypes.POINTER(TRANSIT), ctypes.POINTER(LIMBDARK),
                ctypes.POINTER(SETTINGS), ctypes.POINTER(ARRAYS)]

_Interpolate = lib.Interpolate
_Interpolate.restype = ctypes.c_int
_Interpolate.argtypes = [ndpointer(dtype=ctypes.c_double),
                        ctypes.c_int,
                        ctypes.c_int,
                        ctypes.POINTER(TRANSIT),
                        ctypes.POINTER(LIMBDARK), ctypes.POINTER(SETTINGS),
                        ctypes.POINTER(ARRAYS)]

_dbl_free = lib.dbl_free
_dbl_free.argtypes = [ctypes.POINTER(ctypes.c_double)]

# Error handling
def RaiseError(err):
  if (err == _ERR_NONE):
    return
  elif (err == _ERR_NOT_IMPLEMENTED):
    raise Exception("Option not implemented.")
  elif (err == _ERR_MAX_PTS):
    raise Exception("Maximum points in lightcurve exceeded. " +
                    "Try decreasing `exppts`, increasing `exptime`, or increasing "+
                    "`maxpts`.")
  elif (err == _ERR_NO_TRANSIT):
    raise Exception("Object does not transit the star.")
  elif (err == _ERR_BAD_ECC):
    raise Exception("Bad value for ``ecc``.")
  elif (err == _ERR_RC):
    raise Exception("Error in elliptic integral function RC().")
  elif (err == _ERR_RJ):
    raise Exception("Error in elliptic integral function RJ().")
  elif (err == _ERR_RF):
    raise Exception("Error in elliptic integral function RF().")
  elif (err == _ERR_RADIUS):
    raise Exception("Bad value for ``RpRs``.")
  elif (err == _ERR_EXP_PTS):
    raise Exception("The number of exposure points must be even.")
  elif (err == _ERR_NOT_COMPUTED):
    raise Exception("Lightcurve must be computed before it can be binned.")
  elif (err == _ERR_STAR_CROSS):
    raise Exception("Star-crossing orbit.")
  elif (err == _ERR_RHOS_ARS):
    raise Exception("Must specify one of ``rhos`` or ``aRs``.")
  elif (err == _ERR_RHOS):
    raise Exception("Bad value for ``per``.")
  elif (err == _ERR_ECC_W):
    raise Exception("Bad value for ``esw`` or ``ecw``.")
  elif (err == _ERR_LD):
    raise Exception("Bad value for the limb darkening coefficients.")
  elif (err == _ERR_T0):
    raise Exception("Bad value for ``t0``.")
  elif (err == _ERR_KEPLER):
    raise Exception("Error in Kepler solver.")
  else:
    raise Exception("Error in transit computation (%d)." % err)

class Transit():
  '''
  A user-friendly wrapper around the :py:class:`ctypes` routines.

  :param kwargs: The :py:mod:`pysyzygy` keyword arguments. These are:

    - **b** or **bcirc** - The (circular) impact parameter. Default `0.`
    - **MpMs** - The planet-star mass ratio. Default `0.`
    - **per** - The planet orbital period in days. Default `10.`
    - **RpRs** - The planet-star radius ratio. Default `0.1`
    - **rhos** or **aRs** - The stellar density in `g/cm^3` or the semi-major axis-stellar radius ratio. \
                            Default is `rhos = 1.4`, the density of the Sun
    - **ecc** and **w** or **esw** and **ecw** - The eccentricity and the longitude of pericenter in radians, \
                                                 or the two eccentricity vectors. Default is `ecc = 0.` and `w = 0.`
    - **t0** or **times** - The time of first transit, or the time of each of the transits (in case \
                            they are not periodic) in days. Default is `t0 = 0.`

    - **ldmodel** - The limb darkening model. Default `ps.QUADRATIC` (only option for now!)
    - **u1** and **u2** or **q1** and **q2** - The quadratic limb darkening parameters (u1, u2) or the \
                                               modified quadratic limb darkening parameters (q1, q2) \
                                               from `Kipping (2013) <http://dx.doi.org/10.1093/mnras/stt1435>`_. \
                                               Default is `u1 = 0.40` and `u2 = 0.26`

    - **exptime** - The exposure time in days for binning the model. Default `ps.KEPLONGEXP`
    - **fullorbit** - Compute the orbital parameters for the entire orbit? Only useful if \
                      you're interested in the full arrays of orbital parameters. Default `False`
    - **maxpts** - Maximum number of points in the model. Increase this if you're getting errors. Default `10,000`
    - **exppts** - The number of exposure points per cadence when binning the model. Default `50`
    - **binmethod** - The binning method. Default `ps.RIEMANN` (recommended)
    - **intmethod** - The integration method. Default `ps.SMARTINT` (recommended)
    - **keptol** - The tolerance of the Kepler solver. Default `1.e-15`
    - **maxkepiter** - Maximum number of iterations in the Kepler solver. Default `100`
    - **kepsolver** - The Kepler solver to use. Default `ps.NEWTON` (recommended)

  Once a :py:class:`Transit` model is instantiated, it may be called as follows:

  .. code-block::python

      import pysyzygy as ps
      import numpy as np

      # Instantiate the model
      trn = ps.Transit()

      # The observation times
      time = np.linspace(0, 10, 100)

      # Obtain the actual model, evaluated on the time array
      model = trn(time)

  '''

  def __init__(self, **kwargs):
    self.arrays = ARRAYS()
    self.limbdark = LIMBDARK()
    self.transit = TRANSIT()
    self.settings = SETTINGS()
    self.update(**kwargs)

  def update(self, **kwargs):
    '''
    Update the transit keyword arguments

    '''

    if kwargs.get('verify_kwargs', True):
      valid = [y[0] for x in [TRANSIT, LIMBDARK, SETTINGS] for y in x._fields_]       # List of valid kwargs
      valid += ['b', 'times']                                                         # These are special!
      for k in kwargs.keys():
        if k not in valid:
          raise Exception("Invalid kwarg '%s'." % k)

    if ('q1' in kwargs.keys()) and ('q2' in kwargs.keys()):
      kwargs.update({'ldmodel': KIPPING})
    elif ('c1' in kwargs.keys()) and ('c2' in kwargs.keys()) and \
         ('c3' in kwargs.keys()) and ('c4' in kwargs.keys()):
      kwargs.update({'ldmodel': NONLINEAR})

    self.limbdark.update(**kwargs)
    self.transit.update(**kwargs)
    self.settings.update(**kwargs)

  def __call__(self, t, param = 'binned'):
    if param == 'binned':
      array = _ARR_BFLX
    elif param == 'unbinned':
      array = _ARR_FLUX
    elif param == 'M':
      array = _ARR_M
    elif param == 'E':
      array = _ARR_E
    elif param == 'f':
      array = _ARR_F
    elif param == 'r':
      array = _ARR_R
    elif param == 'x':
      array = _ARR_X
    elif param == 'y':
      array = _ARR_Y
    elif param == 'z':
      array = _ARR_Z
    elif param == 'b':
      array = _ARR_B
    else:
      RaiseError(_ERR_NOT_IMPLEMENTED)

    # Ensure the time is a float array
    if not (type(t) is np.ndarray):
      t = np.array(t, dtype = 'float64')
    elif t.dtype != 'float64':
      t = np.array(t, dtype = 'float64')

    err = _Interpolate(t, len(t), array, self.transit, self.limbdark, self.settings,
                       self.arrays)
    if err != _ERR_NONE: RaiseError(err)
    res = self.arrays.iarr
    _dbl_free(self.arrays._iarr)
    self.arrays._ialloc = 0
    return res

  def Compute(self):
    '''
    Computes the light curve model

    '''

    err = _Compute(self.transit, self.limbdark, self.settings, self.arrays)
    if err != _ERR_NONE: RaiseError(err)

  def Bin(self):
    '''
    Bins the light curve model to the provided time array

    '''

    err = _Bin(self.transit, self.limbdark, self.settings, self.arrays)
    if err != _ERR_NONE: RaiseError(err)

  def Free(self):
    '''
    Frees the memory used by all of the dynamically allocated C arrays.

    '''

    if self.arrays._calloc:
      _dbl_free(self.arrays._time)
      _dbl_free(self.arrays._flux)
      _dbl_free(self.arrays._bflx)
      _dbl_free(self.arrays._M)
      _dbl_free(self.arrays._E)
      _dbl_free(self.arrays._f)
      _dbl_free(self.arrays._r)
      _dbl_free(self.arrays._x)
      _dbl_free(self.arrays._y)
      _dbl_free(self.arrays._z)
      self.arrays._calloc = 0
    if self.arrays._balloc:
      _dbl_free(self.arrays._b)
      self.arrays._balloc = 0
    if self.arrays._ialloc:
      _dbl_free(self.arrays._iarr)
      self.arrays._ialloc = 0

  def __del__(self):
    '''
    Free the C arrays when the last reference to the class goes out of scope!

    '''
    self.Free()