# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md

"""
Material cache for storing state variables at integration points.

Uses the compositional state variable trait system to automatically construct
caches based on material requirements.

# Design Philosophy

1. Automatic construction: Cache structure inferred from material traits
2. Type-stable: NamedTuple-based storage with compile-time known types
3. Zero-allocation: Immutable state updates (functional style)
4. Compositional: State variables are independent, combinable building blocks

# Example

```julia
# For LinearElastic (stateless)
material = LinearElastic(E=210e9, ν=0.3)
cache = create_global_material_cache(material, n_ips=8, n_elems=100)
# → GlobalMaterialCache{NamedTuple{(),Tuple{}}} with empty states

# For PerfectPlasticity (stateful)
material = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
cache = create_global_material_cache(material, n_ips=8, n_elems=100)
# → GlobalMaterialCache{NamedTuple{(:ε_p, :α, :κ), Tuple{...}}} with state variables
```
"""

using Tensors

# ============================================================================
# MATERIAL CACHE STRUCTURE
# ============================================================================

"""
    GlobalMaterialCache{StateType}

Global material state cache for all integration points across all elements.

Stores material state variables using the compositional state variable system.
Used for time-stepping and persistent state storage.

# Type Parameters
- `StateType`: NamedTuple type for state variables (inferred from material)

# Fields
- `states::Matrix{StateType}`: State at each IP and element [nips, nelems]
- `states_old::Matrix{StateType}`: State from previous time step [nips, nelems]

# Design

The cache uses NamedTuples to store state variables, where:
- Keys are state variable symbols (e.g., `:ε_p`, `:α`, `:κ`)
- Values are concrete types (e.g., `SymmetricTensor{2,3,Float64,6}`, `Float64`)

For stateless materials, `StateType = NamedTuple{(),Tuple{}}` (empty).

# Example

```julia
# Stateful material (plasticity)
StateType = NamedTuple{(:ε_p, :α, :κ), Tuple{SymmetricTensor{2,3,Float64,6}, SymmetricTensor{2,3,Float64,6}, Float64}}
cache = GlobalMaterialCache{StateType}(n_ips, n_elems)
cache.states[q, elem_id]  # → (ε_p=..., α=..., κ=...)

# Stateless material (linear elastic)
StateType = NamedTuple{(),Tuple{}}
cache = GlobalMaterialCache{StateType}(n_ips, n_elems)
cache.states[q, elem_id]  # → NamedTuple()
```
"""
struct GlobalMaterialCache{StateType<:NamedTuple}
    states::Matrix{StateType}
    states_old::Matrix{StateType}

    function GlobalMaterialCache{StateType}(n_ips::Int, n_elems::Int) where {StateType<:NamedTuple}
        # Initialize with zero/default states
        zero_state = create_zero_state(StateType)
        states = [zero_state for _ in 1:n_ips, _ in 1:n_elems]
        states_old = [zero_state for _ in 1:n_ips, _ in 1:n_elems]
        new{StateType}(states, states_old)
    end
end

"""
    create_zero_state(::Type{StateType}) where {StateType<:NamedTuple}

Create a zero-initialized state of the given NamedTuple type.

# Example
```julia
StateType = NamedTuple{(:ε_p, :α, :κ), Tuple{SymmetricTensor{2,3,Float64,6}, SymmetricTensor{2,3,Float64,6}, Float64}}
zero_state = create_zero_state(StateType)
# → (ε_p=zero(SymmetricTensor{2,3}), α=zero(SymmetricTensor{2,3}), κ=0.0)
```
"""
function create_zero_state(::Type{StateType}) where {StateType<:NamedTuple}
    # Get field names and types
    field_names = fieldnames(StateType)
    field_types = [fieldtype(StateType, name) for name in field_names]

    if isempty(field_names)
        return NamedTuple()
    else
        # Create zero value for each field
        zero_values = [zero(T) for T in field_types]
        return NamedTuple{field_names}(zero_values)
    end
end

# ============================================================================
# CACHE CONSTRUCTION FROM MATERIAL TRAITS
# ============================================================================

"""
    material_state_type(material::AbstractMaterial) -> Type{<:NamedTuple}

Infer state NamedTuple type from material's required state variables.

Uses the compositional trait system:
1. Query `required_state_variables(material)` → tuple of state variable types
2. For each state variable, query `state_variable_type(var)` → concrete type
3. For each state variable, query `default_symbol(var)` → symbol name
4. Construct NamedTuple type from symbols and concrete types

# Example
```julia
# LinearElastic (stateless)
material = LinearElastic(E=210e9, ν=0.3)
StateType = material_state_type(material)
# → NamedTuple{(),Tuple{}}

# PerfectPlasticity (stateful)
material = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
StateType = material_state_type(material)
# → NamedTuple{(:ε_p, :α, :κ), Tuple{SymmetricTensor{2,3,Float64,6}, SymmetricTensor{2,3,Float64,6}, Float64}}
```
"""
function material_state_type(material::AbstractMaterial)
    # Query required state variables from material instance
    vars = required_state_variables(material)

    if isempty(vars)
        # Stateless material
        return NamedTuple{(),Tuple{}}
    else
        # Stateful material - build NamedTuple type
        symbols = tuple([default_symbol(var) for var in vars]...)
        types = tuple([state_variable_type(var) for var in vars]...)

        return NamedTuple{symbols, Tuple{types...}}
    end
end

