/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.compose.ui.text.android

import android.graphics.text.LineBreakConfig
import android.os.Build
import android.text.Layout.Alignment
import android.text.StaticLayout
import android.text.StaticLayout.Builder
import android.text.TextDirectionHeuristic
import android.text.TextPaint
import android.text.TextUtils.TruncateAt
import android.util.Log
import androidx.annotation.DoNotInline
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.annotation.RequiresApi
import androidx.compose.ui.text.android.LayoutCompat.BreakStrategy
import androidx.compose.ui.text.android.LayoutCompat.HyphenationFrequency
import androidx.compose.ui.text.android.LayoutCompat.JustificationMode
import androidx.compose.ui.text.android.LayoutCompat.LineBreakStyle
import androidx.compose.ui.text.android.LayoutCompat.LineBreakWordStyle
import androidx.core.os.BuildCompat
import java.lang.reflect.Constructor
import java.lang.reflect.InvocationTargetException

private const val TAG = "StaticLayoutFactory"

@OptIn(InternalPlatformTextApi::class)
internal object StaticLayoutFactory {

    private val delegate: StaticLayoutFactoryImpl = if (Build.VERSION.SDK_INT >= 23) {
        StaticLayoutFactory23()
    } else {
        StaticLayoutFactoryDefault()
    }

    /**
     * Builder class for StaticLayout.
     */
    fun create(
        text: CharSequence,
        start: Int = 0,
        end: Int = text.length,
        paint: TextPaint,
        width: Int,
        textDir: TextDirectionHeuristic = LayoutCompat.DEFAULT_TEXT_DIRECTION_HEURISTIC,
        alignment: Alignment = LayoutCompat.DEFAULT_LAYOUT_ALIGNMENT,
        @IntRange(from = 0)
        maxLines: Int = LayoutCompat.DEFAULT_MAX_LINES,
        ellipsize: TruncateAt? = null,
        @IntRange(from = 0)
        ellipsizedWidth: Int = width,
        @FloatRange(from = 0.0)
        lineSpacingMultiplier: Float = LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER,
        lineSpacingExtra: Float = LayoutCompat.DEFAULT_LINESPACING_EXTRA,
        @JustificationMode
        justificationMode: Int = LayoutCompat.DEFAULT_JUSTIFICATION_MODE,
        includePadding: Boolean = LayoutCompat.DEFAULT_INCLUDE_PADDING,
        useFallbackLineSpacing: Boolean = LayoutCompat.DEFAULT_FALLBACK_LINE_SPACING,
        @BreakStrategy
        breakStrategy: Int = LayoutCompat.DEFAULT_BREAK_STRATEGY,
        @LineBreakStyle
        lineBreakStyle: Int = LayoutCompat.DEFAULT_LINE_BREAK_STYLE,
        @LineBreakWordStyle
        lineBreakWordStyle: Int = LayoutCompat.DEFAULT_LINE_BREAK_WORD_STYLE,
        @HyphenationFrequency
        hyphenationFrequency: Int = LayoutCompat.DEFAULT_HYPHENATION_FREQUENCY,
        leftIndents: IntArray? = null,
        rightIndents: IntArray? = null
    ): StaticLayout {
        return delegate.create(
            StaticLayoutParams(
                text = text,
                start = start,
                end = end,
                paint = paint,
                width = width,
                textDir = textDir,
                alignment = alignment,
                maxLines = maxLines,
                ellipsize = ellipsize,
                ellipsizedWidth = ellipsizedWidth,
                lineSpacingMultiplier = lineSpacingMultiplier,
                lineSpacingExtra = lineSpacingExtra,
                justificationMode = justificationMode,
                includePadding = includePadding,
                useFallbackLineSpacing = useFallbackLineSpacing,
                breakStrategy = breakStrategy,
                lineBreakStyle = lineBreakStyle,
                lineBreakWordStyle = lineBreakWordStyle,
                hyphenationFrequency = hyphenationFrequency,
                leftIndents = leftIndents,
                rightIndents = rightIndents
            )
        )
    }

