import rtfunicode
from .Elements import *

DEFAULT_TAB_WIDTH = 720

ParagraphAlignmentMap = {ParagraphPropertySet.LEFT: 'ql',
                         ParagraphPropertySet.RIGHT: 'qr',
                         ParagraphPropertySet.CENTER: 'qc',
                         ParagraphPropertySet.JUSTIFY: 'qj',
                         ParagraphPropertySet.DISTRIBUTE: 'qd'}

TabAlignmentMap = {TabPropertySet.LEFT: '',
                   TabPropertySet.RIGHT: 'tqr',
                   TabPropertySet.CENTER: 'tqc',
                   TabPropertySet.DECIMAL: 'tqdec'}

TableAlignmentMap = {Table.LEFT: 'trql',
                     Table.RIGHT: 'trqr',
                     Table.CENTER: 'trqc'}

CellAlignmentMap = {Cell.ALIGN_TOP: '',  # clvertalt
                    Cell.ALIGN_CENTER: 'clvertalc',
                    Cell.ALIGN_BOTTOM: 'clvertalb'}

CellFlowMap = {Cell.FLOW_LR_TB: '',  # cltxlrtb, Text in a cell flows from left to right and top to bottom (default)
               Cell.FLOW_RL_TB: 'cltxtbrl',  # Text in a cell flows right to left and top to bottom
               Cell.FLOW_LR_BT: 'cltxbtlr',  # Text in a cell flows left to right and bottom to top
               Cell.FLOW_VERTICAL_LR_TB: 'cltxlrtbv',  # Text in a cell flows left to right and top to bottom, vertical
               Cell.FLOW_VERTICAL_TB_RL: 'cltxtbrlv'}  # Text in a cell flows top to bottom and right to left, vertical

ShadingPatternMap = {ShadingPropertySet.HORIZONTAL: 'bghoriz',
                     ShadingPropertySet.VERTICAL: 'bgvert',
                     ShadingPropertySet.FORWARD_DIAGONAL: 'bgfdiag',
                     ShadingPropertySet.BACKWARD_DIAGONAL: 'bgbdiag',
                     ShadingPropertySet.VERTICAL_CROSS: 'bgcross',
                     ShadingPropertySet.DIAGONAL_CROSS: 'bgdcross',
                     ShadingPropertySet.DARK_HORIZONTAL: 'bgdkhoriz',
                     ShadingPropertySet.DARK_VERTICAL: 'bgdkvert',
                     ShadingPropertySet.DARK_FORWARD_DIAGONAL: 'bgdkfdiag',
                     ShadingPropertySet.DARK_BACKWARD_DIAGONAL: 'bgdkbdiag',
                     ShadingPropertySet.DARK_VERTICAL_CROSS: 'bgdkcross',
                     ShadingPropertySet.DARK_DIAGONAL_CROSS: 'bgdkdcross'}

TabLeaderMap = {TabPropertySet.DOTS: 'tldot',
                TabPropertySet.HYPHENS: 'tlhyph',
                TabPropertySet.UNDERLINE: 'tlul',
                TabPropertySet.THICK_LINE: 'tlth',
                TabPropertySet.EQUAL_SIGN: 'tleq'}

BorderStyleMap = {BorderPropertySet.SINGLE: 'brdrs',
                  BorderPropertySet.DOUBLE: 'brdrth',
                  BorderPropertySet.SHADOWED: 'brdrsh',
                  BorderPropertySet.DOUBLED: 'brdrdb',
                  BorderPropertySet.DOTTED: 'brdrdot',
                  BorderPropertySet.DASHED: 'brdrdash',
                  BorderPropertySet.HAIRLINE: 'brdrhair'}

SectionBreakTypeMap = {Section.NONE: 'sbknone',
                       Section.COLUMN: 'sbkcol',
                       Section.PAGE: 'sbkpage',
                       Section.EVEN: 'sbkeven',
                       Section.ODD: 'sbkodd'}


