# for models that are "exported" learning networks (return a Node as
# their fit-result; see MLJ docs:
abstract type ProbabilisticNetwork <: Probabilistic end
abstract type DeterministicNetwork <: Deterministic end
abstract type  UnsupervisedNetwork <: Unsupervised end

## ABSTRACT NODES AND SOURCE NODES

abstract type AbstractNode <: MLJType end

# K is :target, :input, :weight or :unknown
mutable struct Source{K} <: AbstractNode
    data  # training data
end

"""
    Xs = source(X)
    ys = source(y, kind=:target)
    ws = source(w, kind=:weight)

Defines, respectively, learning network `Source` objects for wrapping
some input data `X` (`kind=:input`), some target data `y`, or some
sample weights `w`.  The values of each variable `X, y, w` can be
anything, even `nothing`, if the network is for exporting as a
stand-alone model only. For training and testing the unexported network,
appropriate vectors, tables, or other data containers are expected.

    Xs = source()
    ys = source(kind=:target)
    ws = source(kind=:weight)

Define source nodes wrapping `nothing` instead of concrete data. Such
definitions suffice if a learning network is to be exported without
testing.

The calling behaviour of a `Source` object is this:

    Xs() = X
    Xs(rows=r) = selectrows(X, r)  # eg, X[r,:] for a DataFrame
    Xs(Xnew) = Xnew

See also: [`@from_network`](@ref], [`sources`](@ref),
[`origins`](@ref), [`node`](@ref).

"""
function source(X; kind=:input)
    kind in [:input, :target, :weights] ||
        @warn "`Source` kind is neither :input, :target, or :weights. "
    return Source{kind}(X)
end

source(X::Source; args...) = X
source(; args...) = source(nothing; args...)

kind(::Source{k}) where k = k
Base.isempty(X::Source) = X.data == nothing

is_stale(s::Source) = false

# make source nodes callable:
function (s::Source)(; rows=:)
    rows == (:) && return s.data
    return selectrows(s.data, rows)
end
(s::Source)(Xnew) = Xnew

"""
    rebind!(s)

Attach new data `X` to an existing source node `s`.
"""
function rebind!(s::Source, X)
    s.data = X
    return s
end

"""
    origins(N)

Return a list of all origins of a node `N` accessed by a call `N()`.
These are the source nodes of the acyclic directed graph (DAG)
associated with the learning network terminating at `N`, if edges
corresponding to training arguments are excluded. A `Node` object
cannot be called on new data unless it has a unique origin.

Not to be confused with `sources(N)` which refers to the same graph
but without the training edge deletions.

See also: [`node`](@ref), [`source`](@ref).
"""
origins(s::Source) = [s,]

# Function to incrementally append to `nodes1` all elements in
# `nodes2`, excluding any elements previously added (or any element of
# `nodes1` in its initial state):
function _merge!(nodes1, nodes2)
    for x in nodes2
        if !(x in nodes1)
            push!(nodes1, x)
        end
    end
    return nodes1
end

# Note that `fit!` has already been defined for any AbstractMachine in
# machines.jl

mutable struct NodalMachine{M<:Model} <: AbstractMachine{M}
    model::M
    previous_model::M # for remembering the model used in last call to `fit!`
    fitresult
    cache
    args::Tuple{Vararg{AbstractNode}}
    report
    frozen::Bool
    previous_rows   # for remembering the rows used in last call to `fit!`
    state::Int      # number of times fit! has been called on machine
    upstream_state  # for remembering the upstream state in last call to `fit!`

    function NodalMachine{M}(model::M, args::AbstractNode...) where M<:Model

        # check number of arguments for model subtypes:
        !(M <: Supervised) || length(args) > 1 ||
            throw(ArgumentError("Wrong number of arguments. " *
                        "Use `machine(model, X, y)` or "*
                        "`machine(model, X, y, w)` for supervised models."))

        if M <: Unsupervised && !(M <: Static)
            length(args) == 1 ||
                throw(ArgumentError("Wrong number of arguments. Use " *
                        "`machine(model, X)` for an unsupervised model "*
                        "(or `machine(model)` if there are no training "*
                        "arguments (\"static\" tranformers"))
        end

        machine = new{M}(model)
        machine.frozen = false
        machine.state = 0
        machine.args = args

        machine.upstream_state = Tuple([state(arg) for arg in args])

        return machine
    end
end

NodalMachine(model::M, args...) where M<:Model = NodalMachine{M}(model, args...)

# Note: freeze! and thaw! are possibly not used within MLJ itself.

"""
    freeze!(mach)

Freeze the machine `mach` so that it will never be retrained (unless
thawed).

See also [`thaw!`](@ref).
"""
function freeze!(machine::NodalMachine)
    machine.frozen = true
end

"""
    thaw!(mach)

Unfreeze the machine `mach` so that it can be retrained.

See also [`freeze!`](@ref).
"""
function thaw!(machine::NodalMachine)
    machine.frozen = false
end

"""
    is_stale(mach)

Check if a machine `mach` is stale.

See also [`fit!`](@ref)
"""
function is_stale(machine::NodalMachine)
    !isdefined(machine, :fitresult) ||
        machine.model != machine.previous_model ||
        reduce(|,[is_stale(arg) for arg in machine.args])
end

"""
    state(mach)

Return the state of a machine, `mach`.
"""
state(machine::NodalMachine) = machine.state

## NODES

struct Node{T<:Union{NodalMachine, Nothing}} <: AbstractNode
    operation   # that can be dispatched on a fit-result (eg, `predict`) or a static operation
    machine::T  # is `nothing` for static operations
    args::Tuple{Vararg{AbstractNode}}  # nodes where `operation` looks for its arguments
    origins::Vector{Source}
    nodes::Vector{AbstractNode}  # all upstream nodes, order consistent with DAG order (the node "tape")

    function Node{T}(operation,
                     machine::T,
                     args::AbstractNode...) where {M<:Model, T<:Union{NodalMachine{M},Nothing}}

        # check the number of arguments:
        if machine === nothing && isempty(args)
            throw(error("`args` in `Node(::Function, args...)` must be non-empty. "))
        end

        origins_ = unique(vcat([origins(arg) for arg in args]...))
        # length(origins_) == 1 ||
        #     @warn "A node referencing multiple origins when called " *
        #           "has been defined:\n$(origins_). "

        # initialize the list of upstream nodes:
        nodes_ = AbstractNode[]

        # merge the lists from arguments:
        for arg in args
            _merge!(nodes_, nodes(arg))
        end

        # merge the lists from training arguments:
        if machine !== nothing
            for arg in machine.args
                _merge!(nodes_, nodes(arg))
            end
        end

        return new{T}(operation, machine, args, origins_, nodes_)
    end
end

origins(X::Node) = X.origins

"""
    nodes(N)

Return all nodes upstream of a node `N`, including `N` itself, in an
order consistent with the extended directed acyclic graph of the
network. Here "extended" means edges corresponding to training
arguments are included.
"""
nodes(X::Node) = AbstractNode[X.nodes..., X]
nodes(S::Source) = AbstractNode[S, ]

"""
    is_stale(N)

Check if a node `N` is stale.
"""
function is_stale(X::Node)
    (X.machine !== nothing && is_stale(X.machine)) ||
        reduce(|, [is_stale(arg) for arg in X.args])
end

state(s::Source) = (state = 0, )

"""
    state(N)

Return the state of a node `N`
"""
function state(W::Node)
    mach = W.machine
    state_ = W.machine === nothing ? 0 : state(W.machine)
    if mach === nothing
        trainkeys   = []
        trainvalues = []
    else
        trainkeys   = (Symbol("train_arg", i) for i in eachindex(mach.args))
        trainvalues = (state(arg) for arg in mach.args)
    end
    keys = tuple(:state,
                 (Symbol("arg", i) for i in eachindex(W.args))...,
                 trainkeys...)
    values = tuple(state_,
                   (state(arg) for arg in W.args)...,
                   trainvalues...)
    return NamedTuple{keys}(values)
end

# autodetect type parameter:
Node(operation, machine::M, args...) where M <: Union{NodalMachine,Nothing} =
    Node{M}(operation, machine, args...)

# constructor for static operations:
Node(operation, args::AbstractNode...) = Node(operation, nothing, args...)

# make nodes callable:
(y::Node)(; rows=:) =
    (y.operation)(y.machine, [arg(rows=rows) for arg in y.args]...)
function (y::Node)(Xnew)
    length(y.origins) == 1 ||
        error("Node $y has multiple origins and cannot be called "*
              "on new data. ")
    return (y.operation)(y.machine, [arg(Xnew) for arg in y.args]...)
end

# and for the special case of static operations:
(y::Node{Nothing})(; rows=:) =
    (y.operation)([arg(rows=rows) for arg in y.args]...)
function (y::Node{Nothing})(Xnew)
    length(y.origins) == 1 ||
        error("Node $y has multiple origins and cannot be called "*
              "on new data. ")
    return (y.operation)([arg(Xnew) for arg in y.args]...)
end

"""
    fit!(N::Node; rows=nothing, verbosity::Int=1, force::Bool=false)

Train all machines in the learning network terminating at node `N`, in an
appropriate order. These machines are those returned by `machines(N)`.

"""
function fit!(y::Node; rows=nothing, verbosity::Int=1,
              force::Bool=false)

    if rows === nothing rows = (:) end

    # get non-source nodes:
    nodes_ = filter(nodes(y)) do n
        n isa Node
    end

    # get machines to fit:
    machines = map(n -> n.machine, nodes_)
    machines = filter(unique(machines)) do mach
        mach !== nothing
    end

    for mach in machines
        fit!(mach; rows=rows, verbosity=verbosity, force=force)
    end

    return y
end

# allow arguments of `Nodes` and `NodalMachine`s to appear
# at REPL:
istoobig(d::Tuple{AbstractNode}) = length(d) > 10

# overload show method

function _recursive_show(stream::IO, X::AbstractNode)
    if X isa Source
        printstyled(IOContext(stream, :color=>SHOW_COLOR), handle(X), color=:blue)
    else
        detail = (X.machine === nothing ? "(" : "($(handle(X.machine)), ")
        operation_name = typeof(X.operation).name.mt.name
        print(stream, operation_name, "(")
        if X.machine !== nothing
            color = (X.machine.frozen ? :red : :green)
            printstyled(IOContext(stream, :color=>SHOW_COLOR), handle(X.machine),
                        bold=SHOW_COLOR)
            print(stream, ", ")
        end
        n_args = length(X.args)
        counter = 1
        for arg in X.args
            _recursive_show(stream, arg)
            counter >= n_args || print(stream, ", ")
            counter += 1
        end
        print(stream, ")")
    end
end

function Base.show(stream::IO, ::MIME"text/plain", X::Node)
    id = objectid(X)
    description = string(typeof(X).name.name)
    str = "$description @ $(handle(X))"
    printstyled(IOContext(stream, :color=>SHOW_COLOR), str, color=:blue)
#    if !(X isa Source)
        print(stream, " = ")
        _recursive_show(stream, X)
#    end
end

function Base.show(stream::IO, ::MIME"text/plain", machine::NodalMachine)
    id = objectid(machine)
    description = string(typeof(machine).name.name)
    str = "$description @ $(handle(machine))"
    printstyled(IOContext(stream, :color=>SHOW_COLOR), str, bold=SHOW_COLOR)
    print(stream, " = ")
    print(stream, "machine($(machine.model), ")
    n_args = length(machine.args)
    counter = 1
    for arg in machine.args
        printstyled(IOContext(stream, :color=>SHOW_COLOR), handle(arg), bold=SHOW_COLOR)
        counter >= n_args || print(stream, ", ")
        counter += 1
    end
    print(stream, ")")
end

## SYNTACTIC SUGAR FOR LEARNING NETWORKS

"""
    N = node(f::Function, args...)

Defines a `Node` object `N` wrapping a static operation `f` and arguments
`args`. Each of the `n` elements of `args` must be a `Node` or `Source`
object. The node `N` has the following calling behaviour:

    N() = f(args[1](), args[2](), ..., args[n]())
    N(rows=r) = f(args[1](rows=r), args[2](rows=r), ..., args[n](rows=r))
    N(X) = f(args[1](X), args[2](X), ..., args[n](X))

    J = node(f, mach::NodalMachine, args...)

Defines a dynamic `Node` object `J` wrapping a dynamic operation `f`
(`predict`, `predict_mean`, `transform`, etc), a nodal machine `mach` and
arguments `args`. Its calling behaviour, which depends on the outcome of
training `mach` (and, implicitly, on training outcomes affecting its
arguments) is this:

    J() = f(mach, args[1](), args[2](), ..., args[n]())
    J(rows=r) = f(mach, args[1](rows=r), args[2](rows=r), ..., args[n](rows=r))
    J(X) = f(mach, args[1](X), args[2](X), ..., args[n](X))

Generally `n=1` or `n=2` in this latter case.

    predict(mach, X::AbsractNode, y::AbstractNode)
    predict_mean(mach, X::AbstractNode, y::AbstractNode)
    predict_median(mach, X::AbstractNode, y::AbstractNode)
    predict_mode(mach, X::AbstractNode, y::AbstractNode)
    transform(mach, X::AbstractNode)
    inverse_transform(mach, X::AbstractNode)

Shortcuts for `J = node(predict, mach, X, y)`, etc.

Calling a node is a recursive operation which terminates in the call
to a source node (or nodes). Calling nodes on *new* data `X` fails unless the
number of such nodes is one.

See also: [`source`](@ref), [`origins`](@ref).

"""
node = Node

