/*
 * Copyright 2018-2021 KMath contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package space.kscience.kmath.functions

import space.kscience.kmath.operations.*
import kotlin.math.max
import kotlin.math.min

/**
 * Polynomial model without fixation on specific context they are applied to.
 *
 * @param coefficients constant is the leftmost coefficient.
 */
public data class Polynomial<C>(
    /**
     * List that collects coefficients of the polynomial. Every monomial `a x^d` is represented as a coefficients
     * `a` placed into the list with index `d`. For example coefficients of polynomial `5 x^2 - 6` can be represented as
     * ```
     * listOf(
     *     -6, // -6 +
     *     0,  // 0 x +
     *     5,  // 5 x^2
     * )
     * ```
     * and also as
     * ```
     * listOf(
     *     -6, // -6 +
     *     0,  // 0 x +
     *     5,  // 5 x^2
     *     0,  // 0 x^3
     *     0,  // 0 x^4
     * )
     * ```
     * It is recommended not to put extra zeros at end of the list (as for `0x^3` and `0x^4` in the example), but is not
     * prohibited.
     */
    public val coefficients: List<C>
) : AbstractPolynomial<C> {
    override fun toString(): String = "Polynomial$coefficients"
}

/**
 * Returns a [Polynomial] instance with given [coefficients]. The collection of coefficients will be reversed if
 * [reverse] parameter is true.
 */
@Suppress("FunctionName")
public fun <C> Polynomial(coefficients: List<C>, reverse: Boolean = false): Polynomial<C> =
    Polynomial(with(coefficients) { if (reverse) reversed() else this })

/**
 * Returns a [Polynomial] instance with given [coefficients]. The collection of coefficients will be reversed if
 * [reverse] parameter is true.
 */
@Suppress("FunctionName")
public fun <C> Polynomial(vararg coefficients: C, reverse: Boolean = false): Polynomial<C> =
    Polynomial(with(coefficients) { if (reverse) reversed() else toList() })

public fun <C> C.asPolynomial() : Polynomial<C> = Polynomial(listOf(this))

/**
 * Space of univariate polynomials constructed over ring.
 *
 * @param C the type of constants. Polynomials have them as a coefficients in their terms.
 * @param A type of underlying ring of constants. It's [Ring] of [C].
 * @param ring underlying ring of constants of type [A].
 */
