/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you 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 org.apache.jorphan.gui

import org.apiguardian.api.API
import java.awt.Container
import java.awt.FlowLayout
import java.awt.event.ActionEvent
import javax.swing.AbstractAction
import javax.swing.Box
import javax.swing.JCheckBox
import javax.swing.JComboBox
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JPopupMenu
import javax.swing.SwingUtilities
import javax.swing.event.ChangeEvent

/**
 * A Checkbox that can be converted to an editable field and back.
 * @since 5.6
 */
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public open class JEditableCheckBox(
    label: String,
    private val configuration: Configuration
) : JPanel() {
    public companion object {
        public const val CHECKBOX_CARD: String = "checkbox"
        public const val EDITABLE_CARD: String = "editable"
        public const val VALUE_PROPERTY: String = "value"
    }

    /**
     * The representation of the state.
     */
    public sealed interface Value {
        public companion object {
            @JvmStatic
            public fun of(value: kotlin.Boolean): Boolean =
                if (value) Boolean.TRUE else Boolean.FALSE
        }

        /**
         * The value is a checkbox.
         */
        public sealed interface Boolean : Value {
            public val value: kotlin.Boolean

            public object TRUE : Boolean {
                override val value: kotlin.Boolean
                    get() = true
            }

            public object FALSE : Boolean {
                override val value: kotlin.Boolean
                    get() = false
            }
        }

        /**
         * The value is a free text.
         */
        public data class Text(val value: String) : Value
    }

    /**
     * Supplies the parameters to [JEditableCheckBox].
     */
    public data class Configuration(
        /** Menu item title to "start editing" the checkbox value. */
        val startEditing: String = "Use Expression",
        /** The title to be used for "true" value in the checkbox. */
        val trueValue: String = "true",
        /** The title to be used for "false" value in the checkbox. */
        val falseValue: String = "false",
        /** Extra values to be added for the combobox. */
        val extraValues: List<String> = listOf(),
    )

    private val cards = CardLayoutWithSizeOfCurrentVisibleElement()

    private val useExpressionAction = object : AbstractAction(configuration.startEditing) {
        override fun actionPerformed(e: ActionEvent?) {
            cards.next(this@JEditableCheckBox)
            comboBox.requestFocusInWindow()
            fireValueChanged()
        }
    }

    private val checkbox: JCheckBox = JCheckBox(label).apply {
        val cb = this
        componentPopupMenu = JPopupMenu().apply {
            add(useExpressionAction)
        }
        addItemListener {
            fireValueChanged()
        }
    }

    private val comboBox: JComboBox<String> = JComboBox<String>().apply {
        isEditable = true
        configuration.extraValues.forEach {
            addItem(it)
        }
        addItem(configuration.trueValue)
        addItem(configuration.falseValue)
        addActionListener {
            val jComboBox = it.source as JComboBox<*>
            SwingUtilities.invokeLater {
                if (jComboBox.isPopupVisible) {
                    fireValueChanged()
                    return@invokeLater
                }
                when (val value = jComboBox.selectedItem as String) {
                    configuration.trueValue, configuration.falseValue -> {
                        checkbox.isSelected = value == configuration.trueValue
                        cards.show(this@JEditableCheckBox, CHECKBOX_CARD)
                        checkbox.requestFocusInWindow()
                        fireValueChanged()
                    }
                }
            }
        }
        // TODO: trigger value changed when the text is changed
    }

    private val textFieldLabel = JLabel(label).apply {
        labelFor = comboBox
    }

    @Transient
    private var changeEvent: ChangeEvent? = null

    init {
        layout = cards
        add(
            // A dummy container ensures popup menu appears on top of the checkbox
            Container().apply {
                layout = FlowLayout(FlowLayout.LEADING, 0, 0)
                add(checkbox)
            },
            CHECKBOX_CARD
        )
        add(
            Container().apply {
                // FlowLayout adds horizontal gap before the first element, so we set zero gaps
                // and add the gap between the components manually.
                layout = FlowLayout(FlowLayout.LEADING, 0, 0)
                add(comboBox)
                add(Box.createHorizontalStrut(5))
                add(textFieldLabel)
            },
            EDITABLE_CARD
        )
    }

    private var oldValue = value

    override fun setEnabled(enabled: Boolean) {
        super.setEnabled(enabled)
        checkbox.isEnabled = enabled
        comboBox.isEnabled = enabled
        useExpressionAction.isEnabled = enabled
    }

    private fun fireValueChanged() {
        val newValue = value
        if (value != oldValue) {
            firePropertyChange(VALUE_PROPERTY, oldValue, newValue)
            oldValue = newValue
        }
    }

    public var value: Value
        get() = when (components.indexOfFirst { it.isVisible }) {
            0 -> if (checkbox.isSelected) Value.Boolean.TRUE else Value.Boolean.FALSE
            else -> Value.Text(comboBox.selectedItem as String)
        }
        set(value) {
            when (value) {
                is Value.Boolean -> {
                    comboBox.selectedItem = ""
                    checkbox.isSelected = value.value
                    cards.show(this, CHECKBOX_CARD)
                }

                is Value.Text -> {
                    checkbox.isSelected = false
                    comboBox.selectedItem = value.value
                    cards.show(this, EDITABLE_CARD)
                }
            }
            fireValueChanged()
        }

    @get:JvmSynthetic
    public var booleanValue: Boolean
        @Deprecated(message = "write-only property", level = DeprecationLevel.HIDDEN)
        get() = TODO()
        set(value) {
            this.value = Value.of(value)
        }

    @get:JvmSynthetic
    public var stringValue: String
        @Deprecated(message = "write-only property", level = DeprecationLevel.HIDDEN)
        get() = TODO()
        set(value) {
            this.value = Value.Text(value)
        }

    public fun makeSmall() {
        JFactory.small(checkbox)
        // We do not make combobox small as the expression migh be hard to read
    }
}
