import {escapeLatexText} from "./escape_latex"
import {BIBLIOGRAPHY_HEADERS, FIG_CATS} from "../../schema/i18n"

export class LatexExporterConvert {
    constructor(exporter, imageDB, bibDB, settings) {
        this.exporter = exporter
        this.settings = settings
        this.imageDB = imageDB
        this.bibDB = bibDB
        this.imageIds = []
        this.usedBibDB = {}
        // While walking the tree, we take note of the kinds of features That
        // are present in the file, so that we can assemble an preamble and
        // epilogue based on our findings.
        this.features = {}
        this.internalLinks = []
        this.figureCounter = {} // counters for each type of figure (figure/table/photo)
    }

    init(docContents) {
        this.preWalkJson(docContents)
        const rawTransformation = this.walkJson(docContents)
        const body = this.postProcess(rawTransformation)
        const copyright = this.assembleCopyright()
        const preamble = this.assemblePreamble()
        const epilogue = this.assembleEpilogue()
        const latex = copyright + this.docDeclaration + preamble + '\n\\begin{document}\n' + body + epilogue + '\n\\end{document}\n'
        const returnObject = {
            latex,
            imageIds: this.imageIds,
            usedBibDB: this.usedBibDB
        }
        return returnObject
    }

    get docDeclaration() {
        return '\\documentclass{article}\n'
    }

    // Check for things needed before creating raw transform
    preWalkJson(node) {
        switch (node.type) {
            // Collect all internal links so that we only set the anchors for those
            // that are being linked to.
            case 'text':
                if (node.marks) {
                    const hyperlink = node.marks.find(mark => mark.type === 'link')
                    if (hyperlink) {
                        const href = hyperlink.attrs.href
                        if (href[0] === '#' && !this.internalLinks.includes(href)) {
                            this.internalLinks.push(href.slice(1))
                        }
                    }
                }
                break
        }
        if (node.content) {
            node.content.forEach(child => this.preWalkJson(child))
        }
    }