public open class PolynomialSpace<C, A : Ring<C>>(
    public override val ring: A,
) : AbstractPolynomialSpaceOverRing<C, Polynomial<C>, A> {
    /**
     * Returns sum of the polynomial and the integer represented as polynomial.
     *
     * The operation is equivalent to adding [other] copies of unit polynomial to [this].
     */
    public override operator fun Polynomial<C>.plus(other: Int): Polynomial<C> =
        if (other == 0) this
        else
            Polynomial(
                coefficients
                    .toMutableList()
                    .apply {
                        val result = getOrElse(0) { constantZero } + other
                        val isResultZero = result.isZero()

                        when {
                            size == 0 && !isResultZero -> add(result)
                            size > 1 || !isResultZero -> this[0] = result
                            else -> clear()
                        }
                    }
            )
    /**
     * Returns difference between the polynomial and the integer represented as polynomial.
     *
     * The operation is equivalent to subtraction [other] copies of unit polynomial from [this].
     */
    public override operator fun Polynomial<C>.minus(other: Int): Polynomial<C> =
        if (other == 0) this
        else
            Polynomial(
                coefficients
                    .toMutableList()
                    .apply {
                        val result = getOrElse(0) { constantZero } - other
                        val isResultZero = result.isZero()

                        when {
                            size == 0 && !isResultZero -> add(result)
                            size > 1 || !isResultZero -> this[0] = result
                            else -> clear()
                        }
                    }
            )
    /**
     * Returns product of the polynomial and the integer represented as polynomial.
     *
     * The operation is equivalent to sum of [other] copies of [this].
     */
    public override operator fun Polynomial<C>.times(other: Int): Polynomial<C> =
        if (other == 0) zero
        else Polynomial(
            coefficients
                .subList(0, degree + 1)
                .map { it * other }
        )

    /**
     * Returns sum of the integer represented as polynomial and the polynomial.
     *
     * The operation is equivalent to adding [this] copies of unit polynomial to [other].
     */
    public override operator fun Int.plus(other: Polynomial<C>): Polynomial<C> =
        if (this == 0) other
        else
            Polynomial(
                other.coefficients
                    .toMutableList()
                    .apply {
                        val result = this@plus + getOrElse(0) { constantZero }
                        val isResultZero = result.isZero()

                        when {
                            size == 0 && !isResultZero -> add(result)
                            size > 1 || !isResultZero -> this[0] = result
                            else -> clear()
                        }
                    }
            )
    /**
     * Returns difference between the integer represented as polynomial and the polynomial.
     *
     * The operation is equivalent to subtraction [this] copies of unit polynomial from [other].
     */
    public override operator fun Int.minus(other: Polynomial<C>): Polynomial<C> =
        if (this == 0) other
        else
            Polynomial(
                other.coefficients
                    .toMutableList()
                    .apply {
                        forEachIndexed { index, c -> if (index != 0) this[index] = -c }

                        val result = this@minus - getOrElse(0) { constantZero }
                        val isResultZero = result.isZero()

                        when {
                            size == 0 && !isResultZero -> add(result)
                            size > 1 || !isResultZero -> this[0] = result
                            else -> clear()
                        }
                    }
            )
    /**
     * Returns product of the integer represented as polynomial and the polynomial.
     *
     * The operation is equivalent to sum of [this] copies of [other].
     */
    public override operator fun Int.times(other: Polynomial<C>): Polynomial<C> =
        if (this == 0) zero
        else Polynomial(
            other.coefficients
                .subList(0, other.degree + 1)
                .map { it * this }
        )

    /**
     * Returns sum of the constant represented as polynomial and the polynomial.
     */
    public override operator fun C.plus(other: Polynomial<C>): Polynomial<C> =
        if (this.isZero()) other
        else with(other.coefficients) {
            if (isEmpty()) Polynomial(listOf(this@plus))
            else Polynomial(
                toMutableList()
                    .apply {
                        val result = if (size == 0) this@plus else this@plus + get(0)
                        val isResultZero = result.isZero()

                        when {
                            size == 0 && !isResultZero -> add(result)
                            size > 1 || !isResultZero -> this[0] = result
                            else -> clear()
                        }
                    }
            )
        }
    /**
     * Returns difference between the constant represented as polynomial and the polynomial.
     */
    public override operator fun C.minus(other: Polynomial<C>): Polynomial<C> =
        if (this.isZero()) other
        else with(other.coefficients) {
            if (isEmpty()) Polynomial(listOf(-this@minus))
            else Polynomial(
                toMutableList()
                    .apply {
                        forEachIndexed { index, c -> if (index != 0) this[index] = -c }

                        val result = if (size == 0) this@minus else this@minus - get(0)
                        val isResultZero = result.isZero()

                        when {
                            size == 0 && !isResultZero -> add(result)
                            size > 1 || !isResultZero -> this[0] = result
                            else -> clear()
                        }
                    }
            )
        }
    /**
     * Returns product of the constant represented as polynomial and the polynomial.
     */
    public override operator fun C.times(other: Polynomial<C>): Polynomial<C> =
        if (this.isZero()) other
        else Polynomial(
            other.coefficients
                .subList(0, other.degree + 1)
                .map { it * this }
        )

    /**
     * Returns sum of the constant represented as polynomial and the polynomial.
     */
    public override operator fun Polynomial<C>.plus(other: C): Polynomial<C> =
        if (other.isZero()) this
        else with(coefficients) {
            if (isEmpty()) Polynomial(listOf(other))
            else Polynomial(
                toMutableList()
                    .apply {
                        val result = if (size == 0) other else get(0) + other
                        val isResultZero = result.isZero()

                        when {
                            size == 0 && !isResultZero -> add(result)
                            size > 1 || !isResultZero -> this[0] = result
                            else -> clear()
                        }
                    }
            )
        }
    /**
     * Returns difference between the constant represented as polynomial and the polynomial.
     */
    public override operator fun Polynomial<C>.minus(other: C): Polynomial<C> =
        if (other.isZero()) this
        else with(coefficients) {
            if (isEmpty()) Polynomial(listOf(other))
            else Polynomial(
                toMutableList()
                    .apply {
                        val result = if (size == 0) other else get(0) - other
                        val isResultZero = result.isZero()

                        when {
                            size == 0 && !isResultZero -> add(result)
                            size > 1 || !isResultZero -> this[0] = result
                            else -> clear()
                        }
                    }
            )
        }
    /**
     * Returns product of the constant represented as polynomial and the polynomial.
     */
    public override operator fun Polynomial<C>.times(other: C): Polynomial<C> =
        if (other.isZero()) this
        else Polynomial(
            coefficients
                .subList(0, degree + 1)
                .map { it * other }
        )

    /**
     * Returns negation of the polynomial.
     */
    public override operator fun Polynomial<C>.unaryMinus(): Polynomial<C> =
        Polynomial(coefficients.map { -it })
    /**
     * Returns sum of the polynomials.
     */
    public override operator fun Polynomial<C>.plus(other: Polynomial<C>): Polynomial<C> =
        Polynomial(
            (0..max(degree, other.degree))
                .map {
                    when {
                        it > degree -> other.coefficients[it]
                        it > other.degree -> coefficients[it]
                        else -> coefficients[it] + other.coefficients[it]
                    }
                }
                .ifEmpty { listOf(constantZero) }
        )
    /**
     * Returns difference of the polynomials.
     */
    public override operator fun Polynomial<C>.minus(other: Polynomial<C>): Polynomial<C> =
        Polynomial(
            (0..max(degree, other.degree))
                .map {
                    when {
                        it > degree -> -other.coefficients[it]
                        it > other.degree -> coefficients[it]
                        else -> coefficients[it] - other.coefficients[it]
                    }
                }
                .ifEmpty { listOf(constantZero) }
        )
    /**
     * Returns product of the polynomials.
     */
    public override operator fun Polynomial<C>.times(other: Polynomial<C>): Polynomial<C> {
        val thisDegree = degree
        val otherDegree = other.degree
        return when {
            thisDegree == -1 -> zero
            otherDegree == -1 -> zero
            else ->
                Polynomial(
                    (0..(thisDegree + otherDegree))
                        .map { d ->
                            (max(0, d - otherDegree)..min(thisDegree, d))
                                .map { coefficients[it] * other.coefficients[d - it] }
                                .reduce { acc, rational -> acc + rational }
                        }
                        .run { subList(0, indexOfLast { it.isNotZero() } + 1) }
                )
        }
    }

    /**
     * Check if the instant is zero polynomial.
     */
    public override fun Polynomial<C>.isZero(): Boolean = coefficients.all { it.isZero() }
    /**
     * Check if the instant is unit polynomial.
     */
    public override fun Polynomial<C>.isOne(): Boolean =
        with(coefficients) {
            isNotEmpty() && withIndex().any { (index, c) -> if (index == 0) c.isOne() else c.isZero() }
        }
    /**
     * Check if the instant is minus unit polynomial.
     */
    public override fun Polynomial<C>.isMinusOne(): Boolean =
        with(coefficients) {
            isNotEmpty() && withIndex().any { (index, c) -> if (index == 0) c.isMinusOne() else c.isZero() }
        }

    /**
     * Instance of zero polynomial (zero of the polynomial ring).
     */
    override val zero: Polynomial<C> = Polynomial(emptyList())
    /**
     * Instance of unit constant (unit of the underlying ring).
     */
    override val one: Polynomial<C> = Polynomial(listOf(constantZero))

    /**
     * Checks equality of the polynomials.
     */
    public override infix fun Polynomial<C>.equalsTo(other: Polynomial<C>): Boolean =
        when {
            this === other -> true
            else -> {
                if (this.degree == other.degree)
                    (0..degree).all { coefficients[it] == other.coefficients[it] }
                else false
            }
        }

    /**
     * Degree of the polynomial, [see also](https://en.wikipedia.org/wiki/Degree_of_a_polynomial). If the polynomial is
     * zero, degree is -1.
     */
    public override val Polynomial<C>.degree: Int get() = coefficients.indexOfLast { it != constantZero }

    /**
     * If polynomial is a constant polynomial represents and returns it as constant.
     * Otherwise, (when the polynomial is not constant polynomial) returns `null`.
     */
    public override fun Polynomial<C>.asConstantOrNull(): C? =
        with(coefficients) {
            when {
                isEmpty() -> constantZero
                degree > 0 -> null
                else -> first()
            }
        }

    @Suppress("NOTHING_TO_INLINE")
    public inline fun Polynomial<C>.substitute(argument: C): C = this.substitute(ring, argument)
    @Suppress("NOTHING_TO_INLINE")
    public inline fun Polynomial<C>.substitute(argument: Polynomial<C>): Polynomial<C> = this.substitute(ring, argument)

    @Suppress("NOTHING_TO_INLINE")
    public inline fun Polynomial<C>.asFunction(): (C) -> C = { this.substitute(ring, it) }
    @Suppress("NOTHING_TO_INLINE")
    public inline fun Polynomial<C>.asFunctionOnConstants(): (C) -> C = { this.substitute(ring, it) }
    @Suppress("NOTHING_TO_INLINE")
    public inline fun Polynomial<C>.asFunctionOnPolynomials(): (Polynomial<C>) -> Polynomial<C> = { this.substitute(ring, it) }

    /**
     * Evaluates the polynomial for the given value [arg].
     */
    @Suppress("NOTHING_TO_INLINE")
    public inline operator fun Polynomial<C>.invoke(argument: C): C = this.substitute(ring, argument)
    @Suppress("NOTHING_TO_INLINE")
    public inline operator fun Polynomial<C>.invoke(argument: Polynomial<C>): Polynomial<C> = this.substitute(ring, argument)
}

/**
 * Space of polynomials constructed over ring.
 *
 * @param C the type of constants. Polynomials have them as a coefficients in their terms.
 * @param A type of underlying ring of constants. It's [Ring] of [C].
 * @param ring underlying ring of constants of type [A].
 */
public class ScalablePolynomialSpace<C, A>(
    ring: A,
) : PolynomialSpace<C, A>(ring), ScaleOperations<Polynomial<C>> where A : Ring<C>, A : ScaleOperations<C> {

    override fun scale(a: Polynomial<C>, value: Double): Polynomial<C> =
        ring { Polynomial(List(a.coefficients.size) { index -> a.coefficients[index] * value }) }

}