module AbstractOperations

export ∂x, ∂y, ∂z, @at, @unary, @binary, @multiary
export ∫dx, ∫dy, ∫dz, ∫∫dxdy, ∫∫dxdz, ∫∫dydz, ∫∫∫dxdydz, ∫dV
export Δx, Δy, Δz, Ax, Ay, Az, volume
export Average, Integral, CumulativeIntegral, KernelFunctionOperation, InterpolatedOperation
export UnaryOperation, Derivative, BinaryOperation, MultiaryOperation, ConditionalOperation
export RegriddedOperation

using Adapt: Adapt, adapt
using Base: @propagate_inbounds
using DocStringExtensions: TYPEDSIGNATURES

using Oceananigans: location
using Oceananigans.Architectures: Architectures, architecture, on_architecture
using Oceananigans.Fields: AbstractField, instantiated_location
using Oceananigans.Grids: Center, Face
using Oceananigans.Operators: interpolation_operator

import Oceananigans.BoundaryConditions: fill_halo_regions!
import Oceananigans.Fields: compute_at!, indices

#####
##### Basic functionality
#####

abstract type AbstractOperation{LX, LY, LZ, G, T} <: AbstractField{LX, LY, LZ, G, T, 3} end

const AF = AbstractField # used in unary_operations.jl, binary_operations.jl, etc

const Location = Union{Face, Center, Nothing}

# We have no halos to fill
@inline fill_halo_regions!(::AbstractOperation, args...; kwargs...) = nothing

Architectures.architecture(a::AbstractOperation) = architecture(a.grid)

# AbstractOperation macros add their associated functions to this list
const operators = Set()

"""
$(TYPEDSIGNATURES)

Return `abstract_operation` relocated to `loc`ation.
"""
at(loc, f) = f # fallback

"""
$(TYPEDSIGNATURES)

Validate that `a` may be an operand of an `AbstractOperation`, returning `a`.

The fallback validates everything. Four-dimensional fields like `FieldTimeSeries`
extend `validate_operand` to throw an error, since `AbstractOperation`s are
three-dimensional and would silently drop the time dimension of their operands.
"""
@inline validate_operand(a) = a

include("grid_validation.jl")
include("grid_metrics.jl")
include("metric_field_reductions.jl")
include("integral_operators.jl")
include("unary_operations.jl")
include("binary_operations.jl")
include("multiary_operations.jl")
include("derivatives.jl")
include("constant_field_abstract_operations.jl")
include("kernel_function_operation.jl")
include("regridded_operation.jl")
include("interpolated_operation.jl")
include("conditional_operations.jl")
include("computed_field.jl")
include("at.jl")
include("broadcasting_abstract_operations.jl")
include("show_abstract_operations.jl")

# Make some operators!

# Some operators:
@unary Base.sqrt Base.sin Base.cos Base.exp Base.tanh Base.abs Base.log10 Base.log Base.tan Base.sinh Base.cosh
@unary Base.:-
@unary Base.:+

@binary Base.:+
@binary Base.:-
@binary Base.:/
@binary Base.:^
@binary Base.:>
@binary Base.:<
@binary Base.:>=
@binary Base.:<=
@binary Base.atan
@binary Base.atand
@binary Base.mod

# Disambiguate Base.<(::Missing, ::Any) and Base.<(::Any, ::Missing)
Base.:<(::AbstractField, ::Missing) = missing
Base.:<(::Missing, ::AbstractField) = missing

@multiary +

# For unknown reasons, the operator definition macros @binary and @multiary fail to work
# properly for :*. We thus manually define :* for fields.
import Base: *

eval(define_binary_operator(:*))
push!(operators, :*)
push!(binary_operators, :*)

eval(define_multiary_operator(:*))
push!(operators, :*)
push!(multiary_operators, :*)

end # module
