/*
 * Copyright (C) 2024 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 com.android.settings.connecteddevice.display

import com.android.settings.R

import android.content.Context
import android.graphics.Point
import android.graphics.PointF
import android.graphics.RectF

import androidx.preference.Preference

import java.util.Locale

import kotlin.math.max
import kotlin.math.min

/**
 * Contains the parameters needed for transforming global display coordinates to and from topology
 * pane coordinates. This is necessary for implementing an interactive display topology pane. The
 * pane allows dragging and dropping display blocks into place to define the topology. Conversion to
 * pane coordinates is necessary when rendering the original topology. Conversion in the other
 * direction, to display coordinates, is necessary for resolve a drag position to display space.
 *
 * The topology pane coordinates are integral and represent the relative position from the upper-
 * left corner of the pane. It uses a scale optimized for showing all displays with minimal or no
 * scrolling. The display coordinates are floating point and the origin can be in any position. In
 * practice the origin will be the upper-left coordinate of the primary display.
 */
class TopologyScale(paneWidth : Int, displaysPos : Collection<RectF>) {
    /** Scale of block sizes to real-world display sizes. Should be less than 1. */
    val blockRatio : Float

    /** Height of topology pane needed to allow all display blocks to appear with some padding. */
    val paneHeight : Int

    /** Pane's X view coordinate that corresponds with topology's X=0 coordinate. */
    val originPaneX : Int

    /** Pane's Y view coordinate that corresponds with topology's Y=0 coordinate. */
    val originPaneY : Int

    init {
        val displayBounds = RectF(
                Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE)
        var smallestDisplayDim = Float.MAX_VALUE
        var biggestDisplayHeight = Float.MIN_VALUE

        // displayBounds is the smallest rect encompassing all displays, in display space.
        // smallestDisplayDim is the size of the smallest display edge, in display space.
        for (pos in displaysPos) {
            displayBounds.union(pos)
            smallestDisplayDim = minOf(smallestDisplayDim, pos.height(), pos.width())
            biggestDisplayHeight = max(biggestDisplayHeight, pos.height())
        }

        // Set height according to the width and the aspect ratio of the display bounds.
        // 0.05 is a reasonable limit to the size of display blocks. It appears to match the
        // ratio used in the ChromeOS topology editor. It prevents blocks from being too large,
        // which would make dragging and dropping awkward.
        val rawBlockRatio = min(0.05, paneWidth.toDouble() * 0.6 / displayBounds.width())

        // If the `ratio` is set too low because one of the displays will have an edge less than
        // 48dp long, increase it such that the smallest edge is that long. This may override the
        // 0.05 limit since it is more important than it.
        blockRatio = max(48.0 / smallestDisplayDim, rawBlockRatio).toFloat()

        // Essentially, we just set the pane height based on the pre-determined pane width and the
        // aspect ratio of the display bounds. But we may need to increase it slightly to achieve
        // 20% padding above and below the display bounds - this is where the 0.6 comes from.
        val rawPaneHeight = max(
                paneWidth.toDouble() / displayBounds.width() * displayBounds.height(),
                displayBounds.height() * blockRatio / 0.6)

        // It is easy for the aspect ratio to result in an excessively tall pane, since the width is
        // pre-determined and may be considerably wider than necessary. So we prevent the height
        // from growing too large here, by limiting vertical padding to the size of the tallest
        // display. This improves results for very tall display bounds.
        paneHeight = min(
                rawPaneHeight.toInt(),
                (blockRatio * (displayBounds.height() + biggestDisplayHeight * 2f)).toInt())

        // Set originPaneXY (the location of 0,0 in display space in the pane's coordinate system)
        // such that the display bounds rect is centered in the pane.
        // It is unlikely that either of these coordinates will be negative since blockRatio has
        // been chosen to allow 20% padding around each side of the display blocks. However, the
        // a11y requirement applied above (48.0 / smallestDisplayDim) may cause the blocks to not
        // fit. This should be rare in practice, and can be worked around by moving the settings UI
        // to a larger display.
        val blockMostLeft = (paneWidth - displayBounds.width() * blockRatio) / 2
        val blockMostTop = (paneHeight - displayBounds.height() * blockRatio) / 2

        originPaneX = (blockMostLeft - displayBounds.left * blockRatio).toInt()
        originPaneY = (blockMostTop - displayBounds.top * blockRatio).toInt()
    }

    /** Transforms coordinates in view pane space to display space. */
    fun paneToDisplayCoor(panePos : Point) : PointF {
        return PointF(
                (panePos.x - originPaneX).toFloat() / blockRatio,
                (panePos.y - originPaneY).toFloat() / blockRatio)
    }

    /** Transforms coordinates in display space to view pane space. */
    fun displayToPaneCoor(displayPos : PointF) : Point {
        return Point(
                (displayPos.x * blockRatio).toInt() + originPaneX,
                (displayPos.y * blockRatio).toInt() + originPaneY)
    }

    override fun toString() : String {
        return String.format(
                Locale.ROOT,
                "{TopoScale blockRatio=%f originPaneXY=%d,%d paneHeight=%d}",
                blockRatio, originPaneX, originPaneY, paneHeight)
    }
}

const val PREFERENCE_KEY = "display_topology_preference"

/**
 * DisplayTopologyPreference allows the user to change the display topology
 * when there is one or more extended display attached.
 */
class DisplayTopologyPreference(context : Context) : Preference(context) {
    init {
        layoutResource = R.layout.display_topology_preference

        // Prevent highlight when hovering with mouse.
        isSelectable = false

        key = PREFERENCE_KEY
    }
}