# if `args` in `machine(model, args...)` is empty or includes an
# `AbstractNode`, a `NodalMachine` is to be created.
machine(m::Unsupervised, a::AbstractNode...) =
    NodalMachine(m, a...)
# machine(m::Static, a1::AbstractNode, a::AbstractNode...) =
#     NodalMachine(m, a...)
machine(m::Supervised, a1::AbstractNode, a::AbstractNode...) =
    NodalMachine(m, a1, a...)
machine(model::Model, X, y::AbstractNode) = NodalMachine(model, source(X), y)
machine(model::Model, X::AbstractNode, y) = NodalMachine(model, X, source(y))

MMI.matrix(X::AbstractNode)      = node(matrix, X)
MMI.table(X::AbstractNode)       = node(table, X)
Base.vcat(args::AbstractNode...) = node(vcat, args...)
Base.hcat(args::AbstractNode...) = node(hcat, args...)

Statistics.mean(X::AbstractNode)   = node(v->mean.(v), X)
Statistics.median(X::AbstractNode) = node(v->median.(v), X)
StatsBase.mode(X::AbstractNode)    = node(v->mode.(v), X)

Base.log(X::AbstractNode) = node(v->log.(v), X)
Base.exp(X::AbstractNode) = node(v->exp.(v), X)

+(y1::AbstractNode, y2::AbstractNode) = node(+, y1, y2)
+(y1, y2::AbstractNode) = node(+, y1, y2)
+(y1::AbstractNode, y2) = node(+, y1, y2)

*(lambda::Real, y::AbstractNode) = node(y->lambda*y, y)

"""
    selectcols(X::AbstractNode, c)

Returns `Node` object `N` such that `N() = selectcols(X(), c)`.
"""
MMI.selectcols(X::AbstractNode, r) = node(XX->selectcols(XX, r), X)

"""
    selectrows(X::AbstractNode, r)

Returns a `Node` object `N` such that `N() = selectrows(X(), r)` (and
`N(rows=s) = selectrows(X(rows=s), r)`).

"""
MMI.selectrows(X::AbstractNode, r) = node(XX->selectrows(XX, r), X)

# for accessing and setting model hyperparameters at node:
getindex(n::Node{<:NodalMachine{<:Model}}, s::Symbol) =
    getproperty(n.machine.model, s)
setindex!(n::Node{<:NodalMachine{<:Model}}, v, s::Symbol) =
    setproperty!(n.machine.model, s, v)

## INSPECTING LEARNING NETWORKS

"""
    tree(N)

Return a description of the tree `N` defined by the learning network
terminating at a given node.

"""
function tree(W::Node)
    mach = W.machine
    if mach === nothing
        value2 = nothing
        endkeys = []
        endvalues = []
    else
        value2 = mach.model
        endkeys = (Symbol("train_arg", i) for i in eachindex(mach.args))
        endvalues = (tree(arg) for arg in mach.args)
    end
    keys = tuple(:operation,  :model,
                 (Symbol("arg", i) for i in eachindex(W.args))...,
                 endkeys...)
    values = tuple(W.operation, value2,
                   (tree(arg) for arg in W.args)...,
                   endvalues...)
    return NamedTuple{keys}(values)
end
tree(s::Source) = (source = s,)

"""
   args(tree; train=false)

Return a vector of the top level args of the tree associated with a node.
If `train=true`, return the `train_args`.
"""
function args(tree; train=false)
    keys_ = filter(keys(tree) |> collect) do key
        match(Regex("^$("train_"^train)arg[0-9]*"), string(key)) !== nothing
    end
    return [getproperty(tree, key) for key in keys_]
end

"""
    models(N::AbstractNode)

A vector of all models referenced by a node `N`, each model appearing
exactly once.

"""
function models(W::AbstractNode)
    models_ = filter(flat_values(tree(W)) |> collect) do model
        model isa Model
    end
    return unique(models_)
end

"""
    sources(N::AbstractNode; kind=:any)

A vector of all sources referenced by calls `N()` and `fit!(N)`. These
are the sources of the directed acyclic graph associated with the
learning network terminating at `N`, including training edges. The
return value can be restricted further by specifying `kind=:input`,
`kind=:target`, `kind=:weight`, etc.

Not to be confused with `origins(N)` which refers to the same graph with edges
corresponding to training arguments deleted.

See also: [`origins`](@ref), [`source`](@ref).
"""
function sources(W::AbstractNode; kind=:any)
    if kind == :any
        sources_ = filter(flat_values(tree(W)) |> collect) do value
            value isa Source
        end
    else
        sources_ = filter(flat_values(tree(W)) |> collect) do value
            value isa Source{kind}
        end
    end
    return unique(sources_)
end

"""
    machines(N)

List all machines in the learning network terminating at node `N`.

"""
function machines(W::Node)
    if W.machine === nothing
        return vcat((machines(arg) for arg in W.args)...) |> unique
    else
        return vcat(Any[W.machine, ],
                    (machines(arg) for arg in W.args)...,
                    (machines(arg) for arg in W.machine.args)...) |> unique
    end
end
machines(W::Source) = Any[]

## MANIPULATING LEARNING NETWORKS

"""
    reset!(N::Node)

Place the learning network terminating at node `N` into a state in
which `fit!(N)` will retrain from scratch all machines in its
dependency tape. Does not actually train any machine or alter
fit-results. (The method simply resets `m.state` to zero, for every
machine `m` in the network.)

"""
function reset!(W::Node)
    for mach in machines(W)
        mach.state = 0 # to do: replace with dagger object
    end
end


module NetCDF
using Compat
using Formatting
using Base.Cartesian
include("netcdf_c.jl")
import Base.show
export NcDim,NcVar,NcFile,ncread,ncread!,ncwrite,nccreate,ncsync,ncinfo,ncclose,ncputatt,NC_BYTE,NC_SHORT,NC_INT,NC_FLOAT,NC_DOUBLE,NC_STRING,ncgetatt,NC_NOWRITE,NC_WRITE,NC_CLOBBER,NC_NOCLOBBER,NC_CLASSIC_MODEL,NC_64BIT_OFFSET,NC_NETCDF4
NC_VERBOSE=false
#Some constants

function __init__()
  global const jltype2nctype=@Compat.Dict(Int8=>NC_BYTE,
                   Int16=>NC_SHORT,
                   Int32=>NC_INT,
                   Int64=>NC_INT64,
                   Float32=>NC_FLOAT,
                   Float64=>NC_DOUBLE,
                   UInt8=>NC_CHAR,
                   ASCIIString=>NC_STRING)

  global const nctype2jltype=@Compat.Dict(NC_BYTE=>Int8,
                    NC_SHORT=>Int16,
                    NC_INT=>Int32,
                    NC_LONG=>Int64,
                    NC_FLOAT=>Float32,
                    NC_DOUBLE=>Float64,
                    NC_CHAR=>UInt8,
                    NC_STRING=>ASCIIString)

  global const nctype2string=@Compat.Dict(NC_BYTE=>"BYTE",
                   NC_SHORT=>"SHORT",
                   NC_INT=>"INT",
                   NC_FLOAT=>"FLOAT",
                   NC_DOUBLE=>"DOUBLE",
                   NC_INT64=>"INT64",
                   NC_STRING=>"STRING")
end

"""The type `NcDim` Represents an NcDim object representing a NetCDF dimension of name `name` with dimension values.
"""
type NcDim
  ncid::Int32
  dimid::Int32
  varid::Int32
  name::ASCIIString
  dimlen::UInt
  vals::AbstractArray
  atts::Dict
end

"""

    NcDim(name::String,dimlength::Integer;values::Union{AbstractArray,Number}=[],atts::Dict{Any,Any}=Dict{Any,Any}())`
This constructor creates an NcDim object with the name `name` and length `dimlength`.
"""
function NcDim(name::AbstractString,dimlength::Integer;values::Union{AbstractArray,Number}=[],atts::Dict{Any,Any}=Dict{Any,Any}())
    (length(values)>0 && length(values)!=dimlength) ? error("Dimension value vector must have the same length as dimlength!") : nothing
    NcDim(-1,-1,-1,ascii(name),dimlength,values,atts)
  end

"""
    NcDim(name::AbstractString,dimlength::Integer;values::Union{AbstractArray,Number}=[],atts::Dict{Any,Any}=Dict{Any,Any}())
This constructor creates an NcDim object with the name `name` and and associated values `values`. Upon creation of the NetCDF file a
dimension variable will be generated and the values be written to this variable. Optionally a Dict of attributes can be supplied.
"""
NcDim(name::AbstractString,values::AbstractArray;atts::Dict=Dict{Any,Any}())=
  NcDim(name,length(values),values=values,atts=atts)
NcDim(name::AbstractString,values::AbstractArray,atts::Dict)=
  NcDim(name,length(values),values=values,atts=atts)

"""
The type `NcVar{T,N}` represents a NetCDF variable. It is a subtype of AbstractArray{T,N}, so normal indexing using `[]`
will work for reding and writing data to and from a NetCDF file. `NcVar` objects can be returned by the `open` function , by
indexing an `NcFIle` object (e.g. `myfile["temperature"]`) or, when creating a new file, constructing it directly.
"""
type NcVar{T,N} <: AbstractArray{T,N}
  ncid::Int32
  varid::Int32
  ndim::Int32
  natts::Int32
  nctype::Int32
  name::ASCIIString
  dimids::Vector{Int32}
  dim::Vector{NcDim}
  atts::Dict
  compress::Int32
end

Base.convert{S,T,N}(::Type{NcVar{T,N}},v::NcVar{S,N})=NcVar{T,N}(v.ncid,v.varid,v.ndim,v.natts,v.nctype,v.name,v.dimids,v.dim,v.atts,v.compress)

"""
    NcVar(name::AbstractString,dimin::Union{NcDim,Array{NcDim,1}};atts::Dict{Any,Any}=Dict{Any,Any}(),t::Union{DataType,Integer}=Float64,compress::Integer=-1)
Here varname is the name of the variable, dimlist an array of type NcDim holding the dimensions associated to the variable, varattributes is a Dict
holding pairs of attribute names and values. t is the data type that should be used for storing the variable. You can either specify a julia type
(Int16, Int32, Float32, Float64) which will be translated to (NC_SHORT, NC_INT, NC_FLOAT, NC_DOUBLE) or directly specify one of the latter list.
You can also set the compression level of the variable by setting compress to a number in the range 1..9 This has only an effect in NetCDF4 files.
"""
#Some additional constructors
function NcVar(name::AbstractString,dimin::Union{NcDim,Array{NcDim,1}};atts::Dict{Any,Any}=Dict{Any,Any}(),t::Union{DataType,Integer}=Float64,compress::Integer=-1)
  dim = isa(dimin,NcDim) ? NcDim[dimin] : dimin
  return NcVar{typeof(t)==DataType ? t : nctype2jltype[t],length(dim)}(-1,-1,length(dim),length(atts), typeof(t)==DataType ? jltype2nctype[t] : t,name,Array(Int32,length(dim)),dim,atts,compress)
end
NcVar(name::AbstractString,dimin::Union{NcDim,Array{NcDim,1}},atts::Dict{Any,Any},t::Union{DataType,Integer}=Float64)=NcVar{typeof(t)==DataType ? t : nctype2jltype[t],length(dimin)}(-1,-1,length(dimin),length(atts), typeof(t)==DataType ? jltype2nctype[t] : t,name,Array(Int,length(dimin)),dimin,atts,-1)

#Array methods
@generated function Base.size{T,N}(a::NcVar{T,N})
  :(@ntuple($N,i->Int(a.dim[i].dimlen)))
end
typealias IndR Union{Integer,UnitRange,Colon}
typealias ArNum Union{AbstractArray,Number}

