module concreteBatching
using Unitful
import Base: +, *

abstract type ConcreteIngredient end
abstract type Water <: ConcreteIngredient end
abstract type Cement <: ConcreteIngredient end
abstract type Coarse <: ConcreteIngredient end
abstract type Fine <: ConcreteIngredient end
abstract type Air <: ConcreteIngredient end
abstract type Mixture <: ConcreteIngredient end

function Base.show(io::IO, ::Type{T}) where {T<:ConcreteIngredient}
    print(io, string(nameof(T)))
end

struct BatchComponent{T<:ConcreteIngredient}
    volume::Unitful.Volume
    mass::Unitful.Mass
    density::Unitful.Density
    BatchComponent{T}(m::Unitful.Mass,d::Unitful.Density) where {T<:ConcreteIngredient}=new{T}(m/d,m,d)
    BatchComponent{T}(v::Unitful.Volume,d::Unitful.Density) where {T<:ConcreteIngredient}=new{T}(v,v*d,d)
    BatchComponent{T}(v::Unitful.Volume,m::Unitful.Mass) where {T<:ConcreteIngredient}=new{T}(v,m,m/v)
end

BatchComponent{Water}(m::Unitful.Mass)=BatchComponent{Water}(m,1u"g/ml")
BatchComponent{Water}(v::Unitful.Volume)=BatchComponent{Water}(v,1u"g/ml")
BatchComponent{Air}(v::Unitful.Volume)=BatchComponent{Air}(v,0u"g/ml")
BatchComponent{Cement}(m::Unitful.Mass)=BatchComponent{Cement}(m,3.15u"g/ml")
BatchComponent{Coarse}(m::Unitful.Mass)=BatchComponent{Coarse}(m,2.8u"g/ml")
BatchComponent{Fine}(m::Unitful.Mass)=BatchComponent{Fine}(m,2.8u"g/ml")

function +(a::BatchComponent{T},b::BatchComponent{U}) where {T<:ConcreteIngredient} where {U<:ConcreteIngredient}
    if T==U
        BatchComponent{T}(a.volume+b.volume,a.mass+b.mass)
    else
        BatchComponent{Mixture}(a.volume+b.volume,a.mass+b.mass)
    end
end

function *(a::BatchComponent{T},c::Number) where {T<:ConcreteIngredient}
    if Unitful.dimension(c) !== Unitful.NoDims
        throw(ArgumentError("only multiply batch components by unitless plox"))
    end
    BatchComponent{T}(a.volume*c,a.mass*c)
end

*(c::Number, a::BatchComponent{T}) where {T<:ConcreteIngredient} = *(a,c)

function Base.show(io::IO, c::BatchComponent{T}) where {T<:ConcreteIngredient}
    print(io, string(T), ": ", string(uconvert(u"kg",c.mass)), ", density=", string(uconvert(u"g/cm^3",c.density)))
end
function Base.show(io::IO, c::BatchComponent{Air}) 
    print(io,"Air: ", string(uconvert(u"L",c.volume)))
end

struct Batch
    water::BatchComponent{Water}
    cement::BatchComponent{Cement}
    coarse::BatchComponent{Coarse}
    fine::BatchComponent{Fine}
    air::BatchComponent{Air}
    mixture::BatchComponent{Mixture}
    wcRatio::Number
    waterContent::Unitful.Density
    function Batch(water::BatchComponent{Water}, cement::BatchComponent{Cement}, coarse::BatchComponent{Coarse}, fine::BatchComponent{Fine}, air::BatchComponent{Air})
        mixture=water+cement+coarse+fine+air
        wcRatio=water.mass/cement.mass
        waterContent=water.mass/mixture.volume
        new(water,cement,coarse,fine,air,mixture,wcRatio,waterContent)
    end
end

concreteIngredientField(::Type{T}) where {T<:ConcreteIngredient}= Symbol(lowercase(string(nameof(T))))

function +(b::Batch,c::BatchComponent{T}) where {T<:ConcreteIngredient}
    addedfield=concreteIngredientField(T)
    newComponents = map([:water,:cement,:coarse,:fine,:air]) do f
        if f==addedfield
            getfield(b,f)+c
        else
            getfield(b,f)
        end
    end
    Batch(newComponents...)
end

function *(b::Batch,c::Number)
    newComponents = map([:water,:cement,:coarse,:fine,:air]) do f
        getfield(b,f)*c
    end
    Batch(newComponents...)
end

function BatchFromRatios(waterContent::Unitful.Density, wcRatio::Number,
                         coarseAggRatio::Number; airRatio::Number=0.02,
                         coarseAggDensity::Unitful.Density=2.8u"g/cm^3",
                         fineAggDensity::Unitful.Density=2.8u"g/cm^3",
                         cementDensity::Unitful.Density=3.14u"g/cm^3",
                         totalVolume::Unitful.Volume=1u"m^3")
    water=BatchComponent{Water}(waterContent*totalVolume)
    cement=BatchComponent{Cement}(waterContent*totalVolume/wcRatio,cementDensity)
    air=BatchComponent{Air}(airRatio*totalVolume)
    coarse=BatchComponent{Coarse}(coarseAggRatio*totalVolume,coarseAggDensity)
    fineAggVolume=totalVolume - water.volume - cement.volume - air.volume - coarse.volume
    fine=BatchComponent{Fine}(fineAggVolume,fineAggDensity)
    Batch(water,cement,coarse,fine,air)
end

function Base.show(io::IO, b::Batch)
    println(io,"batch per ", string(uconvert(u"L",b.mixture.volume)))
    println(io,"water = ", string(uconvert(u"kg",b.water.mass)))
    println(io,"cement = ", string(uconvert(u"kg",b.cement.mass)))
    println(io,"coarse = ", string(uconvert(u"kg",b.coarse.mass)))
    println(io,"fine = ", string(uconvert(u"kg",b.fine.mass)))
    println(io,"total weight = ", string(uconvert(u"kg", b.mixture.mass)))
    println(io,"total volume = ", string(uconvert(u"L", b.mixture.volume)))
    println(io,"w/c ratio = ", string(uconvert(NoUnits,b.wcRatio)))
    println(io,"water content per m^3 = ", string(uconvert(u"kg/m^3", b.waterContent)))
end

export Water, Cement, Coarse, Fine, Air, ConcreteIngredient
export BatchComponent, Batch, BatchFromRatios
export +, *
end
