#
# Combinator algebra of vectorized data transformations.
#

import Base:
    OneTo,
    show

using Base.Cartesian

#
# Pipeline interface.
#

"""
    Runtime()

Runtime state for pipeline evaluation.
"""
mutable struct Runtime
end

"""
    Pipeline(op, args...)

A pipeline object represents a vectorized data transformation.

Parameter `op` is a function that performs the transformation; `args` are extra
arguments to be passed to the function.

The pipeline transforms any input vector by invoking `op` with the following
arguments:

    op(rt::Runtime, input::AbstractVector, args...)

The result of `op` must be the output vector, which should be of the same
length as the input vector.
"""
struct Pipeline
    op
    args::Vector{Any}
    sig::Signature

    Pipeline(op; args::Vector{Any}=Any[], sig::Signature=Signature()) =
        new(op, args, sig)
end

Pipeline(op, args...) =
    Pipeline(op, args=Any[args...])

"""
    designate(::Pipeline, ::Signature) :: Pipeline
    designate(::Pipeline, ::AbstractShape, ::AbstractShape) :: Pipeline
    p::Pipeline |> designate(::Signature) :: Pipeline
    p::Pipeline |> designate(::AbstractShape, ::AbstractShape) :: Pipeline

Sets the pipeline signature.
"""
function designate end

designate(p::Pipeline, sig::Signature) =
    Pipeline(p.op, args=p.args, sig=sig)

designate(p::Pipeline, src::Union{AbstractShape,Type}, tgt::Union{AbstractShape,Type}) =
    Pipeline(p.op, args=p.args, sig=Signature(src, tgt))

designate(sig::Signature) =
    p::Pipeline -> designate(p, sig)

designate(src::Union{AbstractShape,Type}, tgt::Union{AbstractShape,Type}) =
    p::Pipeline -> designate(p, Signature(src, tgt))

"""
    signature(::Pipeline) :: Signature

Returns the pipeline signature.
"""
signature(p::Pipeline) = p.sig

source(p::Pipeline) = source(p.sig)

target(p::Pipeline) = target(p.sig)

function (p::Pipeline)(input::DataKnot)
    @assert fits(shape(input), source(p))
    DataKnot(target(p), p(cell(input)))
end

function (p::Pipeline)(@nospecialize input::AbstractVector)::AbstractVector
    rt = Runtime()
    output = p(rt, input)
end

function (p::Pipeline)(rt::Runtime, @nospecialize input::AbstractVector)::AbstractVector
    p.op(rt, input, p.args...)
end

quoteof(p::Pipeline) =
    quoteof(p.op, p.args)

show(io::IO, p::Pipeline) =
    print_expr(io, quoteof(p))

#
# Vectorizing scalar functions.
#

"""
    lift(f) :: Pipeline

`f` is any scalar unary function.

The pipeline applies `f` to each element of the input vector.
"""
lift(f) = Pipeline(lift, f)

lift(f, args...) = Pipeline(lift, f, args)

lift(rt::Runtime, input::AbstractVector, f) =
    f.(input)

lift(rt::Runtime, input::AbstractVector, f, args) =
    f.(input, Ref.(args)...)

"""
    tuple_lift(f) :: Pipeline

`f` is an n-ary function.

The pipeline applies `f` to each row of an n-tuple vector.
"""
tuple_lift(f) = Pipeline(tuple_lift, f)

function tuple_lift(rt::Runtime, input::AbstractVector, f)
    @assert input isa TupleVector
    _tuple_lift(f, length(input), columns(input)...)
end

@generated function _tuple_lift(f, len::Int, cols::AbstractVector...)
    D = length(cols)
    return quote
        I = Tuple{Any[eltype(col) for col in cols]...}
        O = Core.Compiler.return_type(f, I)
        output = Vector{O}(undef, len)
        @inbounds for k = 1:len
            output[k] = @ncall $D f (d -> cols[d][k])
        end
        output
    end
end

"""
    block_lift(f) :: Pipeline
    block_lift(f, default) :: Pipeline

`f` is a function that expects a vector argument.

The pipeline applies `f` to each block of the input block vector.  When a block
is empty, `default` (if specified) is used as the output value.
"""
function block_lift end

block_lift(f) = Pipeline(block_lift, f)

function block_lift(rt::Runtime, input::AbstractVector, f)
    @assert input isa BlockVector
    _block_lift(f, input)
end

block_lift(f, default) = Pipeline(block_lift, f, default)

function block_lift(rt::Runtime, input::AbstractVector, f, default)
    @assert input isa BlockVector
    _block_lift(f, default, input)
end

function _block_lift(f, input)
    cr = cursor(input)
    I = Tuple{typeof(cr)}
    O = Core.Compiler.return_type(f, I)
    output = Vector{O}(undef, length(input))
    @inbounds while next!(cr)
        output[cr.pos] = f(cr)
    end
    output
end

function _block_lift(f, default, input)
    cr = cursor(input)
    I = Tuple{typeof(cr)}
    O = Union{Core.Compiler.return_type(f, I), typeof(default)}
    output = Vector{O}(undef, length(input))
    @inbounds while next!(cr)
        output[cr.pos] = !isempty(cr) ? f(cr) : default
    end
    output
end

"""
    filler(val) :: Pipeline

This pipeline produces a vector filled with the given value.
"""
filler(val) = Pipeline(filler, val)

filler(rt::Runtime, input::AbstractVector, val) =
    fill(val, length(input))

"""
    null_filler() :: Pipeline

This pipeline produces a block vector with empty blocks.
"""
null_filler() = Pipeline(null_filler)

null_filler(rt::Runtime, input::AbstractVector) =
    BlockVector(fill(1, length(input)+1), Union{}[], x0to1)

"""
    block_filler(block::AbstractVector, card::Cardinality) :: Pipeline

This pipeline produces a block vector filled with the given block.
"""
block_filler(block, card::Cardinality=x0toN) = Pipeline(block_filler, block, card)

function block_filler(rt::Runtime, input::AbstractVector, block::AbstractVector, card::Cardinality)
    if isempty(input)
        return BlockVector(:, block[[]], card)
    elseif length(input) == 1
        return BlockVector([1, length(block)+1], block, card)
    else
        len = length(input)
        sz = length(block)
        perm = Vector{Int}(undef, len*sz)
        for k in eachindex(input)
            copyto!(perm, 1 + sz * (k - 1), 1:sz)
        end
        return BlockVector(1:sz:(len*sz+1), block[perm], card)
    end
end

#
# Converting regular vectors to columnar vectors.
#

"""
    adapt_missing() :: Pipeline

This pipeline transforms a vector that contains `missing` elements to a block
vector with `missing` elements replaced by empty blocks.
"""
adapt_missing() = Pipeline(adapt_missing)

function adapt_missing(rt::Runtime, input::AbstractVector)
    if !(Missing <: eltype(input))
        return BlockVector(:, input, x0to1)
    end
    sz = 0
    for elt in input
        if elt !== missing
            sz += 1
        end
    end
    O = Base.nonmissingtype(eltype(input))
    if sz == length(input)
        return BlockVector(:, collect(O, input), x0to1)
    end
    offs = Vector{Int}(undef, length(input)+1)
    elts = Vector{O}(undef, sz)
    @inbounds offs[1] = top = 1
    @inbounds for k in eachindex(input)
        elt = input[k]
        if elt !== missing
            elts[top] = elt
            top += 1
        end
        offs[k+1] = top
    end
    return BlockVector(offs, elts, x0to1)
end

"""
    adapt_vector() :: Pipeline

This pipeline transforms a vector with vector elements to a block vector.
"""
adapt_vector() = Pipeline(adapt_vector)

function adapt_vector(rt::Runtime, input::AbstractVector)
    @assert eltype(input) <: AbstractVector
    if eltype(input) === Union{}
        return BlockVector(fill(1, length(input)+1), Union{}[], x0toN)
    end
    sz = 0
    for v in input
        sz += length(v)
    end
    O = eltype(eltype(input))
    offs = Vector{Int}(undef, length(input)+1)
    elts = Vector{O}(undef, sz)
    @inbounds offs[1] = top = 1
    @inbounds for k in eachindex(input)
        v = input[k]
        copyto!(elts, top, v)
        top += length(v)
        offs[k+1] = top
    end
    return BlockVector(offs, elts, x0toN)
end

"""
    adapt_tuple() :: Pipeline

This pipeline transforms a vector of tuples to a tuple vector.
"""
adapt_tuple() = Pipeline(adapt_tuple)

function adapt_tuple(rt::Runtime, input::AbstractVector)
    @assert eltype(input) <: Union{Tuple,NamedTuple}
    lbls = Symbol[]
    I = eltype(input)
    if typeof(I) == DataType && I <: NamedTuple
        lbls = collect(Symbol, I.parameters[1])
        I = I.parameters[2]
    end
    cols = _adapt_tuple(input, Val(Tuple{I.parameters...}))
    TupleVector(lbls, length(input), cols)
end

@generated function _adapt_tuple(input, vty)
    Is = (vty.parameters[1].parameters...,)
    D = length(Is)
    return quote
        len = length(input)
        @nexprs $D j -> col_j = Vector{$Is[j]}(undef, len)
        @inbounds for k in eachindex(input)
            t = input[k]
            @nexprs $D j -> col_j[k] = t[j]
        end
        @nref $D AbstractVector j -> col_j
    end
end

"""
    assert_type(T::Type, lbl) :: Pipeline

This pipeline asserts the types of the elements of the input vector.
"""
assert_type(T::Type, lbl::Union{Symbol,Nothing}) =
    Pipeline(assert_type, T, lbl)