# noinspection PyPep8Naming
class Settings(list):
    def __init__(self):
        super(Settings, self).__init__()
        self._append = super(Settings, self).append

    def append(self, value, mask=None, fallback=None):
        if (value is not 0) and value in [False, None, '']:
            if fallback:
                self._append(fallback)

        else:
            if mask:
                if value is True:
                    value = mask
                else:
                    value = mask % value
            self._append(value)

    def Join(self):
        if self:
            return r'\%s' % '\\'.join(self)
        return ''

    def __repr__(self):
        return self.Join()


# noinspection PyPep8Naming,PyMethodMayBeStatic
class Renderer:
    def __init__(self, write_custom_element_callback=None):
        self._doc = None
        self._fout = None
        self._CurrentStyle = None

        self.character_style_map = {}
        self.paragraph_style_map = {}
        self.WriteCustomElement = write_custom_element_callback

    #
    # All of the Rend* Functions populate a Settings object with values
    #
    def _RendPageProperties(self, section, settings, in_section):
        # this one is different from the others as it takes the settings from a
        if in_section:
            # paper_size_code = 'psz%s'
            paper_width_code = 'pgwsxn%s'
            paper_height_code = 'pghsxn%s'
            landscape = 'lndscpsxn'
            margin_suffix = 'sxn'

        else:
            # paper_size_code = 'psz%s'
            paper_width_code = 'paperw%s'
            paper_height_code = 'paperh%s'
            landscape = 'landscape'
            margin_suffix = ''

        # settings.append(section.Paper.Code, paper_size_code)
        settings.append(section.Paper.Width, paper_width_code)
        settings.append(section.Paper.Height, paper_height_code)

        if section.Landscape:
            settings.append(landscape)

        if section.FirstPageNumber:
            settings.append(section.FirstPageNumber, 'pgnstarts%s')
            settings.append('pgnrestart')

        self._RendMarginsPropertySet(section.Margins, settings, margin_suffix)

    def _RendShadingPropertySet(self, shading_props, settings, prefix=''):
        if not shading_props:
            return

        settings.append(shading_props.Shading, prefix + 'shading%s')
        settings.append(ShadingPatternMap.get(shading_props.Pattern, False))

        settings.append(self._colour_map.get(shading_props.Foreground, False), prefix + 'cfpat%s')
        settings.append(self._colour_map.get(shading_props.Background, False), prefix + 'cbpat%s')

    def _RendBorderPropertySet(self, edge_props, settings):
        settings.append(BorderStyleMap[edge_props.Style])
        settings.append(edge_props.Width, 'brdrw%s')
        settings.append(self._colour_map.get(edge_props.Colour, False), 'brdrcf%s')
        if edge_props.Spacing:
            settings.append(edge_props.Spacing, 'brsp%s')
        else:
            settings.append(False, 'brsp%s')

    def _RendFramePropertySet(self, frame_props, settings, tag_prefix=''):
        if not frame_props:
            return

        if frame_props.Top:
            settings.append(tag_prefix + 'brdrt')
            self._RendBorderPropertySet(frame_props.Top, settings)

        if frame_props.Left:
            settings.append(tag_prefix + 'brdrl')
            self._RendBorderPropertySet(frame_props.Left, settings)

        if frame_props.Bottom:
            settings.append(tag_prefix + 'brdrb')
            self._RendBorderPropertySet(frame_props.Bottom, settings)

        if frame_props.Right:
            settings.append(tag_prefix + 'brdrr')
            self._RendBorderPropertySet(frame_props.Right, settings)

    def _RendMarginsPropertySet(self, margin_props, settings, suffix=''):
        if not margin_props:
            return

        settings.append(margin_props.Top, 'margt' + suffix + '%s')
        settings.append(margin_props.Left, 'margl' + suffix + '%s')
        settings.append(margin_props.Bottom, 'margb' + suffix + '%s')
        settings.append(margin_props.Right, 'margr' + suffix + '%s')

    def _RendParagraphPropertySet(self, paragraph_props, settings):
        if not paragraph_props:
            return
        settings.append(ParagraphAlignmentMap[paragraph_props.Alignment])

        settings.append(paragraph_props.SpaceBefore, 'sb%s')
        settings.append(paragraph_props.SpaceAfter, 'sa%s')

        # then we have to find out all of the tabs
        width = 0
        for tab in paragraph_props.Tabs:
            settings.append(TabAlignmentMap[tab.Alignment])
            settings.append(TabLeaderMap.get(tab.Leader, ''))

            if tab.Width:
                width += tab.Width
            else:
                width += DEFAULT_TAB_WIDTH
            settings.append('tx%s' % width)

        settings.append(paragraph_props.PageBreakBefore, 'pagebb')

        settings.append(paragraph_props.FirstLineIndent, 'fi%s')
        settings.append(paragraph_props.LeftIndent, 'li%s')
        settings.append(paragraph_props.RightIndent, 'ri%s')

        if paragraph_props.SpaceBetweenLines:
            if paragraph_props.SpaceBetweenLines < 0:
                settings.append(paragraph_props.SpaceBetweenLines, r'sl%s\slmult0')
            else:
                settings.append(paragraph_props.SpaceBetweenLines, r'sl%s\slmult1')

    def _RendTextPropertySet(self, text_props, settings):
        if not text_props:
            return

        if text_props.Expansion:
            settings.append(text_props.Expansion, 'expndtw%s')

        settings.append(text_props.Bold, 'b')
        settings.append(text_props.Italic, 'i')
        settings.append(text_props.Underline, 'ul')
        settings.append(text_props.DottedUnderline, 'uld')
        settings.append(text_props.DoubleUnderline, 'uldb')
        settings.append(text_props.WordUnderline, 'ulw')

        settings.append(self._font_map.get(text_props.Font, False), 'f%s')
        settings.append(text_props.Size, 'fs%s')
        settings.append(self._colour_map.get(text_props.Colour, False), 'cf%s')

        if text_props.Frame:
            frame = text_props.Frame
            settings.append('chbrdr')
            settings.append(BorderStyleMap[frame.Style])
            settings.append(frame.Width, 'brdrw%s')
            settings.append(self._colour_map.get(frame.Colour, False), 'brdrcf%s')

    #
    # All of the Write* functions will write to the internal file object
    #
    # the _ ones probably don't need to be used by anybody outside
    # but the other ones like WriteTextElement could be used in the Custom
    # callback.
    def Write(self, document, fout):
        # write all of the standard stuff based upon the first document
        self._doc = document
        self._fout = fout
        self._WriteDocument()
        self._WriteColours()
        self._WriteFonts()
        self._WriteStyleSheet()

        settings = Settings()
        self._RendPageProperties(self._doc.Sections[0], settings, in_section=False)
        self._write(repr(settings))

        # handle the simplest case first, we don't need to do anymore mucking around
        # with section headers, etc we can just rip the document out
        if len(document.Sections) == 1:
            self._WriteSection(document.Sections[0], is_first=True, add_header=False)
        else:
            for section_idx, section in enumerate(document.Sections):
                is_first = section_idx == 0
                add_header = True
                self._WriteSection(section, is_first, add_header)

        self._write('}')

        del self._fout, self._doc, self._CurrentStyle

    def _write(self, data, *params):
        # ----------------------------------
        # begin modification
        # by Herbert Weinhandl
        # to convert accented characters
        # to their rtf-compatible form
        # for c in range(128, 256):
        #     data = data.replace(chr(c), "\'%x" % c)
        # end modification
        #
        # This isn't the right place for this as it is going to do
        # this loop for all sorts of writes, including settings, control codes, etc.
        #
        # I will create a def _WriteText (or something) method that is used when the
        # actual string that is to be viewed in the document is written, this can then
        # do the final accented character check.
        #
        # I left it here so that I remember to do the right thing when I have time
        # ----------------------------------

        if isinstance(data, str):
            if params:
                data %= params
            self._fout.write(data.encode('ASCII'))
        if isinstance(data, bytes):
            self._fout.write(data)

    def _writeText(self, data, *params):
        if isinstance(data, str):
            if params:
                data %= params
            self._fout.write(data.encode('rtfunicode'))
        if isinstance(data, bytes):
            self._fout.write(data)

    def _WriteDocument(self):
        settings = Settings()

        assert Languages.IsValid(self._doc.DefaultLanguage)
        assert ViewKind.IsValid(self._doc.ViewKind)
        assert ViewZoomKind.IsValid(self._doc.ViewZoomKind)
        assert ViewScale.IsValid(self._doc.ViewScale)

        settings.append(self._doc.DefaultLanguage, 'deflang%s')
        settings.append(self._doc.ViewKind, 'viewkind%s')
        settings.append(self._doc.ViewZoomKind, 'viewzk%s')
        settings.append(self._doc.ViewScale, 'viewscale%s')

        self._write("{\\rtf1\\ansi\\ansicpg1252\\deff0%s\n" % settings)

    def _WriteColours(self):
        self._write(r"{\colortbl ;")

        self._colour_map = {}
        offset = 0
        for colour in self._doc.StyleSheet.Colours:
            self._write(r'\red%s\green%s\blue%s;', colour.Red, colour.Green, colour.Blue)
            self._colour_map[colour] = offset + 1
            offset += 1
        self._write("}\n")

    def _WriteFonts(self):
        self._write(r'{\fonttbl')

        self._font_map = {}
        offset = 0
        for font in self._doc.StyleSheet.Fonts:
            pitch = ''
            panose = ''
            alternate = ''
            if font.Pitch:
                pitch = r'\fprq%s' % font.Pitch
            if font.Panose:
                panose = r'{\*\panose %s}' % font.Panose
            if font.Alternate:
                alternate = r'{\*\falt %s}' % font.Alternate.Name

            self._write(r'{\f%s\f%s%s\fcharset%s%s %s%s;}', offset, font.Family, pitch, font.CharacterSet, panose, font.Name, alternate)

            self._font_map[font] = offset
            offset += 1

        self._write("}\n")

    def _WriteStyleSheet(self):
        self._write(r"{\stylesheet")

        # TODO: character styles, does anybody actually use them?

        offset_map = {}
        for idx, style in enumerate(self._doc.StyleSheet.ParagraphStyles):
            offset_map[style] = idx

        # paragraph styles
        self.paragraph_style_map = {}
        default = None
        for idx, style in enumerate(self._doc.StyleSheet.ParagraphStyles):

            if idx == 0:
                default = style
            else:
                self._write('\n')

            settings = Settings()

            # paragraph properties
            self._RendParagraphPropertySet(style.ParagraphPropertySet, settings)
            self._RendFramePropertySet(style.FramePropertySet, settings)
            self._RendShadingPropertySet(style.ShadingPropertySet, settings)

            # text properties
            self._RendTextPropertySet(style.TextStyle.TextPropertySet, settings)
            self._RendShadingPropertySet(style.TextStyle.ShadingPropertySet, settings)

            # have to take
            based_on = '\\sbasedon%s' % offset_map.get(style.BasedOn, 0)
            next_style = '\\snext%s' % offset_map.get(style.Next, 0)

            inln = '\\s%s%s' % (idx, settings)
            self._write("{%s%s%s %s;}", inln, based_on, next_style, style.Name)

            self.paragraph_style_map[style] = inln

        # if now style is specified for the first paragraph to be written, this one
        # will be used
        self._CurrentStyle = self.paragraph_style_map[default]

        self._write("}\n")

    def _WriteSection(self, section, is_first, add_header):

        def WriteHF(hf, rtfword):
            # if not hf:
            #     return
            
            # if we don't have anything in the header/footer then include
            # a blank paragraph, this stops it from picking up the header/footer
            # from the previous section
            # if not hf:
            #     hf = [Paragraph('')]
            if not hf:
                hf = []

            self._write('{\\%s' % rtfword)
            self._WriteElements(hf)
            self._write('}\n')

        settings = Settings()

        if not is_first:
            # we need to finish off the preceding section
            # and reset all of our defaults back to standard
            settings.append('sect')

        # reset to our defaults
        settings.append('sectd')

        if add_header:
            settings.append(SectionBreakTypeMap[section.BreakType])
            self._RendPageProperties(section, settings, in_section=True)

        settings.append(section.HeaderY, 'headery%s')
        settings.append(section.FooterY, 'footery%s')

        # write all of these out now as we need to do a write elements in the
        # next section
        self._write(repr(settings))

        # finally after all that has settled down we can do the
        # headers and footers
        if section.FirstHeader or section.FirstFooter:
            # include the titlepg flag if the first page has a special format
            self._write(r'\titlepg')
            WriteHF(section.FirstHeader, 'headerf')
            WriteHF(section.FirstFooter, 'footerf')

        WriteHF(section.Header, 'header')
        WriteHF(section.Footer, 'footer')

        # and at last the contents of the section that actually appear on the page
        self._WriteElements(section)

    def _WriteElements(self, elements):
        new_line = ''
        for element in elements:
            self._write(new_line)
            new_line = '\n'

            if isinstance(element, Paragraph):
                self.WriteParagraphElement(element)

            elif isinstance(element, Table):
                self.WriteTableElement(element)

            elif isinstance(element, str):
                self.WriteParagraphElement(Paragraph(element))

            elif isinstance(element, (RawCode, Image)):
                self.WriteRawCode(element)

            # elif isinstance(element.__class__, List):
            #    self._HandleListElement(element)

            elif self.WriteCustomElement:
                self.WriteCustomElement(self, element)

            else:
                raise Exception("Don't know how to handle elements of type %s" % element.__class__)

    def WriteParagraphElement(self, paragraph_elem, tag_prefix='', tag_suffix=r'\par', opening='{', closing='}'):

        # the tag_prefix and the tag_suffix take care of paragraphs in tables. A
        # paragraph in a table requires and extra tag at the front (intbl) and we
        # don't want the ending tag everytime. We want it for all paragraphs but
        # the last.

        overrides = Settings()
        self._RendParagraphPropertySet(paragraph_elem.Properties, overrides)
        self._RendFramePropertySet(paragraph_elem.Frame, overrides)
        self._RendShadingPropertySet(paragraph_elem.Shading, overrides)

        # when writing the RTF the style is carried from the previous paragraph to the next,
        # so if the currently written paragraph has a style then make it the current one,
        # otherwise leave it as it was
        self._CurrentStyle = self.paragraph_style_map.get(paragraph_elem.Style, self._CurrentStyle)

        self._write(r'%s\pard\plain%s %s%s ' % (opening, tag_prefix, self._CurrentStyle, overrides))

        for element in paragraph_elem:

            if isinstance(element, str):
                self._writeText(element)

            elif isinstance(element, RawCode):
                self._write(element.Data)

            elif isinstance(element, Text):
                self.WriteTextElement(element)

            elif isinstance(element, Inline):
                self.WriteInlineElement(element)

            elif element == TAB:
                self._write(r'\tab ')

            elif element == LINE:
                self._write(r'\line ')

            elif self.WriteCustomElement:
                self.WriteCustomElement(self, element)

            else:
                raise Exception('Don\'t know how to handle %s' % element)

        self._write(tag_suffix + closing)

    def WriteRawCode(self, raw_elem):
        self._write(raw_elem.Data)

    def WriteTextElement(self, text_elem):
        overrides = Settings()

        self._RendTextPropertySet(text_elem.Properties, overrides)
        self._RendShadingPropertySet(text_elem.Shading, overrides, 'ch')

        # write the wrapper and then let the custom handler have a go
        if overrides:
            self._write('{%s ' % repr(overrides))

        # if the data is just a string then we can now write it
        if isinstance(text_elem.Data, str):
            if text_elem.Data:
                self._writeText(text_elem.Data)
            else:
                self._write('')
        elif text_elem.Data == TAB:
            self._write(r'\tab ')
        else:
            self.WriteCustomElement(self, text_elem.Data)

        if overrides:
            self._write('}')

    def WriteInlineElement(self, inline_elem):
        overrides = Settings()

        self._RendTextPropertySet(inline_elem.Properties, overrides)
        self._RendShadingPropertySet(inline_elem.Shading, overrides, 'ch')

        # write the wrapper and then let the custom handler have a go
        if overrides:
            self._write('{%s ' % repr(overrides))

        for element in inline_elem:
            # if the data is just a string then we can now write it
            if isinstance(element, str):
                self._writeText(element)
            elif isinstance(element, RawCode):
                self._write(element.Data)
            elif element == TAB:
                self._write(r'\tab ')
            elif element == LINE:
                self._write(r'\line ')
            else:
                self.WriteCustomElement(self, element)

        if overrides:
            self._write('}')

    def WriteText(self, text):
        if text:
            self._write(text)

    def WriteTableElement(self, table_elem):
        vmerge = [False] * table_elem.ColumnCount
        for height, cells in table_elem.Rows:
            # calculate the right hand edge of the cells taking into account the spans
            offset = 0
            if table_elem.LeftOffset:
                offset = table_elem.LeftOffset
            cellx = []
            cell_idx = 0
            for cell in cells:
                cellx.append(offset + sum(table_elem.ColumnWidths[: cell_idx + cell.Span]))
                cell_idx += cell.Span

            self._write(r'{\trowd')

            settings = Settings()

            # the spec says that this value is mandatory and I think that 108 is the default value
            # so I'll take care of it here
            if table_elem.GapBetweenCells is not None:
                settings.append(table_elem.GapBetweenCells, 'trgaph%s')
            else:
                settings.append(108, 'trgaph%s')
            settings.append(TableAlignmentMap[table_elem.Alignment])
            settings.append(height, 'trrh%s')
            settings.append(table_elem.LeftOffset, 'trleft%s')

            # if table_elem.LeftOffset:
            #     width = table_elem.LeftOffset
            # else:
            #     width = 0
            for idx, cell in enumerate(cells):
                self._RendFramePropertySet(cell.Frame, settings, 'cl')

                # cells don't have margins so I don't know why I was doing this
                # I think it might have an affect in some versions of some WPs.
                # self._RendMarginsPropertySet(cell.Margins, settings, 'cl')

                # if we are starting to merge or if this one is the first in what is
                # probably a series of merges then start the vertical merging
                if cell.StartVerticalMerge or (cell.VerticalMerge and not vmerge[idx]):
                    settings.append('clvmgf')
                    vmerge[idx] = True

                elif cell.VerticalMerge:
                    # ..continuing a merge
                    settings.append('clvmrg')

                else:
                    # ..no merging going on so make sure that it is off
                    vmerge[idx] = False

                # for any cell in the next row that is covered by this span we
                # need to run off the vertical merging as we don't want them
                # merging up into this spanned cell
                for vmerge_idx in range(idx + 1, idx + cell.Span - 1):
                    vmerge[vmerge_idx] = False

                settings.append(CellAlignmentMap[cell.Alignment])
                settings.append(CellFlowMap[cell.Flow])

                self._RendShadingPropertySet(cell.Shading, settings, 'cl')

                # this terminates the definition of a cell and represents the right most edge of the cell from the left margin
                settings.append(cellx[idx], 'cellx%s')

            self._write(repr(settings))

            for cell in cells:
                if len(cell):
                    last_idx = len(cell) - 1
                    for element_idx, element in enumerate(cell):
                        # wrap plain strings in paragraph tags
                        if isinstance(element, str):
                            element = Paragraph(element)

                        # don't forget the prefix or else word crashes and does all sorts of strange things
                        if element_idx == last_idx:
                            self.WriteParagraphElement(element, tag_prefix=r'\intbl', tag_suffix='', opening='', closing='')

                        else:
                            self.WriteParagraphElement(element, tag_prefix=r'\intbl', opening='', closing='')

                    self._write(r'\cell')

                else:
                    self._write(r'\pard\intbl\cell')

            self._write('\\row}\n')
