# Copyright (C) [2015-] [Thomson Reuters LLC]
# Copyright (C) [2015-] [Panos Kittenis]

# 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.

"""Graphite template parsing functions per InfluxDB's Graphite service template syntax"""

from __future__ import absolute_import, print_function
import logging
from collections import deque
from heapq import heappush, heappop

logger = logging.getLogger('influxgraph')


class TemplateFilter(object):
    """Filter metric paths on Graphite glob pattern"""

    def __init__(self, pattern):
        self.pattern = [p for p in pattern.split('.') if p]

    def match(self, path):
        path = path.split('.')
        return self.match_split_path(path)

    def match_split_path(self, split_path):
        for i, pat in enumerate(self.pattern):
            if pat == '*':
                continue
            try:
                if not pat == split_path[i]:
                    return False
            except IndexError:
                return False
        return True

class InvalidTemplateError(Exception):
    """Raised on Graphite template configuration validation errors"""
    pass


class TemplateMatchError(Exception):
    """Raised on errors matching template with path"""
    pass


# Function as per Python official documentation
def heapsort(iterable):
    h = []
    for value in iterable:
        heappush(h, value)
    return [heappop(h) for _ in range(len(h))]

def parse_influxdb_graphite_templates(templates, separator='.'):
    """Parse InfluxDB template configuration and return parsed templates

    :param templates: Template patterns to parse. \
    Format is [filter] <template> [tag1=value1,tag2=value2]
    :type templates: list(str)
    :param separator: (Optional) Separator to use when storing greedy matched columns
    :type separator: str

    :raises: :mod:`InvalidTemplateError` on invalid template format used in any
    template pattern
    """
    # Logic converted to Python from InfluxDB's Golang Graphite template parsing
    # Format is [filter] <template> [tag1=value1,tag2=value2]
    parsed_templates = []
    for pattern in templates:
        template = pattern
        _filter = ""
        parts = template.split()
        if len(parts) < 1:
            continue
        elif len(parts) >= 2:
            if '=' in parts[1]:
                template = parts[0]
            else:
                _filter = parts[0]
                template = parts[1]
        # Parse out the default tags specific to this template
        default_tags = {}
        if '=' in parts[-1]:
            tags = [d.strip() for d in parts[-1].split(',')]
            for tag in tags:
                tag_items = [d.strip() for d in tag.split('=')]
                default_tags[tag_items[0]] = tag_items[1]
        filter_parser = TemplateFilter(_filter) if _filter else None
        parsed_templates.append((filter_parser,
                                 _generate_template_tag_index(template),
                                 default_tags, separator))
    for (_, template, _, _) in parsed_templates:
        _template_sanity_check(template)
    return parsed_templates

def _template_sanity_check(template):
    field = ""
    measurement_wildcard, field_wildcard = False, False
    for tag in template.values():
        if tag == 'measurement*':
            measurement_wildcard = True
        if tag == 'field*':
            field_wildcard = True
        if tag == 'field':
            if field:
                raise InvalidTemplateError(
                    "'field' can only be used once in each template - %s",
                    template)
            field = tag
    if measurement_wildcard and field_wildcard:
        raise InvalidTemplateError(
            "Either 'field*' or 'measurement*' can be used in each template, not both - %s",
            template)

def apply_template(metric_path_parts, template, default_tags, separator='.'):
    """Apply template to metric path parts and return measurements, tags and field

    :raises: mod:`TemplateMatchError` on error matching template"""
    measurement = []
    tags = {}
    field = ""
    if len(metric_path_parts) < len(template.keys()):
        raise TemplateMatchError()
    for i, tag in template.items():
        if i >= len(metric_path_parts):
            continue
        if tag == 'measurement':
            measurement.append(metric_path_parts[i])
        elif tag == 'field':
            field = metric_path_parts[i]
        elif tag == 'field*':
            field = separator.join(metric_path_parts[i:])
            break
        elif tag == 'measurement*':
            measurement.extend(metric_path_parts[i:])
            break
        elif tag != "":
            tags.setdefault(tag, []).append(metric_path_parts[i])
    for tag, values in tags.items():
        tags[tag] = separator.join(values)
    if default_tags:
        tags.update(default_tags)
    return separator.join(measurement), tags, field

def _generate_template_tag_index(template):
    _tags = template.split('.')
    tags = {}
    for i, tag in enumerate(_tags):
        if not tag:
            tag = None
        tags[i] = tag
    return tags

def get_series_with_tags(paths, all_fields, graphite_templates,
                         separator='.'):
    if not graphite_templates:
        return [paths[0:1]]
    series = deque()
    split_path, template = _split_series_with_tags(paths, graphite_templates)
    if not split_path:
        # No template match
        return series
    if 'field' in template.values() or 'field*' in template.values():
        _add_fields_to_paths(
            all_fields[paths[0]], split_path, series, separator=separator)
    else:
        series.append(split_path)
    return series

def _split_series_with_tags(paths, graphite_templates):
    split_path, template = deque(), None
    tags_values = [p.split('=') for p in paths[1:]]
    for (_filter, template, _, separator) in graphite_templates:
        _make_path_from_template(
            split_path, paths[0], template, tags_values, separator=separator)
        # Split path should be at least as large as number of wanted
        # template tags taking into account measurement and number of fields
        # in template
        field_inds = len([v for v in template.values()
                          if v and 'field' in v])
        if (len(split_path) + field_inds) >= len(
                [k for k, v in template.items() if v]):
            path = [p[1] for p in heapsort(split_path)]
            if _filter:
                if _filter.match_split_path(path):
                    return path, template
            else:
                return path, template
            split_path = []
            continue
        # Reset path if template does not match
        else:
            split_path = []
    return [], template

def _make_path_from_template(split_path, measurement, template, tags_values,
                             separator='.'):
    measurement_found = False
    if not tags_values and separator in measurement and \
       'measurement*' == [t for t in template.values() if t][0]:
        for i, measurement in enumerate(measurement.split(separator)):
            split_path.append((i, measurement))
        return
    for (tag_key, tag_val) in tags_values:
        for i, tmpl_tag_key in template.items():
            if not tmpl_tag_key:
                continue
            if tag_key == tmpl_tag_key:
                split_path.append((i, tag_val))
            elif 'measurement' in tmpl_tag_key and not measurement_found:
                measurement_found = True
                split_path.append((i, measurement))

def _add_fields_to_paths(fields, split_path, series,
                         separator='.'):
    for field_key in fields:
        field_keys = [f for f in field_key.split(separator)
                      if f != 'value']
        if len(field_keys) > 0:
            series.append(split_path + field_keys)