assert_type(T::Type) =
    Pipeline(assert_type, T)

function assert_type(rt::Runtime, input::AbstractVector, T, lbl=nothing)
    if eltype(input) <: T
        return input
    end
    if isempty(input)
        return T[]
    end
    output = Vector{T}(undef, length(input))
    @inbounds for i in eachindex(input)
        val = input[i]
        val isa T || error(lbl !== nothing ? "\"$lbl\"" : "", "[$i]: expected a value of type $T; got $(typeof(val))")
        output[i] = val
    end
    output
end

#
# Identity and composition.
#

"""
    pass() :: Pipeline

This pipeline returns its input unchanged.
"""
pass() = Pipeline(pass)

pass(rt::Runtime, @nospecialize input::AbstractVector) =
    input

"""
    chain_of(p₁::Pipeline, p₂::Pipeline … pₙ::Pipeline) :: Pipeline

This pipeline sequentially applies `p₁`, `p₂` … `pₙ`.
"""
function chain_of end

chain_of() = pass()

chain_of(p) = p

function chain_of(ps...)
    ps′ = filter(p -> !(p isa Pipeline && p.op == pass), collect(ps))
    isempty(ps′) ? pass() : length(ps′) == 1 ? ps′[1] : chain_of(ps′)
end

chain_of(ps::Vector) =
    Pipeline(chain_of, ps)

quoteof(::typeof(chain_of), args::Vector{Any}) =
    if length(args) == 1 && args[1] isa Vector
        Expr(:call, chain_of, Any[quoteof(arg) for arg in args[1]]...)
    else
        Expr(:call, chain_of, Any[quoteof(arg) for arg in args]...)
    end

function chain_of(rt::Runtime, @nospecialize(input::AbstractVector), ps)
    output = input
    for p in ps
        output = p(rt, output)
    end
    output
end

#
# Operations on tuple vectors.
#

"""
    tuple_of(p₁::Pipeline, p₂::Pipeline … pₙ::Pipeline) :: Pipeline

This pipeline produces an n-tuple vector, whose columns are generated by
applying `p₁`, `p₂` … `pₙ` to the input vector.
"""
tuple_of(ps...) =
    tuple_of(Symbol[], collect(ps))

tuple_of(lps::Pair{Symbol}...) =
    tuple_of(Symbol[first(lp) for lp in lps], Pipeline[last(lp) for lp in lps])

tuple_of(lbls::Vector{Symbol}, ps::Vector) = Pipeline(tuple_of, lbls, ps)

tuple_of(lbls::Vector{Symbol}, width::Int) = Pipeline(tuple_of, lbls, width)

tuple_of(width::Int) = tuple_of(Symbol[], width)

quoteof(::typeof(tuple_of), args::Vector{Any}) =
    if length(args) == 2 && args[1] isa Vector{Symbol} && args[2] isa Vector
        if isempty(args[1])
            Expr(:call, tuple_of, Any[quoteof(p) for p in args[2]]...)
        else
            Expr(:call, tuple_of, Any[quoteof(l => p) for (l, p) in zip(args[1], args[2])]...)
        end
    else
        Expr(:call, tuple_of, Any[quoteof(arg) for arg in args]...)
    end

function tuple_of(rt::Runtime, @nospecialize(input::AbstractVector), lbls::Vector{Symbol}, ps::Vector{Pipeline})
    len = length(input)
    cols = AbstractVector[p(rt, input) for p in ps]
    TupleVector(lbls, len, cols)
end

function tuple_of(rt::Runtime, @nospecialize(input::AbstractVector), lbls::Vector{Symbol}, width::Int)
    len = length(input)
    cols = AbstractVector[input for k in 1:width]
    TupleVector(lbls, len, cols)
end

"""
    column(lbl::Union{Int,Symbol}) :: Pipeline

This pipeline extracts the specified column of a tuple vector.
"""
column(lbl::Union{Int,Symbol}) = Pipeline(column, lbl)

function column(rt::Runtime, input::AbstractVector, lbl)
    @assert input isa TupleVector
    j = locate(input, lbl)
    column(input, j)
end

"""
    with_column(lbl::Union{Int,Symbol}, p::Pipeline) :: Pipeline

This pipeline transforms a tuple vector by applying `p` to the specified
column.
"""
with_column(lbl::Union{Int,Symbol}, p) = Pipeline(with_column, lbl, p)

function with_column(rt::Runtime, input::AbstractVector, lbl, p)
    @assert input isa TupleVector
    j = locate(input, lbl)
    cols′ = copy(columns(input))
    cols′[j] = p(rt, cols′[j])
    TupleVector(labels(input), length(input), cols′)
end

#
# Operations on block vectors.
#

"""
    wrap() :: Pipeline

This pipeline produces a block vector with one-element blocks wrapping the
values of the input vector.
"""
wrap() = Pipeline(wrap)

wrap(rt::Runtime, input::AbstractVector) =
    BlockVector(:, input, x1to1)

"""
    with_elements(p::Pipeline) :: Pipeline

This pipeline transforms a block vector by applying `p` to its vector of
elements.
"""
with_elements(p) = Pipeline(with_elements, p)

function with_elements(rt::Runtime, input::AbstractVector, p)
    @assert input isa BlockVector
    BlockVector(offsets(input), p(rt, elements(input)), cardinality(input))
end

"""
    flatten() :: Pipeline

This pipeline flattens a nested block vector.
"""
flatten() = Pipeline(flatten)

function flatten(rt::Runtime, input::AbstractVector)
    @assert input isa BlockVector && elements(input) isa BlockVector
    offs = offsets(input)
    nested = elements(input)
    nested_offs = offsets(nested)
    elts = elements(nested)
    card = cardinality(input)|cardinality(nested)
    BlockVector(_flatten(offs, nested_offs), elts, card)
end

_flatten(offs1::AbstractVector{Int}, offs2::AbstractVector{Int}) =
    Int[offs2[off] for off in offs1]

_flatten(offs1::OneTo{Int}, offs2::OneTo{Int}) = offs1

_flatten(offs1::OneTo{Int}, offs2::AbstractVector{Int}) = offs2

_flatten(offs1::AbstractVector{Int}, offs2::OneTo{Int}) = offs1

"""
    distribute(lbl::Union{Int,Symbol}) :: Pipeline

This pipeline transforms a tuple vector with a column of blocks to a block
vector with tuple elements.
"""
distribute(lbl) = Pipeline(distribute, lbl)

function distribute(rt::Runtime, input::AbstractVector, lbl)
    @assert input isa TupleVector && column(input, lbl) isa BlockVector
    j = locate(input, lbl)
    _distribute(column(input, j), input, j)
end

function _distribute(col::BlockVector, tv::TupleVector, j)
    lbls = labels(tv)
    cols′ = copy(columns(tv))
    len = length(col)
    card = cardinality(col)
    offs = offsets(col)
    col′ = elements(col)
    if offs isa OneTo{Int}
        cols′[j] = col′
        return BlockVector{card}(offs, TupleVector(lbls, len, cols′))
    end
    len′ = length(col′)
    perm = Vector{Int}(undef, len′)
    l = r = 1
    @inbounds for k = 1:len
        l = r
        r = offs[k+1]
        for n = l:r-1
            perm[n] = k
        end
    end
    for i in eachindex(cols′)
        cols′[i] =
            if i == j
                col′
            else
                cols′[i][perm]
            end
    end
    return BlockVector{card}(offs, TupleVector(lbls, len′, cols′))
end

"""
    distribute_all() :: Pipeline

This pipeline transforms a tuple vector with block columns to a block vector
with tuple elements.
"""
distribute_all() = Pipeline(distribute_all)

function distribute_all(rt::Runtime, input::AbstractVector)
    @assert input isa TupleVector && all(Bool[col isa BlockVector for col in columns(input)])
    cols = columns(input)
    _distribute_all(labels(input), length(input), cols...)
end

@generated function _distribute_all(lbls::Vector{Symbol}, len::Int, cols::BlockVector...)
    D = length(cols)
    CARD = x1to1
    for col in cols
        CARD |= cardinality(col)
    end
    return quote
        @nextract $D offs (d -> offsets(cols[d]))
        @nextract $D elts (d -> elements(cols[d]))
        if @nall $D (d -> offs_d isa OneTo{Int})
            return BlockVector{$CARD}(:, TupleVector(lbls, len, AbstractVector[(@ntuple $D elts)...]))
        end
        len′ = 0
        regular = true
        @inbounds for k = 1:len
            sz = @ncall $D (*) (d -> (offs_d[k+1] - offs_d[k]))
            len′ += sz
            regular = regular && sz == 1
        end
        if regular
            return BlockVector{$CARD}(:, TupleVector(lbls, len, AbstractVector[(@ntuple $D elts)...]))
        end
        offs′ = Vector{Int}(undef, len+1)
        @nextract $D perm (d -> Vector{Int}(undef, len′))
        @inbounds offs′[1] = top = 1
        @inbounds for k = 1:len
            @nloops $D n (d -> offs_{$D-d+1}[k]:offs_{$D-d+1}[k+1]-1) begin
                @nexprs $D (d -> perm_{$D-d+1}[top] = n_d)
                top += 1
            end
            offs′[k+1] = top
        end
        cols′ = @nref $D AbstractVector (d -> elts_d[perm_d])
        return BlockVector{$CARD}(offs′, TupleVector(lbls, len′, cols′))
    end
end

