import { get } from "svelte/store"
import type { Item, Line } from "../../../../types/Show"
import { styles } from "../../../stores"
import { clone } from "../../helpers/array"
import { getFirstActiveOutput } from "../../helpers/output"
import { getStyles } from "../../helpers/style"
import { getItemText } from "../scripts/textStyle"

export class EditboxHelper {
    // Compare text of all the new lines to determine if it's truly a modification or just an index change.
    // Set the cursor to the start of the last line that was modified.
    static determineCaretLine(oldLines: Line[], newLines: Line[]) {
        const oldTexts: string[] = []
        const newTexts: string[] = []

        oldLines?.forEach((line) => {
            if (line.text?.[0]?.value) oldTexts.push(line.text[0].value)
        })

        newLines.forEach((line) => {
            if (line.text?.[0]?.value) newTexts.push(line.text[0].value)
        })

        let lastLineChanged = -1
        if (oldTexts.length === newTexts.length) return lastLineChanged
        for (let i = 0; i < newTexts.length; i++) {
            const nt = newTexts[i]
            const index = oldTexts.indexOf(nt)
            if (index === -1) lastLineChanged = i
            else oldTexts.splice(index, 1)
        }
        return lastLineChanged
    }

    static splitAllCrlf(lines: Line[]) {
        const result: Line[] = []
        lines.forEach((line) => {
            const splitLines = this.splitCrlf(line)
            result.push(...splitLines)
        })
        return result
    }

    static splitCrlf(line: Line) {
        if (!line?.text?.length) return []

        const result: Line[] = []
        let newLine = clone(line)
        newLine.text = []

        if (!Array.isArray(line.text)) line.text = []
        line.text.forEach((text) => {
            const value = text.value || ""
            const parts = value.replace("\r", "").split("\n")
            newLine.text.push({ style: text.style, value: parts[0] })
            if (parts.length > 1) {
                for (let i = 1; i < parts.length; i++) {
                    result.push(clone(newLine))
                    newLine = clone(line)

                    newLine.text = [{ style: text.style, value: parts[i] }]
                }
            }
        })
        result.push(newLine)

        return result
    }