Base.linearindexing(::NcVar)=Base.LinearSlow()
Base.getindex{T}(v::NcVar{T,1},i1::IndR)=readvar(v,i1)
Base.getindex{T}(v::NcVar{T,2},i1::IndR,i2::IndR)=readvar(v,i1,i2)
Base.getindex{T}(v::NcVar{T,3},i1::IndR,i2::IndR,i3::IndR)=readvar(v,i1,i2,i3)
Base.getindex{T}(v::NcVar{T,4},i1::IndR,i2::IndR,i3::IndR,i4::IndR)=readvar(v,i1,i2,i3,i4)
Base.getindex{T}(v::NcVar{T,5},i1::IndR,i2::IndR,i3::IndR,i4::IndR,i5::IndR)=readvar(v,i1,i2,i3,i4,i5)
Base.getindex{T}(v::NcVar{T,6},i1::IndR,i2::IndR,i3::IndR,i4::IndR,i5::IndR,i6::IndR)=readvar(v,i1,i2,i3,i4,i5,i6)

Base.setindex!{T}(v::NcVar{T,1},x::ArNum,i1::IndR)=putvar(v,x,i1)
Base.setindex!{T}(v::NcVar{T,2},x::ArNum,i1::IndR,i2::IndR)=putvar(v,x,i1,i2)
Base.setindex!{T}(v::NcVar{T,3},x::ArNum,i1::IndR,i2::IndR,i3::IndR)=putvar(v,x,i1,i2,i3)
Base.setindex!{T}(v::NcVar{T,4},x::ArNum,i1::IndR,i2::IndR,i3::IndR,i4::IndR)=putvar(v,x,i1,i2,i3,i4)
Base.setindex!{T}(v::NcVar{T,5},x::ArNum,i1::IndR,i2::IndR,i3::IndR,i4::IndR,i5::IndR)=putvar(v,x,i1,i2,i3,i4,i5)
Base.setindex!{T}(v::NcVar{T,6},x::ArNum,i1::IndR,i2::IndR,i3::IndR,i4::IndR,i5::IndR,i6::IndR)=putvar(v,x,i1,i2,i3,i4,i5,i6)

"""
The type `NcFile` represents a link to a NetCDF file. I can be retrieved by `NetCDF.open` or by `NetCDF.create`.
"""
type NcFile
  ncid::Int32
  nvar::Int32
  ndim::Int32
  ngatts::Int32
  vars::Dict{ASCIIString,NcVar}
  dim::Dict{ASCIIString,NcDim}
  gatts::Dict
  nunlimdimid::Int32
  name::ASCIIString
  omode::UInt16
  in_def_mode::Bool
end
#Define getindex method to retrieve a variable
Base.getindex(nc::NcFile,i::AbstractString)=haskey(nc.vars,i) ? nc.vars[i] : error("NetCDF file $(nc.name) does not have a variable named $(i)")

include("netcdf_helpers.jl")

const currentNcFiles=Dict{ASCIIString,NcFile}()

readvar!(nc::NcFile, varname::AbstractString, retvalsa::Array;start::Vector=defaultstart(nc[varname]),count::Vector=defaultcount(nc[varname]))=readvar!(nc[varname],retvalsa,start=start,count=count)

"""
readvar!(v::NcVar, d::Array;start::Vector=ones(UInt,ndims(d)),count::Vector=size(d))
is the mutating form of `readvar` which expects a pre-allocated array d, where the data are written to. It reads the values of the NcVar object `v`
writing the values to the preallocated array `d`.
If only parts of the variable are to be read, you can provide optionally `start` and `count`, which enable you to read blocks of data. `start` and `count` have the same
length as the number of variable dimensions. start gives the initial index for each dimension, while count gives the number of indices to be read along each dimension.
As a special case, setting a value in count to -1 will cause the function to read all values along this dimension.
"""
function readvar!(v::NcVar, retvalsa::Array;start::Vector=defaultstart(v),count::Vector=defaultcount(v))

  length(start) == v.ndim || error("Length of start ($(length(start))) must equal the number of variable dimensions ($(v.ndim))")
  length(count) == v.ndim || error("Length of start ($(length(count))) must equal the number of variable dimensions ($(v.ndim))")

  p=preparestartcount(start,count,v)

  length(retvalsa) != p && error("Size of output array ($(length(retvalsa))) does not equal number of elements to be read ($p)!")

  nc_get_vara_x!(v.ncid,v.varid,gstart,gcount,retvalsa)

  retvalsa
end
#readvar{T<:Integer}(nc::NcFile,varname::AbstractString,start::Array{T,1},count::Array{T,1})=readvar(nc,varname,start=start,count=count)

readvar(nc::NcFile, varname::AbstractString;start::Vector=defaultstart(nc[varname]),count::Vector=defaultcount(nc[varname]))=readvar(nc[varname],start=start,count=count)

"""
readvar{T,N}(v::NcVar{T,N};start::Vector=ones(Int,v.ndim),count::Vector=zeros(Int,v.ndim))
`readvar` reads the values of the NcVar object `v` and returns them.
If only parts of the variable are to be read, you can provide optionally `start` and `count`, which enable you to read blocks of data. `start` and `count` have the same
length as the number of variable dimensions. start gives the initial index for each dimension, while count gives the number of indices to be read along each dimension.
As a special case, setting a value in count to -1 will cause the function to read all values along this dimension.
"""
function readvar{T,N}(v::NcVar{T,N};start::Vector=defaultstart(v),count::Vector=defaultcount(v))
    retvalsa = Array(T,count...)
    readvar!(v, retvalsa, start=start, count=count)
    return retvalsa
end

"""
    readvar{T,N}(v::NcVar{T,N},I::IndR...)
Reading data from NetCDF file with array-style indexing. Integers and UnitRanges are valid indices for each dimension.
"""
function readvar{T,N}(v::NcVar{T,N},I::IndR...)
    count=ntuple(i->counti(I[i],v.dim[i].dimlen),length(I))
    retvalsa = zeros(T,count)
    readvar!(v, retvalsa, I...)
    return retvalsa
end

# Here are some functions for array-style indexing readvar
#For single indices
@generated function readvar{T,N}(v::NcVar{T,N},I::Integer...)
  N==length(I) || error("Dimension mismatch")
  quote
    checkbounds(v,I...)
    @nexprs $N i->gstart[v.ndim+1-i]=I[i]-1
    nc_get_var1_x(v.ncid,v.varid,gstart,T)::T
  end
end

firsti(i::Integer,l::Integer)=i-1
counti(i::Integer,l::Integer)=1
firsti(r::UnitRange,l::Integer)=start(r)-1
counti(r::UnitRange,l::Integer)=length(r)
firsti(r::Colon,l::Integer)=0
counti(r::Colon,l::Integer)=Int(l)
# For ranges
@generated function readvar!{T,N}(v::NcVar{T,N}, retvalsa::Array,I::IndR...)

  N==length(I) || error("Dimension mismatch")

  quote
    checkbounds(v,I...)
    @nexprs $N i->gstart[v.ndim+1-i]=firsti(I[i],v.dim[i].dimlen)
    @nexprs $N i->gcount[v.ndim+1-i]=counti(I[i],v.dim[i].dimlen)
    p=1
    @nexprs $N i->p=p*gcount[v.ndim+1-i]
    length(retvalsa) != p && error(string("Size of output array ($(length(retvalsa))) does not equal number of elements to be read (",p,")!"))
    nc_get_vara_x!(v.ncid,v.varid,gstart,gcount,retvalsa)
    retvalsa
  end
end

for (t,ending,arname) in funext
    fname = symbol("nc_get_vara_$ending")
    fname1 = symbol("nc_get_var1_$ending")
    arsym=symbol(arname)
    @eval nc_get_vara_x!(ncid::Integer,varid::Integer,start::Vector{UInt},count::Vector{UInt},retvalsa::Array{$t})=$fname(ncid,varid,start,count,retvalsa)
    @eval nc_get_var1_x(ncid::Integer,varid::Integer,start::Vector{UInt},::Type{$t})=begin $fname1(ncid,varid,start,$(arsym)); $(arsym)[1] end
end

function nc_get_vara_x!(ncid::Integer,varid::Integer,start::Vector{UInt},count::Vector{UInt},retvalsa::Array{AbstractString})
  retvalsa_c=Array(Ptr{UInt8},length(retvalsa))
  nc_get_vara_string(ncid,varid,start,count,retvalsa_c)
  for i=1:length(retvalsa)
    retvalsa[i]=bytestring(retvalsa_c[i])
  end
  nc_free_string(length(retvalsa_c),retvalsa_c)
  retvalsa
end

function nc_get_var1_x(ncid::Integer,varid::Integer,start::Vector{UInt},::Type{AbstractString})
  retvalsa_c=Array(Ptr{UInt8},1)
  nc_get_var1_string(ncid,varid,start,retvalsa_c)
  retval=bytestring(retvalsa_c[1])
  nc_free_string(1,retvalsa_c)
  retval
end

function putatt(ncid::Integer,varid::Integer,atts::Dict)
  for a in atts
    name=a[1]
    val=a[2]
    nc_put_att(ncid,varid,name,val)
  end
end

function putatt(nc::NcFile,varname::AbstractString,atts::Dict)
  varid = haskey(nc.vars,varname) ? nc.vars[varname].varid : NC_GLOBAL
  chdef=false
  if (!nc.in_def_mode)
    chdef=true
    nc_redef(nc.ncid)
  end
  putatt(nc.ncid,varid,atts)
  chdef ? nc_enddef(nc.ncid) : nothing
end

"""
    ncputatt(nc::String,varname::String,atts::Dict)
Writes name-value pairs of attributes given in `atts` to the file `nc`. If `varname` is a valid variable indentifier, the attribute(s) will be written to this variable.
Otherwise, global attributes will be generated. This function automatically overwrites existing attributes.
"""
function ncputatt(nc::AbstractString,varname::AbstractString,atts::Dict)
  nc = haskey(currentNcFiles,abspath(nc)) ? currentNcFiles[abspath(nc)] : open(nc,mode=NC_WRITE)
  if (nc.omode==NC_NOWRITE)
    fil=nc.name
    close(nc)
    println("reopening file in WRITE mode")
    open(fil,mode=NC_WRITE)
  end
  putatt(nc,varname,atts)
end

putvar(nc::NcFile,varname::AbstractString,vals::Array;start=ones(Int,length(size(vals))),count=[size(vals)...])=putvar(nc[varname],vals,start=start,count=count)

"""
    putvar(v::NcVar,vals::Array;start::Vector=ones(Int,length(size(vals))),count::Vector=[size(vals)...])
This function writes the values from the array `vals` to a netcdf file. `v` is the NcVar handle of the respective variable and `vals` an array
with the same dimension as the variable in the netcdf file. The optional parameter start gives the first index in each dimension along which the writing should
begin. It is assumed that the input array vals has the same number of dimensions as the and writing happens along these dimensions. However, you can specify
the number of values to be written along each dimension by adding an optional count argument, which is a vector whose length equals the number of variable dimensions.
"""
function putvar(v::NcVar,vals::Array;start::Vector=ones(Int,length(size(vals))),count::Vector=[size(vals)...])
  p=preparestartcount(start,count,v)
  nc_put_vara_x(v.ncid,v.varid,gstart,gcount,vals)
end

@generated function putvar{T,N}(v::NcVar{T,N},val::Any,I::IndR...)

  N==length(I) || error("Dimension mismatch")

  quote
    checkbounds(v,I...)
    @nexprs $N i->gstart[v.ndim+1-i]=firsti(I[i],v.dim[i].dimlen)
    @nexprs $N i->gcount[v.ndim+1-i]=counti(I[i],v.dim[i].dimlen)
    p=1
    @nexprs $N i->p=p*gcount[v.ndim+1-i]
    length(val) != p && error(string("Size of output array ($(length(retvalsa))) does not equal number of elements to be read (",p,")!"))
    nc_put_vara_x(v.ncid,v.varid,gstart,gcount,val)
  end
end

function putvar{T,N}(v::NcVar{T,N},val::Any,I::Integer...)

    N==length(I) || error("Dimension mismatch")
    quote
      checkbounds(v,I...)
      @nexprs $N i->gstart[v.ndim+1-i]=I[i]-1
      nc_put_var1_x(v.ncid,v.varid,gstart,val)
    end

end

for (t,ending,arname) in funext
    fname = symbol("nc_put_vara_$ending")
    fname1= symbol("nc_put_var1_$ending")
    arsym=symbol(arname)
    @eval nc_put_vara_x(ncid::Integer, varid::Integer, start, count, vals::Array{$t})=$fname(ncid,varid,start,count,vals)
    @eval nc_put_var1_x(ncid::Integer,varid::Integer,start::Vector{UInt},val::$t)=begin $(arsym)[1]=val; $fname1(ncid,varid,start,$(arsym)) end
end

function nc_put_vara_x{T<:AbstractString}(ncid::Integer, varid::Integer, start, count,vals::Array{T})
  vals_p=map(x->pointer(x.data),vals)
  nc_put_vara_string(ncid,varid,start,count,vals_p)