    /**
     * Returns whether fallbackLineSpacing is enabled for the given layout.
     *
     * @param layout StaticLayout instance
     * @param useFallbackLineSpacing fallbackLineSpacing configuration passed while creating the
     * StaticLayout.
     */
    fun isFallbackLineSpacingEnabled(
        layout: StaticLayout,
        useFallbackLineSpacing: Boolean
    ): Boolean {
        return delegate.isFallbackLineSpacingEnabled(layout, useFallbackLineSpacing)
    }
}

@OptIn(InternalPlatformTextApi::class)
private class StaticLayoutParams constructor(
    val text: CharSequence,
    val start: Int = 0,
    val end: Int,
    val paint: TextPaint,
    val width: Int,
    val textDir: TextDirectionHeuristic,
    val alignment: Alignment,
    val maxLines: Int,
    val ellipsize: TruncateAt?,
    val ellipsizedWidth: Int,
    val lineSpacingMultiplier: Float,
    val lineSpacingExtra: Float,
    val justificationMode: Int,
    val includePadding: Boolean,
    val useFallbackLineSpacing: Boolean,
    val breakStrategy: Int,
    val lineBreakStyle: Int,
    val lineBreakWordStyle: Int,
    val hyphenationFrequency: Int,
    val leftIndents: IntArray?,
    val rightIndents: IntArray?
) {
    init {
        require(start in 0..end)
        require(end in 0..text.length)
        require(maxLines >= 0)
        require(width >= 0)
        require(ellipsizedWidth >= 0)
        require(lineSpacingMultiplier >= 0f)
    }
}

private interface StaticLayoutFactoryImpl {

    @DoNotInline // API level specific, do not inline to prevent ART class verification breakages
    fun create(params: StaticLayoutParams): StaticLayout

    fun isFallbackLineSpacingEnabled(layout: StaticLayout, useFallbackLineSpacing: Boolean): Boolean
}

@RequiresApi(23)
private class StaticLayoutFactory23 : StaticLayoutFactoryImpl {

    @DoNotInline
    override fun create(params: StaticLayoutParams): StaticLayout {
        return Builder.obtain(params.text, params.start, params.end, params.paint, params.width)
            .apply {
                setTextDirection(params.textDir)
                setAlignment(params.alignment)
                setMaxLines(params.maxLines)
                setEllipsize(params.ellipsize)
                setEllipsizedWidth(params.ellipsizedWidth)
                setLineSpacing(params.lineSpacingExtra, params.lineSpacingMultiplier)
                setIncludePad(params.includePadding)
                setBreakStrategy(params.breakStrategy)
                setHyphenationFrequency(params.hyphenationFrequency)
                setIndents(params.leftIndents, params.rightIndents)
                if (Build.VERSION.SDK_INT >= 26) {
                    StaticLayoutFactory26.setJustificationMode(this, params.justificationMode)
                }
                if (Build.VERSION.SDK_INT >= 28) {
                    StaticLayoutFactory28.setUseLineSpacingFromFallbacks(
                        this,
                        params.useFallbackLineSpacing
                    )
                }
                if (Build.VERSION.SDK_INT >= 33) {
                    StaticLayoutFactory33.setLineBreakConfig(
                        this,
                        params.lineBreakStyle,
                        params.lineBreakWordStyle
                    )
                }
            }.build()
    }

    @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
    override fun isFallbackLineSpacingEnabled(
        layout: StaticLayout,
        useFallbackLineSpacing: Boolean
    ): Boolean {
        return if (BuildCompat.isAtLeastT()) {
            StaticLayoutFactory33.isFallbackLineSpacingEnabled(layout)
        } else if (Build.VERSION.SDK_INT >= 28) {
            useFallbackLineSpacing
        } else {
            false
        }
    }
}

@RequiresApi(26)
private object StaticLayoutFactory26 {
    @JvmStatic
    @DoNotInline
    fun setJustificationMode(builder: Builder, justificationMode: Int) {
        builder.setJustificationMode(justificationMode)
    }
}