    static cutLinesInTwo({ sel, lines, currentIndex, textPos, start }) {
        let firstLines: Line[] = []
        let secondLines: Line[] = []

        // remove empty lines and trim text (that messes up the selection)
        // lines = lines
        //     .map((line) => {
        //         line.text = (line.text || []).map((text) => ({ ...text, value: (text.value || "").trim() })).filter((a) => a.value.length)
        //         return line
        //     }).filter((a) => a.text?.[0]?.value?.length)

        // split lines in two
        lines.forEach((line, i) => {
            // Update start position if this line has a selection start
            if (sel[i]?.start !== undefined) start = currentIndex + sel[i].start!

            const lineStartIndex = currentIndex
            if (start > -1 && currentIndex >= start) secondLines.push({ align: line.align, text: [] })
            else firstLines.push({ align: line.align, text: [] })
            const firstHalf = start > -1 && lineStartIndex >= start ? null : firstLines[firstLines.length - 1]

            textPos = 0
            if (!Array.isArray(line.text)) line.text = []
            line.text.forEach(splitLines)

            // keep chords with their line, a mid-line split partitions them by the split offset
            const lineChords = line.chords || []
            if (lineChords.length) {
                if (start > -1 && lineStartIndex >= start) {
                    const target = secondLines[secondLines.length - 1]
                    if (target) target.chords = lineChords
                } else if (start > lineStartIndex && start < currentIndex) {
                    const localSplit = start - lineStartIndex
                    const firstChords = lineChords.filter((a) => a.pos < localSplit)
                    const secondChords = lineChords.filter((a) => a.pos >= localSplit).map((a) => ({ ...a, pos: a.pos - localSplit }))
                    if (firstHalf && firstChords.length) firstHalf.chords = firstChords
                    const secondTarget = secondLines[secondLines.length - 1]
                    if (secondTarget && secondChords.length) secondTarget.chords = secondChords
                } else if (firstHalf) {
                    firstHalf.chords = lineChords
                }
            }

            if (!firstLines.at(-1)?.text.length) firstLines.pop()
            if (!secondLines.at(0)?.text.length) secondLines.shift()

            function splitLines(text) {
                const value = text.value || ""

                const segmentStart = currentIndex
                const segmentEnd = currentIndex + value.length
                currentIndex = segmentEnd

                // Entire segment is before split point
                if (start < 0 || segmentEnd <= start) {
                    if (!firstLines.length) firstLines.push({ align: line.align, text: [] })
                    firstLines[firstLines.length - 1].text.push(text)
                    textPos += value.length
                    return
                }

                // Entire segment is after split point
                if (segmentStart >= start) {
                    if (!secondLines.length) secondLines.push({ align: line.align, text: [] })
                    secondLines[secondLines.length - 1].text.push(text)
                    textPos += value.length
                    return
                }

                // Split point is within this segment
                if (!secondLines.length) secondLines.push({ align: line.align, text: [] })
                const pos = start - segmentStart

                if (pos > 0) {
                    if (!firstLines.length) firstLines.push({ align: line.align, text: [] })
                    firstLines[firstLines.length - 1].text.push({
                        style: text.style,
                        value: value.slice(0, pos),
                        customType: text.customType,
                        sourceDynamicKey: text.sourceDynamicKey
                    })
                }
                secondLines[secondLines.length - 1].text.push({
                    style: text.style,
                    value: value.slice(pos),
                    customType: text.customType,
                    sourceDynamicKey: text.sourceDynamicKey
                })

                textPos += value.length
            }
        })

        // remove first line if empty
        if (!getItemText({ lines: secondLines } as any)) secondLines.shift()

        const defaultLine = [
            {
                align: lines[0]?.align || "",
                text: [{ style: lines[0].text[0]?.style || "", value: "" }]
            }
        ]
        if (!firstLines.length || !firstLines[0].text.length) firstLines = defaultLine
        if (!secondLines.length) secondLines = defaultLine

        return { firstLines, secondLines }
    }

    static getStyleHtml(item: Item, plain: boolean, currentStyle: string, useNormalWrap: boolean = false) {
        currentStyle = ""
        let html = ""
        let firstTextStyleArchive = ""
        const lineStyleBg = item.specialStyle?.lineBg ? `background: ${item.specialStyle.lineBg};` : ""
        const lineStyleRadius = item.specialStyle?.lineRadius ? `border-radius: ${item.specialStyle.lineRadius}px;` : ""
        const listStyle = "" // item.list?.enabled ? `;list-style${item.list?.style?.includes("disclosure") ? "-type:" : ": inside"} ${item.list?.style || "disc"};` : "" // item.list?.enabled ? ";display: list-item;" : ""

        // a contenteditable without any line blocks can't receive line breaks, so always render at least one
        const lines: Line[] = item?.lines?.length ? item.lines : [{ align: "", text: [{ style: "", value: "" }] }]

        lines.forEach((line, i) => {
            const align = (typeof line.align === "string" ? line.align : "").replaceAll(lineStyleBg, "").replaceAll(lineStyleRadius, "") + ";"
            currentStyle += align + lineStyleBg + lineStyleRadius // + line.chords?.map((a) => a.key)
            const style = align || lineStyleBg || lineStyleRadius || listStyle ? 'style="' + align + lineStyleBg + lineStyleRadius + listStyle + '"' : ""

            const normalWrap = useNormalWrap || align.includes("justify") || align.includes("left") || JSON.stringify(line).includes("nowrap")

            // plain mode has no style attributes, identity attributes map styles back to the source lines (they are cloned when the browser splits a line)
            html += `<div class="break ${normalWrap ? "normalWrap" : ""}" ${plain ? `data-line-index="${i}"` : style}>`

            // fix removing all text in a line
            if (i === 0 && line.text?.[0]?.style) firstTextStyleArchive = line.text?.[0]?.style || ""
            if (!line.text?.length) line.text = [{ style: firstTextStyleArchive || "", value: "" }]

            const currentChords = line.chords || []
            let textIndex = 0

            if (!Array.isArray(line.text)) line.text = []
            line.text.forEach((a, tIndex) => {
                currentStyle += this.getTextStyle(a)

                // each chord is stored in exactly one segment, the last segment absorbs end-of-line positions
                const isLastSegment = tIndex === line.text.length - 1
                const textEnd = textIndex + a.value.length
                const textChords = currentChords.filter((chord) => chord.pos >= textIndex && (chord.pos < textEnd || isLastSegment))
                textIndex = textEnd

                const textStyle = a.style || listStyle ? 'style="' + this.getCustomTextStyle(a.style) + listStyle + '"' : ""
                let value = a.value?.replaceAll("\n", "<br>") || "<br>"
                // if (value === " ") value = "&nbsp;"

                // this will "hide" any HTML tags if any in the actual text content (not chords or text editor)
                html += `<span data-freeshow-text="true" class="${a.customType && !a.customType.includes("jw") ? "custom" : ""}" ${plain ? `data-text-index="${tIndex}"` : textStyle} data-customtype='${a.customType || ""}' data-sourcedynamickey='${a.sourceDynamicKey || ""}' data-chords='${JSON.stringify(textChords)}'>` + value + "</span>"
            })
            html += "</div>"
        })

        // currentStyle = currentStyle.replaceAll(";;", ";")
        return { html, currentStyle }
    }