end

function nc_put_var1_x(ncid::Integer,varid::Integer,start::Vector{UInt},val::AbstractString)
  val_p=fill(pointer(val.data),1)
  nc_put_var1_string(ncid,varid,start,val_p)
end

"Synchronizes the changes made to the file and writes changes to the disk. If the argument is omitted, all open files are synchronized. "
function ncsync()
  for ncf in currentNcFiles
    _nc_sync_c(ncf[2].ncid)
  end
end

function sync(nc::NcFile)
  _nc_sync_c(nc.ncid)
end

#Function to close netcdf files
"Closes the file and writes changes to the disk. If argument is omitted, all open files are closed.   "
function ncclose(fil::AbstractString)
  if (haskey(currentNcFiles,abspath(fil)))
    close(currentNcFiles[abspath(fil)])
  else
    println("File $fil not currently opened.")
  end
end

function ncclose()
  for f in keys(currentNcFiles)
    ncclose(f)
  end
end

function setcompression(v::NcVar,mode)
    if v.compress > -1
        if (NC_NETCDF4 & mode)== 0
            warn("Compression only possible for NetCDF4 file format. Compression will be ingored.")
            v.compress=-1
        else
            v.compress=max(v.compress,9)
            nc_def_var_deflate(v.ncid,v.varid,Int32(1),Int32(1),v.compress);
        end
    end
end
"""
    NetCDF.create(name::String,varlist::Array{NcVar};gatts::Dict{Any,Any}=Dict{Any,Any}(),mode::UInt16=NC_NETCDF4)
This creates a new NetCDF file. Here, `filename`
 is the name of the file to be created and `varlist` an array of `NcVar` holding the variables that should appear in the file. In the optional
argument `gatts` you can specify a Dict containing global attributes and `mode` is the file type you want to create (NC_NETCDF4, NC_CLASSIC_MODEL or NC_64BIT_OFFSET).
"""
function create(name::AbstractString,varlist::Array{NcVar};gatts::Dict{Any,Any}=Dict{Any,Any}(),mode::UInt16=NC_NETCDF4)

  #Create the file
  id = nc_create(name,mode)

  # Collect Dimensions and set NetCDF ID
  vars=Dict{ASCIIString,NcVar}();
  dims=Set{NcDim}();
  for v in varlist
      v.ncid=id
      for d in v.dim
          push!(dims,d);
      end
  end
  nunlim=0
  ndim=Int32(length(dims))

  #Create the NcFile Object
  nc=NcFile(id,Int32(length(vars)),ndim,zero(Int32),vars,Dict{ASCIIString,NcDim}(),Dict{Any,Any}(),zero(Int32),name,NC_WRITE,true)

  for d in dims

      create_dim(nc,d)

    #Create dimension variable, if necessary
    if (length(d.vals)>0) & (!haskey(vars,d.name))
      push!(varlist,NcVar{Float64,1}(id,varida[1],1,length(d.atts),NC_DOUBLE,d.name,[d.dimid],[d],d.atts,-1))
    end

  end

  # Create variables in the file
  for v in varlist
      create_var(nc,v,mode)
  end

  # Put global attributes
  if !isempty(gatts)
    putatt(id,NC_GLOBAL,gatts)
  end

  # Leave define mode
  nc_enddef(nc)

  currentNcFiles[abspath(nc.name)]=nc

  for d in nc.dim
    #Write dimension variable
    if (length(d[2].vals)>0)
      putvar(nc,d[2].name,d[2].vals)
    end
  end

  return(nc)
end

create(name::AbstractString,varlist::NcVar...;gatts::Dict{Any,Any}=Dict{Any,Any}(),mode::UInt16=NC_NETCDF4)=create(name,NcVar[varlist[i] for i=1:length(varlist)];gatts=gatts,mode=mode)

"""
    NetCDF.close(nc::NcFile)
closes a NetCDF file handle
"""
function close(nco::NcFile)
  #Close file
  nc_close(nco.ncid)
  delete!(currentNcFiles,abspath(nco.name))
  return nco.ncid
end

"""
    NetCDF.open(fil::AbstractString,v::AbstractString; mode::Integer=NC_NOWRITE, readdimvar::Bool=false)
opens a NetCDF variable `v` in the NetCDF file `fil` and returns an `NcVar` handle that implements the AbstractArray interface for reading and writing.
"""
function open(fil::AbstractString,v::AbstractString; mode::Integer=NC_NOWRITE, readdimvar::Bool=false)
    nc=open(fil,mode=mode,readdimvar=readdimvar)
    nc.vars[v]
end

"""
    open(fil::AbstractString; mode::Integer=NC_NOWRITE, readdimvar::Bool=false)
opens the NetCDF file `fil` and returns a `NcFile` handle. The optional argument mode determines the `mode` in which the files is opened (`NC_NOWRITE` or `NC_WRITE`).
If you set `readdimvar=true`, then the dimension variables will be read when opening the file and added to the NcFIle object.
"""
function open(fil::AbstractString; mode::Integer=NC_NOWRITE, readdimvar::Bool=false)
  # Open netcdf file
  ncid=nc_open(fil,mode)

  #Get initial information
  ndim,nvar,ngatt,nunlimdimid = nc_inq(ncid)

  #Create ncdf object
  ncf=NcFile(ncid,Int32(nvar-ndim),ndim,ngatt,Dict{ASCIIString,NcVar}(),Dict{ASCIIString,NcDim}(),Dict{Any,Any}(),nunlimdimid,abspath(fil),mode,false)

  #Read global attributes
  ncf.gatts=getatts_all(ncid,NC_GLOBAL,ngatt)

  #Read dimensions
  for dimid = 0:ndim-1
    (name,dimlen)=nc_inq_dim(ncid,dimid)
    ncf.dim[name]=NcDim(ncid,dimid,-1,name,dimlen,[],Dict{Any,Any}())
  end

  #Read variable information
  for varid = 0:(nvar-1)
    (name,nctype,dimids,natts,vndim,isdimvar)=nc_inq_var(ncf,varid)
    if (isdimvar)
      ncf.dim[name].varid=varid
    end
    atts=getatts_all(ncid,varid,natts)
    vdim=Array(NcDim,length(dimids))
    i=1;
    for did in dimids
      vdim[i]=ncf.dim[getdimnamebyid(ncf,did)]
      i=i+1
    end
    ncf.vars[name]=NcVar{nctype2jltype[nctype],Int(vndim)}(ncid,Int32(varid),vndim,natts,nctype,name,dimids[vndim:-1:1],vdim[vndim:-1:1],atts,0)
  end
  readdimvar==true ? _readdimvars(ncf) : nothing
  currentNcFiles[abspath(ncf.name)]=ncf
  return ncf
end

# Define some high-level functions
"""
ncread(filename, varname, start=[1,1,...], count=[-1,-1,...] )
reads the values of the variable varname from file filename and returns the values in an array.
If only parts of the variable are to be read, you can provide optionally `start` and `count`, which enable you to read blocks of data. `start` and `count` have the same
length as the number of variable dimensions. start gives the initial index for each dimension, while count gives the number of indices to be read along each dimension.
As a special case, setting a value in count to -1 will cause the function to read all values along this dimension.
"""
function ncread{T<:Integer}(fil::AbstractString,vname::AbstractString;start::Array{T}=Array(Int,0),count::Array{T}=Array(Int,0))
  nc = haskey(currentNcFiles,abspath(fil)) ? currentNcFiles[abspath(fil)] : open(fil)
  length(start)==0 && (start=defaultstart(nc[vname]))
  length(count)==0 && (count=defaultcount(nc[vname]))
  x  = readvar(nc[vname],start=start,count=count)
  return x
end
ncread{T<:Integer}(fil::AbstractString,vname::AbstractString,start::Array{T,1},count::Array{T,1})=ncread(fil,vname,start=start,count=count)

"""
ncread! ( filename, varname, d, start=[1,1,...], count=[-1,-1,...] )
is the mutating form of `ncread` which expects a pre-allocated array d, where the data are written to. It reads the values of the variable varname from file filename
writing the values to the preallocated array `d`.
If only parts of the variable are to be read, you can provide optionally `start` and `count`, which enable you to read blocks of data. `start` and `count` have the same
length as the number of variable dimensions. start gives the initial index for each dimension, while count gives the number of indices to be read along each dimension.
As a special case, setting a value in count to -1 will cause the function to read all values along this dimension.
"""
function ncread!{T<:Integer}(fil::AbstractString,vname::AbstractString,vals::Array;start::Array{T}=ones(Int,ndims(vals)),count::Array{T}=Array(Int,size(vals)))
  nc = haskey(currentNcFiles,abspath(fil)) ? currentNcFiles[abspath(fil)] : open(fil)
  x  = readvar!(nc,vname,vals,start=start,count=count)
  return x
end

"prints information on the variables, dimension and attributes conatained in the file"
function ncinfo(fil::AbstractString)
  nc = haskey(currentNcFiles,abspath(fil)) ? currentNcFiles[abspath(fil)] : open(fil)
  return(nc)
end

iswritable(nc::NcFile)=(nc.omode & NC_WRITE) != zero(UInt16)

#High-level functions for writing data to a file
"""
    ncwrite{T<:Integer}(x::Array,fil::AbstractString,vname::AbstractString;start::Array{T,1}=ones(Int,length(size(x))),count::Array{T,1}=[size(x)...])
Writes the array `x` to the file `fil` and variable `vname`. If no `start` argument is supplied, writing starts at index 1 in each dimension.
You can supply the argument `start`, a vector that has the same number as the number of variable dimensions, that provides the indices where to start writing the data.
As default the number of values written along each dimension equals the dimension of the input array. However you can specify the along which dimension the data will be
written by setting a `count` argument, an integer vector indicating the number of values written along each dimension.
"""
function ncwrite{T<:Integer}(x::Array,fil::AbstractString,vname::AbstractString;start::Array{T,1}=ones(Int,length(size(x))),count::Array{T,1}=[size(x)...])
  nc = haskey(currentNcFiles,abspath(fil)) ? currentNcFiles[abspath(fil)] : open(fil,mode=NC_WRITE)
  if (nc.omode==NC_NOWRITE)
    close(nc)
    println("reopening file in WRITE mode")
    open(fil,mode=NC_WRITE)
  end
  putvar(nc,vname,x,start=start,count=count)
end
ncwrite(x::Array,fil::AbstractString,vname::AbstractString,start::Array)=ncwrite(x,fil,vname,start=start)

"This reads an attribute from the specified file and variable. To read global attributes, set varname to `Global`."
function ncgetatt(fil::AbstractString,vname::AbstractString,att::AbstractString)
  nc= haskey(currentNcFiles,abspath(fil)) ? currentNcFiles[abspath(fil)] : open(fil,NC_WRITE)
  return ( haskey(nc.vars,vname) ? get(nc.vars[vname].atts,att,nothing) : get(nc.gatts,att,nothing) )
end

#High-level function for creating files and variables
#
# if the file does not exist, it will be created
# if the file already exists, the variable will be added to the file

function create_dim(nc,dim)
    nc_redef(nc)
    nc_def_dim(nc.ncid,dim.name,dim.dimlen,dima);
    dim.dimid=dima[1];
    if !isempty(dim.vals)
      dumids[1]=dim.dimid
      nc_def_var(nc.ncid,dim.name,NC_DOUBLE,1,dumids,vara)
      nc.vars[dim.name]=NcVar{Float64,1}(nc.ncid,vara[1],1,0,NC_DOUBLE,dim.name,Int32[dim.dimid],NcDim[dim],Dict{Any,Any}(),-1)
    end
    nc.dim[dim.name]=dim;
end

function create_var(nc,v,mode)

    nc_redef(nc)

    v.dimids=Int32[v.dim[i].dimid for i=1:length(v.dim)]
    for i=1:v.ndim dumids[i]=v.dimids[v.ndim+1-i] end

    nc_def_var(nc.ncid,v.name,v.nctype,v.ndim,dumids,vara)

    v.varid=vara[1];
    nc.vars[v.name]=v;
    putatt(nc.ncid,v.varid,v.atts)
    setcompression(v,mode)
end

