from lxml import etree

from ..base import BasePackageElement


class Story(BasePackageElement):
    """
    Story which represents `p`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `blockquote`, `pre`, `headline`, `byline` html tags.

    As Adobe's idml specification explains:
    ``
    A story, or “text flow” is the basic text container in an InDesign document;
    all text exists inside stories. Stories are associated with at least one text frame or text path,
    and can span any number of linked text frames or text paths in a document.
    ``
    """

    STORY_DEFAULTS = {
        'AppliedNamedGrid': 'n',
        'AppliedTOCStyle': 'n',
        'TrackChanges': 'false',
        'StoryTitle': '$ID/'
    }
    STORYPREFERENCE_DEFAULTS = {
        'OpticalMarginAlignment': 'false',
        'OpticalMarginSize': '12',
        'FrameType': 'TextFrameType',
        'StoryOrientation': 'Horizontal',
        'StoryDirection': 'LeftToRightDirection'
    }
    HYPERLINKTEXTSOURCE_DEFAULTS = {
        'Hidden': 'false',
        'AppliedCharacterStyle': 'CharacterStyle/$ID/Hyperlink'
    }
    PARAGRAPHSTYLERANGE_DEFAULTS = {
        'AppliedParagraphStyle': 'ParagraphStyle/$ID/NormalParagraphStyle'
    }
    CHARACTERSTYLERANGE_DEFAULTS = {
        'AppliedCharacterStyle': 'CharacterStyle/$ID/[No character style]',
    }
    BLOCK_TAGS_MAPPING = {
        'p': {
            'markup_tag': 'NormalParagraph',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Paragraphs%3aNormalParagraph'
            },
        },
        'blockquote': {
            'markup_tag': 'Blockquote',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Paragraphs%3aBlockquote'
            },
        },
        'pre': {
            'markup_tag': 'Preformatted',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Paragraphs%3aPreformatted'
            },
        },
        'h1': {
            'markup_tag': 'Heading1',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Headings%3aHeading1'
            },
        },
        'h2': {
            'markup_tag': 'Heading2',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Headings%3aHeading2'
            },
        },
        'h3': {
            'markup_tag': 'Heading3',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Headings%3aHeading3'
            },
        },
        'h4': {
            'markup_tag': 'Heading4',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Headings%3aHeading4'
            },
        },
        'h5': {
            'markup_tag': 'Heading5',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Headings%3aHeading5'
            },
        },
        'h6': {
            'markup_tag': 'Heading6',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Headings%3aHeading6'
            },
        },
        'table': {
            'markup_tag': 'Table',
            'Table': {
                'AppliedTableStyle': 'TableStyle/$ID/NormalTable'
            },
        },
        'ul': {
            'markup_tag': 'UnorderedList',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Lists%3aUnorderedList',
                'BulletsAndNumberingListType': 'BulletList',
            },
        },
        'ol': {
            'markup_tag': 'OrderedList',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Lists%3aOrderedList',
                'BulletsAndNumberingListType': 'NumberedList',
            },
        },
        # custom
        'headline': {
            'markup_tag': 'Headline',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Custom%3aHeadline'
            },
        },
        'byline': {
            'markup_tag': 'Byline',
            'ParagraphStyleRange': {
                'AppliedParagraphStyle': 'ParagraphStyle/Custom%3aByline'
            },
        },
    }
    INLINE_TAGS_MAPPING = {
        'b': [
            {
                'attribute': 'FontStyle',
                'value': 'Bold',
                'appendable': True
            }
        ],
        'i': [
            {
                'attribute': 'FontStyle',
                'value': 'Italic',
                'appendable': True
            }
        ],
        'u': [
            {
                'attribute': 'Underline',
                'value': 'true',
                'appendable': False
            }
        ],
        's': [
            {
                'attribute': 'StrikeThru',
                'value': 'true',
                'appendable': False
            }
        ],
        'sub': [
            {
                'attribute': 'Position',
                'value': 'Subscript',
                'appendable': False
            }
        ],
        'sup': [
            {
                'attribute': 'Position',
                'value': 'Superscript',
                'appendable': False
            }
        ],
        'a': [
            {
                'attribute': 'AppliedCharacterStyle',
                'value': 'CharacterStyle/$ID/Hyperlink',
                'appendable': False
            }
        ]
    }

    def __init__(self, self_id, element, attributes=None, markup_tag=None):
        self.self_id = self_id
        self._element = element
        self._markup_tag = markup_tag
        self.links = []
        super().__init__(attributes)

    @property
    def filename(self):
        """
        Filename inside IDML package.
        Used as a filename for a file inside zip container.
        :return str: filename
        """
        return 'Stories/Story_{}.xml'.format(self.self_id)

    def _build_etree(self):
        self._etree = etree.Element(
            etree.QName(self.XMLNS_IDPKG, 'Story'),
            nsmap={'idPkg': self.XMLNS_IDPKG}
        )
        self._etree.set('DOMVersion', self.DOM_VERSION)
        self._add_story()

    def _add_story(self):
        """
        Create <Story..> tag
        :return lxml.etree: Story etree instance
        """
        # merge Story attributes
        story_attributes = self.merge_attributes(
            self.STORY_DEFAULTS,
            self._attributes.get('Story', {})
        )
        story_attributes.update({'Self': self.self_id})
        # Story
        story = etree.SubElement(
            self._etree,
            'Story',
            attrib=story_attributes
        )
        # StoryPreference
        etree.SubElement(
            story,
            'StoryPreference',
            attrib=self.merge_attributes(
                self.STORYPREFERENCE_DEFAULTS,
                self._attributes.get('StoryPreference', {})
            )
        )

        if self._markup_tag:
            # XMLElement to tag a story
            paragraphstylerange_container = etree.SubElement(
                story,
                'XMLElement',
                attrib={
                    'Self': '{}_{}'.format(self.self_id, self._markup_tag.lower()),
                    'XMLContent': self.self_id,
                    'MarkupTag': 'XMLTag/{}'.format(self._markup_tag)
                }
            )
        else:
            paragraphstylerange_container = story

        # ParagraphStyleRange
        paragraphstylerange = etree.SubElement(
            paragraphstylerange_container,
            'ParagraphStyleRange',
            attrib=self.merge_attributes(
                self.PARAGRAPHSTYLERANGE_DEFAULTS,
                self._attributes.get('ParagraphStyleRange', {})
            )
        )

        paragraphstylerange[:] = self._handle_inline_tags(self._element)

        return story

    def _handle_inline_tags(self, element):
        """
        Convert html element's inline style to idml representation.
        :param lxml.etree element: html element
        :return list: list of CharacterStyleRange lxml.etree instances
        """
        element_dict = self._etree_element_to_dict(element)
        return self._create_characterstylerange_recursive(element_dict)

    def _etree_element_to_dict(self, element, parents=None):
        """
        Converts html element to dict.
        Example:
            `<p>normal <b>bold <u>bold-unerline</u> bold</b> normal</p>`
            will be converted to:
            ``
            {
                'attrib': {},
                'childs': (
                    {
                        'attrib': {},
                        'childs': (
                            {
                                'attrib': {},
                                'childs': (),
                                'parents': ('p', 'b'),
                                'tag': 'u',
                                'tail': ' bold',
                                'text': 'bold-unerline',
                            },
                        ),
                        'parents': ('p', ),
                        'tag': 'b',
                        'tail': ' normal',
                        'text': 'bold ',
                    },
                ),
                'parents': (),
                'tag': 'p',
                'tail': None,
                'text': 'normal ',
            }
            ``
        :param lxml.etree element: html element
        :param list parents: list of parent tags
        :return dict: dictionary
        """
        if not parents:
            parents = []

        element_dict = {
            'tag': element.tag,
            'attrib': element.attrib,
            'parents': tuple(parents),
            'text': element.text,
            'tail': element.tail
        }
        parents.append(element.tag)
        element_dict['childs'] = tuple(
            self._etree_element_to_dict(
                child,
                list(parents)
            ) for child in element.iterchildren()
        )

        return element_dict

    def _create_characterstylerange_recursive(self, element_dict, tail=False):
        """
        Create CharacterStyleRange(s) recursively.
        Example:
        ``
        <ParagraphStyleRange ...>
        <CharacterStyleRange AppliedCharacterStyle="..." FontStyle="Bold">
          <Content>Curabitur </Content>
        </CharacterStyleRange>
        <CharacterStyleRange AppliedCharacterStyle="..." FontStyle="Bold Italic">
          <Content>arcu erat</Content>
        </CharacterStyleRange>
        <CharacterStyleRange AppliedCharacterStyle="..." FontStyle="Bold">
          <Content>, accumsan </Content>
        </CharacterStyleRange>
        <CharacterStyleRange AppliedCharacterStyle="..." FontStyle="Bold" Underline="true">
          <Content>id imperdiet et</Content>
        </CharacterStyleRange>
        ...
        </ParagraphStyleRange>
        ``
        :param dict element_dict: dict representation of html element
        :param bool tail: if element has tail
        :return:
        """

        characterstylerange_list = []

        def apply_tag_style(characterstylerange, tag):
            for rule in self.INLINE_TAGS_MAPPING[tag]:
                attribute = rule['attribute']
                value = rule['value']
                appendable = rule['appendable']

                if characterstylerange.get(attribute) and appendable:
                    if attribute == 'FontStyle' and value == 'Bold':
                        characterstylerange.set(
                            attribute,
                            '{} {}'.format(value, characterstylerange.get(attribute))
                        )
                    else:
                        characterstylerange.set(
                            attribute,
                            '{} {}'.format(characterstylerange.get(attribute), value)
                        )
                else:
                    characterstylerange.set(attribute, value)

        # text
        if element_dict['text'] and len(element_dict['text']):
            characterstylerange_list.append(
                etree.Element('CharacterStyleRange', attrib=self.CHARACTERSTYLERANGE_DEFAULTS)
            )
            # apply parents styles
            for tag in element_dict['parents']:
                if self.INLINE_TAGS_MAPPING.get(tag):
                    apply_tag_style(characterstylerange_list[-1], tag)

            # apply own style
            tag = element_dict['tag']
            if self.INLINE_TAGS_MAPPING.get(tag):
                apply_tag_style(characterstylerange_list[-1], tag)

            content = etree.Element('Content')
            content.text = element_dict['text']

            if tag == 'a':
                self.links.append({
                    'self_id': '{}_{}'.format(self.self_id, len(self.links) + 1),
                    'text': element_dict['text'],
                    'href': element_dict['attrib'].get('href')
                })

                hyperlinktextsource = etree.SubElement(
                    characterstylerange_list[-1],
                    'HyperlinkTextSource',
                    attrib=self.merge_attributes(
                        self.HYPERLINKTEXTSOURCE_DEFAULTS,
                        {
                            'Self': self.links[-1]['self_id'],
                            'Name': 'Hyperlink {}'.format(self.links[-1]['self_id']),
                        }
                    )
                )
                hyperlinktextsource.append(content)
            else:
                characterstylerange_list[-1].append(content)

        # childs
        for child in element_dict['childs']:
            characterstylerange_list += self._create_characterstylerange_recursive(child, tail=True)

        # tail
        if tail and element_dict['tail'] and len(element_dict['tail']):
            characterstylerange_list.append(
                etree.Element(
                    'CharacterStyleRange',
                    attrib=self.CHARACTERSTYLERANGE_DEFAULTS
                )
            )
            # apply parents styles
            for tag in element_dict['parents']:
                if self.INLINE_TAGS_MAPPING.get(tag):
                    apply_tag_style(characterstylerange_list[-1], tag)

            content = etree.SubElement(
                characterstylerange_list[-1],
                'Content'
            )
            content.text = element_dict['tail']

        return characterstylerange_list

    @property
    def length(self):
        return len(" ".join(etree.XPath(".//text()")(self._element)))

    @staticmethod
    def guess_height(story, inner_width):
        style = story._etree.xpath('.//ParagraphStyleRange')[-1].get('AppliedParagraphStyle')
        if not style:
            return 100

        style = style.rsplit('%3a', 1)[-1]
        style_map = {
            'Headline': {
                'PointSize': 48,
                'Leading': 48,
                'LeftIndent': 0,
                'RightIndent': 0
            },
            'Byline': {
                'PointSize': 20,
                'Leading': 20,
                'LeftIndent': 0,
                'RightIndent': 0
            },
            'Heading1': {
                'PointSize': 40,
                'Leading': 40,
                'LeftIndent': 0,
                'RightIndent': 0
            },
            'Heading2': {
                'PointSize': 30,
                'Leading': 30,
                'LeftIndent': 0,
                'RightIndent': 0
            },
            'Heading3': {
                'PointSize': 20,
                'Leading': 20,
                'LeftIndent': 0,
                'RightIndent': 0
            },
            'Heading4': {
                'PointSize': 14,
                'Leading': 14,
                'LeftIndent': 0,
                'RightIndent': 0
            },
            'Heading5': {
                'PointSize': 11,
                'Leading': 11,
                'LeftIndent': 0,
                'RightIndent': 0
            },
            'Heading6': {
                'PointSize': 9,
                'Leading': 9,
                'LeftIndent': 0,
                'RightIndent': 0
            },
            'Blockquote': {
                'PointSize': 12,
                'Leading': 12,
                'LeftIndent': 30,
                'RightIndent': 30
            },
            'default': {
                'PointSize': 12,
                'Leading': 12,
                'LeftIndent': 0,
                'RightIndent': 0
            }
        }

        point_size = style_map.get(style, style_map['default'])['PointSize']
        leading = style_map.get(style, style_map['default'])['Leading']
        left_indent = style_map.get(style, style_map['default'])['LeftIndent']
        right_indent = style_map.get(style, style_map['default'])['RightIndent']

        length = story.length
        inner_width -= float(left_indent) + float(right_indent)
        length *= float(point_size) / style_map['default']['PointSize']
        length *= float(leading) / style_map['default']['Leading']
        height = length / inner_width * 75 + 10

        return height
