#!/usr/bin/env python
# encoding: utf-8
#
# Copyright SAS Institute
#
#  Licensed under the Apache License, Version 2.0 (the License);
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#

''' ESP Continuous Query '''

from __future__ import print_function, division, absolute_import, unicode_literals

import collections
import os
import re
import requests
import six
import xml.etree.ElementTree as ET
from six.moves import urllib
from .base import ESPObject, attribute
from .config import get_option
from .metadata import Metadata
from .windows import BaseWindow, get_window_class
from .templates.template import Template
from .utils import xml
from .utils.rest import get_params
from .utils.data import gen_name
from .utils.events import get_events
from .utils.notebook import scale_svg
from .utils.project import expand_path


class WindowDict(collections.MutableMapping):
    '''
    Dictionary for holding window objects

    Attributes
    ----------
    project : string
        The name of the project
    contquery : string
        The name of the continuous query
    session : requests.Session
        The session for the windows

    Parameters
    ----------
    *args : one-or-more arguments, optional
        Positional arguments to MutableMapping
    **kwargs : keyword arguments, optional
        Keyword arguments to MutableMapping

    '''

    def __init__(self, *args, **kwargs):
        collections.MutableMapping.__init__(self, *args, **kwargs)
        self._data = dict()
        self.project = None
        self.project_handle = None
        self.contquery = None
        self.session = None

    @property
    def session(self):
        '''
        The session for the windows

        Returns
        -------
        string

        '''
        return self._session

    @session.setter
    def session(self, value):
        self._session = value
        for item in self._data.values():
            item.session = self._session

    @property
    def project(self):
        '''
        The project that windows are associated with

        Returns
        -------
        string

        '''
        return self._project

    @project.setter
    def project(self, value):
        self._project = getattr(value, 'name', value)
        for item in self._data.values():
            item.project = self._project

    @property
    def contquery(self):
        '''
        The continuous query that windows are associated with

        Returns
        -------
        string

        '''
        return self._contquery

    @contquery.setter
    def contquery(self, value):
        self._contquery = getattr(value, 'name', value)
        for item in self._data.values():
            item.contquery = self._contquery
        if hasattr(value, 'project'):
            self.project = value.project

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        if not isinstance(value, BaseWindow):
            raise TypeError('Only Window objects can be values '
                            'in a ContinuousQuery')
        value._register_to_project(self.project_handle)

        oldname = value.name
        if not value.template:
            value.base_name = key
        value.project = self.project
        value.contquery = self.contquery
        value.session = self.session
        self._data[key] = value

        # Make sure targets get updated with new name
        if oldname != key:
            for window in self._data.values():
                for target in set(window.targets):
                    if target.name == oldname:
                        role = target.role
                        window.targets.discard(target)
                        window.add_target(key, role=role)

    def __delitem__(self, key):
        del self._data[key]
        for window in self._data.values():
            window.delete_target(key)

    def __iter__(self):
        return iter(self._data)

    def __len__(self):
        return len(self._data)

    def __str__(self):
        return str(self._data)

    def __repr__(self):
        return repr(self._data)


class TemplateDict(WindowDict):
    '''
    Dictionary for holding template objects

    Attributes
    ----------
    project : string
        The name of the project
    contquery : string
        The name of the continuous query
    session : requests.Session
        The session for the windows

    Parameters
    ----------
    *args : one-or-more arguments, optional
        Positional arguments to MutableMapping
    **kwargs : keyword arguments, optional
        Keyword arguments to MutableMapping

    '''

    def __init__(self, *args, **kwargs):
        WindowDict.__init__(self, *args, **kwargs)

    def __setitem__(self, key, value):
        if not isinstance(value, Template):
            raise TypeError('Only Template objects are valid values')

        value.name = key
        value.project = self.project
        value.contquery = self.contquery
        value.session = self.session
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]


