package icurves.diagram

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import icurves.description.*
import icurves.diagram.curve.CircleCurve
import icurves.util.Bug
import icurves.util.Log
import icurves.util.Logable
import javafx.geometry.Point2D
import javafx.geometry.Rectangle2D
import java.util.*

/**
 * An Euler diagram, d = (C, l).
 *
 * @author Almas Baimagambetov (almaslvl@gmail.com)
 */
@JsonIgnoreProperties("zones", "shadedZones", "outsideZone")
class EulerDiagram(val originalDescription: Description,
                   val actualDescription: Description,
                   @JsonProperty("curves") curvesInternal: Set<Curve>) : Logable {

    //val curves: SortedSet<Curve> = Collections.unmodifiableSortedSet(curvesInternal.toSortedSet())
    val curves = Collections.unmodifiableSet(curvesInternal)

    /**
     * All zones of this Euler diagram, including shaded zones.
     * Does not include the outside zone.
     */
    val zones = actualDescription.abstractZones.minus(azEmpty).map { Zone(it, curves) }.toSet()

    val shadedZones = zones.filter { !originalDescription.includesZone(it.abstractZone) }

    val outsideZone = Zone(azEmpty, curves)

    fun numCircles() = curves.count { it is CircleCurve }

    fun copyWithNewLabels(map: Map<String, String>): EulerDiagram {
        return EulerDiagram(
                originalDescription.copyWithNewLabels(map),
                actualDescription.copyWithNewLabels(map),
                curves.map { it.copyWithNewLabel(map[it.label]!!) }.toSet()
        )
    }

    fun embedIntoZone(az: AbstractZone, diagram: EulerDiagram): EulerDiagram {
        Log.d("Embedding into $az", diagram)

        val zone = zones.find { it.abstractZone == az } ?: throw Bug("No zone $az found in $diagram")

        val minRadius = zone.shortestDistanceToOtherZone(zone.center)

        val bbox = diagram.bbox()
        val diagramCenter = Point2D(bbox.minX + bbox.maxX / 2, bbox.minY + bbox.maxY / 2)

        val actualRadius = maxOf(bbox.width, bbox.height)

        val scaleRatio = minRadius / actualRadius

        val scaledCurves = diagram.curves.map { it.scale(scaleRatio, diagramCenter) }

        val newCurves = curves.plus(scaledCurves.map { it.translate(zone.center.subtract(diagramCenter)) })

        return EulerDiagram(originalDescription + diagram.originalDescription, actualDescription + diagram.actualDescription, newCurves)
    }

    fun bbox(): Rectangle2D {
        val polygons = curves.map { it.getPolygon() }
        val vertices = polygons.flatMap { it.vertices() }

        val minX = vertices.minBy { it.x() }!!.x()
        val minY = vertices.minBy { it.y() }!!.y()
        val maxX = vertices.maxBy { it.x() }!!.x()
        val maxY = vertices.maxBy { it.y() }!!.y()

        return Rectangle2D(minX, minY, maxX - minX, maxY - minY)
    }

    fun size(): Double {
        val bbox = bbox()
        return maxOf(bbox.width, bbox.height)
    }

    fun center(): Point2D {
        val bbox = bbox()
        return Point2D(bbox.minX + bbox.maxX / 2, bbox.minY + bbox.maxY / 2)
    }

    fun translate(vector: Point2D): EulerDiagram {
        return EulerDiagram(originalDescription, actualDescription, curves.map { it.translate(vector) }.toSet())
    }

    fun scale(pivot: Point2D, ratio: Double): EulerDiagram {
        return EulerDiagram(originalDescription, actualDescription, curves.map { it.scale(ratio, pivot) }.toSet())
    }

    override fun toLog(): String {
        return "ED[o=$originalDescription, actual=$actualDescription, curves=$curves, zones=$zones]"
    }

    override fun hashCode(): Int {
        return Objects.hash(originalDescription, actualDescription, curves, zones, shadedZones, outsideZone)
    }

    override fun equals(other: Any?): Boolean {
        if (other !is EulerDiagram)
            return false

        return originalDescription == other.originalDescription &&
                actualDescription == other.actualDescription &&
                curves == other.curves &&
                zones == other.zones &&
                shadedZones == other.shadedZones &&
                outsideZone == other.outsideZone
    }

    override fun toString(): String {
        return "EulerDiagram[D=$actualDescription]"
    }
}

// Logic DSL

fun C(d: EulerDiagram) = d.curves

fun Z(d: EulerDiagram) = d.zones

fun combine(original: Description, diagrams: List<EulerDiagram>): EulerDiagram {
    val actual = diagrams.map { it.actualDescription.getInformalDescription() }.joinToString(" ")

    println(actual)
    println(original)

    var index = 0

    return EulerDiagram(original, D(actual), diagrams.flatMap {
        val translate = Point2D((index % 3) * 7000.0, (index++ / 3) * 7000.0)

        it.curves.map { it.translate(translate) }
    }.toSet())
}

fun combineNoTranslate(original: Description, diagrams: List<EulerDiagram>): EulerDiagram {
    val actual = diagrams.map { it.actualDescription.getInformalDescription() }.joinToString(" ")

    println(actual)
    println(original)

    var index = 0

    return EulerDiagram(original, D(actual), diagrams.flatMap {
        val translate = Point2D.ZERO

        it.curves.map { it.translate(translate) }
    }.toSet())
}