# -*- coding: utf-8 -*-
"""
API Blueprint (https://github.com/apiaryio/api-blueprint) parser which uses
Markdown (https://pythonhosted.org/Markdown/).

Released under New BSD License.

Copyright © 2015, Vadim Markovtsev :: AO InvestGroup
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the AO InvestGroup nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL VADIM MARKOVTSEV BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
from copy import deepcopy

from itertools import chain
from collections import OrderedDict
import json
from markdown import to_html_string
import re
from six import add_metaclass, string_types
import sys
from types import GeneratorType
from uritemplate import URITemplate
import weakref
from xml.etree import ElementTree


report_warnings = True

try:
    ustr = unicode
except NameError:
    ustr = str


def select_pos(*args):
    pos = 100500
    if len(args) == 1 and isinstance(args[0], GeneratorType):
        args = args[0]
    for arg in args:
        if -1 < arg < pos:
            pos = arg
    if pos == 100500:
        pos = -1
    return pos


def get_section_name(txt):
    if txt is None:
        return None
    sep_pos = select_pos(txt.find(c) for c in (' ', '\t', ':'))
    if sep_pos < 0:
        sep_pos = len(txt)
    return txt[:sep_pos]


def get_pre_contents(node):
    cnt = node.text
    if node.tag == "pre" and not cnt and len(node) == 1 and \
            node[0].tag == "code":
        cnt = node[0].text
    return cnt


def parse_description(sequence, index, *stop_tags):
    desc = ""
    while len(sequence) > index and sequence[index].tag not in stop_tags:
        desc += to_html_string(sequence[index]) + "\n"
        index += 1
    return desc.strip() if desc else None, index


def from_none(exc):
    """Emulates raise ... from None (PEP 409) on older Python-s
    """
    try:
        exc.__cause__ = None
    except AttributeError:
        exc.__context__ = None
    return exc


def property_with_parent(name, ptype):
    def getter(self):
        return getattr(self, "_" + name)

    def setter(self, value):
        if value is not None:
            assert isinstance(value, ptype)
            if value.parent is None:
                value._parent = self
        setattr(self, "_" + name, value)

    return property(getter, setter)


class OrderedDefaultDict(OrderedDict):
    def __init__(self, factory, *args, **kwargs):
        super(OrderedDefaultDict, self).__init__(*args, **kwargs)
        self.default_factory = factory

    def __missing__(self, key):
        result = self[key] = self.default_factory()
        return result


class SelfParsingSectionRegistryDict(type):
    registry = {}

    def __getitem__(cls, item):
        return cls.registry[item]


@add_metaclass(SelfParsingSectionRegistryDict)
class SelfParsingSectionRegistry(type):
    def __init__(cls, what, bases=None, clsdict=None):
        try:
            section_type = clsdict["SECTION_TYPE"]
        except KeyError:
            # base classes
            pass
        else:
            if not isinstance(section_type, (tuple, list, set)):
                section_type = section_type,
            for val in section_type:
                assert val not in SelfParsingSectionRegistry.registry
                SelfParsingSectionRegistry.registry[val] = cls
        super(SelfParsingSectionRegistry, cls).__init__(what, bases, clsdict)


class SmartReprMixin(object):
    def __repr__(self):
        s = str(self)
        if s.startswith(type(self).__name__):
            s = s[len(type(self).__name__):].strip()
        return "%s %s" % (super(SmartReprMixin, self).__repr__(), s)


class Section(SmartReprMixin):
    NESTED_ATTRS = tuple()

    def __init__(self, parent):
        super(Section, self).__init__()
        self._parent = parent

    @property
    def parent(self):
        return self._parent

    @property
    def _parent(self):
        return self.__parent

    @_parent.setter
    def _parent(self, value):
        if value is not None and not isinstance(value, weakref.ProxyType):
            value = weakref.proxy(value)
        self.__parent = value

    def _fix_parents(self, parent):
        self._parent = parent
        for attr in self.NESTED_ATTRS:
            attr = getattr(self, attr)
            if attr is None:
                continue
            if isinstance(attr, Section):
                attr._fix_parents(self)
                continue
            children = getattr(attr, "values", None)
            children = children() if children is not None else attr
            for child in children:
                if isinstance(child, Section):
                    child._fix_parents(self)


class NamedSection(Section):
    def __init__(self, parent, name, description):
        super(NamedSection, self).__init__(parent)
        self._name = name
        self._description = description

    @property
    def name(self):
        return self._name

    @property
    def description(self):
        return self._description


def Collection(child_type):
    @add_metaclass(SelfParsingSectionRegistry)
    class Base(Section):
        NESTED_ATTRS = "_children",

        def __init__(self, parent, children):
            super(Base, self).__init__(parent)
            self._children = OrderedDict()
            for child in children:
                assert isinstance(child, child_type)
                if child.parent is None:
                    child._parent = self
                self._children[child.name] = child
            self.__dict__.update(self._children)

        def __iter__(self):
            for child in self._children.values():
                yield child

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

        def __getitem__(self, item):
            return self._children[item]

        @classmethod
        def parse_from_etree(cls, parent, node):
            if len(node) == 0 or node[0].tag != "ul":
                raise ValueError("Invalid format: %s" % cls.__name__)
            return cls(
                parent,
                (child_type.parse_from_etree(None, c) for c in node[0]))

        def __str__(self):
            return "%s with %d items" % (
                type(self).__name__, len(self._children))

    return Base


class Attribute(NamedSection):
    def __init__(self, parent, name, type_, required, description, value):
        super(Attribute, self).__init__(parent, name, description)
        self._type = type_ or "object"
        self._required = required
        self._description = description
        self._value = value

    @property
    def type(self):
        return self._type

    @property
    def required(self):
        return self._required

    @property
    def value(self):
        return self._value

    @property
    def is_array(self):
        return self.type.startswith("array")

    @staticmethod
    def extract_array_subtype(type_):
        if type_ is None or not type_.startswith("array"):
            raise ValueError("Type %s is not an array type" % type_)
        subtype = type_[len("array"):]
        if subtype:
            if subtype[0] != '[' or subtype[-1] != ']':
                raise ValueError("Invalid type format: %s")
            subtype = subtype[1:-1]
        else:
            subtype = "object"
        return subtype

    def __str__(self):
        res = self.name
        if self.value is not None:
            multivalue = not isinstance(self.value, string_types)
            if not multivalue:
                res += ": " + self.value
        else:
            multivalue = False
        if self.type is not None:
            res += " (" + self.type
            if isinstance(self.required, bool):
                res += ", " + ("optional", "required")[self.required]
            res += ")"
        if self.description is not None:
            res += " - " + self.description.replace('\n', ' ')
        if multivalue:
            res += "\n"
            for v in self.value:
                for line in str(v).split('\n'):
                    res += "  %s\n" % line
        return res

    def _fix_parents(self, parent):
        super(Attribute, self)._fix_parents(parent)
        if isinstance(self.value, list):
            for v in self.value:
                if isinstance(v, Attribute):
                    v._fix_parents(self)

    @classmethod
    def parse_from_string(cls, parent, line):
        if line[0] in ('-', '+'):
            line = line[1:]
        desc_pos = line.rfind('-')
        if desc_pos > -1:
            desc = line[desc_pos + 1:].strip()
            line = line[:desc_pos].strip()
        else:
            desc = None
        if line and line[-1] == ')':
            type_pos = line.rfind('(')
            if type_pos < 0:
                raise ValueError("Invalid type format")
            type_ = line[type_pos + 1:-1].strip()
            req_pos = type_.rfind(',')
            if req_pos > -1:
                word = type_[req_pos + 1:].strip()
                type_ = type_[:req_pos].strip()
                required = word == "required"
                if not required and word == "optional":
                    required = False
            else:
                required = None
            line = line[:type_pos].strip()
        else:
            type_ = None
            required = None
        colon_pos = line.find(':')
        if colon_pos > -1:
            name = line[:colon_pos].strip()
            value = line[colon_pos + 1:].strip() or None
        else:
            name = line
            value = None
        if value is not None:
            try:
                subtype = Attribute.extract_array_subtype(type_)
            except ValueError:
                pass
            else:
                value = [Attribute(None, None, subtype, None, None,
                                   v.split()) for v in value.split(',')]
        instance = cls(parent, name, type_, required, desc, value)
        if isinstance(instance.value, list):
            for child in instance.value:
                if isinstance(child, Attribute):
                    child._parent = instance
        return instance

    @classmethod
    def parse_from_etree(cls, parent, node):
        attr = cls.parse_from_string(parent, node.text)
        desc, index = parse_description(node, 0, "ul")
        if attr._description is None:
            attr._description = desc
        elif desc is not None:
            attr._description += "\n" + desc
        if len(node) <= index:
            return attr
        if attr.value is not None:
            raise ValueError("Multiple value for the same attribute %s" %
                             attr.name)
        children = [Attribute.parse_from_etree(attr, c) for c in node[index]]
        attr._value = children
        if attr.is_array:
            subtype = attr.extract_array_subtype(attr.type)
            for c in children:
                if c.type == "object":
                    c._type = subtype
        return attr


class ParameterMember(NamedSection):
    @staticmethod
    def parse_from_string(parent, txt):
        attr = Attribute.parse_from_string(parent, txt)
        return ParameterMember(parent, attr.name, attr.description)

    def __str__(self):
        return "%s - %s" % (self.name, self.description)


class Parameter(Attribute):
    NESTED_ATTRS = Attribute.NESTED_ATTRS + ("_members",)

    def __init__(self, parent, name, type_, required, description, value,
                 default_value, members):
        super(Parameter, self).__init__(
            parent, name, type_, required, description, value)
        self._default_value = default_value
        if members is not None:
            assert isinstance(members, (tuple, list))
            self._members = tuple(members)
        else:
            self._members = tuple()

    @property
    def default_value(self):
        return self._default_value

    @property
    def members(self):
        return self._members

    @classmethod
    def parse_from_etree(cls, parent, node):
        attr = Attribute.parse_from_string(parent, node.text)
        desc, index = parse_description(node, 0, "ul")
        if attr.description is None:
            attr._description = desc
        elif desc is not None:
            attr._description += "\n" + desc
        defval = None
        members = None
        if len(node) > index:
            for li in node[index]:
                if li.text.startswith("Default"):
                    if attr.required or attr.required is None:
                        raise ValueError(
                            "Default value was specified for a non-optional "
                            "parameter %s" % attr.name)
                    sep_pos = li.text.find(':')
                    if sep_pos < 0:
                        raise ValueError("Invalid format")
                    defval = li.text[sep_pos + 1:].strip()
                elif li.text.startswith("Members"):
                    if len(li) == 0 or li[0].tag != "ul":
                        raise ValueError("Invalid format: %s" % attr.name)
                    members = [ParameterMember.parse_from_string(None, m.text)
                               for m in li[0]]
        instance = Parameter(
            parent, attr.name, attr.type, attr.required, desc, attr.value,
            defval, members)
        for member in instance.members:
            member._parent = instance
        return instance


class ReferenceableMixin(object):
    def __init__(self, *args, **kwargs):
        super(ReferenceableMixin, self).__init__(*args, **kwargs)
        self._reference = None

    @staticmethod
    def _extract_reference(txt):
        if len(txt) > 4 and txt[0] == "[" and txt.endswith("][]"):
            return txt[1:-3]
        return None


class DataStructure(Attribute, ReferenceableMixin):
    @classmethod
    def parse_from_etree(cls, parent, node):
        instance = super(DataStructure, cls).parse_from_etree(parent, node)
        if len(node) == 1 and node[0].tag in ("p", "pre"):
            reference = cls._extract_reference(get_pre_contents(node[0]))
            if reference is not None:
                instance._description = None
                instance._reference = reference
        return instance


class Parameters(Collection(Parameter)):
    NESTED_SECTION_ID = "parameters"
    SECTION_TYPE = "Parameters", "Parameter"


class Attributes(Collection(Attribute)):
    NESTED_SECTION_ID = "attributes"
    SECTION_TYPE = "Attributes", "Attribute"

    def __init__(self, parent, children, reference=None):
        if reference is not None:
            assert children is None
            children = tuple()
        super(Attributes, self).__init__(parent, children)
        self._reference = reference

    @classmethod
    def parse_from_etree(cls, parent, node):
        try:
            return super(Attributes, cls).parse_from_etree(parent, node)
        except ValueError as e:
            if node.text[-1] == ')':
                br_pos = node.text.rfind('(')
                if br_pos > -1:
                    reference = node.text[br_pos + 1:-1]
                    return Attributes(None, None, reference)
            raise from_none(e)


@add_metaclass(SelfParsingSectionRegistry)
class Headers(Section):
    NESTED_SECTION_ID = "headers"
    SECTION_TYPE = "Headers", "Header"
    NESTED_ATTRS = "_headers",

    def __init__(self, parent, headers):
        super(Headers, self).__init__(parent)
        self._headers = OrderedDict(headers) if headers else OrderedDict()

    def __iter__(self):
        for p in self._headers.items():
            yield p

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

    def __getitem__(self, item):
        return self._headers[item]

    def keys(self):
        return self._headers.keys()

    def values(self):
        return self._headers.values()

    def __str__(self):
        return "\n".join("%s: %s" % p for p in self)

    @staticmethod
    def parse_from_etree(parent, node):
        if len(node) == 0 or node[0].tag not in ("p", "pre"):
            raise ValueError("Invalid headers section format")
        text = get_pre_contents(node[0])
        if text:
            headers = dict(tuple(map(ustr.strip, line.split(':')))
                           for line in text.split('\n') if line)
        else:
            headers = None
        return Headers(parent, headers)


class AssetSection(Section):
    def __init__(self, parent, keyword, content):
        super(AssetSection, self).__init__(parent)
        self._keyword = keyword
        self._content = content

    @property
    def keyword(self):
        return self._keyword

    @property
    def content(self):
        return self._content

    def __str__(self):
        return "%s\n%s" % (self.keyword, self.content)


@add_metaclass(SelfParsingSectionRegistry)
class PredefinedAssetSection(AssetSection):
    def __init__(self, parent, content):
        super(PredefinedAssetSection, self).__init__(
            parent, self.SECTION_TYPE, content)

    @classmethod
    def parse_from_etree(cls, parent, node):
        if len(node) == 0:
            raise ValueError("Assets section is empty")
        if node[0].tag not in ("pre", "p"):
            raise ValueError("Invalid format of asset section")
        return cls(parent, get_pre_contents(node[0]))


class Body(PredefinedAssetSection):
    NESTED_SECTION_ID = "body"
    SECTION_TYPE = "Body"


class Schema(PredefinedAssetSection):
    NESTED_SECTION_ID = "schema"
    SECTION_TYPE = "Schema"


class PayloadSection(NamedSection):
    NESTED_ATTRS = "_headers", "_attributes", "_body", "_schema", "_reference"

    def __init__(self, parent, keyword, name, media_type, description,
                 headers, attributes, body, schema, reference=None):
        super(PayloadSection, self).__init__(parent, name, description)
        self._keyword = keyword
        if media_type is not None:
            assert isinstance(media_type, (tuple, list))
        if headers is not None:
            assert isinstance(headers, Headers)
        if body is not None:
            assert isinstance(body, Body)
        if schema is not None:
            assert isinstance(schema, Schema)
        self._media_type = \
            tuple(media_type) if media_type is not None else None
        self._headers = headers
        self._attributes = attributes
        self._body = body
        self._schema = schema
        self._reference = reference

    @property
    def keyword(self):
        return self._keyword

    @property
    def media_type(self):
        return self._media_type

    @property
    def headers(self):
        return self._headers

    @property
    def attributes(self):
        return self._attributes

    @property
    def body(self):
        return self._body

    @property
    def schema(self):
        return self._schema

    _headers = property_with_parent("_headers", Headers)
    _attributes = property_with_parent("_attributes", Attributes)
    _body = property_with_parent("_body", Body)
    _schema = property_with_parent("_schema", Schema)

    def value(self):
        if self.media_type == ("application", "json"):
            return json.loads(self.body.content)
        elif self.media_type == ("application", "xml"):
            return ElementTree.fromstring(self.body.content)
        elif self.media_type == ("text", "plain"):
            return self.body.content.strip()
        raise NotImplemented(
            "value() is not implemented for media type %s/%s" %
            self.media_type)

    def __str__(self):
        res = "%s%s" % (self.keyword, (" " + self.name) if self.name else "")
        if self.media_type is not None:
            res += " (%s/%s)" % self.media_type
        return res

    @staticmethod
    def parse_definition(txt):
        txt = txt.strip()
        if txt[-1] == ')':
            br_pos = txt.rfind('(')
            if br_pos < 0:
                raise ValueError("Invalid format")
            mt = txt[br_pos + 1:-1].split('/')
            txt = txt[:br_pos].strip()
        else:
            mt = None
        sep_pos = select_pos(txt.find(c) for c in (' ', '\t'))
        if sep_pos < 0:
            raise ValueError("Invalid format: no keyword")
        keyword = txt[:sep_pos]
        name = txt[sep_pos + 1:].strip()
        return keyword, name, mt


@add_metaclass(SelfParsingSectionRegistry)
class PredefinedPayloadSection(PayloadSection):
    def __init__(self, parent, name, media_type, description,
                 headers, attributes, body, schema):
        super(PredefinedPayloadSection, self).__init__(
            parent, self.SECTION_TYPE, name, media_type, description, headers,
            attributes, body, schema)

    @classmethod
    def parse_from_etree(cls, parent, node):
        defs = cls.parse_definition(node.text)
        desc, index = parse_description(node, 0, "pre", "ul")
        defs += desc,
        kwargs = {
            "headers": None,
            "attributes": None,
            "body": None,
            "schema": None
        }
        if len(node) > index:
            if node[index].tag == "pre":
                kwargs["body"] = Body(None, get_pre_contents(node[index]))
            elif node[index].tag == "ul":
                for li in node[index]:
                    section_name = get_section_name(li.text)
                    try:
                        section = SelfParsingSectionRegistry[
                            section_name].parse_from_etree(None, li)
                    except KeyError:
                        if report_warnings:
                            sys.stderr.write(
                                "Section \"%s\" is unknown\n" % section_name)
                    except ValueError as e:
                        if report_warnings:
                            sys.stderr.write(
                                "Failed to parse section \"%s\" in payload "
                                "section %s: %s\n" % (
                                    section_name, defs[0], e))
                    else:
                        kwargs[section.NESTED_SECTION_ID] = section
        return cls(parent, *defs, **kwargs)

    @staticmethod
    def parse_definition(txt):
        if "\n" in txt:
            if report_warnings:
                sys.stderr.write(
                    "Invalid format, description was discarded: \"%s\"\n"
                    % txt)
            txt = txt[:txt.find("\n")]
        sep_pos = select_pos(txt.find(c) for c in (' ', '\t'))
        if sep_pos < 0:
            return None, None
        txt = txt[sep_pos + 1:].strip()
        if not txt:
            return None, None
        if txt[-1] == ')':
            br_pos = txt.rfind('(')
            if br_pos < 0:
                raise ValueError("Invalid payload section format")
            media_type = txt[br_pos + 1:-1].strip().split("/")
            if br_pos > 0:
                name = txt[:br_pos - 1]
            else:
                name = None
        else:
            name = txt
            media_type = None
        return name, media_type


class Model(PredefinedPayloadSection):
    NESTED_SECTION_ID = "model"
    SECTION_TYPE = "Model"


class RRPredefinedPayloadSection(PredefinedPayloadSection, ReferenceableMixin):
    def _copy_from_payload(self, payload):
        if self.name is None:
            self._name = payload.name
        self._description = payload.description
        self._media_type = payload.media_type
        self._headers = payload.headers
        self._attributes = payload.attributes
        self._body = payload.body
        self._schema = payload.schema

    @classmethod
    def parse_from_etree(cls, parent, node):
        obj = super(RRPredefinedPayloadSection, cls).parse_from_etree(
            parent, node)
        if obj.headers is None and obj.attributes is None and \
                obj.body is None and obj.schema is None and len(node) == 1 \
                and node[0].tag in ("p", "pre"):
            obj._reference = cls._extract_reference(get_pre_contents(node[0]))
        return obj


class Request(RRPredefinedPayloadSection):
    SECTION_TYPE = "Request"
    NESTED_SECTION_ID = "requests"

    def __init__(self, parent, name, media_type, description,
                 headers, attributes, body, schema):
        super(Request, self).__init__(
            parent, name, media_type, description, headers, attributes, body,
            schema)
        self._responses = []

    @property
    def responses(self):
        return {r.http_code: r for r in self._responses}

    @property
    def uri(self):
        values = {}
        for p in chain(self.parent.parent.parameters or tuple(),
                       self.parent.parameters or tuple()):
            if p.default_value is not None:
                values[p.name] = p.default_value
            if p.value is not None:
                values[p.name] = p.value
        return self.uri_template.expand(values)

    def _add_response(self, response):
        assert isinstance(response, Response)
        self._responses.append(weakref.proxy(response))

    def _fix_parents(self, parent):
        super(Request, self)._fix_parents(parent)
        responses = tuple(self._responses)
        del self._responses[:]
        for r in responses:
            self._add_response(self.parent.responses[r.http_code])


class Response(RRPredefinedPayloadSection):
    SECTION_TYPE = "Response"
    NESTED_SECTION_ID = "responses"

    def __init__(self, parent, name, media_type, description,
                 headers, attributes, body, schema):
        super(Response, self).__init__(
            parent, name, media_type, description, headers, attributes, body,
            schema)
        self._request = None

    @property
    def request(self):
        return self._request

    @property
    def _request(self):
        return self.__request

    @_request.setter
    def _request(self, value):
        if value is not None and not isinstance(value, weakref.ProxyType):
            value = weakref.proxy(value)
        self.__request = value

    @property
    def http_code(self):
        return int(self._name)

    def _fix_parents(self, parent):
        super(Response, self)._fix_parents(parent)
        if self.request is not None:
            self._request = self.parent.requests[self.request.name]


class ApiSection(NamedSection):
    NESTED_SECTIONS = "parameters", "attributes"
    URL_PATH_PATH_REGEXP = re.compile("^[\w\-\.]*$]")
    NESTED_ATTRS = "_parameters", "_attributes"

    def __init__(self, parent, name, description, request_method, uri_template,
                 parameters, attributes):
        assert parameters is None or isinstance(parameters, Parameters)
        assert attributes is None or isinstance(attributes, Attributes)
        super(ApiSection, self).__init__(parent, name, description)
        self._request_method = request_method
        self._uri_template = URITemplate(uri_template) \
            if uri_template else None
        self._parameters = parameters
        self._attributes = attributes

    @property
    def request_method(self):
        return self._request_method

    @property
    def uri_template(self):
        return self._uri_template

    @property
    def parameters(self):
        return self._parameters

    @property
    def attributes(self):
        return self._attributes

    _parameters = property_with_parent("_parameters", Parameters)
    _attributes = property_with_parent("_attributes", Attributes)

    @property
    def id(self):
        if self.name is not None:
            return self.name
        res = ""
        if self.request_method is not None:
            res += self.request_method + " "
        if self.uri_template is not None:
            res += str(self.uri_template) + " "
        return res.strip()


@add_metaclass(SelfParsingSectionRegistry)
class Relation(Section):
    NESTED_SECTION_ID = "relation"
    SECTION_TYPE = "Relation"

    def __init__(self, parent, link_id):
        super(Relation, self).__init__(parent)
        self._link_id = link_id

    @property
    def link_id(self):
        return self._link_id

    def __str__(self):
        return "Relation " + self._link_id

    @staticmethod
    def parse_from_string(parent, txt):
        txt = txt.strip()
        rel_key = "Relation:"
        if not txt.startswith(rel_key):
            raise ValueError("Invalid format")
        return Relation(parent, txt[len(rel_key):].strip())

    @staticmethod
    def parse_from_etree(parent, node):
        return Relation(parent, node.text.split(":")[-1].strip())


class Action(ApiSection):
    NESTED_SECTIONS = ApiSection.NESTED_SECTIONS + ("relation",)
    NESTED_ATTRS = ApiSection.NESTED_ATTRS + \
        ("_relation", "_requests", "_responses")

    def __init__(self, parent, name, request_method, uri_template, description,
                 relation, parameters, attributes, requests, responses):
        super(Action, self).__init__(parent, name, description, request_method,
                                     uri_template, parameters, attributes)
        if relation is not None:
            assert isinstance(relation, Relation)
        self._relation = relation
        self._requests = OrderedDict()
        index = 0
        for item in requests:
            name = item.name
            if not name:
                name = "#%d" % index
                item._name = name
                index += 1
            self._requests[name] = item
        self._responses = OrderedDefaultDict(list)
        for item in responses:
            code = item.http_code
            if code is None:
                code = 200
            self._responses[code].append(item)
        for r in chain(requests, responses):
            r._parent = self

    @property
    def relation(self):
        return self._relation

    @property
    def requests(self):
        return self._requests

    @property
    def responses(self):
        return self._responses

    @property
    def uri(self):
        values = {}
        for p in chain(self.parent.parameters or tuple(),
                       self.parameters or tuple()):
            if p.default_value is not None:
                values[p.name] = p.default_value
            if p.value is not None:
                values[p.name] = p.value
        return self.uri_template.expand(values)

    _relation = property_with_parent("_relation", Relation)

    def __str__(self):
        res = "Action "
        if self.name is None:
            res += self.request_method
            return res
        res += self.name
        bpe = self.request_method is not None or self.uri_template is not None
        if bpe:
            res += " ["
        middle = ""
        if self.request_method is not None:
            middle += self.request_method + " "
        if self.uri_template is not None:
            middle += str(self.uri_template)
        res += middle.strip() + "]"
        return res

    def __iter__(self):
        if not self.requests:
            yield Request(self, "default", None, None, None,
                          self.attributes, None, None), \
                list(chain.from_iterable(self.responses.values()))
        else:
            for request in self.requests.values():
                yield request, request.responses

    def __len__(self):
        if not self.requests:
            return 1
        return len(self.requests)

    @staticmethod
    def parse_definition(txt):
        txt = txt.strip()
        if txt[-1] == ']':
            br_pos = txt.rfind('[')
            if br_pos < 0:
                raise ValueError("Invalid format")
            part = txt[br_pos + 1:-1].strip()
            sep_pos = select_pos(part.find(c) for c in (' ', '\t'))
            if sep_pos > -1:
                method = part[:sep_pos]
                template = part[sep_pos:].strip()
            else:
                method = part
                template = None
            name = txt[:br_pos].strip()
        else:
            name = None
            method = txt
            template = None
        return name, method, template

    @staticmethod
    def parse_from_etree(parent, sequence, index):
        adef = Action.parse_definition(sequence[index].text)
        desc, index = parse_description(sequence, index + 1, "ul")
        kwargs = {
            "description": desc,
            "relation": None,
            "parameters": None,
            "attributes": None,
            "requests": [],
            "responses": []
        }
        current_requests = []
        clear_requests = False
        if len(sequence) > index:
            for li in sequence[index]:
                section_name = get_section_name(li.text)
                try:
                    section = SelfParsingSectionRegistry[
                        section_name].parse_from_etree(None, li)
                except KeyError:
                    if report_warnings:
                        sys.stderr.write(
                            "Section \"%s\" is unknown\n" % section_name)
                except ValueError as e:
                    if report_warnings:
                        sys.stderr.write(
                            "Failed to parse section \"%s\" in action "
                            "%s: %s\n" % (section_name, adef[0], e))
                else:
                    if isinstance(section, Request):
                        if clear_requests:
                            del current_requests[:]
                        current_requests.append(section)
                    elif isinstance(section, Response):
                        clear_requests = True
                        for i, cr in enumerate(current_requests):
                            section._request = cr
                            cr._add_response(section)
                            if i < len(current_requests) - 1:
                                kwargs["responses"].append(section)
                                section = deepcopy(section)
                    if section.SECTION_TYPE in ("Request", "Response"):
                        kwargs[section.NESTED_SECTION_ID].append(section)
                    else:
                        kwargs[section.NESTED_SECTION_ID] = section
            index += 1
        # This section may include one nested Attributes section defining the
        # input (request) attributes of the section. If present, these
        # attributes should be inherited in every Action's Request section
        # unless specified otherwise.
        for req in kwargs["requests"]:
            if req.attributes is None:
                req._attributes = kwargs["attributes"]
        return Action(parent, *adef, **kwargs), index


class Resource(ApiSection):
    NESTED_SECTIONS = ApiSection.NESTED_SECTIONS + ("model",)
    NESTED_ATTRS = ApiSection.NESTED_ATTRS + ("_model", "_actions")

    def __init__(self, parent, name, request_method, uri_template, description,
                 parameters, attributes, model):
        assert model is None or isinstance(model, Model)
        super(Resource, self).__init__(
            parent, name, description, request_method, uri_template,
            parameters, attributes)
        self._model = model
        self._actions = OrderedDict()

    @property
    def model(self):
        return self._model

    @property
    def uri(self):
        values = {}
        for p in self.parameters or tuple():
            if p.default_value is not None:
                values[p.name] = p.default_value
            if p.value is not None:
                values[p.name] = p.value
        return self.uri_template.expand(values)

    _model = property_with_parent("_model", Model)

    def __iter__(self):
        for action in self._actions.values():
            yield action

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

    def __getitem__(self, item):
        return self._actions[item]

    def __str__(self):
        res = "Resource "
        bpe = self.request_method is not None or self.uri_template is not None
        if self.name is not None:
            res += self.name + " "
            if bpe:
                res += "["
        middle = ""
        if self.request_method is not None:
            middle += self.request_method + " "
        if self.uri_template is not None:
            middle += str(self.uri_template)
        res += middle.strip()
        if self.name is not None and bpe:
            res += ']'
        return res

    @staticmethod
    def parse_definition(txt):
        txt = txt.strip()
        if txt[-1] == ']':
            br_pos = txt.rfind('[')
            if br_pos < 0:
                raise ValueError("Invalid format")
            part = txt[br_pos + 1:-1].strip()
            sep_pos = select_pos(part.find(c) for c in (' ', '\t'))
            if sep_pos > -1:
                method = part[:sep_pos]
                template = part[sep_pos:].strip()
            else:
                method = None
                template = part
            name = txt[:br_pos].strip()
        else:
            name = None
            sep_pos = select_pos(txt.find(c) for c in (' ', '\t'))
            if sep_pos > -1:
                method = txt[:sep_pos]
                if method not in ("GET", "POST", "PUT", "PATCH", "DELETE",
                                  "HEAD"):
                    method = None
                    template = txt
                else:
                    template = txt[sep_pos + 1:].strip()
            else:
                method = None
                template = txt
        return name, method, template


class ResourceGroup(NamedSection):
    NESTED_ATTRS = "_resources",

    def __init__(self, parent, name, description):
        super(ResourceGroup, self).__init__(parent, name, description)
        self._resources = OrderedDict()

    def __getitem__(self, item):
        return self._resources[item]

    def __iter__(self):
        for resource in self._resources.values():
            yield resource

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

    def __str__(self):
        return "ResourceGroup with %d resources (%d actions)" % (
            len(self), sum(len(r) for r in self)
        )

    def print_resources(self):
        for r in self:
            print(r)