"""
    nccreate (filename, varname, dimensions ..., atts=atts,gatts=gatts,compress=compress,t=t,mode=mode)
This creates a variable in an existing netcdf file or creates a new file. `filename` and `varname` are strings.
After that follows a list of dimensions. Each dimension entry starts with a dimension name (a String), and
may be followed by a dimension length, an array with dimension values or a Dict containing dimension attributes.
Then the next dimension is entered and so on. Have a look at examples/high.jl for an example use.

Possible optional arguments are:

- **atts** Dict of attribute names and values to be assigned to the variable created
- **gatts** Dict of attribute names and values to be written as global attributes
- **compress** Integer [0..9] setting the compression level of the file, only valid if mode=NC_NETCDF4
- **t** variable type, currently supported types are: const NC_BYTE, NC_CHAR, NC_SHORT, NC_INT, NC_FLOAT, NC_LONG, NC_DOUBLE
- **mode** file creation mode, only valid when new file is created, choose one of: NC_NETCDF4, NC_CLASSIC_MODEL, NC_64BIT_OFFSET
"""
function nccreate(fil::AbstractString,varname::AbstractString,dims...;atts::Dict=Dict{Any,Any}(),gatts::Dict=Dict{Any,Any}(),compress::Integer=-1,t::Union{DataType,Integer}=NC_DOUBLE,mode::UInt16=NC_NETCDF4)
    # Checking dims argument for correctness
    dim=parsedimargs(dims)
    # open the file
    # create the NcVar object
    v=NcVar(varname,dim,atts=atts,compress=compress,t=t)
    # Test if the file already exists
    if (isfile(fil))
        nc=haskey(currentNcFiles,abspath(fil)) ? currentNcFiles[abspath(fil)] : open(fil,mode=NC_WRITE)
        if (nc.omode==NC_NOWRITE)
            close(nc)
            println("reopening file in WRITE mode")
            open(fil,NC_WRITE)
        end
        v.ncid = nc.ncid
        haskey(nc.vars,varname) ? error("Variable $varname already exists in file fil") : nothing
        # Check if dimensions exist, if not, create
        # Remember if dimension was created
        dcreate=Array(Bool,length(dim))
        for i=1:length(dim)
            if !haskey(nc.dim,dim[i].name)
                create_dim(nc,dim[i])
                v.dimids[i]=dim[i].dimid;
                dcreate[i] = true
            else
                v.dimids[i]=nc.dim[dim[i].name].dimid;
                v.dim[i] = nc.dim[dim[i].name]
                dcreate[i] = false
            end
        end
        # Create variable
        create_var(nc,v,mode)
        nc_enddef(nc)
        for i=1:length(dim)
            if dcreate[i] & !isempty(dim[i].vals)
                ncwrite(dim[i].vals,fil,dim[i].name)
            end
        end
    else
        nc = create(fil,v,gatts=gatts,mode=mode | NC_NOCLOBBER)
        for d in dim
            !isempty(d.vals) && ncwrite(d.vals,fil,d.name)
        end
    end
end

#show{T<:Any,N}(io::IO,a::NcVar{T,N})=println(io,a.name)
#showcompact{T<:Any,N}(io::IO,a::NcVar{T,N})=println(io,a.name)
function show(io::IO,nc::NcFile)
    nrow,ncol=Base.tty_size()
    hline = repeat("-",ncol)
    l1=div(ncol,3)
    l2=2*l1
    println(io,"")
    println(io,"##### NetCDF File #####")
    println(io,"")
    println(io,nc.name)
    println(io,"")
    println(io,"##### Dimensions #####")
    println(io,"")
    println(io,tolen("Name",l2),tolen("Length",l1))
    println(hline)
  for d in nc.dim
    println(io,tolen(d[2].name,l2),tolen(d[2].dimlen,l1))
  end
  l1=div(ncol,5)
  l2=2*l1
  println(io,"")
  println(io,"##### Variables #####")
  println(io,"")
  println(io,tolen("Name",l2),tolen("Type",l1),tolen("Dimensions",l2))
  println(hline)
  for v in nc.vars
    s1=string(tolen(v[2].name,l2))
    s2=string(tolen(nctype2string[Int(v[2].nctype)],l1))
    s3=""
    for d in v[2].dim
      s3=string(s3,d.name," ")
    end
    println(s1,s2,tolen(s3,l2))
  end
  l1=div(ncol,4)
  l2=2*l1
  println(io,"")
  println(io,"##### Attributes #####")
  println(io,"")
  println(io,tolen("Variable",l1),tolen("Name",l1),tolen("Value",l2))
  println(hline)
  for a in nc.gatts
    println(io,tolen("global",l1),tolen(a[1],l1),tolen(a[2],l2))
  end
  for v in nc.vars
    for a in v[2].atts
      println(io,tolen(v[2].name,l1),tolen(a[1],l1),tolen(a[2],l2))
    end
  end

end
tolen(s::Any,l::Number)=tolen(string(s),round(Int,l))
function tolen(s::AbstractString,l::Integer)
  ls = length(s)
  if ls<l
    return rpad(s,l)
  elseif ls>l
    return string(s[1:prevind(s,l-1)],"..")
  else
    return s
  end
end

end # Module


Base.@kwdef mutable struct Options

    # Printing options
    log_verbose::Bool = false
    log_freq::Int = 1000
    timer_verbose::Bool = false
    timer_file::Bool = false
    disable_julia_logger::Bool = true

    # time options
    time_limit::Float64 = 3600_00. #100 hours

    warn_on_limit::Bool = false
    extended_log::Bool = false
    extended_log2::Bool = false
    log_repeat_header::Bool = false

    # Default tolerances
    tol_gap::Float64 = 1e-4
    tol_feasibility::Float64 = 1e-4
    tol_feasibility_dual::Float64 = 1e-4
    tol_primal::Float64 = 1e-4
    tol_dual::Float64 = 1e-4
    tol_psd::Float64 = 1e-7
    tol_soc::Float64 = 1e-7

    check_dual_feas::Bool = false
    check_dual_feas_freq::Int = 1000

    max_obj::Float64 = 1e20
    min_iter_max_obj::Int = 10

    # infeasibility check
    min_iter_time_infeas::Int = 1000
    infeas_gap_tol::Float64 = 1e-4
    infeas_limit_gap_tol::Float64 = 1e-1
    infeas_stable_gap_tol::Float64 = 1e-4
    infeas_feasibility_tol::Float64 = 1e-4
    infeas_stable_feasibility_tol::Float64 = 1e-8

    certificate_search::Bool = true
    certificate_obj_tol::Float64 = 1e-1
    certificate_fail_tol::Float64 = 1e-8

    # Bounds on beta (dual_step / primal_step) [larger bounds may lead to numerical inaccuracy]
    min_beta::Float64 = 1e-5
    max_beta::Float64 = 1e+5
    initial_beta::Float64 = 1.

    # Adaptive primal-dual steps parameters [adapt_decay above .7 may lead to slower convergence]
    initial_adapt_level::Float64 = .9
    adapt_decay::Float64 = .8
    adapt_window::Int64 = 50

    # PDHG parameters
    convergence_window::Int = 200
    convergence_check::Int = 50
    max_iter::Int = 0
    min_iter::Int = 40
    divergence_min_update::Int = 50
    max_iter_lp::Int = 10_000_000
    max_iter_conic::Int = 1_000_000
    max_iter_local::Int = 0 #ignores user setting

    advanced_initialization::Bool = true

    # Linesearch parameters
    line_search_flag::Bool = true
    max_linsearch_steps::Int = 5000
    delta::Float64 = .9999
    initial_theta::Float64 = 1.
    linsearch_decay::Float64 = .75

    # Spectral decomposition parameters
    full_eig_decomp::Bool = false
    max_target_rank_krylov_eigs::Int = 16
    min_size_krylov_eigs::Int = 100
    warm_start_eig::Bool = true
    rank_increment::Int = 1 # 0=multiply, 1 = add
    rank_increment_factor::Int = 1 # 0 multiply, 1 = add

    # eigsolver selection
    #=
        1: Arpack [dsaupd] (tipically non-deterministic)
        2: KrylovKit [eigsolve/Lanczos] (DEFAULT)
    =#
    eigsolver::Int = 2
    eigsolver_min_lanczos::Int = 25
    eigsolver_resid_seed::Int = 1234

    # Arpack
    # note that Arpack is Non-deterministic
    # (https://github.com/mariohsouto/ProxSDP.jl/issues/69)
    arpack_tol::Float64 = 1e-10
    #=
        0: arpack random [usually faster - NON-DETERMINISTIC - slightly]
        1: all ones [???]
        2: julia random uniform (eigsolver_resid_seed) [medium for DETERMINISTIC]
        3: julia normalized random normal (eigsolver_resid_seed) [best for DETERMINISTIC]
    =#
    arpack_resid_init::Int = 3
    arpack_reset_resid::Bool = true # true for determinism
    # larger is more stable to converge and more deterministic
    arpack_max_iter::Int = 10_000
    # see remark for of dsaupd

    # KrylovKit
    krylovkit_reset_resid::Bool = false
    krylovkit_resid_init::Int = 3
    krylovkit_tol::Float64 = 1e-12
    krylovkit_max_iter::Int = 100
    krylovkit_eager::Bool = false
    krylovkit_verbose::Int = 0

    # Reduce rank [warning: heuristics]
    reduce_rank::Bool = false
    rank_slack::Int = 3

    full_eig_freq::Int = 10_000_000
    full_eig_len::Int = 0

    # equilibration parameters
    equilibration::Bool = false
    equilibration_iters::Int = 1000
    equilibration_lb::Float64 = -10.0
    equilibration_ub::Float64 = +10.0
    equilibration_limit::Float64 = 0.9
    equilibration_force::Bool = false

    # spectral norm [using exact norm via svds may result in nondeterministic behavior]
    approx_norm::Bool = true
end


using ArgParse
import ArgParse: parse_item

using Formatting
using Statistics

using Plots
# pyplot(reuse=true, size=(900,300))
pyplot()
theme(:ggplot2)

import TimeSeries
import Dates
import FracFin

function ArgParse.parse_item(::Type{AbstractVector{Int}}, S::AbstractString)
    # return ParseAbstractVector{Int}(S) #
    return ParseAbstractVector(Int, S)
end

function ArgParse.parse_item(::Type{NTuple{N, Int}}, S::AbstractString) where N
    return ParseNTuple(Int, S)
end

# function ParseAbstractVector{T}(S::AbstractString) where {T<:Real}
#     A = if something(findfirst(isequal(':'), S), 0) != 0  # step range
#         N = [parse(T, s) for s in split(S, ':')]
#         # collect(N[1]:N[2]:N[3])
#         N[1]:N[2]:N[3]
#     elseif S[1] == '[' && S[end] == ']'  # vector
#         [parse(T, s) for s in split(S[2:end-1], ',')]
#     else
#         error("Invalid input: $S")
#     end
#     # all((A .% 2) .== 0) || error("Invalid input: $S")

#     return A
# end

function ParseAbstractVector(T::Type{P}, S::AbstractString) where P<:Real
    A = if something(findfirst(isequal(':'), S), 0) != 0  # step range
        N = [parse(Int, s) for s in split(S, ':')]
        # collect(N[1]:N[2]:N[3])
        s = (length(N) == 2) ? 1 : N[2]
        StepRange(N[1], s, N[end])
    elseif S[1] == '[' && S[end] == ']'  # vector
        [parse(Int, s) for s in split(S[2:end-1], ',')]
    else
        error("Invalid input: $S")
    end
    # all((A .% 2) .== 0) || error("Invalid input: $S")

    return A
end

function ParseNTuple(T::Type{P}, S::AbstractString) where {P<:Real}
    return (parse(T, s) for s in split(S, ','))
#     A = if S[1] == '(' && S[end] == ')'  # tuple
#         (parse(T, s) for s in split(S[2:end-1], ','))
#     else
#         error("Invalid input: $S")
#     end
#     return A
end

function prepare_fGn(X0::AbstractVector{T}, dlag::Int) where {T<:Real}
    dX = fill(NaN, size(X0))  # fGn estimator is not friendly with NaN
    dX[1+dlag:end] = X0[1+dlag:end]-X0[1:end-dlag]
    return dX
end

function prepare_bspline(X0::AbstractVector{T}, sclrng::AbstractVector{Int}, vm::Int) where {T<:Real}
    W0, Mask = FracFin.cwt_bspline(X0, sclrng, vm, :left)
    t0 = findall(all(Mask, dims=2)[:])[1]
    # t1 = findall(all(Mask, dims=2)[:])[end]
    W0[1:t0-1,:] .= NaN
    return transpose(W0)
end

function equalize_daynight(sdata)
    edata = [sdata[1]]
    for n=2:length(sdata)
        d0 = TimeSeries.values(edata[n-1])
        d1 = TimeSeries.values(sdata[n])
        t1 = TimeSeries.timestamp(sdata[n])
        push!(edata, TimeSeries.TimeArray(t1, d1.-(d1[1]-d0[end]), TimeSeries.colnames(sdata[1])))
    end
    return edata
end