    walkJson(node, options = {}) {
        let start = '', content = '', end = '',
            placeFootnotesAfterBlock = false
        switch (node.type) {
            case 'article':
                break
            case 'title':
                start += '\n\\title{'
                end = '}' + end
                break
            case 'heading_part':
                if (node.attrs.metadata === 'subtitle' && node.content) {
                    start += '\n\\subtitle{'
                    end = '}' + end
                    this.features.subtitle = true
                    options = Object.assign({}, options)
                    options.ignoreHeading = true
                } else if (!options.madeTitle) {
                    start += '\n\n\\maketitle\n'
                    options.madeTitle = true
                }
                break
            case 'contributor':
                // Ignore - we deal with contributors_part instead.
                break
            case 'contributors_part':
                if (node.content) {
                    if (node.attrs.metadata === 'authors') {
                        // TODO: deal with node.attrs.metadata === 'editors'
                        const authorsPerAffil = node.content.map(node => {
                            const author = node.attrs,
                                nameParts = []
                            let affiliation = false
                            if (author.firstname) {
                                nameParts.push(author.firstname)
                            }
                            if (author.lastname) {
                                nameParts.push(author.lastname)
                            }
                            if (nameParts.length && author.institution) {
                                affiliation = author.institution
                            } else if (author.institution) {
                                // We have an institution but no names. Use institution as name.
                                nameParts.push(author.institution)
                            }
                            return {
                                name: nameParts.join(' '),
                                affiliation,
                                email: author.email
                            }
                        }).reduce((affils, author) => {
                            const affil = author.affiliation
                            affils[affil] = affils[affil] || []
                            affils[affil].push(author)
                            return affils
                        }, {})

                        Object.values(authorsPerAffil).forEach(
                            affil => {
                                affil.forEach(
                                    author => {
                                        content +=
                                            `\n\\author{${escapeLatexText(author.name)}${
                                                author.email ?
                                                `\\thanks{${
                                                    escapeLatexText(author.email)
                                                }}` :
                                                ''
                                            }}`
                                    }
                                )

                                content += `\n\\affil{${
                                    affil[0].affiliation ?
                                    escapeLatexText(affil[0].affiliation) :
                                    ''
                                }}`
                            }
                        )
                        this.features.authors = true
                    } else {
                        if (!options.madeTitle) {
                            start += '\n\n\\maketitle\n'
                            options.madeTitle = true
                        }
                        // TODO: deal with contributor lists of non-authors properly
                        content += node.content.map(
                            contributorNode => {
                                const nameParts = []
                                if (contributorNode.attrs.firstname) {
                                    nameParts.push(contributorNode.attrs.firstname)
                                }
                                if (contributorNode.attrs.lastname) {
                                    nameParts.push(contributorNode.attrs.lastname)
                                }
                                if (!nameParts.length && contributorNode.attrs.institution) {
                                    // We have an institution but no names. Use institution as name.
                                    nameParts.push(contributorNode.attrs.institution)
                                }
                                return nameParts.join(' ')
                            }
                        ).join(', ')

                    }
                    content += "\n\n"
                }

                break
            case 'tags_part':
                if (node.content) {
                    if (node.attrs.metadata === 'keywords') {
                        start += '\n\\keywords{'
                        end = '}' + end
                        this.features.keywords = true
                    } else if (!options.madeTitle) {
                        start += '\n\n\\maketitle\n'
                        options.madeTitle = true
                    }
                    content += node.content.map(
                        keyword => escapeLatexText(keyword.attrs.tag)
                    ).join('\\sep ')
                }
                break
            case 'tag':
                // Ignore - we already took all the tags_part from the keywords node.
                break
            case 'richtext_part':
                if (!options.madeTitle) {
                    start += '\n\n\\maketitle\n'
                    options.madeTitle = true
                }
                if (node.content && node.attrs.metadata === 'abstract') {
                    start += '\n\\begin{abstract}\n'
                    end = '\n\\end{abstract}\n' + end
                }
                break
            case 'table_of_contents':
                start += '\n\n\\tableofcontents\n'
                break
            case 'separator_part':
            case 'table_part':
                // part separators as in page breaks should usually already be handled
                // by LaTeX and table parts will simply show the table inside of them.
                break
            case 'paragraph':
                start += '\n\n'
                end = '\n' + end
                break
            case 'heading1':
            case 'heading2':
            case 'heading3':
            case 'heading4':
            case 'heading5':
            case 'heading6': {
                if (options.ignoreHeading) {
                    break
                }
                const level = parseInt(node.type.slice(-1))
                switch (level) {
                    case 1:
                        start += '\n\n\\section{'
                        break
                    case 2:
                        start += '\n\n\\subsection{'
                        break
                    case 3:
                    case 4:
                    case 5:
                    case 6:
                        // TODO: Add support for levels 4/5/6
                        start += '\n\n\\subsubsection{'
                        break
                }
                end = `}\\label{${node.attrs.id}}\n\n` + end
                // Check if this heading is being linked to. If this is the case,
                // place a protected hypertarget here that does not add an extra
                // entry into the PDF TOC.
                if (this.internalLinks.includes(node.attrs.id)) {
                    // Add a link target
                    end = `\\texorpdfstring{\\protect\\hypertarget{${node.attrs.id}}{}}{}` + end
                }
                if (!options.onlyFootnoteMarkers) {
                    placeFootnotesAfterBlock = true
                    options = Object.assign({}, options)
                    options.onlyFootnoteMarkers = true
                    options.unplacedFootnotes = []
                }
                break
            }
            case 'code_block':
                start += '\n\\begin{code}\n\n'
                end = '\n\n\\end{code}\n' + end
                this.features.code = true
                break
            case 'blockquote':
                start += '\n\\begin{quote}\n\n'
                end = '\n\n\\end{quote}\n' + end
                break
            case 'ordered_list':
                start += '\n\\begin{enumerate}'
                end = '\n\\end{enumerate}' + end
                if (!options.onlyFootnoteMarkers) {
                    placeFootnotesAfterBlock = true
                    options = Object.assign({}, options)
                    options.onlyFootnoteMarkers = true
                    options.unplacedFootnotes = []
                }
                break
            case 'bullet_list':
                start += '\n\\begin{itemize}'
                end = '\n\\end{itemize}' + end
                if (!options.onlyFootnoteMarkers) {
                    placeFootnotesAfterBlock = true
                    options = Object.assign({}, options)
                    options.onlyFootnoteMarkers = true
                    options.unplacedFootnotes = []
                }
                break
            case 'list_item':
                start += '\n\\item '
                end = '\n' + end
                break
            case 'footnote':
                if (options.onlyFootnoteMarkers) {
                    // We are inside a headline or a list and can only place a
                    // footnote marker here. The footnote will have to be put
                    // beyond the block node instead.
                    start += '\\protect\\footnotemark{}'
                    options.unplacedFootnotes.push(node.attrs.footnote)
                } else {
                    if (!node.attrs.footnote.find(par => par.type === 'figure')) {
                        // LaTeX doesn't allow figures in footnotes, so well move
                        // this footnote into the regular text.
                        start += '\\footnote{'
                        end = '}' + end
                    }
                    let fnContent = ''
                    node.attrs.footnote.forEach(footPar => {
                        fnContent += this.walkJson(footPar, options)
                    })
                    content += fnContent.replace(/^\s+|\s+$/g, '')
                }
                break
            case 'text': {
                let strong, em, underline, hyperlink
                // Check for hyperlink, bold/strong, italic/em and underline
                if (node.marks) {
                    strong = node.marks.find(mark => mark.type === 'strong')
                    em = node.marks.find(mark => mark.type === 'em')
                    underline = node.marks.find(mark => mark.type === 'underline')
                    hyperlink = node.marks.find(mark => mark.type === 'link')
                }
                if (em) {
                    start += '\\emph{'
                    end = '}' + end
                }
                if (strong) {
                    start += '\\textbf{'
                    end = '}' + end
                }
                if (underline) {
                    start += '\\underline{'
                    end = '}' + end
                }
                if (hyperlink) {
                    const href = hyperlink.attrs.href
                    if (href[0] === '#') {
                        // Internal link
                        start += `\\hyperlink{${href.slice(1)}}{`
                    } else {
                        // External link
                        start += `\\href{${href}}{`
                    }
                    end = '}' + end
                    this.features.hyperlinks = true
                }
                content += escapeLatexText(node.text)
                break
            }
            case 'cross_reference': {
                content += `\\hyperref[${node.attrs.id}]{${node.attrs.title || 'MISSING TARGET'}}`
                this.features.hyperlinks = true
                break
            }
            case 'citation': {
                const references = node.attrs.references
                const format = node.attrs.format
                let citationCommand = '\\' + format

                if (references.length > 1 &&
                    references.every(ref => !ref.locator && !ref.prefix)
                ) {
                    // multi source citation without page numbers or text before.
                    const citationEntryKeys = []

                    const allCitationItemsPresent = references.map(ref => ref.id).every(
                        citationEntry => {
                            const bibDBEntry = this.bibDB.db[citationEntry]
                            if (bibDBEntry) {
                                if (!bibDBEntry) {
                                    // Not present in bibliography database, skip it.
                                    // TODO: Throw an error?
                                    return false
                                }
                                if (!this.usedBibDB[citationEntry]) {
                                    const citationKey = this.createUniqueCitationKey(
                                        bibDBEntry.entry_key
                                    )
                                    this.usedBibDB[citationEntry] = Object.assign({}, bibDBEntry)
                                    this.usedBibDB[citationEntry].entry_key = citationKey
                                }
                                citationEntryKeys.push(this.usedBibDB[citationEntry].entry_key)
                            }
                            return true
                        }
                    )
                    if (allCitationItemsPresent) {
                        citationCommand += `{${citationEntryKeys.join(',')}}`
                    } else {
                        citationCommand = false
                    }
                } else {
                    if (references.length > 1) {
                        citationCommand += 's' // Switching from \autocite to \autocites
                    }

                    const allCitationItemsPresent = references.every(
                        ref => {
                            const bibDBEntry = this.bibDB.db[ref.id]
                            if (!bibDBEntry) {
                                // Not present in bibliography database, skip it.
                                // TODO: Throw an error?
                                return false
                            }

                            if (ref.prefix) {
                                citationCommand += `[${ref.prefix}]`
                                if (!ref.locator) {
                                    citationCommand += '[]'
                                }
                            }
                            if (ref.locator) {
                                citationCommand += `[${ref.locator}]`
                            }
                            citationCommand += '{'

                            if (!this.usedBibDB[ref.id]) {
                                const citationKey = this.createUniqueCitationKey(
                                    bibDBEntry.entry_key
                                )
                                this.usedBibDB[ref.id] = Object.assign({}, bibDBEntry)
                                this.usedBibDB[ref.id].entry_key = citationKey
                            }
                            citationCommand += this.usedBibDB[ref.id].entry_key
                            citationCommand += '}'

                            return true
                        }
                    )

                    if (!allCitationItemsPresent) {
                        citationCommand = false
                    }
                }
                if (citationCommand) {
                    content += citationCommand
                    this.features.citations = true
                }
                break
            }
            case 'figure': {
                const figureType = node.attrs.figureCategory
                let caption = node.attrs.caption
                if (figureType!=='none') {
                    if (!this.figureCounter[figureType]) {
                        this.figureCounter[figureType] = 1
                    }
                    const figCount = this.figureCounter[figureType]++
                    const figLabel = `${FIG_CATS[figureType][this.settings.language]} ${figCount}`
                    if (caption.length) {
                        caption = `${figLabel}: ${caption}`
                    } else {
                        caption = figLabel
                    }
                }
                let innerFigure = ''
                let aligned = 'left'
                if (node.attrs.width !== '100') {
                    aligned = node.attrs.aligned
                }
                if (aligned === 'center') {
                    start += '\n\n\\begin{center}'
                    end = '\n\n\\end{center}\n' + end
                } else if (aligned === 'right') {
                    start += '\n\n{\\raggedleft' // This is not a typo - raggedleft = aligned: right
                    end = '\n\n}\n' + end
                } // aligned === 'left' is default
                let copyright
                if (node.attrs.image) {
                    this.imageIds.push(node.attrs.image)
                    const imageDBEntry = this.imageDB.db[node.attrs.image],
                        filePathName = imageDBEntry.image,
                        filename = filePathName.split('/').pop()
                    copyright = imageDBEntry.copyright
                    if (filename.split('.').pop() === 'svg') {
                        innerFigure += `\\includesvg[width=${parseInt(node.attrs.width)/100}\\textwidth]{${filename}}\n`
                        this.features.SVGs = true
                    } else {
                        innerFigure += `\\scaledgraphics{${filename}}{${parseInt(node.attrs.width)/100}}\n`
                        this.features.images = true
                    }
                } else {
                    const equation = node.attrs.equation
                    innerFigure += `\\begin{displaymath}\n${equation}\n\\end{displaymath}\n`
                }
                if (figureType==='table') {
                    start += `\n\\begin{table}\n`
                    content += `\\caption*{${caption}}\\label{${node.attrs.id}}\n${innerFigure}`
                    end = `\\end{table}\n` + end
                } else { // TODO: handle photo figure types in a special way
                    start += `\n\\begin{figure}\n`
                    content += `${innerFigure}\\caption*{${caption}}\\label{${node.attrs.id}}\n`
                    end = `\\end{figure}\n` + end
                }
                if (copyright && copyright.holder) {
                    content += `% © ${copyright.year ? copyright.year : new Date().getFullYear()} ${copyright.holder}\n`
                }
                if (copyright && copyright.licenses.length) {
                    copyright.licenses.forEach(
                        license => {
                            content += `% ${license.title}: ${license.url}${license.start ? ` (${license.start})\n` : ''}\n`
                        }
                    )
                }
                if (this.internalLinks.includes(node.attrs.id)) {
                    // Add a link target
                    end = `\\texorpdfstring{\\protect\\hypertarget{${node.attrs.id}}{}}{}\n` + end
                }
                this.features.captions = true
                break
            }
            case 'table':
                if (node.content && node.content.length) {
                    const columns = node.content[0].content.reduce(
                        (columns, node) => columns + node.attrs.colspan,
                        0
                    )
                    let aligned = 'left'
                    if (node.attrs.width !== '100') {
                        aligned = node.attrs.aligned
                    }
                    if (aligned === 'center') {
                        start += '\n\n\\begin{center}'
                        end = '\n\n\\end{center}\n' + end
                    } else if (aligned === 'right') {
                        start += '\n\n{\\raggedleft' // This is not a typo - raggedleft = aligned: right
                        end = '\n\n}\n'
                    } // aligned === 'left' is default
                    start += `\n\n\\begin{tabu} to ${
                        node.attrs.width === '100' ? '' : parseInt(node.attrs.width)/100
                    }\\textwidth { |${'X|'.repeat(columns)} }\n\\hline\n\n`
                    end = `\\hline\n\n\\end{tabu}` + end
                    this.features.tables = true
                }
                break
            case 'table_row':
                end += ' \\\\\n'
                break
            case 'table_cell':
            case 'table_header':
                if (node.attrs.colspan > 1) {
                    start += `\\multicolumn{${node.attrs.colspan}}{c}{`
                    end += '}'
                }
                // TODO: these multirow outputs don't work very well with longer text.
                // If there is another alternative, please change!
                if (node.attrs.rowspan > 1) {
                    start += `\\multirow{${node.attrs.rowspan}}{*}{`
                    end += '}'
                    this.features.rowspan = true
                }
                end += ' & '
                break
            case 'equation':
                content += `$${node.attrs.equation}$`
                break
            case 'hard_break':
                content += '\n\n'
                break
            default:
                break
        }

        if (node.content) {
            node.content.forEach(child => {
                content += this.walkJson(child, options)
            })
        }
        if (
            placeFootnotesAfterBlock &&
            options.unplacedFootnotes &&
            options.unplacedFootnotes.length
        ) {
            // There are footnotes that needed to be placed behind the node.
            // This happens in the case of headlines and lists.
            end += `\\addtocounter{footnote}{-${(options.unplacedFootnotes.length)}}`
            options.unplacedFootnotes.forEach(footnote => {
                end += '\\stepcounter{footnote}\n'
                end += '\\footnotetext{'
                let fnContent = ''
                footnote.forEach(footPar => {
                    fnContent += this.walkJson(footPar, options)
                })
                end += fnContent.replace(/^\s+|\s+$/g, '')
                end += '}'
            })
            options.unplacedFootnotes = []
        }
        if (['table_cell', 'table_header'].includes(node.type) && node.attrs.rowspan > 1) {
            // \multirow doesn't allow multiple paragraphs.
            content = content.trim().replace(/\n\n/g, ' \\\\ ')
        }

        return start + content + end
    }