"""
    create_global_material_cache(material::AbstractMaterial; n_ips::Int, n_elems::Int)

Create global material cache for the given material, integration points, and elements.

Automatically infers the correct cache structure from material traits.

# Arguments
- `material`: Material model instance
- `n_ips`: Number of integration points per element
- `n_elems`: Number of elements

# Returns
`GlobalMaterialCache{StateType}` where `StateType` is inferred from material

# Example
```julia
# Stateless material
mat = LinearElastic(E=210e9, ν=0.3)
cache = create_global_material_cache(mat, n_ips=8, n_elems=100)
# → GlobalMaterialCache{NamedTuple{(),Tuple{}}}

# Stateful material
mat = PerfectPlasticity(E=210e9, ν=0.3, σ_y=250e6, H=1e9)
cache = create_global_material_cache(mat, n_ips=8, n_elems=100)
# → GlobalMaterialCache{NamedTuple{(:ε_p, :α, :κ), Tuple{...}}}
```
"""
function create_global_material_cache(material::AbstractMaterial; n_ips::Int, n_elems::Int)
    StateType = material_state_type(material)
    return GlobalMaterialCache{StateType}(n_ips, n_elems)
end

# ============================================================================
# CACHE ACCESS AND UPDATE FUNCTIONS
# ============================================================================

"""
    get_state(cache::GlobalMaterialCache, ip::Int, elem_id::Int) -> NamedTuple

Get current state at integration point for element.

# Arguments
- `cache`: Global material cache
- `ip`: Integration point index (1-based)
- `elem_id`: Element ID (1-based)

# Returns
NamedTuple of state variables (empty for stateless materials)
"""
@inline function get_state(cache::GlobalMaterialCache, ip::Int, elem_id::Int)
    return cache.states[ip, elem_id]
end

"""
    get_old_state(cache::GlobalMaterialCache, ip::Int, elem_id::Int) -> NamedTuple

Get state from previous time step at integration point for element.

# Arguments
- `cache`: Global material cache
- `ip`: Integration point index (1-based)
- `elem_id`: Element ID (1-based)

# Returns
NamedTuple of state variables from previous step
"""
@inline function get_old_state(cache::GlobalMaterialCache, ip::Int, elem_id::Int)
    @inbounds return cache.states_old[ip, elem_id]
end

"""
    set_state!(cache::GlobalMaterialCache, ip::Int, elem_id::Int, state::NamedTuple)

Update current state at integration point for element (in-place).

# Arguments
- `cache`: Global material cache
- `ip`: Integration point index (1-based)
- `elem_id`: Element ID (1-based)
- `state`: New state NamedTuple

# Note
This is an in-place operation.
"""
@inline function set_state!(cache::GlobalMaterialCache, ip::Int, elem_id::Int, state::NamedTuple)
    @inbounds cache.states[ip, elem_id] = state
    return nothing
end

"""
    update_cache!(cache::GlobalMaterialCache)

Copy current states to states_old (for time stepping).

Call this at the beginning of each time/load step to save the converged
state from the previous step.

# Example
```julia
# At the start of a new time step
update_cache!(cache)

# Now solve for new states
for elem_id in 1:n_elems
    for ip in 1:n_ips
        state_old = get_old_state(cache, ip, elem_id)
        # ... solve for state_new ...
        set_state!(cache, ip, elem_id, state_new)
    end
end
```
"""
function update_cache!(cache::GlobalMaterialCache)
    cache.states_old .= cache.states
    return nothing
end

"""
    reset_cache!(cache::GlobalMaterialCache)

Reset all states to zero (for restarting analysis).

# Example
```julia
reset_cache!(cache)
# All states now zero-initialized
```
"""
function reset_cache!(cache::GlobalMaterialCache{StateType}) where {StateType}
    zero_state = create_zero_state(StateType)
    fill!(cache.states, zero_state)
    fill!(cache.states_old, zero_state)
    return nothing
end

# ============================================================================
# HELPER FUNCTIONS FOR STATE VARIABLE ACCESS
# ============================================================================

"""
    get_state_variable(state::NamedTuple, var_type::Type{<:AbstractStateVariable})

Extract a specific state variable from a state NamedTuple.

# Arguments
- `state`: State NamedTuple (e.g., from `get_state(cache, ip)`)
- `var_type`: State variable type (e.g., `PlasticStrain`)

# Returns
Value of the requested state variable

# Example
```julia
state = get_state(cache, 1)  # → (ε_p=..., α=..., κ=...)
ε_p = get_state_variable(state, PlasticStrain)
α = get_state_variable(state, Backstress)
κ = get_state_variable(state, EquivalentPlasticStrain)
```
"""
@generated function get_state_variable(state::StateType, var_type::Type{VarType}) where {StateType<:NamedTuple, VarType<:AbstractStateVariable}
    # Get the symbol for this state variable
    sym = default_symbol(VarType)

    # Check if state has this field
    if hasfield(StateType, sym)
        return :(state.$sym)
    else
        error("State type $StateType does not have field $sym for variable $VarType")
    end
end

"""
    set_state_variable(state::NamedTuple, var_type::Type{<:AbstractStateVariable}, value)

Create new state NamedTuple with updated variable value.

Returns a new NamedTuple (immutable update).

# Arguments
- `state`: Current state NamedTuple
- `var_type`: State variable type to update
- `value`: New value for the state variable

# Returns
New state NamedTuple with updated value

# Example
```julia
state = get_state(cache, 1)
new_state = set_state_variable(state, PlasticStrain, new_ε_p)
set_state!(cache, 1, new_state)
```
"""
@generated function set_state_variable(state::StateType, var_type::Type{VarType}, value) where {StateType<:NamedTuple, VarType<:AbstractStateVariable}
    # Get the symbol for this state variable
    sym = default_symbol(VarType)

    # Check if state has this field
    if hasfield(StateType, sym)
        return :(merge(state, NamedTuple{($(QuoteNode(sym)),)}((value,))))
    else
        error("State type $StateType does not have field $sym for variable $VarType")
    end
end