function parse_commandline()
    settings = ArgParseSettings("Apply rolling window estimator of fBm.")

    @add_arg_table settings begin
        "--wsize"
        help = "size of rolling window"
        arg_type = Int
        default = 180
        "--ssize"
        help = "size of sub window"
        arg_type = Int
        default = 60
        "--dlen"
        help = "length of decorrelation"
        arg_type = Int
        default = 5
        "--pov"
        help = "period of innovation"
        arg_type = Int
        default = 5
        "--ipts"
        help = "number of points per day (for intraday data only), 0 for no down-sampling"
        arg_type = Int
        default = 0
        "--idta"
        help = " starting time of truncation (for intraday data only)"
        arg_type = String
        default = "09:05"
        "--idtb"
        help = " ending time of truncation (for intraday data only)"
        arg_type = String
        default = "17:25"
        "--eql"
        help = "Equalize the opening and close values of two consecutive days (for ipts=0 only)"
        action = :store_true
        "--tfmt"
        help = "time format for parsing csv file"
        arg_type = String
        default = "yyyy-mm-dd HH:MM:SS"
        "--ncol"
        help = "index of the column to be analyzed, if the input file contains multiple columns"
        arg_type = Int
        default = 1
        "--verbose", "-v"
        help = "print message"
        action = :store_true
        "infile"
        help = "input file (with suffix '.csv' or '.txt') containing a time series"
        required = true
        "outdir"
        help = "output directory"
        required = true
        "powlaw"
        action = :command        # adds a command which will be read from an argument
        help = "power law method: 'ssize' and 'dlen' are ignored."
        "fGn-MLE"
        action = :command        # adds a command which will be read from an argument
        help = "fGn-MLE"
        "fWn-bspline-MLE"
        action = :command
        help = "fWn-bspline-MLE"
        "bspline-scalogram"
        action = :command
        help = "bspline-scalogram method: 'ssize' and 'dlen' are ignored."
    end

    @add_arg_table settings["powlaw"] begin    # add command arg_table: same as usual, but invoked on s["cmd"]
        "--pow"
        help = "compute the 'pow'-th moment "
        arg_type = Float64
        default = 1.
        "--dlags"
        help = "time lags for finite difference"
        arg_type = AbstractVector{Int}
        default = 2:10
    end

    settings["powlaw"].description = "power law description"  # this is how settings are tweaked for commands
    settings["powlaw"].commands_are_required = true # this makes the sub-commands optional
    settings["powlaw"].autofix_names = true # this uses dashes in long options, underscores in auto-generated dest_names

    @add_arg_table settings["fGn-MLE"] begin    # add command arg_table: same as usual, but invoked on s["cmd"]
        "--dlag"
        help = "time lag for finite difference"
        arg_type = Int
        default = 1
    end

    settings["fGn-MLE"].description = "fGn description"  # this is how settings are tweaked for commands
    settings["fGn-MLE"].commands_are_required = true # this makes the sub-commands optional
    settings["fGn-MLE"].autofix_names = true # this uses dashes in long options, underscores in auto-generated dest_names

    @add_arg_table settings["fWn-bspline-MLE"] begin    # add command arg_table: same as usual, but invoked on s["cmd"]
        "--vm"
        help = "vanishing moments of the wavelet"
        arg_type = Int
        default = 2
        "--sclidx"
        help = "indices of integer scales (scale must be an even number)"
        arg_type = AbstractVector{Int}
        default = 2:10
    end

    settings["fWn-bspline-MLE"].description = "fWn description"  # this is how settings are tweaked for commands
    settings["fWn-bspline-MLE"].commands_are_required = true # this makes the sub-commands optional

    @add_arg_table settings["bspline-scalogram"] begin    # add command arg_table: same as usual, but invoked on s["cmd"]
        "--vm"
        help = "vanishing moments of the wavelet"
        arg_type = Int
        default = 2
        "--sclidx"
        help = "indices of even integer scales (scale must be an even number)"
        arg_type = AbstractVector{Int}
        default = 2:10
        "--pow"
        help = "compute the 'pow'-th moment "
        arg_type = Float64
        default = 1.
    end

    settings["bspline-scalogram"].description = "fWn description"  # this is how settings are tweaked for commands
    settings["bspline-scalogram"].commands_are_required = true # this makes the sub-commands optional

    return parse_args(settings)
end

function main()
    parsed_args = parse_commandline()
    # println("Parsed args:")
    # for (arg,val) in parsed_args
    #     println("  $arg  =>  $val")
    # end

    # recover global parameters and options
    infile = parsed_args["infile"]
    outdir0 = parsed_args["outdir"]

    i1 = something(findlast(isequal('/'), infile), 0)
    i2 = something(findlast(isequal('.'), infile), length(infile)+1)
    sname = infile[i1+1:i2-1]  # filename without extension

    cmd = parsed_args["%COMMAND%"]  # name of command
    wsize = parsed_args["wsize"]  # size of rolling window
    ssize = parsed_args["ssize"]  # size of sub window
    dlen = parsed_args["dlen"]  # length of decorrelation
    pov = parsed_args["pov"]  # period of innovation
    ipts = parsed_args["ipts"]  # number of pts per day
    eql = parsed_args["eql"]
    idta = parsed_args["idta"]
    idtb = parsed_args["idtb"]
    tfmt = parsed_args["tfmt"]  # time format
    ncol = parsed_args["ncol"]  # index of column
    verbose = parsed_args["verbose"]  # print messages

    # recover command options
    prepare::Function = X->()  # function for data preparation
    estim::Function = X->()
    trans::Function = X->vec(X)
    outstr::String = ""

    if cmd  == "powlaw"
        ssize = wsize
        dlen = 1
        pow = parsed_args[cmd]["pow"]  # time-lags of finite difference
        dlags = parsed_args[cmd]["dlags"]  # time-lags of finite difference
        outstr = format("wsize[{}]_ipts[{}]_pov[{}]_pow[{}]_dlags[{}]", wsize, ipts, pov, pow, dlags)
        prepare = X -> X
        estim = X -> FracFin.powlaw_estim(X, dlags, pow)
    elseif cmd  == "fGn-MLE"
        dlag = parsed_args[cmd]["dlag"]  # time-lag of finite difference
        outstr = format("wsize[{}]_ssize[{}]_dlen[{}]_ipts[{}]_pov[{}]_dlag[{}]", wsize, ssize, dlen, ipts, pov, dlag)
        prepare = X -> prepare_fGn(X, dlag)
        estim = X -> FracFin.fGn_MLE_estim(X, dlag; method=:optim, ε=1e-2)
    elseif cmd == "fWn-bspline-MLE"
        sclrng = 2 * parsed_args[cmd]["sclidx"]  # range of scales
        vm = parsed_args[cmd]["vm"]  # vanishing moments
        outstr = format("wsize[{}]_ssize[{}]_dlen[{}]_ipts[{}]_pov[{}]_vm[{}]_sclrng[{}]", wsize, ssize, dlen, ipts, pov, vm, sclrng)
        prepare = X -> prepare_bspline(X, sclrng, vm)
        estim = X -> FracFin.fWn_bspline_MLE_estim(X, sclrng, vm; method=:optim, ε=1e-2)
    elseif cmd == "bspline-scalogram"
        ssize = wsize
        dlen = 1
        sclrng = 2 * parsed_args[cmd]["sclidx"]  # range of scales
        pow = parsed_args[cmd]["pow"]  # time-lags of finite difference
        vm = parsed_args[cmd]["vm"]  # vanishing moments
        outstr = format("wsize[{}]_ipts[{}]_pov[{}]_pow[{}]_vm[{}]_sclrng[{}]", wsize, ipts, pov, pow, vm, sclrng)
        prepare = X -> prepare_bspline(X, sclrng, vm)
        estim = X -> FracFin.bspline_scalogram_estim(X, sclrng, vm; p=pow, mode=:left)
        # trans = X -> var(X, dims=2)
        # trans = X -> vec(FracFin.robustvar(X, dims=2))
        # trans = X -> vec(mean(abs.(X .- mean(X, dims=2)).^pow, dims=2))
        trans = X -> vec(mean(abs.(X).^pow, dims=2))
    else
        error("Unknown command")
    end

    # load data
    if verbose
        printfmtln("Loading file {}...", infile)
    end
    # data0 = CSV.read(infile)
    toto = TimeSeries.readtimearray(infile, format=tfmt, delim=',')
    cname = TimeSeries.colnames(toto)[ncol]  # name of the column
    unit = Dates.Minute(minimum(diff(TimeSeries.timestamp(toto))))
    data0 = toto[cname]

    if Int(unit/Dates.Minute(1)) < 1440  # intraday data
        tdta, tdtb = Dates.Time(idta), Dates.Time(idtb)
        dha, dhb = Dates.Hour(tdta), Dates.Hour(tdtb)
        dma, dmb = Dates.Minute(tdta), Dates.Minute(tdtb)
        daytime = (dha+dma, dhb+dmb)

        Xm, Tm, Xt, Tt = [], [], [], []
        sdata0 = []

        if ipts>0
            wh = (dhb-dha) ÷ ipts + dmb-dma # length of intraday window
            nh = Int((dhb-dha)/Dates.Hour(1)) # number of hours in intraday data
            dh = nh ÷ ipts
            windows = [(w=daytime[1]+Dates.Hour(n); (w,w+wh)) for n=0:dh:(nh-1)]

            # splitted data of identical length
            Xms = []
            Tms = []
            for (wa,wb) in windows
                sdata0 = FracFin.window_split_timearray(data0, Dates.Hour(24), (wa,wb), fillmode=:fb, endpoint=false)
                push!(Xms, log.(hcat([TimeSeries.values(v) for v in sdata0]...)))
                # println(length(sdata0))
                # println(size(Xms[end]))
                push!(Tms, hcat([TimeSeries.timestamp(v) for v in sdata0]...))
            end

            Xm = reshape(vcat(Xms...), (size(Xms[1],1), :))
            Tm = reshape(vcat(Tms...), (size(Tms[1],1), :))
            Xt = mean(Xm, dims=1)[:]
            Tt = [Dates.DateTime(t) for t in Tm[1,:]]
        else
            sdata0 = FracFin.window_split_timearray(data0, Dates.Hour(24), daytime, fillmode=:fb, endpoint=false)
            sdata1 = eql ? sdata0 : equalize_daynight(sdata0)
            data1 = vcat(sdata1...)
            Tt = TimeSeries.timestamp(data1)
            Xm = log.(reshape(TimeSeries.values(data1), 1, :))
            Xt = log.(TimeSeries.values(vcat(sdata0...)))
            # Xm = log.(reshape(vcat(TimeSeries.values.(sdata0)...), 1, :))
            # Tt = vcat(TimeSeries.timestamp.(sdata0)...)
            # Xt = Xm[1, :]
        end
        # println(size(Xm))
        # println(size(Tm))
        # println(size(Xt))
        # println(size(Tt))

        Res = []
        for r = 1:size(Xm,1)
            Xi = prepare(Xm[r,:])  # input to rolling estimator
            res = FracFin.rolling_estim(estim, Xi, (wsize,ssize,dlen), pov, trans; mode=:causal)
            push!(Res, res)
        end

        Hm = hcat([[x[2][1][1] for x in res] for res in Res]...)'
        σm = hcat([[x[2][1][2] for x in res] for res in Res]...)'
        tidx = [x[1] for x in Res[1]]

        # embedding into a longer time series
        Ht = fill(NaN, length(Tt)); Ht[tidx] = mean(Hm, dims=1)[:]
        σt = fill(NaN, length(Tt)); σt[tidx] = mean(σm, dims=1)[:]
        Hs = fill(NaN, length(Tt)); Hs[tidx] = std(Hm, dims=1)[:]
        σs = fill(NaN, length(Tt)); σs[tidx] = std(σm, dims=1)[:]

        At = TimeSeries.TimeArray(Tt, [Xt Ht Hs σt σs], Symbol.(["Log_Price", "Hurst", "Hurst_std", "σ", "σ_std"]))

        # make the output folder
        outdir = format("{}/{}/{}/{}/{}/", outdir0, sname, cname, cmd, outstr)
        try
            mkpath(outdir)
        catch
        end

        TimeSeries.writetimearray(At, format("{}/estimate.csv",outdir))
        # At = TimeArray(T0[tidx], [X0[tidx] Ht σt Hs σs], ["Log_Price", "Hurst", "Hurst_std" "σ" "σ_std"])
        # TimeSeries.writetimearray(At, format("{}/estimate-ts.csv",outdir))

        fig1 = plot(Tt, Xt, ylabel="Log Price", label="")
        fig2 = plot(Tt, Ht, ribbon=(2*Hs, 2*Hs), ylim=[0,1], shape=:circle, ylabel="Hurst", label="")
        hline!(fig2, [0.5], width=[3], color=[:red], label="")
        fig3 = plot(Tt, σt, ribbon=(2*σs, 2*σs), shape=:circle, ylabel="σ", label="")
        plot(fig1, fig2, fig3, layout=(3,1), size=(1200,900), legend=true)
        savefig(format("{}/estimate.pdf",outdir))

        if ipts==0
            sdata1 = FracFin.window_split_timearray(At, Dates.Hour(24); fillmode=:o, endpoint=true)
            for Y0 in sdata1
                T0 = TimeSeries.timestamp(Y0)
                fig1 = plot(Y0[:Log_Price], ylabel="Log Price", label="")
                fig2 = plot(Y0[:Hurst], shape=:circle, ylabel="Hurst", label="")
                fig3 = plot(Y0[:σ], shape=:circle, ylabel="σ", label="")
                fig4 = plot(Y0[:Hurst], ylim=[0,1], shape=:circle, ylabel="Hurst", label="")
                hline!(fig4, [0.5], width=[3], color=[:red], label="")
                plot(fig1, fig2, fig3, fig4, layout=(4,1), size=(1200,900), xticks=T0[1]:Dates.Hour(1):T0[end], legend=true)
                savefig(format("{}/{}.pdf",outdir, Dates.Date(T0[1])))
                # TimeSeries.writetimearray(Yt, format("{}/csv/{}.csv",outdir, day))
            end
        end

        if verbose
            printfmtln("Outputs saved in {}", outdir)
        end
    else

    end