class ContinuousQuery(ESPObject, collections.MutableMapping):
    '''
    Continuous Query

    Parameters
    ----------
    name : string
        Name of the continuous query
    trace : string, optional
        One or more space-separated window names or IDs
    index_type : string, optional
        A default index type for all windows in the continuous query that
        do not explicitly specify an index type
        Valid values: 'rbtree', 'hash', 'ln_hash', 'cl_hash', 'fw_hash', 'empty'
    timing_threshold : int, optional
        When a window in the query takes more than value microseconds to
        compute for a given event or event block, a warning message is logged
    include_singletons : bool, optional
        Specify whether to add unattached source windows
    description : string, optional
        Description of the continuous query

    Attributes
    ----------
    project : string or Project
        Name of the project the query is associated with
    windows : dict
        Collection of windows in the continuous query
    metadata : dict
        Metadata dictionary
    url : string
        URL of the continuous query

    Notes
    -----
    All parameters are also available as instance attributes.

    Returns
    -------
    :class:`ContinuousQuery`

    '''
    trace = attribute('trace', dtype='string')
    index_type = attribute('index', dtype='string',
                           values={'rbtree': 'pi_RBTREE', 'hash': 'pi_HASH',
                                   'ln_hash': 'pi_LN_HASH', 'cl_hash': 'pi_CL_HASH',
                                   'fw_hash': 'pi_FW_HASH', 'empty': 'pi_EMPTY'})
    timing_threshold = attribute('timing-threshold', dtype='int')
    include_singletons = attribute('include-singletons', dtype='bool')

    def __init__(self, name=None, trace=None, index_type=None,
                 timing_threshold=None, include_singletons=None, description=None):
        self.windows = WindowDict()
        self.templates = TemplateDict()
        ESPObject.__init__(self, attrs=locals())
        self.project = None
        self.name = name or gen_name(prefix='cq_')
        self.description = description
        self.metadata = {}

    @property
    def session(self):
        '''
        The requests.Session object for the continuous query

        Returns
        -------
        string

        '''
        return ESPObject.session.fget(self)

    @session.setter
    def session(self, value):
        ESPObject.session.fset(self, value)
        self.windows.session = value

    @property
    def name(self):
        '''
        The name of the continuous query

        Returns
        -------
        string

        '''
        return self._name

    @name.setter
    def name(self, value):
        self._name = value
        self.windows.contquery = value
        self.templates.contquery = value

    @property
    def project(self):
        '''
        The name of the project

        Returns
        -------
        string

        '''
        return self._project

    @project.setter
    def project(self, value):
        self._project = getattr(value, 'name', value)
        self.windows.project = self._project
        self.templates.project = self._project

    def add_window(self, window):
        '''
        Add a window to the continuous query

        Parameters
        ----------
        window : Window
            The Window object to add

        Returns
        -------
        :class:`Window`

        '''
        if not window.name:
            window.name = gen_name(prefix='w_')

        self.windows[window.name] = window

        return window

    def add_windows(self, *windows):
        '''
        Add one or more windows to the continuous query

        Parameters
        ----------
        windows : one-or-more-Windows
            The Window objects to add

        Returns
        -------
        tuple of :class:`Window`s

        '''
        for item in windows:
            self.add_window(item)
        return windows

    def add_template(self, template):
        '''
        Add a template object

        Parameters
        ----------
        template : Template
        a Template object to add to the project

        Returns
        -------
        :class:`Template`

        '''
        if not template.name:
            template.name = gen_name(prefix='t_')

        self.templates[template.name] = template

        for key, window in sorted(six.iteritems(template.windows)):
            self.add_window(window)
        return template

    def delete_templates(self, *templates):
        '''
        Delete templates and related windows

        Parameters
        ----------
        templates : one-or-more strings or Template objects
            The template to delete

        '''
        for item in templates:
            template_key = getattr(item, 'name', item)
            template = self.templates[template_key]
            self.delete_windows(*six.itervalues(template.windows))
            del self.templates[template_key]

        return self.templates

    delete_template = delete_templates

    # def get_template(self, template):
    #     return

    def copy(self, deep=False):
        '''
        Return a copy of the object

        Parameters
        ----------
        deep : bool, optional
            Should sub-objects be copied as well?

        Returns
        -------
        :class:`ContinuousQuery`

        '''
        out = type(self)()

        out.session = self.session
        out.project = self.project

        for key, value in self._get_attributes(use_xml_values=False).items():
            setattr(out, key, value)

        if deep:
            out.windows = dict([(k, v.copy(deep=True))
                                for k, v in self.windows.items()])
        else:
            out.windows.update(self.windows)

        return out

    def __copy__(self):
        return self.copy(deep=False)

    def __deepcopy__(self, memo):
        return self.copy(deep=True)

    @property
    def fullname(self):
        return '%s.%s' % (self.project, self.name)

    @property
    def url(self):
        '''
        URL of the continuous query

        Returns
        -------
        string

        '''
        if not self.project:
            raise ValueError('This continuous query is not associated with a project.')
        return urllib.parse.urljoin(self.base_url, '%s/%s/' % (self.project, self.name))

    @classmethod
    def from_xml(cls, data, project=None, session=None):
        '''
        Create continous query from XML definition

        Parameters
        ----------
        data : xml-string or ElementTree.Element
            XML continuous query definition
        session : requests.Session, optional
            The session object

        Returns
        -------
        :class:`ContinuousQuery`

        '''
        out = cls()
        out.session = session
        out.project = project

        if isinstance(data, six.string_types):
            data = xml.from_xml(data)

        out._set_attributes(data.attrib)

        for desc in data.findall('./description'):
            out.description = desc.text

        for item in data.findall('./windows/*'):
            try:
                wcls = get_window_class(item.tag)
            except KeyError:
                raise TypeError('Unknown window type: %s' % item.tag)

            window = wcls.from_xml(item, session=session)
            out.windows[window.name] = window

        for item in data.findall('./edges/*'):
            for target in re.split(r'\s+', item.attrib.get('target', '').strip()):
                if not target:
                    continue
                for source in re.split(r'\s+', item.attrib.get('source', '').strip()):
                    if not source:
                        continue
                    out.windows[source].add_target(out.windows[target], role=item.get('role'),
                                                   slot=item.get('slot'))

        for item in data.findall('./metadata/meta'):
            if 'id' in item.attrib.keys():
                out.metadata[item.attrib['id']] = item.text
            elif 'name' in item.attrib.keys():
                out.metadata[item.attrib['name']] = item.text

        return out

    from_element = from_xml

    def to_element(self):
        '''
        Export continuous query definition to ElementTree.Element

        Returns
        -------
        :class:`ElementTree.Element`

        '''
        out = xml.new_elem('contquery', xml.get_attrs(self, exclude='project'))

        if self.description:
            xml.add_elem(out, 'description', text_content=self.description)

        if self.metadata:
            metadata = xml.add_elem(out, 'metadata')
            for key, value in sorted(six.iteritems(self.metadata)):
                xml.add_elem(metadata, 'meta', attrib=dict(id=key),
                             text_content=value)

        windows = xml.add_elem(out, 'windows')

        sources = {}
        if self.windows:
            edges = []
            for name, window in sorted(six.iteritems(self.windows)):
                xml.add_elem(windows, window.to_element(query=self))
                for target in window.targets:
                    sources.setdefault(target.name, []).append(window.name)
                    attrib = dict(source=window.name, target=target.name)
                    if target.role:
                        attrib['role'] = target.role
                    if target.slot:
                        attrib['slot'] = target.slot
                    edges.append((target._index, attrib))
            if edges:
                elem = xml.add_elem(out, 'edges')
                for i, attrib in sorted(edges):
                    xml.add_elem(elem, 'edge', attrib=attrib)

        else:
            xml.add_elem(windows,
                get_window_class('window-source')().to_element(query=self))

        # Replace "inherit" data types with the real data type
        n_inherit = -1
        while True:
            inherit = out.findall('./windows/*/schema/fields/field[@type="inherit"]')
            if len(inherit) == n_inherit:
                break
            n_inherit = len(inherit)
            for window in out.findall('./windows/*'):
                for field in window.findall('./schema/fields/field[@type="inherit"]'):
                    for source in sources[window.attrib['name']]:
                        fname = field.attrib['name']
                        if source not in self.windows:
                            raise ValueError("Could not determine data type of "
                                             "field '%s' on window '%s'" % (fname, source))
                        win = self.windows[source]
                        if hasattr(win, 'schema') and fname in win.schema:
                            dtype = win.schema[fname].type
                            field.set('type', dtype)

        return out

    def to_xml(self, pretty=False):
        '''
        Export continuous query definition to XML

        Parameters
        ----------
        pretty : bool, optional
            Should the output embed whitespaced for readability?

        Returns
        -------
        string

        '''
        return xml.to_xml(self.to_element(), pretty=pretty)

    def _persist_metadata(self):
        if self.metadata:
            self._set_metadata(self.metadata)

    def _clear_metadata(self):
        self.metadata.clear()

    def _set_metadata(self, data):
        for key, value in six.iteritems(data):
            self._put(urllib.parse.urljoin(self.base_url,
                                           'projectMetadata/%s/%s/%s' %
                                           (self.project, self.name, key)),
                      data='%s' % value)

    def _del_metadata(self, *data):
        for key in data:
            self._delete(urllib.parse.urljoin(self.base_url,
                                              'projectMetadata/%s/%s/%s' %
                                              (self.project, self.name, key)))

    def save_xml(self, dest, mode='w', pretty=True, **kwargs):
        '''
        Save the continuous query XML to a file

        Parameters
        ----------
        dest : string or file-like
            The destination of the XML content
        mode : string, optional
            The write mode for the output file (only used if `dest` is a string)
        pretty : boolean, optional
            Should the XML include whitespace for readability?

        '''
        if isinstance(dest, six.string_types):
            with open(dest, mode=mode, **kwargs) as output:
                output.write(self.to_xml(pretty=pretty))
        else:
            dest.write(self.to_xml(pretty=pretty))

    def to_graph(self, graph=None, schema=False, template_detail=False):
        '''
        Export continuous query definition to graphviz.Digraph

        Parameters
        ----------
        graph : graphviz.Graph, optional
            The parent graph to add to
        schema : bool, optional
            Should window schemas be included?
        template_detail : bool, optional
            Should template detail be shown?

        Returns
        -------
        :class:`graphviz.Digraph`

        '''
        try:
            import graphviz as gv
        except ImportError:
            raise ImportError('The graphviz module is required for exporting to graphs.')

        if graph is None:
            graph = gv.Digraph(format='svg')
            graph.attr('node', shape='rect')
            graph.attr('graph', rankdir='LR', center='false')
            graph.attr('edge', fontname='times-italic')

        if self.windows:
            qgraph = gv.Digraph(format='svg', name='cluster_%s' % self.fullname.replace('.', '_'))
            qgraph.attr('node', shape='rect')
            qgraph.attr('graph', fontname='helvetica')
            qgraph.attr('edge', fontname='times-italic')
            qgraph.attr(label=self.name, labeljust='l',
                        style='filled,rounded', color='#a0a0a0', fillcolor='#f0f0f0',
                        fontcolor='black')

            if self.templates:
                for tkey, template in sorted(self.templates.items()):
                    qgraph.subgraph(template.to_graph(schema=schema, detail=template_detail))

            for wkey, win in sorted(self.windows.items()):
                if not win.template:
                    win.to_graph(graph=qgraph, schema=schema or get_option('display.show_schema'))
                for target in win.targets:
                    if target.name not in self.windows:
                        continue
                    tail_name = win.template.fullname if win.template and not template_detail else win.fullname
                    head_name = target.template.fullname if target.template and not template_detail \
                        else self.windows[target.name].fullname
                    if win.template and target.template and win.template == target.template:
                        continue
                    graph.edge(tail_name, head_name, label=target.role or '')

            graph.subgraph(qgraph)

        else:
            graph.node(self.fullname, label=self.name, labeljust='l',
                       style='filled,rounded', color='#a0a0a0', fillcolor='#f0f0f0',
                       fontcolor='black')

        return graph

    def _repr_svg_(self):
        try:
            return scale_svg(self.to_graph()._repr_svg_())
        except ImportError:
            raise AttributeError('_repr_svg_')

    def __str__(self):
        return '%s(name=%s, project=%s)' % (type(self).__name__,
                                            repr(self.name),
                                            repr(self.project))

    def __repr__(self):
        return str(self)

    def rename_window(self, window, newname):
        '''
        Rename a window and update targets

        Parameters
        ----------
        window : string or Window object
            The window to rename
        newname : string
            The new name of the window

        '''
        self.windows[newname] = self.windows[getattr(window, 'name', window)]
        self.delete_window(window)

    def delete_windows(self, *windows):
        '''
        Delete windows and update targets

        Parameters
        ----------
        windows : one-or-more strings or Window objects
            The window to delete

        '''
        for item in windows:
            del self.windows[getattr(item, 'name', item)]

    delete_window = delete_windows

    def get_windows(self, name=None, type=None, filter=None):
        '''
        Retrieve windows from the server

        Parameters
        ----------
        name : string or list-of-strings, optional
            Names of the windows which you want to retrieve
        type : string or list-of-strings, optional
            Types of windows you want to retrieve
        filter : string or list-of-strings, optional
            Function filter indicating which windows to retrieve

        Notes
        -----
        This method retrieves window definitions from the server, not
        from the collection of windows held locally in this projects
        variables.

        Returns
        -------
        dict of :class:`Window`

        '''
        path = [None] + expand_path(name)
        res = self._get(urllib.parse.urljoin(self.base_url, 'windowXml'),
                        params=get_params(project=self.project,
                                          contquery=self.name,
                                          name=path[-1], type=type,
                                          filter=filter))
        windows = dict()
        for item in res.findall('./*'):
            try:
                wcls = get_window_class(item.tag)
            except KeyError:
                raise TypeError('Unknown window type: %s' % item.tag)
            window = wcls.from_xml(item, session=self.session)
            windows[window.fullname.split('.', 1)[-1]] = window
        return windows

    def get_window(self, name):
        '''
        Retrieve specified window

        Parameters
        ----------
        name : string
            Name of the window

        Notes
        -----
        This method retrieves window definitions from the server, not
        from the collection of windows held locally in this projects
        variables.

        Returns
        -------
        :class:`Window`

        '''
        out = self.get_windows(name)
        if out:
            return list(out.values())[0]
        raise KeyError("No window with the name '%s'." % name)

#
# MutableMapping methods
#

    def __getitem__(self, key):
        return self.windows[key]

    def __setitem__(self, key, value):
        self.windows[key] = value

    def __delitem__(self, key):
        del self.windows[key]

    def __iter__(self):
        return iter(self.windows)

    def __len__(self):
        return len(self.windows)

    def __contains__(self, value):
        return value in self.windows