"""
    block_cardinality(card::Cardinality, src_lbl, tgt_lbl) :: Pipeline

This pipeline asserts the cardinality of the input block vector.
"""
block_cardinality(card::Cardinality, src_lbl::Union{Symbol,Nothing}, tgt_lbl::Union{Symbol,Nothing}) =
    Pipeline(block_cardinality, card, src_lbl, tgt_lbl)

block_cardinality(card::Cardinality) =
    Pipeline(block_cardinality, card)

function block_cardinality(rt::Runtime, input::AbstractVector, card, src_lbl=nothing, tgt_lbl=nothing)
    @assert input isa BlockVector
    cardinality(input) != card || return input
    if !fits(cardinality(input), card) && !(offsets(input) isa OneTo)
        offs = offsets(input)
        @inbounds off = offs[1]
        for k = 2:lastindex(offs)
            @inbounds off′ = offs[k]
            !issingular(card) || off′ <= off+1 || _cardinality_error(src_lbl, tgt_lbl, "singular")
            !ismandatory(card) || off′ >= off+1 || _cardinality_error(src_lbl, tgt_lbl, "mandatory")
            off = off′
        end
    end
    card != x1to1 || return BlockVector(:, elements(input))
    @inbounds output = BlockVector(offsets(input), elements(input), card)
    return output
end

_cardinality_error(src_lbl, tgt_lbl, kind) =
    error(tgt_lbl !== nothing ? "\"$tgt_lbl\": " : "",
          "expected a $kind value",
          src_lbl !== nothing ? ", relative to \"$src_lbl\"" : "")

"""
    block_length() :: Pipeline

This pipeline converts a block vector to a vector of block lengths.
"""
block_length() = Pipeline(block_length)

function block_length(rt::Runtime, input::AbstractVector)
    @assert input isa BlockVector
    _block_length(offsets(input))
end

_block_length(offs::OneTo{Int}) =
    fill(1, length(offs)-1)

function _block_length(offs::AbstractVector{Int})
    len = length(offs) - 1
    output = Vector{Int}(undef, len)
    @inbounds for k = 1:len
        output[k] = offs[k+1] - offs[k]
    end
    output
end

"""
    block_not_empty() :: Pipeline

This pipeline converts a block vector to a vector of Boolean values, where each
value indicates whether the corresponding block is empty or not.
"""
block_not_empty() = Pipeline(block_not_empty)

function block_not_empty(rt::Runtime, input::AbstractVector)
    @assert input isa BlockVector
    _block_not_empty(offsets(input))
end

_block_not_empty(offs::OneTo{Int}) =
    fill(true, length(offs)-1)

function _block_not_empty(offs::AbstractVector{Int})
    len = length(offs) - 1
    output = Vector{Bool}(undef, len)
    @inbounds for k = 1:len
        output[k] = offs[k+1] > offs[k]
    end
    output
end

"""
    block_any() :: Pipeline

This pipeline applies `any` to a block vector with `Bool` elements.
"""
block_any() = Pipeline(block_any)

function block_any(rt::Runtime, input::AbstractVector)
    @assert input isa BlockVector && eltype(elements(input)) <: Bool
    len = length(input)
    offs = offsets(input)
    elts = elements(input)
    if offs isa OneTo
        return elts
    end
    output = Vector{Bool}(undef, len)
    l = r = 1
    @inbounds for k = 1:len
        val = false
        l = r
        r = offs[k+1]
        for i = l:r-1
            if elts[i]
                val = true
                break
            end
        end
        output[k] = val
    end
    return output
end

#
# Filtering.
#

"""
    sieve_by() :: Pipeline

This pipeline filters a vector of pairs by the second column.  It expects a
pair vector, whose second column is a `Bool` vector, and produces a block
vector with 0- or 1-element blocks containing the elements of the first column.
"""
sieve_by() = Pipeline(sieve_by)

function sieve_by(rt::Runtime, input::AbstractVector)
    @assert input isa TupleVector && width(input) == 2 && eltype(column(input, 2)) <: Bool
    val_col, pred_col = columns(input)
    _sieve_by(val_col, pred_col)
end

function _sieve_by(@nospecialize(v), bv)
    len = length(bv)
    sz = count(bv)
    if sz == len
        return BlockVector(:, v, x0to1)
    elseif sz == 0
        return BlockVector(fill(1, len+1), v[[]], x0to1)
    end
    offs = Vector{Int}(undef, len+1)
    perm = Vector{Int}(undef, sz)
    @inbounds offs[1] = top = 1
    for k = 1:len
        if bv[k]
            perm[top] = k
            top += 1
        end
        offs[k+1] = top
    end
    return BlockVector(offs, v[perm], x0to1)
end

#
# Extracting and slicing.
#

"""
    get_by(N::Int) :: Pipeline

This pipeline extracts the `N`-th element from the given block vector.
"""
get_by(N::Int) =
    Pipeline(get_by, N)

get_by(rt::Runtime, input::AbstractVector, N::Int) =
    get_by(rt, input, N, x0to1)

get_by(N::Int, card::Cardinality) =
    Pipeline(get_by, N, card)

function get_by(::Runtime, input::AbstractVector, N::Int, card::Cardinality)
    @assert input isa BlockVector
    len = length(input)
    offs = offsets(input)
    elts = elements(input)
    sz = 0
    R = 1
    for k = 1:len
        L = R
        @inbounds R = offs[k+1]
        sz += checkindex(Bool, L:R-1, _get_index(L, R-1, N))
    end
    if sz == len
        if N == 1
            elts′ = elts[view(offs, 1:len)]
            return BlockVector(:, elts′, card)
        end
        perm = Vector{Int}(undef, sz)
        R = 1
        for k = 1:len
            L = R
            @inbounds R = offs[k+1]
            @inbounds perm[k] = _get_index(L, R-1, N)
        end
        elts′ = elts[perm]
        return BlockVector(:, elts′, card)
    end
    offs′ = Vector{Int}(undef, len+1)
    perm = Vector{Int}(undef, sz)
    @inbounds offs′[1] = top = 1
    R = 1
    for k = 1:len
        L = R
        @inbounds R = offs[k+1]
        i = _get_index(L, R-1, N)
        if checkindex(Bool, L:R-1, i)
            perm[top] = i
            top += 1
        end
        offs′[k+1] = top
    end
    elts′ = elts[perm]
    return BlockVector(offs′, elts′, card)
end

"""
    get_by() :: Pipeline

This pipeline takes a pair vector of blocks and integers, and returns the first
column indexed by the second column.
"""
get_by() =
    Pipeline(get_by)

function get_by(::Runtime, input::AbstractVector)
    @assert input isa TupleVector
    cols = columns(input)
    @assert length(cols) == 2
    vals, Ns = cols
    @assert vals isa BlockVector
    @assert eltype(Ns) <: Int
    _get_by(elements(vals), offsets(vals), Ns)
end

function _get_by(@nospecialize(elts), offs, Ns)
    len = length(Ns)
    R = 1
    sz = 0
    for k = 1:len
        L = R
        @inbounds N = Ns[k]
        @inbounds R = offs[k+1]
        sz += checkindex(Bool, L:R-1, _get_index(L, R-1, N))
    end
    if sz == len
        perm = Vector{Int}(undef, sz)
        R = 1
        for k = 1:len
            L = R
            @inbounds N = Ns[k]
            @inbounds R = offs[k+1]
            @inbounds perm[k] = _get_index(L, R-1, N)
        end
        elts′ = elts[perm]
        return BlockVector(:, elts′, x0to1)
    end
    offs′ = Vector{Int}(undef, len+1)
    perm = Vector{Int}(undef, sz)
    @inbounds offs′[1] = top = 1
    R = 1
    for k = 1:len
        L = R
        @inbounds N = Ns[k]
        @inbounds R = offs[k+1]
        i = _get_index(L, R-1, N)
        if checkindex(Bool, L:R-1, i)
            perm[top] = i
            top += 1
        end
        offs′[k+1] = top
    end
    elts′ = elts[perm]
    return BlockVector(offs′, elts′, x0to1)
end

@inline _get_index(l, r, n) =
    n >= 0 ? l + n - 1 : r + n + 1

"""
    slice_by(N::Int, inv::Bool=false) :: Pipeline

This pipeline transforms a block vector by keeping the first `N` elements of
each block.  If `inv` is true, the pipeline drops the first `N` elements of
each block.
"""
slice_by(N::Union{Int,Missing}, inv::Bool=false) =
    Pipeline(slice_by, N, inv)

slice_by(N::Union{Int,Missing}, card::Cardinality, inv::Bool=false) =
    Pipeline(splice_by, N, card, inv)

slice_by(rt::Runtime, input::AbstractVector, N::Union{Int,Missing}, inv::Bool) =
    slice_by(rt, input, N, cardinality(input)|x0to1, inv)

function slice_by(rt::Runtime, input::AbstractVector, ::Missing, card::Cardinality, inv::Bool)
    @assert input isa BlockVector
    offs′ = !inv ? offsets(input) : fill(1, length(input)+1)
    elts′ = !inv ? elements(input) : elements(input)[Int[]]
    return BlockVector(offs′, elts′, card)
end