end

main()


# Equality

# equals() now depends on == instead
# of the other way round.
function ==(x::Decimal, y::Decimal)
    a = norm(x)
    b = norm(y)
    a.c == b.c && a.q == b.q && a.s == b.s
end

function ==(x::Number, y::Decimal)
    decimal(x) == y
end

function ==(x::Decimal, y::Number)
    x == decimal(y)
end


function link_table(file::String)

    id = Symbol(ncgetatt(file, "dim_space", "id"))

    df_links = CSV.File(joinpath(dirname(pathof(IcanProj)), "..", "data", "df_links.csv")) |> DataFrame

    eval(:(df_nc = DataFrame($(id) = convert(Array{Int64}, ncread($(file), "id")),
                             nc = 1:length(ncread($(file), "id")))))

    df_final = join(df_links, df_nc, on = id)

    return df_final

end

function link_table(path::String, variable::String, space_res::String)

    file = get_filename(path, variable, space_res, 1)

    id = Symbol(ncgetatt(file, "id", "id"))

    df_links = CSV.File(joinpath(dirname(pathof(IcanProj)), "..", "data", "df_links.csv")) |> DataFrame

    eval(:(df_nc = DataFrame($(id) = convert(Array{Int64}, ncread($(file), "id")),
                             nc = 1:length(ncread($(file), "id")))))

    df_final = join(df_links, df_nc, on = id)

    return df_final

end

function link_results(file_fine::String, file_coarse::String) #, id_fine, id_coarse)

    # Metadata table

    df_meta = CSV.File(joinpath(dirname(pathof(IcanProj)), "..", "data", "df_links.csv")) |> DataFrame

    # Get attributes

    id_fine = ncgetatt(file_fine, "id", "id")

    if typeof(id_fine) == Nothing
        id_fine = ncgetatt(file_fine, "dim_space", "id")
    end

    id_coarse = ncgetatt(file_coarse, "id", "id")

    if typeof(id_coarse) == Nothing
        id_coarse = ncgetatt(file_coarse, "dim_space", "id")
    end

    # Fine resolution

    df_fine = DataFrame(id_fine = convert(Array{Int64}, ncread(file_fine, "id")),
                        nc_fine = 1:length(ncread(file_fine, "id")))

    # Coarse resolution

    df_coarse = DataFrame(id_coarse = convert(Array{Int64}, ncread(file_coarse, "id")),
                          nc_coarse = 1:length(ncread(file_coarse, "id")))

    # Link resolutions

    df_links = DataFrame(id_fine = df_meta[Symbol(id_fine)],
                         id_coarse = df_meta[Symbol(id_coarse)],
                         ind_julia = df_meta[:ind_julia],
                         elev = df_meta[:elev])

    df_links = join(df_links, df_fine, on = :id_fine)

    df_links = join(df_links, df_coarse, on = :id_coarse)

    # Compute number of fine scale grid cells in coarse grid

    tmp = DataFrame(nc_coarse = df_links[:nc_coarse],
                    n = 1)

    tmp = aggregate(tmp, :nc_coarse, sum)

    df_links = join(df_links, tmp, on = :nc_coarse)

    # Sort results

    sort!(df_links, [:id_fine])

    return df_links

end

"""
Aggregate fine scale results and return together with coarse scale results.
"""
function unify_results(file_fine, file_coarse, df_links, variable)

    var_fine = ncread(file_fine, variable)

    dim_space = ncread(file_fine, "id")  # convert to int?

    if size(var_fine, 2) != length(dim_space)
        var_fine = var_fine'
    end

    var_coarse = ncread(file_coarse, variable)

    dim_space = ncread(file_coarse, "id")  # convert to int?

    if size(var_coarse, 2) != length(dim_space)
        var_coarse = var_coarse'
    end

    var_agg = fill(0.0, size(var_coarse))

    ngrids = fill(0.0, size(var_coarse, 2))

    for row in eachrow(df_links)

        # This works even though some of the gridcells lack values

        tmp = var_fine[:, row[:nc_fine]]

        if !any(isnan.(tmp))

            var_agg[:, row[:nc_coarse]] += var_fine[:, row[:nc_fine]]

            ngrids[row[:nc_coarse]] += 1

        end

        # This should work if all grid cells have results

        # var_agg[:, row[:nc_coarse]] += var_fine[:, row[:nc_fine]] / row[:n_sum]

        # ngrids[row[:nc_coarse]] += 1

    end

    var_agg = var_agg ./ repeat(permutedims(ngrids), size(var_agg, 1))

    ncclose()

    return var_coarse, var_agg, ngrids

end

"""
Project results to a map using senorge extents.
"""
function project_results(values, df_links, on = :nc_coarse)

    map_values = fill(NaN, 1550, 1195)

    map_values[df_links[:ind_julia]] = values[df_links[on]]

    return map_values

end


"""
    Either <: Augmentor.ArrayOperation

Description
--------------

Chooses between the given `operations` at random when applied.
This is particularly useful if one for example wants to first
either rotate the image 90 degree clockwise or anticlockwise (but
never both), and then apply some other operation(s) afterwards.

When compiling a pipeline, `Either` will analyze the provided
`operations` in order to identify the preferred formalism to use
when applied. The chosen formalism is chosen such that it is
supported by all given `operations`. This way the output of
applying `Either` will be inferable and the whole pipeline will
remain type-stable (even though randomness is involved).

By default each specified image operation has the same
probability of occurrence. This default behaviour can be
overwritten by specifying the `chance` manually.

Usage
--------------

    Either(operations, [chances])

    Either(operations...; [chances])

    Either(pairs...)

    *(operations...)

    *(pairs...)

Arguments
--------------

- **`operations`** : `NTuple` or `Vararg` of `Augmentor.ArrayOperation`
    that denote the possible choices to sample from when applied.

- **`chances`** : Optional. Denotes the relative chances for an
    operation to be sampled. Has to contain the same number of
    elements as `operations`. Either an `NTuple` of numbers if
    specified as positional argument, or alternatively a
    `AbstractVector` of numbers if specified as a keyword
    argument. If omitted every operation will have equal
    probability of occurring.

- **`pairs`** : `Vararg` of `Pair{<:Real,<:Augmentor.ArrayOperation}`.
    A compact way to specify an operation and its chance of
    occurring together.

See also
--------------

[`NoOp`](@ref), [`augment`](@ref)

Examples
--------------

```julia
using Augmentor
img = testpattern()

# all three operations have equal chance of occuring
augment(img, Either(FlipX(), FlipY(), NoOp()))
augment(img, FlipX() * FlipY() * NoOp())

# NoOp is twice as likely as either FlipX or FlipY
augment(img, Either(1=>FlipX(), 1=>FlipY(), 2=>NoOp()))
augment(img, Either(FlipX(), FlipY(), NoOp(), chances=[1,1,2]))
augment(img, Either((FlipX(), FlipY(), NoOp()), (1,1,2)))
augment(img, (1=>FlipX()) * (1=>FlipY()) * (2=>NoOp()))
```
"""
struct Either{N,T<:Tuple} <: ArrayOperation
    operations::T
    chances::SVector{N,Float64}
    cum_chances::SVector{N,Float64}

    function Either(operations::NTuple{N,ArrayOperation}, chances::SVector{N}) where N
        all(c->c>=0, chances) || throw(ArgumentError("All provided \"chances\" must be positive"))
        length(operations) > 0 || throw(ArgumentError("Must provide at least one operation in the constructor of \"Either\""))
        sum_chances = sum(chances)
        sum_chances > 0 || throw(ArgumentError("The sum of all provided \"chances\" must be strictly positive"))
        norm_chances = map(x -> Float64(x/sum_chances), chances)
        cum_chances = SVector(cumsum(norm_chances))
        new{N,typeof(operations)}(operations, norm_chances, cum_chances)
    end
end

Either() = throw(ArgumentError("Must provide at least one operation in the constructor of \"Either\""))

function Either(operations::NTuple{N,ArrayOperation}, chances::NTuple{N,Real} = map(op -> 1/length(operations), operations)) where N
    Either(operations, SVector{N}(chances))
end

function Either(operations::Vararg{ArrayOperation,N}; chances = map(op -> 1/length(operations), operations)) where N
    Either(operations, SVector{N}(map(Float64, chances)))
end

function Either(operations::Pair...)
    Either(map(last, operations), map(first, operations))
end

function Either(op::ArrayOperation, p::Real = .5)
    0 <= p <= 1. || throw(ArgumentError("The propability \"p\" has to be in the interval [0, 1]"))
    p1 = Float64(p)
    p2 = 1 - p1
    Either((op, NoOp()), (p1, p2))
end

Base.:*(op1::Pair{<:Number,<:Operation}, ops::Pair...) =
    Either(op1, ops...)
Base.:*(op1::Operation, ops::Operation...) = Either((op1, ops...))

for FUN in (:supports_view,
            :supports_stepview,
            :supports_permute,
            :supports_affine,
            :supports_affineview)
    # A predicate must be true for all the contained operations,
    # in order for it to be true for the "Either" containing them.
    @eval @inline ($FUN)(::Type{Either{N,T}}) where {N,T} =
        all($FUN, T.types)
end

function randparam(op::Either, img)
    p = safe_rand()
    for (i, p_i) in enumerate(op.cum_chances)
        if p <= p_i
            return i
        end
    end
    error("unreachable code reached")
end

# Choose lazy strategy based on shared support of operations.
# Note: We prefer "affine" only if "img" already is some
#   "InvWarpedView", otherwise the preference is
#   view > stepview > permute > affine > affineview
@generated function applylazy(op::Either, img::AbstractArray, idx)
    if isinvwarpedview(img) && supports_affine(op)
        :(applyaffine(op, img, idx))
    elseif isinvwarpedview(img) && supports_affineview(op)
        :(applyaffineview(op, img, idx))
    elseif supports_view(op)
        :(applyview(op, img, idx))
    elseif supports_stepview(op)
        :(applystepview(op, img, idx))
    elseif supports_permute(op)
        :(applypermute(op, img, idx))
    elseif supports_affine(op)
        :(applyaffine(op, prepareaffine(img), idx))
    elseif supports_affineview(op)
        :(applyaffineview(op, prepareaffine(img), idx))
    else
        :(throw(MethodError(applylazy, (op, img, idx))))
    end
end

@inline isinvwarpedview(::Type{SubArray{T,N,P,I,L}}) where {T,N,P<:InvWarpedView,I,L} = true
@inline isinvwarpedview(::Type{<:InvWarpedView}) = true
@inline isinvwarpedview(::Type) = false

# Only provided for the sake of interface completeness (not used)
function toaffinemap(op::Either, img, idx)
    supports_affine(typeof(op)) || throw(MethodError(toaffinemap, (op, img, idx)))
    op_inner = op.operations[idx]
    toaffinemap_common(op_inner, img, randparam(op_inner, img))
end

# To preserve type stability of "applyeager", always promote
# arrays to offset arrays
@inline _offsetarray(A::Array) = OffsetArray(A, 0, 0)
@inline _offsetarray(A::OffsetArray) = A
function applyeager(op::Either, img::AbstractArray, idx)
    _offsetarray(applyeager(op.operations[idx], img))
end

# Sample a random operation and pass the function call along.
# Note: "applyaffine" needs to map to "applyaffine_common" for
#   type stability, because otherwise the concrete type of the
#   "AffineMap" may differ from one operation to the next
#   (e.g. "Rotate" uses a "RotMatrix" by default, while "Scale"
#   for obvious reasons does not)
for KIND in (:permute, :view, :stepview, :affine, :affineview)
    FUN = Symbol(:apply, KIND)
    SUP = Symbol(:supports_, KIND)
    APP = startswith(String(KIND),"affine") ? Symbol(FUN, :_common) : FUN
    @eval function ($FUN)(op::Either, img::AbstractArray, idx)
        ($SUP)(typeof(op)) || throw(MethodError($FUN, (op, img, idx)))
        ($APP)(op.operations[idx], img)
    end
