/*
 * Use of this source code is governed by the MIT license that can be
 * found in the LICENSE file.
 */

package org.rust.lang.core.crate.impl

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.vfs.VirtualFile
import org.rust.cargo.CfgOptions
import org.rust.cargo.project.model.CargoProject
import org.rust.cargo.project.workspace.CargoWorkspace
import org.rust.cargo.project.workspace.CargoWorkspaceData
import org.rust.cargo.project.workspace.FeatureState
import org.rust.cargo.project.workspace.PackageOrigin
import org.rust.lang.core.crate.Crate
import org.rust.lang.core.crate.CratePersistentId
import org.rust.lang.core.psi.RsFile
import org.rust.lang.core.psi.rustFile
import org.rust.openapiext.fileId
import org.rust.openapiext.toPsiFile

/**
 * [equals]/[hashCode] are based on [cargoTarget] and [id] fields
 * because these fields equality guarantee other fields equality
 */
class CargoBasedCrate(
    override var cargoProject: CargoProject,
    override var cargoTarget: CargoWorkspace.Target,
    override val dependencies: Collection<Crate.Dependency>,
    override val flatDependencies: LinkedHashSet<Crate>,
    override var procMacroArtifact: CargoWorkspaceData.ProcMacroArtifact? = null,
) : UserDataHolderBase(), Crate {
    override val reverseDependencies = mutableListOf<CargoBasedCrate>()
    override var features: Map<String, FeatureState> = cargoTarget.pkg.featureState

    // These properties are fields (not just delegates to `cargoTarget`) because [Crate] must be immutable
    override val rootModFile: VirtualFile? = cargoTarget.crateRoot
    override val id: CratePersistentId? = rootModFile?.fileId

    /** See docs for [org.rust.lang.core.crate.CrateGraphService] */
    var cyclicDevDeps: List<Crate.Dependency> = emptyList()

    override val dependenciesWithCyclic: Collection<Crate.Dependency>
        get() = dependencies + cyclicDevDeps

    init {
        for (dependency in dependencies) {
            (dependency.crate as CargoBasedCrate).reverseDependencies += this
        }
    }

    override val cargoWorkspace: CargoWorkspace get() = cargoTarget.pkg.workspace
    override val kind: CargoWorkspace.TargetKind get() = cargoTarget.kind

    override val cfgOptions: CfgOptions get() = cargoTarget.cfgOptions

    override val evaluateUnknownCfgToFalse: Boolean
        get() = origin == PackageOrigin.STDLIB || // TODO: think about it more precisely
            cargoTarget.pkg.cfgOptions != null || // `true` if there is `build.rs` and it is evaluated
            !cargoTarget.pkg.hasCustomBuildScript // `true` if there isn't `build.rs`

    override val env: Map<String, String> get() = cargoTarget.pkg.env
    override val outDir: VirtualFile? get() = cargoTarget.pkg.outDir

    override val hasCyclicDevDependencies: Boolean get() = cyclicDevDeps.isNotEmpty()

    override val rootMod: RsFile? get() = rootModFile?.toPsiFile(project)?.rustFile
    override val project: Project get() = cargoProject.project

    override val origin: PackageOrigin get() = cargoTarget.pkg.origin
    override val edition: CargoWorkspace.Edition get() = cargoTarget.edition
    override val areDoctestsEnabled: Boolean get() = cargoTarget.doctest && cargoTarget.isDoctestable
    override val presentableName: String get() = cargoTarget.name
    override val normName: String get() = cargoTarget.normName

    override fun toString(): String = "${cargoTarget.name}(${kind.name})"

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as CargoBasedCrate

        if (cargoTarget != other.cargoTarget) return false
        if (id != other.id) return false

        return true
    }

    override fun hashCode(): Int {
        var result = cargoTarget.hashCode()
        result = 31 * result + (id ?: 0)
        return result
    }
}

// See https://github.com/rust-lang/cargo/blob/5a0c31d81/src/cargo/core/manifest.rs#L775
private val CargoWorkspace.Target.isDoctestable: Boolean
    get() {
        val kind = kind as? CargoWorkspace.TargetKind.Lib ?: return false
        return CargoWorkspace.LibKind.LIB in kind.kinds ||
            CargoWorkspace.LibKind.RLIB in kind.kinds ||
            CargoWorkspace.LibKind.PROC_MACRO in kind.kinds
    }