function slice_by(rt::Runtime, input::AbstractVector, N::Int, card::Cardinality, inv::Bool)
    @assert input isa BlockVector
    len = length(input)
    offs = offsets(input)
    elts = elements(input)
    sz = 0
    R = 1
    for k = 1:len
        L = R
        @inbounds R = offs[k+1]
        (l, r) = _slice_range(N, R-L, inv)
        sz += r - l + 1
    end
    if sz == length(elts)
        return BlockVector(offs, elts, card)
    end
    offs′ = Vector{Int}(undef, len+1)
    perm = Vector{Int}(undef, sz)
    @inbounds offs′[1] = top = 1
    R = 1
    for k = 1:len
        L = R
        @inbounds R = offs[k+1]
        (l, r) = _slice_range(N, R-L, inv)
        for j = (L + l - 1):(L + r - 1)
            perm[top] = j
            top += 1
        end
        offs′[k+1] = top
    end
    elts′ = elts[perm]
    return BlockVector(offs′, elts′, card)
end

"""
    slice_by(inv::Bool=false) :: Pipeline

This pipeline takes a pair vector of blocks and integers, and returns the first
column sliced by the second column.
"""
slice_by(inv::Bool=false) =
    Pipeline(slice_by, inv)

function slice_by(rt::Runtime, input::AbstractVector, inv::Bool)
    @assert input isa TupleVector
    cols = columns(input)
    @assert length(cols) == 2
    vals, Ns = cols
    @assert vals isa BlockVector
    @assert eltype(Ns) <: Union{Missing,Int}
    _slice_by(elements(vals), offsets(vals), cardinality(vals), Ns, inv)
end

function _slice_by(@nospecialize(elts), offs, card, Ns, inv)
    card′ = card|x0to1
    len = length(Ns)
    R = 1
    sz = 0
    for k = 1:len
        L = R
        @inbounds N = Ns[k]
        @inbounds R = offs[k+1]
        (l, r) = _slice_range(N, R-L, inv)
        sz += r - l + 1
    end
    if sz == length(elts)
        return BlockVector(offs, elts, card′)
    end
    offs′ = Vector{Int}(undef, len+1)
    perm = Vector{Int}(undef, sz)
    @inbounds offs′[1] = top = 1
    R = 1
    for k = 1:len
        L = R
        @inbounds N = Ns[k]
        @inbounds R = offs[k+1]
        (l, r) = _slice_range(N, R-L, inv)
        for j = (L + l - 1):(L + r - 1)
            perm[top] = j
            top += 1
        end
        offs′[k+1] = top
    end
    elts′ = elts[perm]
    return BlockVector(offs′, elts′, card′)
end

@inline _slice_range(n::Int, l::Int, inv::Bool) =
    if !inv
        (1, n >= 0 ? min(l, n) : max(0, l + n))
    else
        (n >= 0 ? min(l + 1, n + 1) : max(1, l + n + 1), l)
    end

@inline _slice_range(::Missing, l::Int, inv::Bool) =
    !inv ? (1, l) : (1, 0)

#
# Ordering extensions.
#

struct LexOrdering{O<:Base.Ordering} <: Base.Ordering
    order::O
end

Base.@propagate_inbounds function Base.lt(o::LexOrdering, a, b)
    b > zero(b) && (a == zero(a) || Base.lt(o.order, a, b))
end

Base.ordtype(o::LexOrdering, vs::AbstractArray) = ordtype(o.order, vs)

ord_eq(o::Base.ForwardOrdering, a, b) = isequal(a, b)
ord_eq(o::Base.ReverseOrdering, a, b) = ord_eq(o.fwd, b, a)
ord_eq(o::Base.By, a, b) = isequal(o.by(a), o.by(b))
ord_eq(o::Base.Lt, a, b) = !o.lt(a, b) && !o.lt(b, a)
Base.@propagate_inbounds ord_eq(o::Base.Perm, a, b) =
    ord_eq(o.order, o.data[a], o.data[b])   # intentionally does not match `lt(::Perm)`
Base.@propagate_inbounds ord_eq(o::LexOrdering, a, b) =
    isequal(a, b) || (a > zero(a) && b > zero(b) && ord_eq(o.order, a, b))

#
# Grouping.
#

unique_by() = Pipeline(unique_by)

function unique_by(::Runtime, input::AbstractVector)
    @assert input isa BlockVector
    card = cardinality(input)
    offs = offsets(input)
    elts = elements(input)
    len = length(elts)
    perm = collect(1:len)
    sep = falses(len+1)
    for off in offs
        sep[off] = true
    end
    if len > 1
        _group_by!(elts, sep, perm, OneTo(len))
    end
    offs_outer, offs_inner = _partition(offs, sep)
    elts′ = elts[perm[view(offs_inner, 1:length(offs_inner)-1)]]
    BlockVector{card}(offs_outer, elts′)
end

sort_by() = Pipeline(sort_by)

function sort_by(::Runtime, input::AbstractVector)
    @assert input isa BlockVector
    card = cardinality(input)
    offs = offsets(input)
    elts = elements(input)
    @assert elts isa TupleVector
    cols = columns(elts)
    @assert length(cols) == 2
    vals, keys = cols
    len = length(elts)
    perm = collect(1:len)
    sep = falses(len+1)
    for off in offs
        sep[off] = true
    end
    if len > 1
        _group_by!(keys, sep, perm, OneTo(len))
    end
    output = BlockVector{card}(offs, vals[perm])
    output
end

group_by() = Pipeline(group_by)

function group_by(::Runtime, input::AbstractVector)
    @assert input isa BlockVector
    card = cardinality(input)
    offs = offsets(input)
    elts = elements(input)
    @assert elts isa TupleVector
    cols = columns(elts)
    @assert length(cols) == 2
    vals, keys = cols
    len = length(elts)
    perm = collect(1:len)
    sep = falses(len+1)
    for off in offs
        sep[off] = true
    end
    if len > 1
        _group_by!(keys, sep, perm, OneTo(len))
    end
    offs_outer, offs_inner = _partition(offs, sep)
    card′ = card & x1toN
    vals′ = BlockVector{card′}(offs_inner, vals[perm])
    sz = length(vals′)
    keys′ = keys[perm[view(offs_inner, 1:sz)]]
    output = BlockVector{card}(offs_outer, TupleVector(sz, AbstractVector[vals′, keys′]))
    output
end

function _group_by!(keys::TupleVector, sep, perm, idxs)
    for col in columns(keys)
        _group_by!(col, sep, perm, idxs)
    end
end

_group_by!(keys::BlockVector{CARD,OneTo{Int}}, sep, perm, idxs) where {CARD} =
    _group_by!(elements(keys), sep, perm, idxs)

function _group_by!(keys::BlockVector{CARD}, sep, perm, idxs) where {CARD}
    if CARD == x1to1
        return _group_by!(elements(keys), sep, perm, idxs)
    end
    offs = offsets(keys)
    elts = elements(keys)
    pos = 0
    idxs′ = Vector{Int}(undef, length(perm))
    done = false
    while !done
        for k = 1:length(perm)
            idx = idxs[k]
            if idx > 0
                l = offs[idx]
                r = offs[idx+1]
                if l + pos < r
                    idxs′[k] = l + pos
                else
                    idxs′[k] = 0
                end
            else
                idxs′[k] = 0
            end
        end
        done = true
        p = idxs′[perm[1]]
        for k = 2:length(perm)
            q = idxs′[perm[k]]
            if !sep[k] && p != q
                done = false
                break
            end
            p = q
        end
        if !done
            _group_by!(elts, sep, perm, idxs′)
        end
        if CARD == x0to1
            done = true
        end
        pos += 1
    end
end

_group_order(keys, ::OneTo) =
    Base.Perm(Base.Forward, keys)

_group_order(keys, idxs) =
    Base.Perm(LexOrdering(Base.Perm(Base.Forward, keys)), idxs)

function _group_by!(keys, sep, perm, idxs)
    o = _group_order(keys, idxs)
    _group_by!(o, sep, perm)
end

function _group_by!(o::Base.Perm, sep, perm)
    l = 1
    for r = 2:length(sep)
        if sep[r]
            if l < r-1
                sort!(perm, l, r-1, QuickSort, o)
            end
            da = o.data[perm[l]]
            for n = l+1:r-1
                db = o.data[perm[n]]
                if !ord_eq(o.order, da, db)
                    sep[n] = true
                end
                da = db
            end
            l = r
        end
    end
end

function _partition(offs, sep)
    sz = 0
    l = 1
    for k = 1:length(offs)-1
        r = offs[k+1]
        for n = l+1:r
            sz += sep[n]
        end
        l = r
    end
    offs_inner = Vector{Int}(undef, sz+1)
    offs_inner[1] = 1
    offs_outer = Vector{Int}(undef, length(offs))
    offs_outer[1] = top = 1
    l = 1
    for k = 1:length(offs)-1
        r = offs[k+1]
        for n = l+1:r
            if sep[n]
                top += 1
                offs_inner[top] = n
            end
        end
        offs_outer[k+1] = top
        l = r
    end
    return (offs_outer, offs_inner)
end

#
# Pipeline matching.
#

macro match_pipeline(ex)
    return esc(_match_pipeline(ex))
end

function _match_pipeline(@nospecialize ex)
    if Meta.isexpr(ex, :call, 3) && ex.args[1] == :~
        val = gensym()
        p = ex.args[2]
        pat = ex.args[3]
        cs = Expr[]
        as = Expr[]
        _match_pipeline!(val, pat, cs, as)
        c = foldl((l, r) -> :($l && $r), cs)
        quote
            local $val = $p
            if $c
                $(as...)
                true
            else
                false
            end
        end
    elseif ex isa Expr
        Expr(ex.head, Any[_match_pipeline(arg) for arg in ex.args]...)
    else
        ex
    end
end