    // The database doesn't ensure that citation keys are unique.
    // So here we need to make sure that the same key is not used twice in one
    // document.
    createUniqueCitationKey(suggestedKey) {
        const usedKeys = Object.keys(this.usedBibDB).map(key=>{
            return this.usedBibDB[key].entry_key
        })
        if (usedKeys.includes(suggestedKey)) {
            suggestedKey += 'X'
            return this.createUniqueCitationKey(suggestedKey)
        } else {
            return suggestedKey
        }
    }

    postProcess(latex) {
        return latex
        // join blocks of the same type that follow oneanother.
        .replace(/\\end{code}\n\n\\begin{code}\n\n/g, '')
        .replace(/\\end{quote}\n\n\\begin{quote}\n\n/g, '')
        // Remove the last divider in any any table row.
        .replace(/& {2}\\\\/g, '\\\\')
        // Remove new lines between table cells.
        .replace(/\n & \n\n/g, ' & ')
        // Remove new lines within itemization
        .replace(/\\item \n\n/g, '\\item ')
    }

    assembleEpilogue() {
        let epilogue = ''
        if (this.features.citations) {
            const bibliographyHeader = this.settings.bibliography_header[this.settings.language] || BIBLIOGRAPHY_HEADERS[this.settings.language]
            epilogue += `\n\n\\printbibliography[title={${escapeLatexText(bibliographyHeader)}}]`
        }
        return epilogue
    }