@RequiresApi(28)
private object StaticLayoutFactory28 {
    @JvmStatic
    @DoNotInline
    fun setUseLineSpacingFromFallbacks(builder: Builder, useFallbackLineSpacing: Boolean) {
        builder.setUseLineSpacingFromFallbacks(useFallbackLineSpacing)
    }
}

@RequiresApi(33)
private object StaticLayoutFactory33 {
    @JvmStatic
    @DoNotInline
    fun isFallbackLineSpacingEnabled(layout: StaticLayout): Boolean {
        return layout.isFallbackLineSpacingEnabled
    }

    @JvmStatic
    @DoNotInline
    fun setLineBreakConfig(builder: Builder, lineBreakStyle: Int, lineBreakWordStyle: Int) {
        val lineBreakConfig =
            LineBreakConfig.Builder()
                .setLineBreakStyle(lineBreakStyle)
                .setLineBreakWordStyle(lineBreakWordStyle)
                .build()
        builder.setLineBreakConfig(lineBreakConfig)
    }
}

private class StaticLayoutFactoryDefault : StaticLayoutFactoryImpl {

    companion object {
        private var isInitialized = false
        private var staticLayoutConstructor: Constructor<StaticLayout>? = null

        private fun getStaticLayoutConstructor(): Constructor<StaticLayout>? {
            if (isInitialized) return staticLayoutConstructor
            isInitialized = true
            try {
                staticLayoutConstructor =
                    StaticLayout::class.java.getConstructor(
                        CharSequence::class.java,
                        Int::class.javaPrimitiveType, /* start */
                        Int::class.javaPrimitiveType, /* end */
                        TextPaint::class.java,
                        Int::class.javaPrimitiveType, /* width */
                        Alignment::class.java,
                        TextDirectionHeuristic::class.java,
                        Float::class.javaPrimitiveType, /* lineSpacingMultiplier */
                        Float::class.javaPrimitiveType, /* lineSpacingExtra */
                        Boolean::class.javaPrimitiveType, /* includePadding */
                        TruncateAt::class.java,
                        Int::class.javaPrimitiveType, /* ellipsizeWidth */
                        Int::class.javaPrimitiveType /* maxLines */
                    )
            } catch (e: NoSuchMethodException) {
                staticLayoutConstructor = null
                Log.e(TAG, "unable to collect necessary constructor.")
            }

            return staticLayoutConstructor
        }
    }

    @DoNotInline
    override fun create(params: StaticLayoutParams): StaticLayout {
        // On API 21 to 23, try to call the StaticLayoutConstructor which supports the
        // textDir and maxLines.
        val result = getStaticLayoutConstructor()?.let {
            try {
                it.newInstance(
                    params.text,
                    params.start,
                    params.end,
                    params.paint,
                    params.width,
                    params.alignment,
                    params.textDir,
                    params.lineSpacingMultiplier,
                    params.lineSpacingExtra,
                    params.includePadding,
                    params.ellipsize,
                    params.ellipsizedWidth,
                    params.maxLines
                )
            } catch (e: IllegalAccessException) {
                staticLayoutConstructor = null
                Log.e(TAG, "unable to call constructor")
                null
            } catch (e: InstantiationException) {
                staticLayoutConstructor = null
                Log.e(TAG, "unable to call constructor")
                null
            } catch (e: InvocationTargetException) {
                staticLayoutConstructor = null
                Log.e(TAG, "unable to call constructor")
                null
            }
        }

        if (result != null) return result

        // On API 21 to 23 where it failed to find StaticLayout.Builder, create with
        // deprecated constructor, textDir and maxLines won't work in this case.
        @Suppress("DEPRECATION")
        return StaticLayout(
            params.text,
            params.start,
            params.end,
            params.paint,
            params.width,
            params.alignment,
            params.lineSpacingMultiplier,
            params.lineSpacingExtra,
            params.includePadding,
            params.ellipsize,
            params.ellipsizedWidth
        )
    }

    override fun isFallbackLineSpacingEnabled(
        layout: StaticLayout,
        useFallbackLineSpacing: Boolean
    ): Boolean {
        return false
    }
}
