/* Copyright 2018 Jochem Raat <jchmrt@riseup.net>
 *
 * This file is a part of Tusky.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with Tusky; if not,
 * see <http://www.gnu.org/licenses>. */

package com.keylesspalace.tusky.util

import android.graphics.Matrix
import com.keylesspalace.tusky.entity.Attachment.Focus

/**
 * Calculates the image matrix needed to maintain the correct cropping for image views based on
 * their focal point.
 *
 * The purpose of this class is to make sure that the focal point information on media
 * attachments are honoured. This class uses the custom matrix option of android ImageView's to
 * customize how the image is cropped into the view.
 *
 * See the explanation of focal points here:
 *    https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
 */
object FocalPointUtil {
    /**
     * Update the given matrix for the given parameters.
     *
     * How it works is using the following steps:
     *   - First we determine if the image is too wide or too tall for the view size. If it is
     *   too wide, we need to crop it horizontally and scale the height to fit the view
     *   exactly. If it is too tall we need to crop vertically and scale the width to fit the
     *   view exactly.
     *   - Then we determine what translation is needed to get the focal point in view. We
     *   prefer to get the focal point at the center of the preview. However if that would
     *   result in some part of the preview being empty, we instead align the image so that it
     *   fills the view, but still the focal point is always in view.
     *
     * @param viewWidth The width of the imageView.
     * @param viewHeight The height of the imageView
     * @param imageWidth The width of the actual image
     * @param imageHeight The height of the actual image
     * @param focus The focal point to focus
     * @param mat The matrix to update, this matrix is reset() and then updated with the new
     * configuration. We reuse the old matrix to prevent unnecessary allocations.
     *
     * @return The matrix which correctly crops the image
     */
    fun updateFocalPointMatrix(
        viewWidth: Float,
        viewHeight: Float,
        imageWidth: Float,
        imageHeight: Float,
        focus: Focus,
        mat: Matrix
    ) {
        // Reset the cached matrix:
        mat.reset()

        // calculate scaling:
        val scale = calculateScaling(viewWidth, viewHeight, imageWidth, imageHeight)
        mat.preScale(scale, scale)

        // calculate offsets:
        var top = 0f
        var left = 0f
        if (isVerticalCrop(viewWidth, viewHeight, imageWidth, imageHeight)) {
            top = focalOffset(viewHeight, imageHeight, scale, focalYToCoordinate(focus.y ?: 0f))
        } else { // horizontal crop
            left = focalOffset(viewWidth, imageWidth, scale, focalXToCoordinate(focus.x ?: 0f))
        }

        mat.postTranslate(left, top)
    }

    /**
     * Calculate the scaling of the image needed to make it fill the screen.
     *
     * The scaling used depends on if we need a vertical of horizontal crop.
     */
    fun calculateScaling(
        viewWidth: Float,
        viewHeight: Float,
        imageWidth: Float,
        imageHeight: Float
    ): Float {
        return if (isVerticalCrop(viewWidth, viewHeight, imageWidth, imageHeight)) {
            viewWidth / imageWidth
        } else { // horizontal crop:
            viewHeight / imageHeight
        }
    }

    /**
     * Return true if we need a vertical crop, false for a horizontal crop.
     */
    fun isVerticalCrop(
        viewWidth: Float,
        viewHeight: Float,
        imageWidth: Float,
        imageHeight: Float
    ): Boolean {
        val viewRatio = viewWidth / viewHeight
        val imageRatio = imageWidth / imageHeight

        return viewRatio > imageRatio
    }

    /**
     * Transform the focal x component to the corresponding coordinate on the image.
     *
     * This means that we go from a representation where the left side of the image is -1 and
     * the right side +1, to a representation with the left side being 0 and the right side
     * being +1.
     */
    fun focalXToCoordinate(x: Float): Float {
        return (x + 1) / 2
    }

    /**
     * Transform the focal y component to the corresponding coordinate on the image.
     *
     * This means that we go from a representation where the bottom side of the image is -1 and
     * the top side +1, to a representation with the top side being 0 and the bottom side
     * being +1.
     */
    fun focalYToCoordinate(y: Float): Float {
        return (-y + 1) / 2
    }

    /**
     * Calculate the relative offset needed to focus on the focal point in one direction.
     *
     * This method works for both the vertical and horizontal crops. It simply calculates
     * what offset to take based on the proportions between the scaled image and the view
     * available. It also makes sure to always fill the bounds of the view completely with
     * the image. So it won't put the very edge of the image in center, because that would
     * leave part of the view empty.
     */
    fun focalOffset(view: Float, image: Float, scale: Float, focal: Float): Float {
        // The fraction of the image that will be in view:
        val inView = view / (scale * image)
        var offset = 0f

        // These values indicate the maximum and minimum focal parameter possible while still
        // keeping the entire view filled with the image:
        val maxFocal = 1 - inView / 2
        val minFocal = inView / 2

        if (focal > maxFocal) {
            offset = -((2 - inView) / 2) * image * scale + view * 0.5f
        } else if (focal > minFocal) {
            offset = -focal * image * scale + view * 0.5f
        }

        return offset
    }
}