    assembleCopyright() {
        let note = ''
        if (this.settings.copyright) {
            if (this.settings.copyright.holder) {
                note += `% © ${this.settings.copyright.year ? this.settings.copyright.year : new Date().getFullYear()} ${this.settings.copyright.holder}\n`
            }
            if (this.settings.copyright.licenses.length) {
                this.settings.copyright.licenses.forEach(
                    license => {
                        note += `% ${license.url}${license.start ? ` (${license.start})` : ''}\n`
                    }
                )
            }
        }

        if (note.length) {
            note += '\n\n'
        }
        return note
    }

    assemblePreamble() {
        let preamble = ''

        if (this.features.subtitle) {
            preamble += `
                \n\\usepackage{titling}
                \n\\newcommand{\\subtitle}[1]{%
                    \n\t\\posttitle{%
                        \n\t\t\\par\\end{center}
                        \n\t\t\\begin{center}\\large#1\\end{center}
                        \n\t\t\\vskip 0.5em}%
                }
            `
        }
        if (this.features.authors) {
            preamble += `
                \n\\usepackage{authblk}
                \n\\makeatletter
                \n\\let\\@fnsymbol\\@alph
                \n\\makeatother
            `
        }

        if (this.features.keywords) {
            preamble += `
                \n\\def\\keywords{\\vspace{.5em}
                \n{\\textit{Keywords}:\\,\\relax%
                \n}}
                \n\\def\\endkeywords{\\par}
                \n\\newcommand{\\sep}{, }
            `
        }

        if (this.features.hyperlinks) {
            preamble += '\n\\usepackage{hyperref}'
        }

        if (this.features.captions) {
            preamble += '\n\\usepackage{caption}'
        }

        if (this.features.citations) {
            preamble += `
                \n\\usepackage[backend=biber,hyperref=false,citestyle=authoryear,bibstyle=authoryear]{biblatex}
                \n\\bibliography{bibliography}
            `
        }

        if (this.features.SVGs) {
            preamble += '\n\\usepackage{svg}'
        }

        if (this.features.images) {
            preamble += '\n\\usepackage{graphicx}'
            // The following scales graphics down to text width, but not scaling them up if they are smaller
            preamble += `
                \n\\usepackage{calc}
                \n\\newlength{\\imgwidth}
                \n\\newcommand\\scaledgraphics[2]{%
                \n\\settowidth{\\imgwidth}{\\includegraphics{#1}}%
                \n\\setlength{\\imgwidth}{\\minof{\\imgwidth}{#2\\textwidth}}%
                \n\\includegraphics[width=\\imgwidth,height=\\textheight,keepaspectratio]{#1}%
                \n}
            `
        }

        if (this.features.tables) {
            preamble += '\n\\usepackage{tabu}'
        }
        if (this.features.rowspan) {
            preamble += '\n\\usepackage{multirow}'
        }

        if (this.features.code) {
            // See https://tex.stackexchange.com/questions/445424/making-a-multiline-code-environment
            preamble += `
            \n\\usepackage{xcolor}
            \\definecolor{mygray}{gray}{0.9}
            \\usepackage{fvextra}
            \\usepackage{tcolorbox}
            \\newenvironment{code}%
            {\\VerbatimEnvironment
            \\begin{tcolorbox}[colback=mygray, boxsep=0pt, arc=0pt, boxrule=0pt]
            \\begin{Verbatim}[fontsize=\\scriptsize, commandchars=\\\\\\{\\},
            breaklines, breakafter=*, breaksymbolsep=0.5em,
            breakaftersymbolpre={\\,\\tiny\\ensuremath{\\rfloor}}]}%
            {\\end{Verbatim}%
             \\end{tcolorbox}}
            `

        }

        return preamble

    }

}
