Exit Full View

Games Cupboard / gamescupboard-client / src / commonMain / kotlin / uk / co / nickthecoder / gamescupboard / client / Rules.kt

package uk.co.nickthecoder.gamescupboard.client

import com.soywiz.korge.view.ClipContainer
import com.soywiz.korge.view.solidRect
import com.soywiz.korim.color.Colors
import com.soywiz.korim.color.RGBA
import uk.co.nickthecoder.gamescupboard.common.dockHeight

private open class RulesStyle(
    val prefix: String,
    val suffix: String,
    val color: RGBA
)

private class HyperlinkStyle(val separator: String = "|") : RulesStyle("[", "]", hyperlinks)

class Rules(str: String) : TextButtonDockable("Rules") {

    override val panel = ClipContainer(400.0, dockHeight.toDouble()).apply {
        solidRect(width, height, dockableColor)
    }

    init {
        // Within the text, we can change styles.
        // This map is keyed on the string which starts and ends the style.
        // The values are RGBA colors for that style.
        // e.g. For fake-bold text, place words in backticks `Hello World`.
        // NOTE. Styles cannot intersect. So `Hello _world_` is not allowed, because the fake-underscore
        // is within the fake-bold. In this example, the output would be "Hello _world_" in fake-bold's color.
        val styles = listOf(
            RulesStyle("`", "`", boldText),
            RulesStyle("_", "_", underscoreText),
            HyperlinkStyle()
        )

        val printContainer =
            PrintContainer(panel.width, panel.height - 20, paddingTop = 10.0, paddingLeft = 20.0).apply {
                backgroundColor = Colors.TRANSPARENT_WHITE
            }
        panel.addChild(printContainer)
        val lines = str.split("\n")

        // We start off with no styles. When we loop through each line, this holds the style in force at the
        // end of that line (and is used as the initial style for the next line).
        var trailingStyle: RulesStyle? = null

        // Start of nested functions

        /**
         * Prints to printContainer, using the style according to [style].
         * If it is null, then the "plain" style is used.
         * Otherwise, look up the color in the styles map.
         */
        fun styledPrint(str: String, style: RulesStyle?, action: (() -> Unit)? = null) {
            if (style == null) {
                printContainer.print(str)
            } else {
                printContainer.print(str, style.color, action)
            }
        }

        /**
         * Prints str to the printContainer. The initial style is `initialStyle`.
         * However, if we find a new style within [str], then we use recursion to
         * print the parts.
         * Note. the first level of recursion, [initialStyle] = `trailingStyleKey`
         * i.e. the style left over from the previous line.
         * For nested calls, [initialStyle] is the style left over from the previous
         * level of recursion.
         *
         * When we recurse, [str] will always be smaller, so we are guaranteed to exit.
         *
         * The return value is the RulesStyle that has not been closed, or null
         * when [str] ends with no styles applied.
         */
        fun processRemainder(str: String, initialStyle: RulesStyle?): RulesStyle? {
            if (str.isBlank()) return initialStyle

            if (initialStyle == null) {
                // Look for the FIRST beginning of one of the special styles ` or _
                var lowestIndex = Int.MAX_VALUE
                var foundStyle: RulesStyle? = null
                for (style in styles) {
                    val i = str.indexOf(style.prefix)
                    if (i >= 0 && i < lowestIndex) {
                        lowestIndex = i
                        foundStyle = style
                    }
                }
                if (foundStyle != null && lowestIndex != Int.MAX_VALUE) {
                    // We've found a special style, so print up to that character
                    styledPrint(str.substring(0, lowestIndex), null)
                    val endIndex = str.indexOf(foundStyle.suffix, lowestIndex + foundStyle.prefix.length)
                    if (endIndex >= 0) {
                        // The end of the style is on the same line
                        if (foundStyle is HyperlinkStyle) {
                            val sepIndex = str.indexOf(foundStyle.separator, lowestIndex + foundStyle.prefix.length)
                            if (sepIndex < 0) {
                                // No separator, so the url and link-text are one and the same.
                                val linkText = str.substring(lowestIndex + foundStyle.prefix.length, endIndex)
                                styledPrint(linkText, foundStyle) { FollowLink.follow(linkText) }
                            } else {
                                // We found the separator, so print up to that (which is the text).
                                // The url is after sepIndex
                                val linkText = str.substring(lowestIndex + foundStyle.prefix.length, sepIndex)
                                val url = str.substring(sepIndex + foundStyle.separator.length, endIndex)
                                styledPrint(linkText, foundStyle) { FollowLink.follow(url) }
                            }

                        } else {
                            styledPrint(str.substring(lowestIndex + foundStyle.prefix.length, endIndex), foundStyle)
                        }
                        // Now recursively process the remainder of the line.
                        return processRemainder(str.substring(endIndex + foundStyle.suffix.length), null)
                    } else {
                        // There is no end-of-style on this line.
                        styledPrint(str.substring(lowestIndex + foundStyle.prefix.length), foundStyle)
                        return null
                    }
                } else {
                    // There are no styles, so print it plain.
                    styledPrint(str, null)
                    return null
                }

            } else {
                // The line started with a special style, so we need to look for the close
                val closeIndex = str.indexOf(initialStyle.suffix)
                if (closeIndex >= 0) {
                    // We've found the end.
                    styledPrint(str.substring(0, closeIndex), initialStyle)
                    return processRemainder(str.substring(closeIndex + initialStyle.suffix.length), null)
                } else {
                    // There is no close, so use the same style for the whole line
                    styledPrint(str, initialStyle)
                    return initialStyle
                }
            }
        }

        // End of nested functions!

        for (line in lines) {
            if (line.isBlank()) {
                printContainer.println()
                printContainer.println()
            } else {

                // Process a single line of text.
                trailingStyle = processRemainder(line, trailingStyle)

                // At the end of each line, we add an extra space, so that newlines are treated as whitespace.
                printContainer.print(" ")
            }
        }
        printContainer.scrollTopRatio = 0.0

    }
}