    static getTextStyle(lineText: any) {
        if (!lineText) return ""
        const style = lineText.style || ""
        return style
    }

    static getCustomTextStyle(style: string) {
        if (!style) return ""

        // fix quotes (for font family names with spaces)
        style = style.replaceAll('"', "'")

        // text gradient

        if (style.includes("-gradient")) {
            // can't edit properly when this is applied in the editor
            // let styles = getStyles(style)
            // styles.color = extractPlainColorFromGradient(styles.color)
            // let newStyles = ""
            // Object.entries(styles).forEach((key, value) => {
            //     newStyles += `${key}: ${value};`
            // })
            // style = newStyles
        }

        // custom font size ratio

        const fontSize = Number(getStyles(style, true)["font-size"] || 100)

        // get first output style
        const currentOutput = getFirstActiveOutput()
        const outputStyle = get(styles)[currentOutput?.style || ""] || {}
        if (!Object.keys(outputStyle).length) return style

        const customFontSizeRatio = (outputStyle.aspectRatio?.fontSizeRatio ?? 100) / 100

        // remove custom font size
        // let customIndex = style.indexOf("--custom")
        // if (customIndex > -1) style = style.slice(0, customIndex)

        return `${style};--custom:true;font-size: ${fontSize * customFontSizeRatio}px;`
    }
}

// function extractPlainColorFromGradient(gradient: string) {
//     const fallback = "#FFFFFF"

//     const splitStops = (str: string): string[] => {
//         const parts: string[] = []
//         let buffer = ""
//         let depth = 0

//         for (let char of str) {
//             if (char === "(") depth++
//             if (char === ")") depth--
//             if (char === "," && depth === 0) {
//                 parts.push(buffer.trim())
//                 buffer = ""
//             } else {
//                 buffer += char
//             }
//         }
//         if (buffer) parts.push(buffer.trim())
//         return parts
//     }

//     if (gradient.startsWith("linear-gradient")) {
//         const match = gradient.match(/linear-gradient\(([^,]+),\s*(.+)\)/)
//         if (!match) return fallback

//         const stops = splitStops(match[2])
//         const plainColor = stops[0]
//         return plainColor
//     }

//     if (gradient.startsWith("radial-gradient")) {
//         const match = gradient.match(/radial-gradient\(([^,]+),\s*(.+)\)/)
//         if (!match) return fallback

//         const stops = splitStops(match[2])
//         const plainColor = stops[stops.length - 1]
//         return plainColor
//     }

//     return fallback
// }