function _match_pipeline!(val, pat, cs, as)
    if pat === :_
        return
    elseif pat isa Symbol
        push!(as, :(local $pat = $val))
        return
    elseif Meta.isexpr(pat, :(::), 2)
        ty = pat.args[2]
        pat = pat.args[1]
        push!(cs, :($val isa $ty))
        _match_pipeline!(val, pat, cs, as)
        return
    elseif Meta.isexpr(pat, :call) && length(pat.args) >= 1 && pat.args[1] isa Symbol
        fn = pat.args[1]
        args = pat.args[2:end]
        push!(cs, :($val isa $DataKnots.Pipeline))
        push!(cs, :($val.op === $DataKnots.$fn))
        _match_pipeline!(:($val.args), args, cs, as)
        return
    elseif Meta.isexpr(pat, :vect)
        push!(cs, :($val isa Vector{$DataKnots.Pipeline}))
        _match_pipeline!(val, pat.args, cs, as)
        return
    end
    error("expected a pipeline pattern; got $(repr(pat))")
end

function _match_pipeline!(val, pats::Vector{Any}, cs, as)
    minlen = 0
    varlen = false
    for pat in pats
        if Meta.isexpr(pat, :..., 1)
            !varlen || error("duplicate vararg pattern in $(repr(:([$pat])))")
            varlen = true
        else
            minlen += 1
        end
    end
    push!(cs, !varlen ? :(length($val) == $minlen) : :(length($val) >= $minlen))
    nearend = false
    for (k, pat) in enumerate(pats)
        if Meta.isexpr(pat, :..., 1)
            pat = pat.args[1]
            k = Expr(:call, :(:), k, Expr(:call, :-, :end, minlen-k+1))
            nearend = true
        elseif nearend
            k = Expr(:call, :-, :end, minlen-k+1)
        end
        _match_pipeline!(:($val[$k]), pat, cs, as)
    end
end