end

function showconstruction(io::IO, op::Either)
    chances_float = map(c->round(c,  digits=3), op.chances)
    if all(x->x≈chances_float[1], chances_float)
        for (i, op_i) in enumerate(op.operations)
            showconstruction(io, op_i)
            i < length(op.operations) && print(io, " * ")
        end
    else
        for (i, (op_i, p_i)) in enumerate(zip(op.operations, chances_float))
            print(io, '(', p_i, "=>")
            showconstruction(io, op_i)
            print(io, ')')
            i < length(op.operations) && print(io, " * ")
        end
    end
end

function Base.show(io::IO, op::Either)
    if get(io, :compact, false)
        print(io, "Either:")
        for (op_i, p_i) in zip(op.operations, op.chances)
            print(io, " (", round(Int, p_i*100), "%) ")
            show(IOContext(io, :compact => true), op_i)
            print(io, '.')
        end
    else
        print(io, "Augmentor.", typeof(op).name, " (1 out of ", length(op.operations), " operation(s)):")
        percent_int   = map(c->round(Int, c*100), op.chances)
        percent_float = map(c->round(c*100, digits=1), op.chances)
        percent = if any(i != f for (i,f) in zip(percent_int,percent_float))
            percent_float
        else
            percent_int
        end
        k = maximum(length(string(c)) for c in percent)
        for (op_i, p_i) in zip(op.operations, percent)
            println(io)
            print(io, "  - ", lpad(string(p_i), k, " "), "% chance to: ")
            show(IOContext(io, :compact => true), op_i)
        end
    end
end


module TestLoading

using Test
using MLJModels

function isloaded(name::String, pkg::String)
    (name, pkg) in map(localmodels()) do m
        (m.name, m.package_name)
    end
end

@load AdaBoostStumpClassifier pkg=DecisionTree verbosity=0
@test isloaded("AdaBoostStumpClassifier", "DecisionTree")

# built-ins load fine:
@load Standardizer verbosity=0

# load one version of a RidgeRegressor:
@test !isloaded("RidgeRegressor", "MultivariateStats")
@load RidgeRegressor pkg=MultivariateStats verbosity=0
@test isloaded("RidgeRegressor", "MultivariateStats")

# error if ambiguous:
@test_throws ArgumentError @load RidgeRegressor

# error if not in project:
@test !isloaded("KMeans", "Clustering")
@test_throws ArgumentError @load KMeans pkg=Clustering verbosity=0

# use add option:
@load KMeans pkg=Clustering verbosity=0 add=true
@test isloaded("KMeans", "Clustering")

# deprecated methods:
@test_throws Exception load("model", pkg = "pkg")
@test_throws Exception load(models()[1])

module FooBar
using MLJModels
function regressor()
    Regressor = @load LinearRegressor pkg=MultivariateStats verbosity=0
    return Regressor()
end
end
using .FooBar

@testset "@load from within a function within a module" begin
    model = FooBar.regressor()
    @test isdefined(model, :bias)
end

end # module

true


using Flux
using Flux.Data
using Test
using Random, Statistics, LinearAlgebra
using IterTools: ncycle

Random.seed!(0)

@testset "Utils" begin
  include("utils.jl")
end

@testset "Onehot" begin
  include("onehot.jl")
end

@testset "Optimise" begin
  include("optimise.jl")
end

@testset "Data" begin
  include("data.jl")
end

@testset "Losses" begin
  include("losses.jl")
end

@testset "Layers" begin
  include("layers/basic.jl")
  include("layers/normalisation.jl")
  include("layers/stateless.jl")
  include("layers/recurrent.jl")
  include("layers/conv.jl")
end

@testset "CUDA" begin
  if Flux.use_cuda[]
    include("cuda/runtests.jl")
  else
    @warn "CUDA unavailable, not testing GPU support"
  end
end

@static if VERSION >= v"1.4"
  using Documenter
  @testset "Docs" begin
    DocMeta.setdocmeta!(Flux, :DocTestSetup, :(using Flux); recursive=true)
    doctest(Flux)
  end
end


module NLSat

export NLSolver, Z3Solver, YicesSolver, SMTSolver
export NLStatus, NLModel
export variables!, constraints!, solve

using DelimitedFiles
using Distributed
using MacroTools: walk, postwalk, @capture, @match, replace
using Dates

const NLModel = Dict{Symbol,Number}

# ------------------------------------------------------------------------------

abstract type AlgebraicNumber end
export AlgebraicNumber

function AlgebraicNumber(x::Expr, n::Int)
    @warn "Algebraic numbers not yet supported, got $((x, n)), returning $(NaN)"
    NaN
end

# ------------------------------------------------------------------------------

include("../utils.jl")
include("clauseset.jl")
include("smtlib.jl")
include("lisp.jl")

# ------------------------------------------------------------------------------

@enum NLStatus sat unsat unknown timeout

abstract type NLSolver end

function variables!(s::NLSolver, d::Dict{Symbol,Type}) end
function solve(s::NLSolver; timeout::Int = -1) end
constraints!(s::NLSolver, cs::ClauseSet) = s.cs &= cs

# ------------------------------------------------------------------------------

# include("mathematica.jl")
include("smtsolver.jl")

end # module


"""
    GlobalPencilArray{T,N} <: AbstractArray{T,N}

Alias for an `OffsetArray` wrapping a [`PencilArray`](@ref).

Unlike `PencilArray`s, `GlobalPencilArray`s take *global* indices, which
in general don't start at 1 for a given MPI process.

The [`global_view`](@ref) function should be used to create a
`GlobalPencilArray` from a `PencilArray`.
"""
const GlobalPencilArray{T,N} = OffsetArray{T,N,A} where {A <: PencilArray}

"""
    global_view(x::PencilArray)

Create an [`OffsetArray`](https://github.com/JuliaArrays/OffsetArrays.jl) of a
`PencilArray` that takes global indices in logical order.
"""
function global_view(x::PencilArray)
    r = range_local(x, LogicalOrder())
    offsets = first.(r) .- 1
    xo = OffsetArray(x, offsets)
    @assert parent(xo) === x  # OffsetArray shouldn't create a copy...
    xo :: GlobalPencilArray
end

Base.similar(x::GlobalPencilArray, ::Type{S}) where {S} =
    global_view(similar(parent(x), S))

# Account for index permutation in global views.
@inline Base._sub2ind(x::GlobalPencilArray, I...) =
    Base._sub2ind(parent(x), (I .- x.offsets)...)


module DataAssim

export timecorr_noise
export resample
export enkf

include("utils_pfilter.jl")
include("utils_enkf.jl")

end


using CUDA
using Colors: RGB
using FastAI
using FilePathsBase
using FastAI.Datasets
using DLPipelines
using DataAugmentation
using DataAugmentation: getbounds
using Flux
using StaticArrays
using Test
using TestSetExtensions

ENV["DATADEPS_ALWAYS_ACCEPT"] = "true"
include("testdata.jl")

function test_gpu(f)
    if CUDA.functional()
        f()
    else
        @test_broken "No GPU"
    end
end


mutable struct HarvestOption
    f0_ceil::Cdouble
    f0_floor::Cdouble
    frame_period::Cdouble

    # allow uninitialized creation
    HarvestOption() = new()
end

function harvest(x::Vector{Float64}, fs::Int; kwargs...)
    harvestoption = Ref(HarvestOption())
    @ccall libworld.InitializeHarvestOption(harvestoption::Ptr{HarvestOption})::Cvoid

    for (symbol, value) in kwargs
        setproperty!(harvestoption[], symbol, value)
    end
    frame_period = harvestoption[].frame_period

    n_harvest_samples = @ccall libworld.GetSamplesForHarvest(fs::Cint, length(x)::Cint, frame_period::Cdouble)::Cint

    # allocate f0 and temporal positions vectors
    f0 = Vector{Cdouble}(undef, n_harvest_samples)
    temporal_positions = similar(f0)

    @ccall libworld.Harvest(x::Ptr{Cdouble}, length(x)::Cint, fs::Cint, harvestoption::Ptr{HarvestOption},
        temporal_positions::Ptr{Cdouble}, f0::Ptr{Cdouble})::Cvoid

    f0, temporal_positions
end


module NonIterableCellMapsTests

using Test
using Gridap

struct Foo <: NonIterableCellMap{Point{2},1,Int,1} end

foo = Foo()

@test isa(foo,CellMap)

try
  iterate(foo)
catch LoadError
end

try
  iterate(foo,2)
catch LoadError
end

end # module


using Documenter, DiffEqOperators

makedocs(
    sitename="DiffEqOperators.jl",
    authors="Chris Rackauckas et al.",
    clean=true,
    doctest=false,
    modules=[DiffEqOperators],

    format=Documenter.HTML(assets=["assets/favicon.ico"],
                           canonical="https://diffeqoperators.sciml.ai/stable/"),

    pages=[
        "DiffEqOperators.jl: Linear operators for Scientific Machine Learning" => "index.md",
        "Symbolic Tutorials" => [
            "symbolic_tutorials/mol_heat.md"
        ],
        "Operators" => [
            "operators/operator_overview.md",
            "operators/derivative_operators.md",
            "operators/jacobian_vector_product.md",
            "operators/matrix_free_operators.md"
        ],
        "Symbolic PDE Solver" => [
            "symbolic/molfinitedifference.md"
        ],
        "Nonlinear Derivatives" => [
            "nonlinear_derivatives/nonlinear_diffusion.md"
        ]
    ]
)

deploydocs(
    repo="github.com/SciML/DiffEqOperators.jl";
    push_preview=true
)


# GPUArrays.jl interface

#
# Device functionality
#

## execution

struct oneArrayBackend <: AbstractGPUBackend end

struct oneKernelContext <: AbstractKernelContext end

@inline function GPUArrays.launch_heuristic(::oneArrayBackend, f::F, args::Vararg{Any,N};
                                             elements::Int, elements_per_thread::Int) where {F,N}
    kernel = @oneapi launch=false f(oneKernelContext(), args...)

    items = suggest_groupsize(kernel.fun, elements).x
    # XXX: the z dimension of the suggested group size is often non-zero.
    #      preserve this in GPUArrays?
    # XXX: how many groups is a good number? the API doesn't tell us.
    #      measured on a low-end IGP, 32 blocks seems like a good sweet spot.
    #      note that this only matters for grid-stride kernels, like broadcast.
    return (threads=items, blocks=32)
end

function GPUArrays.gpu_call(::oneArrayBackend, f, args, threads::Int, blocks::Int;
                            name::Union{String,Nothing})
    @oneapi items=threads groups=blocks name=name f(oneKernelContext(), args...)
end

## on-device

# indexing

GPUArrays.blockidx(ctx::oneKernelContext) = oneAPI.get_group_id(0)
GPUArrays.blockdim(ctx::oneKernelContext) = oneAPI.get_local_size(0)
GPUArrays.threadidx(ctx::oneKernelContext) = oneAPI.get_local_id(0)
GPUArrays.griddim(ctx::oneKernelContext) = oneAPI.get_num_groups(0)

# math

@inline GPUArrays.cos(ctx::oneKernelContext, x) = oneAPI.cos(x)
@inline GPUArrays.sin(ctx::oneKernelContext, x) = oneAPI.sin(x)
@inline GPUArrays.sqrt(ctx::oneKernelContext, x) = oneAPI.sqrt(x)
@inline GPUArrays.log(ctx::oneKernelContext, x) = oneAPI.log(x)

# memory

@inline function GPUArrays.LocalMemory(::oneKernelContext, ::Type{T}, ::Val{dims}, ::Val{id}
                                      ) where {T, dims, id}
    ptr = oneAPI.emit_localmemory(Val(id), T, Val(prod(dims)))
    oneDeviceArray(dims, LLVMPtr{T, onePI.AS.Local}(ptr))
end

# synchronization

@inline GPUArrays.synchronize_threads(::oneKernelContext) = oneAPI.barrier()

#
# Host abstractions
#

GPUArrays.backend(::Type{<:oneArray}) = oneArrayBackend()

const GLOBAL_RNGs = Dict{ZeDevice,GPUArrays.RNG}()
function GPUArrays.default_rng(::Type{<:oneArray})
    dev = device()
    get!(GLOBAL_RNGs, dev) do
        N = oneL0.compute_properties(dev).maxTotalGroupSize
        state = oneArray{NTuple{4, UInt32}}(undef, N)
        rng = GPUArrays.RNG(state)
        Random.seed!(rng)
        rng
    end
end