#  Copyright 2008-2015 Nokia Networks
#  Copyright 2016-     Robot Framework Foundation
#
#  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.

from robot.errors import DataError
from robot.model import SuiteVisitor
from robot.utils import ET, ETSource, get_error_message

from .executionresult import Result, CombinedResult
from .flattenkeywordmatcher import (FlattenByNameMatcher, FlattenByTypeMatcher,
                                    FlattenByTagMatcher)
from .merger import Merger
from .xmlelementhandlers import XmlElementHandler


def ExecutionResult(*sources, **options):
    """Factory method to constructs :class:`~.executionresult.Result` objects.

    :param sources: XML source(s) containing execution results.
        Can be specified as paths, opened file objects, or strings/bytes
        containing XML directly. Support for bytes is new in RF 3.2.
    :param options: Configuration options.
        Using ``merge=True`` causes multiple results to be combined so that
        tests in the latter results replace the ones in the original.
        Setting ``rpa`` either to ``True`` (RPA mode) or ``False`` (test
        automation) sets execution mode explicitly. By default it is got
        from processed output files and conflicting modes cause an error.
        Other options are passed directly to the
        :class:`ExecutionResultBuilder` object used internally.
    :returns: :class:`~.executionresult.Result` instance.

    Should be imported by external code via the :mod:`robot.api` package.
    See the :mod:`robot.result` package for a usage example.
    """
    if not sources:
        raise DataError('One or more data source needed.')
    if options.pop('merge', False):
        return _merge_results(sources[0], sources[1:], options)
    if len(sources) > 1:
        return _combine_results(sources, options)
    return _single_result(sources[0], options)


def _merge_results(original, merged, options):
    result = ExecutionResult(original, **options)
    merger = Merger(result, rpa=result.rpa)
    for path in merged:
        merged = ExecutionResult(path, **options)
        merger.merge(merged)
    return result


def _combine_results(sources, options):
    return CombinedResult(ExecutionResult(src, **options) for src in sources)


def _single_result(source, options):
    ets = ETSource(source)
    result = Result(source, rpa=options.pop('rpa', None))
    try:
        return ExecutionResultBuilder(ets, **options).build(result)
    except IOError as err:
        error = err.strerror
    except:
        error = get_error_message()
    raise DataError(f"Reading XML source '{ets}' failed: {error}")


class ExecutionResultBuilder:
    """Builds :class:`~.executionresult.Result` objects based on output files.

    Instead of using this builder directly, it is recommended to use the
    :func:`ExecutionResult` factory method.
    """

    def __init__(self, source, include_keywords=True, flattened_keywords=None):
        """
        :param source: Path to the XML output file to build
            :class:`~.executionresult.Result` objects from.
        :param include_keywords: Boolean controlling whether to include
            keyword information in the result or not. Keywords are
            not needed when generating only report. Although the the option name
            has word "keyword", it controls also including FOR and IF structures.
        :param flatten_keywords: List of patterns controlling what keywords to
            flatten. See the documentation of ``--flattenkeywords`` option for
            more details.
        """
        self._source = source \
            if isinstance(source, ETSource) else ETSource(source)
        self._include_keywords = include_keywords
        self._flattened_keywords = flattened_keywords

    def build(self, result):
        # Parsing is performance optimized. Do not change without profiling!
        handler = XmlElementHandler(result)
        with self._source as source:
            self._parse(source, handler.start, handler.end)
        result.handle_suite_teardown_failures()
        if not self._include_keywords:
            result.suite.visit(RemoveKeywords())
        return result

    def _parse(self, source, start, end):
        context = ET.iterparse(source, events=('start', 'end'))
        if not self._include_keywords:
            context = self._omit_keywords(context)
        elif self._flattened_keywords:
            context = self._flatten_keywords(context, self._flattened_keywords)
        for event, elem in context:
            if event == 'start':
                start(elem)
            else:
                end(elem)
                elem.clear()

    def _omit_keywords(self, context):
        omitted_kws = 0
        for event, elem in context:
            # Teardowns aren't omitted yet to allow checking suite teardown status.
            # They'll be removed later when not needed in `build()`.
            omit = elem.tag in ('kw', 'for', 'if') and elem.get('type') != 'TEARDOWN'
            start = event == 'start'
            if omit and start:
                omitted_kws += 1
            if not omitted_kws:
                yield event, elem
            elif not start:
                elem.clear()
            if omit and not start:
                omitted_kws -= 1

    def _flatten_keywords(self, context, flattened):
        # Performance optimized. Do not change without profiling!
        name_match, by_name = self._get_matcher(FlattenByNameMatcher, flattened)
        type_match, by_type = self._get_matcher(FlattenByTypeMatcher, flattened)
        tags_match, by_tags = self._get_matcher(FlattenByTagMatcher, flattened)
        started = -1  # if 0 or more, we are flattening
        tags = []
        containers = {'kw', 'for', 'while', 'iter', 'if', 'try'}
        inside_kw = 0  # to make sure we don't read tags from a test
        seen_doc = False
        for event, elem in context:
            tag = elem.tag
            start = event == 'start'
            end = not start
            if start:
                if tag in containers:
                    inside_kw += 1
                    if started >= 0:
                        started += 1
                    elif by_name and name_match(elem.get('name', ''), elem.get('library')):
                        started = 0
                        seen_doc = False
                    elif by_type and type_match(tag):
                        started = 0
                        seen_doc = False
                    tags = []
            else:
                if tag in containers:
                    inside_kw -= 1
                    if started == 0 and not seen_doc:
                        doc = ET.Element('doc')
                        doc.text = '_*Content flattened.*_'
                        yield 'start', doc
                        yield 'end', doc
                elif by_tags and inside_kw and started < 0 and tag == 'tag':
                    tags.append(elem.text or '')
                    if tags_match(tags):
                        started = 0
                        seen_doc = False
                elif started == 0 and tag == 'doc':
                    seen_doc = True
                    elem.text = f"{elem.text or ''}\n\n_*Content flattened.*_".strip()
            if started <= 0 or tag == 'msg':
                yield event, elem
            else:
                elem.clear()
            if started >= 0 and end and tag in containers:
                started -= 1

    def _get_matcher(self, matcher_class, flattened):
        matcher = matcher_class(flattened)
        return matcher.match, bool(matcher)


class RemoveKeywords(SuiteVisitor):

    def start_suite(self, suite):
        suite.setup = None
        suite.teardown = None

    def visit_test(self, test):
        test.body = []