"""
`module  JAC.ImpactExcitation`
    ... a submodel of JAC that contains all methods for computing electron impact excitation cross sections and collision strengths.
"""
module ImpactExcitation

    using Printf, ..AngularMomentum, ..Basics, ..Defaults, ..ManyElectron, ..Radial, ..Nuclear, ..TableStrings

    """
    `struct  ImpactExcitationSettings`  ... defines a type for the details and parameters of computing electron-impact excitation lines.

        + electronEnergies        ::Array{Float64,1}             ... List of impact-energies of the incoming elecgtrons.
        + includeBreit            ::Bool                         ... True if the Breit interaction is to be included, and false otherwise.
        + printBeforeComputation  ::Bool                         ... True, if all energies and lines are printed before their evaluation.
        + selectLines             ::Bool                         ... True if particular lines are selected for the computations.
        + selectedLines           ::Array{Tuple{Int64,Int64},1}  ... List of lines, given by tupels (inital-level, final-level).
        + maxKappa                ::Int64                        ... Maximum kappa value of partial waves to be included.
        + energyShift             ::Float64                      ... An overall energy shift for all transitions |i> --> |f>.
    """
    struct Settings
        electronEnergies          ::Array{Float64,1}
        includeBreit              ::Bool
        printBeforeComputation    ::Bool
        selectLines               ::Bool
        selectedLines             ::Array{Tuple{Int64,Int64},1}
        maxKappa                  ::Int64
        energyShift               ::Float64
    end

    """
    `ImpactExcitation.Settings()`  ... constructor for the default values of electron-impact excitation line computations.
    """
    function Settings()
       Settings( Float64[], false, false, false, Tuple{Int64,Int64}[], 0, 0.)
    end

    # `Base.show(io::IO, settings::ImpactExcitation.Settings)`  ... prepares a proper printout of the variable settings::ImpactExcitation.Settings.
    function Base.show(io::IO, settings::ImpactExcitation.Settings)
        println(io, "electronEnergies:           $(settings.electronEnergies)  ")
        println(io, "includeBreit:               $(settings.includeBreit)  ")
        println(io, "printBeforeComputation:     $(settings.printBeforeComputation)  ")
        println(io, "selectLines:                $(settings.selectLines)  ")
        println(io, "selectedLines:              $(settings.selectedLines)  ")
        println(io, "maxKappa:                   $(settings.maxKappa)  ")
        println(io, "energyShift:                $(settings.energyShift)  ")
    end

    """
    `struct  ImpactExcitation.Channel`
        ... defines a type for a electron-impact excitaiton channel to help characterize the incoming and outgoing (continuum) states of
            many electron-states with a single free electron

        + initialKappa     ::Int64              ... partial-wave of the incoming free electron
        + finalKappa       ::Int64              ... partial-wave of the outgoing free electron
        + symmetry         ::LevelSymmetry      ... total angular momentum and parity of the scattering state
        + initialPhase     ::Float64            ... phase of the incoming partial wave
        + finalPhase       ::Float64            ... phase of the outgoing partial wave
        + amplitude        ::Complex{Float64}   ... Collision amplitude associated with the given channel.
    """
    struct  Channel
        initialKappa       ::Int64
        finalKappa         ::Int64
        symmetry           ::LevelSymmetry
        initialPhase       ::Float64
        finalPhase         ::Float64
        amplitude          ::Complex{Float64}
    end

    # `Base.show(io::IO, channel::ImpactExcitation.Channel)`  ... prepares a proper printout of the variable channel::ImpactExcitation.Channel.
    function Base.show(io::IO, channel::ImpactExcitation.Channel)
        println(io, "initialKappa:       $(channel.initialKappa)  ")
        println(io, "finalKappa:         $(channel.finalKappa)  ")
        println(io, "symmetry:           $(channel.symmetry)  ")
        println(io, "initialPhase:       $(channel.initialPhase)  ")
        println(io, "finalPhase:         $(channel.finalPhase)  ")
        println(io, "amplitude:          $(channel.amplitude)  ")
    end

    """
    `struct  ImpactExcitation.Line`
        ... defines a type for a electron-impact excitation line that may include the definition of channels and their corresponding amplitudes.

        + initialLevel           ::Level         ... initial- (bound-state) level
        + finalLevel             ::Level         ... final- (bound-state) level
        + initialElectronEnergy  ::Float64       ... energy of the incoming (initial-state) free-electron
        + finalElectronEnergy    ::Float64       ... energy of the outgoing (final-state) free-electron
        + crossSection           ::Float64       ... total cross section of this line
        + hasChannels            ::Bool          ... Determines whether the individual scattering (sub-) channels are defined in terms of their
                                                     kappa's, phases and the total angular momentum/parity as well as the amplitude, or not.
        + channels               ::Array{ImpactExcitation.Channel,1}  ... List of ImpactExcitation channels of this line.
    """
    struct  Line
        initialLevel             ::Level
        finalLevel               ::Level
        initialElectronEnergy    ::Float64
        finalElectronEnergy      ::Float64
        crossSection             ::Float64
        hasChannels              ::Bool
        channels                 ::Array{ImpactExcitation.Channel,1}
    end

    """
    `ImpactExcitation.Line()`  ... 'empty' constructor for an electron-impact excitation line between a specified initial and final level.
    """
    function Line()
        Line(Level(), Level(), 0., 0., 0., false, ImpactExcitation.Channel[] )
    end

    """
    `ImpactExcitation.Line(initialLevel::Level, finalLevel::Level, crossSection::Float64)`
        ... constructor for an electron-impact excitation line between a specified initial and final level.
    """
    function Line(initialLevel::Level, finalLevel::Level, crossSection::Float64)
        Line(initialLevel, finalLevel, 0., 0., crossSection, false, ImpactExcitation.Channel[] )
    end

    # `Base.show(io::IO, line::ImpactExcitation.Line)`  ... prepares a proper printout of the variable line::ImpactExcitation.Line.
    function Base.show(io::IO, line::ImpactExcitation.Line)
        println(io, "initialLevel:            $(line.initialLevel)  ")
        println(io, "finalLevel:              $(line.finalLevel)  ")
        println(io, "initialElectronEnergy:   $(line.initialElectronEnergy)  ")
        println(io, "finalElectronEnergy:     $(line.finalElectronEnergy)  ")
        println(io, "crossSection:            $(line.crossSection)  ")
        println(io, "hasChannels:             $(line.hasChannels)  ")
        println(io, "channels:                $(line.channels)  ")
    end

    """
    `ImpactExcitation.computeAmplitudesProperties(line::ImpactExcitation.Line, grid::Radial.Grid, settings::ImpactExcitation.Settings)`
        ... to compute all amplitudes and properties of the given line; a line::ImpactExcitation.Line is returned for which the amplitudes and
            properties are now evaluated.
    """
    function  computeAmplitudesProperties(line::ImpactExcitation.Line, grid::Radial.Grid, settings::ImpactExcitation.Settings)
        global JAC_counter
        newChannels = ImpactExcitation.Channel[]
        for channel in line.channels
            # Generate two continuum orbitals
            JAC_counter = JAC_counter + 1
            if   JAC_counter < 20   println("ImpactExcitation.computeAmplitudesProperties-aa: warning ... no coninuum orbitals are generated.") end
            initialPhase = 0.;    finalPhase = 0.
            # Define a proper continuum basis from the initialLevel.basis and finalLevel.basis with the two continuum orbital
            JAC_counter = JAC_counter + 1
            if   JAC_counter < 20   println("ImpactExcitation.computeAmplitudesProperties-ab: warning ... no continuum bases are generated.") end
            # Compute the transition matrix for the two constructed continuum bases
            JAC_counter = JAC_counter + 1
            if   JAC_counter < 20   println("ImpactExcitation.computeAmplitudesProperties-ac: warning ... no transition matrix is computed.") end
            # matrix    = ImpactExcitation.computeMatrix(channel.multipole, channel.gauge, line.omega, line.finalLevel.basis,
            #                                                line.initialLevel.basis, grid, settings)
            # amplitude = line.finalLevel.mc * matrix * line.initialLevel.mc
            amplitude = 1.0
            push!( newChannels, ImpactExcitation.Channel( channel.initialKappa, channel.finalKappa, channel.symmetry,
                                                          initialPhase, finalPhase, amplitude) )
        end
        # Calculate the electron-impact excitation strength and cross section
        JAC_counter = JAC_counter + 1
        if   JAC_counter < 20   println("ImpactExcitation.computeAmplitudesProperties-ba: warning ... cs and strength set to -1.") end
        crossSection = -1.
        line = ImpactExcitation.Line( line.initialLevel, line.finalLevel, line.initialElectronEnergy, line.finalElectronEnergy,
                                      crossSection, true, newChannels)
        return( line )
    end

    """
    `ImpactExcitation.computeLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid,
                                   settings::ImpactExcitation.Settings; output=true)`
        ... to compute the electron-impact excitation transition amplitudes and all properties as requested by the given settings.
            A list of lines::Array{ImpactExcitation.Lines} is returned.
    """
    function  computeLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, nm::Nuclear.Model, grid::Radial.Grid, settings::ImpactExcitation.Settings;
                           output=true)
        println("")
        printstyled("ImpactExcitation.computePathways(): The computation of electron-impact excitation cross sections starts now ... \n", color=:light_green)
        printstyled("--------------------------------------------------------------------------------------------------------------- \n", color=:light_green)
        println("")
        #
        lines = ImpactExcitation.determineLines(finalMultiplet, initialMultiplet, settings)
        # Display all selected lines before the computations start
        if  settings.printBeforeComputation    ImpactExcitation.displayLines(lines)    end
        # Calculate all amplitudes and requested properties
        newLines = ImpactExcitation.Line[]
        for  line in lines
            newLine = ImpactExcitation.computeAmplitudesProperties(line, grid, settings)
            push!( newLines, newLine)
        end
        # Print all results to screen
        ImpactExcitation.displayResults(lines)
        #
        if    output    return( lines )
        else            return( nothing )
        end
    end

    """
    `ImpactExcitation.computeMatrix(finalBasis::Basis, initialBasis::Basis, settings::ImpactExcitation.Settings)`
        ... to compute the transition matrix  (<finalContinuumCSF_r|| V(e-e) ||initialContinuumCSF_s>)  between the CSF_r from the
            finalContinuumBasis and the CSF_s from the initialContinuumBasis. A (non-quadratic) matrix::Array{Float64,2} with
            dimensions [length(finalContinuumBasis.csfs) x length(initialContinuumBasis.csfs)] is returned. Note that this transition
            matrix is typically specific to just one Eimex channel due to the different energies, partial waves and overall symmetry
            of the scattering states. **Not yet implemented !**
    """
    function computeMatrix(finalBasis::Basis, initialBasis::Basis, settings::ImpactExcitation.Settings)
        error("Not yet implemented.")
    end

    """
    `ImpactExcitation.determineChannels(finalLevel::Level, initialLevel::Level, settings::ImpactExcitation.Settings)`
        ... to determine a list of electron-impact excitation Channels for a transitions from the initial to the final level and by
            taking into account the particular settings of for this computation; an Array{ImpactExcitation.Channel,1} is returned.
    """
    function determineChannels(finalLevel::Level, initialLevel::Level, settings::ImpactExcitation.Settings)
        channels = ImpactExcitation.Channel[];
        symi = LevelSymmetry(initialLevel.J, initialLevel.parity);    symf = LevelSymmetry(finalLevel.J, finalLevel.parity)
        #
        for  inKappa = -(settings.maxKappa+1):settings.maxKappa
            if  inKappa == 0    continue    end
            symtList = AngularMomentum.allowedTotalSymmetries(symi, inKappa)
            for  symt in symtList
                outKappaList = AngularMomentum.allowedKappaSymmetries(symt, symf)
                for  outKappa in outKappaList
                    push!(channels, ImpactExcitation.Channel( inKappa, outKappa, symt, 0., 0.,Complex(0.)) )
                end
            end
        end
        return( channels )
    end

    """
    `ImpactExcitation.determineLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, settings::ImpactExcitation.Settings)`
        ... to determine a list of ImpactExcitation.Line's for transitions between levels from the initial- and final-state multiplets,
            and by taking into account the particular selections and settings for this computation; an Array{ImpactExcitation.Line,1} is
            returned. Apart from the level specification, all physical properties are set to zero during the initialization process.
    """
    function  determineLines(finalMultiplet::Multiplet, initialMultiplet::Multiplet, settings::ImpactExcitation.Settings)
        if    settings.selectLines    selectLines   = true;   selectedLines = Basics.determine("selected lines", settings.selectedLines)
        else                          selectLines   = false
        end

        lines = ImpactExcitation.Line[]
        for  i = 1:length(initialMultiplet.levels)
            for  f = 1:length(finalMultiplet.levels)
                if  selectLines  &&  !(haskey(selectedLines, (i,f)) )    continue   end
                for  en in settings.electronEnergies
                    initialElectronEnergy  = en
                    finalElectronEnergy    = en - (initialMultiplet.levels[i].energy - finalMultiplet.levels[f].energy) + settings.energyShift
                    if  finalElectronEnergy < 0    continue   end

                    channels = ImpactExcitation.determineChannels(finalMultiplet.levels[f], initialMultiplet.levels[i], settings)
                    push!( lines, ImpactExcitation.Line(initialMultiplet.levels[i], finalMultiplet.levels[f],
                                                        initialElectronEnergy, finalElectronEnergy, 0., true, channels) )
                end
            end
        end
        return( lines )
    end

    """
    `ImpactExcitation.displayLines(lines::Array{ImpactExcitation.Line,1})`
        ... to display a list of lines and channels that have been selected due to the prior settings. A neat table of all
            selected transitions and energies is printed but nothing is returned otherwise.
    """
    function  displayLines(lines::Array{ImpactExcitation.Line,1})
        println(" ")
        println("  Selected electron-impact ionization lines:")
        println(" ")
        println("  ", TableStrings.hLine(180))
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(18, "i-level-f"; na=2);                                sb = sb * TableStrings.hBlank(20)
        sa = sa * TableStrings.center(18, "i--J^P--f"; na=3);                                sb = sb * TableStrings.hBlank(22)
        sa = sa * TableStrings.center(12, "f--Energy--i"; na=4)
        sb = sb * TableStrings.center(12, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(12, "Energy e_in"; na=3);
        sb = sb * TableStrings.center(12, TableStrings.inUnits("energy"); na=3)
        sa = sa * TableStrings.center(12, "Energy e_out"; na=4);
        sb = sb * TableStrings.center(12, TableStrings.inUnits("energy"); na=4)
        sa = sa * TableStrings.flushleft(57, "List of partial waves and total symmetries"; na=4)
        sb = sb * TableStrings.flushleft(57, "partial-in [total J^P] partial-out        "; na=4)
        println(sa);    println(sb);    println("  ", TableStrings.hLine(180))
        #
        NoLines = 0
        for  line in lines
            NoLines = NoLines + 1
            sa  = "  ";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                           fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            en = line.finalLevel.energy - line.initialLevel.energy
            sa = sa * @sprintf("%.6e", Defaults.convertUnits("energy: from atomic", en))                          * "    "
            sa = sa * @sprintf("%.6e", Defaults.convertUnits("energy: from atomic", line.initialElectronEnergy))  * "    "
            sa = sa * @sprintf("%.6e", Defaults.convertUnits("energy: from atomic", line.finalElectronEnergy))    * "    "
            kappaInOutSymmetryList = Tuple{Int64,Int64,LevelSymmetry}[]
            for  i in 1:length(line.channels)
                push!( kappaInOutSymmetryList, (line.channels[i].initialKappa, line.channels[i].finalKappa, line.channels[i].symmetry) )
            end
            wa = TableStrings.kappaKappaSymmetryTupels(95, kappaInOutSymmetryList)
            sb = sa * wa[1];    println( sb )
            for  i = 2:length(wa)
                NoLines = NoLines + 1
                sb = TableStrings.hBlank( length(sa) ) * wa[i];    println( sb )
            end
            # Avoid long table printouts
            if NoLines > 100  println("\n  ImpactExcitation.displayLines():  A maximum of 100 lines are printed in this table. \n")
               break
            end
        end
        println("  ", TableStrings.hLine(180))
        #
        return( nothing )
    end

    """
    `ImpactExcitation.displayResults(lines::Array{ImpactExcitation.Line,1})`
        ... to list all results, energies, cross sections, etc. of the selected lines. A neat table is printed but nothing is
            returned otherwise.
    """
    function  displayResults(lines::Array{ImpactExcitation.Line,1})
        println(" ")
        println("  Electron-impact excitation cross sections:")
        println(" ")
        println("  ", TableStrings.hLine(113))
        sa = "  ";   sb = "  "
        sa = "  ";   sb = "  "
        sa = sa * TableStrings.center(18, "i-level-f"; na=2);                                sb = sb * TableStrings.hBlank(20)
        sa = sa * TableStrings.center(18, "i--J^P--f"; na=3);                                sb = sb * TableStrings.hBlank(22)
        sa = sa * TableStrings.center(12, "f--Energy--i"; na=4)
        sb = sb * TableStrings.center(12, TableStrings.inUnits("energy"); na=5)
        sa = sa * TableStrings.center(12, "Energy e_in"; na=3);
        sb = sb * TableStrings.center(12, TableStrings.inUnits("energy"); na=3)
        sa = sa * TableStrings.center(12, "Energy e_out"; na=3);
        sb = sb * TableStrings.center(12, TableStrings.inUnits("energy"); na=3)
        sa = sa * TableStrings.center(15, "Cross section"; na=3)
        sb = sb * TableStrings.center(15, TableStrings.inUnits("cross section"); na=3)
        println(sa);    println(sb);    println("  ", TableStrings.hLine(113))
        #
        for  line in lines
            sa  = "  ";    isym = LevelSymmetry( line.initialLevel.J, line.initialLevel.parity)
                           fsym = LevelSymmetry( line.finalLevel.J,   line.finalLevel.parity)
            sa = sa * TableStrings.center(18, TableStrings.levels_if(line.initialLevel.index, line.finalLevel.index); na=2)
            sa = sa * TableStrings.center(18, TableStrings.symmetries_if(isym, fsym); na=4)
            en = line.finalLevel.energy - line.initialLevel.energy
            sa = sa * @sprintf("%.6e", Defaults.convertUnits("energy: from atomic", en))                          * "    "
            sa = sa * @sprintf("%.6e", Defaults.convertUnits("energy: from atomic", line.initialElectronEnergy))  * "    "
            sa = sa * @sprintf("%.6e", Defaults.convertUnits("energy: from atomic", line.finalElectronEnergy))    * "     "
            sa = sa * @sprintf("%.6e", Defaults.convertUnits("cross section: from atomic", line.crossSection))    * "    "
            println(sa)
        end
        println("  ", TableStrings.hLine(113))
        #
        return( nothing )
    end

end # module


lattice = load_lattice(joinpath(@__DIR__, "CrFe2Ga_qe.in"))
atoms   = load_atoms(  joinpath(@__DIR__, "CrFe2Ga_qe.in"))
atoms   = attach_pseudos(atoms, Cr="hgh/pbe/cr-q14.hgh",
                                Fe="hgh/pbe/fe-q16.hgh",
                                Ga="hgh/pbe/ga-q3.hgh")
magnetic_moments = build_magnetic_moments(atoms; Ga=0.0, Fe=5.0, Cr=5.0)

cases = Dict(
    "maxcond_1e2" => (damping=DFTK.FixedDamping(0.3), diagtol_max=1e-8,
                      acceleration=DFTK.AndersonAcceleration(; maxcond=1e2)),
    "maxcond_1e4" => (damping=DFTK.FixedDamping(0.3), diagtol_max=1e-8,
                      acceleration=DFTK.AndersonAcceleration(; maxcond=1e4)),
    "maxcond_1e6" => (damping=DFTK.FixedDamping(0.3), diagtol_max=1e-8,
                      acceleration=DFTK.AndersonAcceleration(; maxcond=1e6)),
    "maxcond_1e8" => (damping=DFTK.FixedDamping(0.3), diagtol_max=1e-8,
                      acceleration=DFTK.AndersonAcceleration(; maxcond=1e8)),
    "maxcond_1e10" => (damping=DFTK.FixedDamping(0.3), diagtol_max=1e-8,
                       acceleration=DFTK.AndersonAcceleration(; maxcond=1e10)),
)

function load_Fe2CrGa_potential_40(basis)
    guessfile = joinpath(@__DIR__, "Fe2CrGa40_initV.jld2")
    if !isfile(guessfile)
        ρ, ψ0, V = load_guess(basis, joinpath(@__DIR__, "../Fe2CrGa/init.jld2"))
        resultsfile = joinpath(@__DIR__, "init.scfres.json")

        scfres = run_dftk(basis, resultsfile; ρ=ρ, ψ0=ψ0, V=V,
                          mixing=KerkerMixing(), damping=DFTK.FixedDamping(0.3),
                          acceleration=DFTK.AndersonAcceleration(; maxcond=1e6), maxiter=40)
        V = DFTK.total_local_potential(scfres.ham)
        jldopen(guessfile, "w") do file
            file["V"] = V
        end
    end
    jldopen(file -> file["V"], guessfile, "r")
end

config[@__DIR__] = (
    directory        =  @__DIR__,
    lattice          = lattice,
    atoms            = atoms,
    magnetic_moments = magnetic_moments,
    #
    kwargs_model     = (smearing=Smearing.Gaussian(), temperature=0.01),
    Ecut             = 45,
    kgrid            = [13, 13, 13],
    supersampling    = 2.0,
    potential        = load_Fe2CrGa_potential_40,
    kwargs_scf       = (; ),
    mixing           = KerkerMixing(),
    cases            = cases,
)


using Plasm
using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation
using DataStructures

(V, EV) = ([0.4147 0.3513 1.0727 1.0207 0.9985 0.9819 0.9589 0.9551 0.2472 0.3382 0.412 0.6323 0.6883 0.6905 0.7134 0.7493 0.9692 0.96975 0.97765 0.98495 0.98675 1.0061 1.0118 1.0196 1.0212 0.7513 0.8803 0.9053 1.1325 0.8074 0.7588 0.8152 1.0346 1.071 1.2059 1.145 1.12855 1.1198 1.0817 1.0793 0.9135 0.894 0.8909 0.5633 0.5419 0.5337 0.8759 0.8777 0.8805 0.6816 0.678 0.67025 0.6533 0.6396 0.6011 0.5156 0.4937 0.4369 0.3842 0.1423 0.188 0.3885 0.3111 0.3739 0.5279 0.6445 0.4494 0.4046 0.3577 0.5681 0.6194 0.6245 0.6312 0.6457 0.7953 0.7642 0.4687 0.5068 0.4716 0.4184 0.9952 0.9689 0.8672 0.8052 0.8873 1.0581 0.7346 0.6812 0.6802 0.6416 0.5996 0.2089 0.1971 0.1637 0.5901 0.6027 0.6054 0.6599 0.6653 0.6873 0.8794 0.8735 0.8456 0.8338 1.0287 0.9359 0.9698 0.9867 1.0316 1.0844 1.1472 1.192 1.1772 1.1558 1.116 0.9926 1.0564 0.8367 0.6039 0.5565 0.7989 0.868 0.8747 0.8885 0.8912 1.1651 1.2045 1.2104 1.0543 1.039 1.0099 0.4669 0.6515 0.6552 0.6721 0.6753 0.7547 0.7621 0.995 1.0185 1.1216 1.1358 0.1423 0.1606 0.1941 0.5786 0.4388 0.8556 0.8585 0.9516 1.0371 1.1562 0.396 0.3752 1.0705 1.09785 1.1442 1.1637 0.3416 0.2369 0.0492 0.0 1.2505 1.0215 0.9192 0.944 0.9664 1.0162 0.4171 0.3993 0.3606 0.254 0.2095 0.0984 1.2328 1.1039 1.033 1.0084 0.3604 0.3398 0.2462 0.2247 0.2226 0.042 0.5514 0.5684 0.5978 0.66255 0.664 0.6678 0.742 0.4943 0.5545 0.579 0.5919 0.225 0.2006 0.194 0.2391 0.2201 0.7876 0.7645 0.756 0.7482 0.5815 0.5367 0.5016 0.4217 0.3506 0.3548 0.3662 1.0513 0.965 0.9079 0.7189 0.7069 0.6592 0.6005 0.4101 0.4085 0.3773 1.0137 1.00855 0.8911 0.7751 0.7739 0.7613 0.5301 0.4518 0.3103 1.0211 1.1883 1.2607 1.0758 1.0546 1.0327 0.89135 0.8844 0.9508 0.9783 0.9904 1.0214 0.8913 0.8323 0.9119 0.7358 0.7209 0.5202 0.9087 0.8283 1.2488 0.9538 1.1125 0.9496 0.8673 0.4435 0.688 0.7065 0.9402 1.0538 1.0825 1.33 0.6925 0.7525 0.8367 0.3249 0.2557 0.1853 0.9102 1.00805 1.1588 0.6854 0.6691 0.555 0.2412 0.2551 0.2636 0.4074 0.5142 0.3093 0.1165 1.0201 0.7699 0.7295 0.7237 0.8186 0.6238 0.3453 0.3965 0.7716 0.704 0.8378 0.7839 0.6726 0.6944 0.6354 0.8668 0.8745 0.8808 0.9726 1.1682 0.4632 0.419 0.6471 0.6784 0.8065 0.4551 0.7609 0.859 0.7564 1.1209 0.8271 0.54 0.715 0.5082 0.4986 0.3978 0.1423 0.3351 0.4001 0.6971 0.2674 0.2182 0.685 0.6432 0.6226 0.7084 0.6228 0.7452; 0.4904 0.5168 0.9814 0.9485 0.9344 0.9239 0.9094 0.907 1.0989 0.9581 0.8439 0.5094 0.4607 0.4581 0.438 0.4064 0.9969 0.9937 0.9482 0.9061 0.8957 0.7843 0.7513 0.7061 0.6971 1.0753 1.0703 1.0693 1.0605 0.126 0.139 0.0947 0.1359 0.1427 0.4411 0.5693 0.604 0.62245 0.7027 0.7077 0.3983 0.3189 0.3063 1.0409 0.9443 0.9075 0.6632 0.6004 0.4992 0.7729 0.7751 0.78 0.7907 0.7992 0.8234 0.87695 0.8907 0.9263 0.9593 0.7807 0.8221 1.0037 0.3333 0.328 0.3149 1.0586 0.8413 0.7914 0.7392 0.8981 0.7818 0.7703 0.7551 0.7223 0.4622 0.4731 0.577 0.4283 0.4861 0.5735 0.7424 0.741 0.7355 0.7322 0.7214 0.7624 0.9219 0.83545 0.8342 0.7718 0.7041 0.7098 0.7733 0.9528 0.2515 0.2782 0.2841 0.40015 0.4117 0.4586 0.1956 0.2002 0.222 0.2312 0.7575 0.8673 0.3942 0.3897 0.3778 0.3638 0.3471 0.3352 0.2386 0.1788 0.0674 0.8776 0.7764 0.969 0.7684 0.7276 0.8568 0.921 0.9272 0.94 0.4834 0.6226 0.6426 0.6456 0.2169 0.154 0.0345 0.2752 0.1923 0.1906 0.183 0.1816 0.1459 0.1426 0.1651 0.1592 0.1333 0.1297 0.5157 0.4583 0.3529 0.4549 0.4957 0.2896 0.2918 0.3629 0.4282 0.5192 0.7772 0.7094 0.9229 0.8917 0.8389 0.8167 0.0061 0.07755 0.20555 0.2391 0.3048 0.0 0.9417 0.9705 0.9966 1.0545 0.6364 0.8568 0.4165 0.4388 0.4481 0.4713 0.2841 0.379 0.4312 0.4493 0.3815 0.3698 0.3169 0.3048 0.3036 0.2015 0.3637 0.3353 0.2863 0.17805 0.17595 0.17035 0.0459 0.4395 0.3708 0.3428 0.3281 0.2774 0.5455 0.6184 0.0357 0.3895 0.8019 0.7491 0.7296 0.7117 0.0774 0.1127 0.1404 0.2034 0.2004 0.1761 0.1098 0.3943 0.322 0.2742 0.1159 1.0629 1.0605 1.0575 0.2903 0.2855 0.1907 0.6278 0.6221 0.4932 0.366 0.5464 0.4516 0.1362 0.2323 0.406 0.4165 0.6226 0.7119 0.8832 0.8972 0.9118 1.00515 1.01 0.3652 0.2815 0.2447 0.1504 0.5897 0.6359 1.0997 0.2456 0.25 0.3092 0.4978 0.477 0.6228 0.622 0.3856 0.2593 0.1954 0.3299 0.4201 0.4269 1.0199 0.9191 0.8937 0.6741 0.7285 0.7296 0.7311 0.661 0.5967 0.5312 0.869 0.9032 0.9558 0.7758 0.7743 0.764 0.2396 0.45575 0.5878 0.3142 0.1172 0.8137 0.7443 0.2341 0.3234 0.3378 0.3399 1.0191 0.8927 0.1943 0.0961 0.7605 0.6513 0.3507 0.3443 0.3311 0.902 0.604 0.7244 0.9202 1.0818 0.7086 0.698 0.7478 1.0476 0.1394 0.1863 0.3782 0.6345 0.5357 1.0131 0.9593 0.8888 0.9262 0.3152 0.4392 0.8675 1.1366 1.0586 0.7229 0.5083 0.0418 0.196 0.4482 0.4784 0.7245 0.6409 0.2299 0.1181 0.4672 0.4508], Array{Int64,1}[[1, 2], [3, 4], [4, 5], [4, 18], [4, 260], [5, 6], [5, 19], [5, 236], [6, 7], [6, 19], [6, 20], [7, 8], [7, 20], [7, 298], [9, 10], [10, 11], [10, 61], [10, 62], [12, 13], [13, 14], [13, 100], [14, 15], [14, 100], [14, 329], [15, 16], [15, 257], [15, 314], [17, 18], [18, 19], [18, 167], [19, 166], [20, 21], [20, 270], [21, 22], [21, 269], [21, 270], [22, 23], [22, 105], [22, 106], [23, 24], [23, 82], [23, 86], [24, 25], [24, 39], [24, 300], [26, 27], [27, 28], [27, 123], [27, 299], [28, 29], [28, 237], [28, 245], [30, 31], [32, 33], [33, 34], [33, 130], [33, 131], [35, 36], [36, 37], [36, 177], [36, 232], [37, 38], [37, 125], [37, 126], [38, 39], [38, 126], [38, 223], [39, 40], [39, 301], [41, 42], [42, 43], [42, 149], [42, 150], [44, 45], [45, 46], [45, 57], [45, 66], [47, 48], [48, 49], [48, 243], [48, 244], [50, 51], [51, 52], [51, 272], [51, 273], [52, 53], [52, 88], [52, 273], [53, 54], [53, 89], [53, 90], [54, 55], [54, 71], [54, 89], [55, 56], [55, 70], [55, 71], [56, 57], [56, 217], [56, 315], [57, 58], [57, 67], [58, 59], [58, 67], [58, 303], [60, 61], [61, 93], [61, 94], [63, 64], [64, 65], [64, 180], [64, 220], [67, 68], [67, 302], [68, 69], [68, 169], [68, 170], [71, 72], [71, 119], [72, 73], [72, 90], [72, 119], [73, 74], [73, 90], [73, 91], [75, 76], [76, 77], [76, 226], [76, 227], [78, 79], [79, 80], [79, 146], [79, 147], [81, 82], [82, 83], [82, 85], [83, 84], [83, 297], [83, 298], [87, 88], [88, 89], [88, 118], [88, 295], [90, 273], [92, 93], [93, 280], [93, 281], [95, 96], [96, 97], [96, 134], [96, 187], [97, 98], [97, 187], [97, 247], [98, 99], [98, 194], [98, 257], [99, 100], [99, 193], [99, 257], [100, 328], [101, 102], [102, 103], [102, 254], [102, 255], [103, 104], [103, 137], [103, 214], [107, 108], [108, 109], [108, 150], [108, 151], [109, 110], [109, 212], [109, 213], [110, 111], [110, 176], [110, 240], [111, 112], [111, 175], [111, 176], [113, 114], [114, 115], [114, 141], [114, 163], [116, 117], [119, 120], [119, 274], [121, 122], [122, 123], [122, 298], [122, 312], [123, 124], [123, 298], [126, 127], [126, 232], [127, 128], [127, 232], [127, 233], [129, 130], [130, 140], [130, 141], [132, 133], [133, 134], [133, 188], [133, 326], [134, 135], [134, 188], [135, 136], [135, 188], [135, 305], [136, 137], [136, 190], [136, 305], [137, 138], [137, 215], [139, 140], [140, 241], [140, 242], [141, 142], [141, 164], [143, 144], [144, 145], [144, 173], [144, 174], [148, 149], [149, 214], [149, 283], [150, 213], [150, 239], [151, 152], [151, 176], [151, 177], [153, 154], [155, 156], [156, 157], [156, 261], [156, 311], [157, 158], [157, 261], [157, 262], [159, 160], [160, 161], [160, 182], [160, 199], [161, 162], [161, 183], [161, 184], [165, 166], [166, 167], [166, 237], [167, 168], [167, 259], [171, 172], [172, 173], [172, 181], [172, 276], [173, 183], [173, 197], [176, 253], [177, 178], [177, 231], [179, 180], [180, 181], [180, 230], [181, 182], [181, 275], [182, 183], [182, 200], [183, 196], [185, 186], [186, 187], [186, 194], [186, 313], [187, 248], [188, 189], [188, 206], [189, 190], [190, 191], [190, 304], [190, 327], [192, 193], [193, 194], [193, 256], [194, 195], [197, 198], [197, 267], [197, 268], [201, 202], [202, 203], [202, 264], [202, 290], [203, 204], [203, 264], [203, 265], [205, 206], [206, 207], [206, 320], [207, 208], [207, 229], [207, 279], [209, 210], [210, 211], [210, 288], [210, 289], [213, 214], [213, 240], [214, 254], [216, 217], [217, 218], [219, 220], [220, 221], [220, 229], [222, 223], [223, 224], [223, 252], [224, 225], [224, 249], [224, 250], [228, 229], [229, 278], [232, 251], [234, 235], [235, 236], [235, 261], [235, 270], [236, 260], [236, 270], [237, 238], [240, 241], [240, 254], [241, 254], [241, 282], [246, 247], [247, 283], [247, 305], [257, 258], [260, 261], [260, 271], [263, 264], [264, 291], [266, 267], [267, 318], [267, 319], [273, 296], [276, 277], [276, 322], [276, 323], [283, 284], [283, 293], [284, 285], [284, 293], [284, 294], [286, 287], [292, 293], [293, 306], [305, 321], [307, 308], [309, 310], [316, 317], [324, 325]])

model = V,EV
V,EVs = Lar.biconnectedComponent(model)
EW = convert(Lar.Cells, cat(EVs))
VV = [[k] for k=1:size(V,2)]

hpc = Plasm.lar2hpc(V,EW)
cyan = Plasm.color("cyan")
out = cyan(hpc)
Plasm.view( out )

colors = ["orange", "red", "green", "blue", "cyan", "magenta", "yellow",
"white", "black", "purple", "gray", "brown"]

hpccolors = [Plasm.color(key) for key in colors]
comps = [Plasm.lar2hpc(V,EV) for EV in EVs]::Array{Plasm.Hpc,1}

using PyCall
p = PyCall.pyimport("pyplasm")

Plasm.view(p["STRUCT"]([hpccolors[k](comps[k]) for k=1:length(comps)]))