using LAR

#""" The `spacePartition` function takes as input a **non-valid** (with the meaning used in solid modeling field --- see~\cite{Requicha:1980:RRS:356827.356833}) `LAR` model of dimension d-1, i.e.~a triple `(V,FV,EV)`, The `spacePartition` function returns the **valid** `LAR` boundary model `(W,FW,EW)` of the space partition induced by `FV`. First an array `buckets` indexed on faces is computed. It contains the subsets of faces with greatest probability of intersecting each indexing face, respectively.
#"""

""" Half-line crossing test """
function crossingTest(new,old,count,status)
    if status == 0
        status = new
        count += 0.5
    else
        if status == old
        	count += 0.5
        else
        	count -= 0.5
        end
        status = 0
    end
end

function setTile(box)
	tiles = [[9,1,5],[8,0,4],[10,2,6]]
	b1,b2,b3,b4 = box
	function tileCode(point)
		x,y = point
		code = 0
		if y>b1 code=code|1 end
		if y<b2 code=code|2 end
		if x>b3 code=code|4 end
		if x<b4 code=code|8 end
		return code
	end
	return tileCode
end

# Point in polygon classification #
function pointInPolygonClassification(V,EV)
	V = V[1:2,:]
    function pointInPolygonClassification0(pnt)
        x,y = pnt
        xmin,xmax,ymin,ymax = x,x,y,y
        tilecode = setTile([ymax,ymin,xmax,xmin])
        count,status = 0,0

        for k in size(EV,2)
        	edge = EV[:,k]
            p1,p2 = V[:,edge[1]],V[:,edge[2]]
            (x1,y1),(x2,y2) = p1,p2
            c1,c2 = tilecode(p1),tilecode(p2)
            c_edge, c_un, c_int = c1$c2, c1|c2, c1&c2

            if (c_edge == 0) & (c_un == 0) return "p_on"
            elseif (c_edge == 12) & (c_un == c_edge) return "p_on"
            elseif c_edge == 3
                if c_int == 0 return "p_on"
                elseif c_int == 4 count += 1 end
            elseif c_edge == 15
                x_int = ((y-y2)*(x1-x2)/(y1-y2))+x2
                if x_int > x count += 1
                elseif x_int == x return "p_on" end
            elseif (c_edge == 13) & ((c1==4) | (c2==4))
                    crossingTest(1,2,status,count)
            elseif (c_edge == 14) & ((c1==4) | (c2==4))
                    crossingTest(2,1,status,count)
            elseif c_edge == 7 count += 1
            elseif c_edge == 11 count = count
            elseif c_edge == 1
                if c_int == 0 return "p_on"
                elseif c_int == 4 crossingTest(1,2,status,count) end
            elseif c_edge == 2
                if c_int == 0 return "p_on"
                elseif c_int == 4 crossingTest(2,1,status,count) end
            elseif (c_edge == 4) & (c_un == c_edge) return "p_on"
            elseif (c_edge == 8) & (c_un == c_edge) return "p_on"
            elseif c_edge == 5
                if (c1==0) | (c2==0) return "p_on"
                else crossingTest(1,2,status,count) end
            elseif c_edge == 6
                if (c1==0) | (c2==0) return "p_on"
                else crossingTest(2,1,status,count) end
            elseif (c_edge == 9) & ((c1==0) | (c2==0)) return "p_on"
            elseif (c_edge == 10) & ((c1==0) | (c2==0)) return "p_on"
            end
        end
        if (round(count)%2)==1
        	return "p_in"
        else
        	return "p_out"
        end
    end
    return pointInPolygonClassification0
end

function crossRelation(EV::Array{Int64,2},FV::Array{Array{Int64,1},1})
	sparseEV = cellComplex(EV)
	sparseFV = cellComplex(FV)
	sparseEF = (sparseFV' * sparseEV)'
	I,J,V = findnz(sparseEF)
	EF = [Int64[] for k=1:size(EV,2)]
	for (i,j,v) in zip(I,J,V)
		if v==2
			push!(EF[i],j)
		end
	end
	return EF
end

function crossRelation(FV::Array{Int64,2},EV::Array{Int64,2})
	FW = [FV[:,k] for k=1:size(FV,2)]
	out = crossRelation(FW,EV)
end

function crossRelation(FV::Array{Array{Int64,1},1},EV::Array{Int64,2})
	sparseFV = cellComplex(FV)
	sparseEV = cellComplex(EV)
	sparseEF = (sparseFV' * sparseEV)'
	I,J,V = findnz(sparseEF)
	FE = [Int64[] for k=1:length(FV)]
	for (i,j,v) in zip(I,J,V)
		if v==2
			push!(FE[j],i)
		end
	end
	return FE
end

function subModel(V::Array{Float64,2}, FV::Array{Int64,2}, EV::Array{Int64,2},
				bucket::Array{Int64,1},f::Int64,FE::Array{Array{Int64,1},1})
	FW = [FV[:,k] for k=1:size(FV,2)]
	out = subModel(V,FW,EV,bucket,f,FE)
end

function subModel(V::Array{Float64,2}, FV::Array{Array{Int64,1},1},
					EV::Array{Int64,2},bucket::Array{Int64,1},f::Int64,
					FE::Array{Array{Int64,1},1})
	FW = [FV[f] for f in bucket]
	EW = hcat(collect(Set([EV[:,e] for f in bucket for e in FE[f]]))...)

	oldVerts = Set(Int64[])
	for k=1:length(bucket)
		union!(oldVerts, Set(FW[k]))
	end
	olds = collect(oldVerts)
	vdict = Dict(zip(olds, 1:length(olds)))
	fz = [[vdict[v] for v in FW[k]] for k=1:length(FW)]
	ez = hcat([[vdict[v] for v in EW[:,k]] for k=1:size(EW,2)]...)
	z = hcat([V[:,v] for v in olds]...)
	pivot = find(bucket .== f)[1]
	return z,fz,ez,pivot
end

function visualize(Z,FZ::Array{Int64,2},EZ,f)
	FW = [FZ[:,k] for k=1:size(FZ,2)]
	out = visualize(Z,FW,EZ,f)
end

function visualize(Z,FZ::Array{Array{Int64,1},1},EZ,f)
	bucket = lar2hpc(Z,FZ)
	bucketEdges = lar2hpc(Z,EZ)
	params = PyObject(pyeval("list([1.,0.,0.,0.1,  0.,1.,0.,0.1,
				0.,0.,1.,0.1, 0.,0.,0.,0.1, 100.])"))
	glass = p.MATERIAL(params)
	pivot = p.COLOR(p.RED)(p.JOIN(lar2hpc(Z,[FZ[f]])))
	p.VIEW(p.STRUCT([glass(bucket),bucketEdges,pivot]))
end

function meetZero( Z, vpair )
	(u,v) = vpair
    testValue = Z[:,u][3] * Z[:,v][3]
    if testValue > abs(10^-6.)
        return false
    else
    	return true
    end
end

function crossZero( Z, vpair )
	(u,v) = vpair
    testValue = sign(vcode(Z[:,u])[3]) * sign(vcode(Z[:,v])[3])
    if testValue == -1
        return true
    else
    	return false
    end
end

function edgefilter(Z,EZ,e)
	cross = crossZero(Z,EZ[:,e])
	side1 = (abs(Z[3,EZ[1,e]])<10^-6.)
	side2 = (abs(Z[3,EZ[2,e]])<10^-6.)
	#@show cross,side1,side2
	out = cross | (side1 $ side2)
	#if out @show out end
	return out
end

function intersectSegmentWithZero(p1::Array{Float64,1}, p2::Array{Float64,1})
	x1,y1,z1 = p1
	x2,y2,z2 = p2
	# z = z1 + alpha(z2 - z1)
	alpha_0 = (0 - z1)/(z2 - z1)
	point = Float64[x1 + alpha_0*(x2 - x1) , y1 + alpha_0*(y2 - y1)]
	return point
end

function remap(W,FW,EW)
	W,close,clusters,vmap = p.pruneVertices(W')
	if typeof(W) == Array{Any,1}
		W = Array{Float64,2}(hcat(W...))
	else
		W = Array{Float64,2}(W')
	end
	vmap = vmap + 1
	EW = [sort([vmap[EW[1,k]],vmap[EW[2,k]]]) for k in 1:size(EW,2)]
	EW = collect(Set([EW[k] for k=1:length(EW)]))
	EW = hcat(EW...)
	FW = [sort([vmap[FW[k][h]] for h=1:length(FW[k])]) for k=1:length(FW)]
	FW = collect(Set([FW[k] for k=1:length(FW)]))
	return W,FW,EW
end

function checkLines4equalPoints(lines)
	out = []
	for line in lines
		if length(line) > 4
			b = reshape(line,2,Int(length(line)/2))
			line = vcat(p.pruneVertices(b')[1]...)
		end
		if (line[1,:]!=line[3,:]) | (line[2,:]!=line[4,:])
			push!(out,line)
		end
	end
	out
end

function onlyOne_INorON_vertex(Z,EZ,pivotEdges,lines)
	ev = hcat([EZ[:,e] for e in pivotEdges]...)
	classify = pointInPolygonClassification(Z,ev)
	out = Array{Array{Float64,1},1}()
	for line in lines
		p1,p2 = [line[1],line[2]],[line[3],line[4]]
		code1 = classify(p1)
		code2 = classify(p2)
		#@show code1,code2
		if !((code1=="p_out") & (code2=="p_out"))
			push!(out,line)
		end
	end
	return out
end

function spacePartition(V::Array{Float64,2}, FV::Array{Int64,2}, EV::Array{Int64,2},
						debug=false)
	FW = [FV[:,k] for k=1:size(FV,2)]
	out = spacePartition(V,FW,EV,debug)
end

function spacePartition(V::Array{Float64,2}, FV::Array{Array{Int64,1},1},
						EV::Array{Int64,2},debug=false)
	""" input: face index f; candidate incident faces F[f]; """
	boxes = lar2boxes(V,FV)
	buckets = boxBucketing(boxes)
	FE = crossRelation(FV,EV)

	Vertices = Array{Float64,2}()
	Edges = Array{Int64,2}()
	Faces = Array{Array{Int64,1},1}()
	nverts = 0

	for (f,F) in enumerate(buckets)
		@show (f,F)
		""" F[f] submodel extraction from (V,FV,EV) """
		Z,FZ,EZ,pivot = subModel(V,FV,EV,F,f,FE)
		if debug visualize(Z,FZ,EZ,pivot) end

		""" Computation of submanifold map M moving f to z=0 """
		M = submanifoldMapping(V,FV,f)

		""" Transformation of F(f) by M, giving (W,EW,FW) := M(F(f)) """
		Z1 = vcat(Z,ones((1,size(Z,2))))
		Y = M * Z1
		Z = Y[ 1:3, : ]
		if debug visualize(Z,FZ,EZ,pivot) end

		""" filtering of EW edges traversing z=0, """
		fe = crossRelation(FZ,EZ)
		edges4face = [[e for e in face] for (k,face) in enumerate(fe) if k!=pivot]

		pivotEdges = fe[pivot]
		crossingEdges = [[e for e in edges if edgefilter(Z,EZ,e)] for edges in edges4face]
		edges = [edgeList for edgeList in crossingEdges if edgeList!=[]]
		EW = vcat(edges...,pivotEdges...)
		ez = hcat([EZ[:,e] for e in EW]...)
		if debug visualize(Z,FZ,ez,pivot) end

		""" for each face in EW, computate the aligned set of points p(z=0) """
		lines = Array{Array{Float64,1},1}()
		for face in edges
			#assert(length(face)==2) # may fail with non convex faces TODO: make general
			line = Array{Float64,1}()
			for e in face
				v1,v2 = EZ[:,e]
				px,py = vcode(intersectSegmentWithZero(Z[:,v1],Z[:,v2]))[1:2]
				push!(line,px); push!(line,py)
			end
			push!(lines,line)
		end

		""" Further filtering of lines with one non-external vertex """
		lines = onlyOne_INorON_vertex(Z,EZ,pivotEdges,lines)

		for e in pivotEdges
			line = Array{Float64,1}()
			push!(line,vcode(Z[:,EZ[1,e]])[1]); push!(line,vcode(Z[:,EZ[1,e]])[2])
			push!(line,vcode(Z[:,EZ[2,e]])[1]); push!(line,vcode(Z[:,EZ[2,e]])[2])
			push!(lines,line)
		end
		lines = checkLines4equalPoints(lines)
		lineArray = hcat(lines...)
		W,FW,EW = larFromLines(lineArray')

		""" remove external cycle """
		chains = boundary(W,EW)
		if length(chains) == 2
			FW = [FW[1]]
		else
			areaPairing = chainAreas(W,EW,chains)
			maxAreas = maximum([abs(area) for area in areaPairing])
			FW = [face for (k,face) in enumerate(FW) if abs(areaPairing[k]) != maxAreas]
		end
		if debug viewLarIndices(W,EW,FW,3) end

		""" Apply the inverse submanifold transform """
		W1 = vcat(W,zeros((1,size(W,2))),ones((1,size(W,2))))
		Y = inv(M) * W1
		Z = Y[ 1:3, : ]
		if debug viewLarIndices(Z,EW,FW,3) end

		""" Accumulate the submodel parts """
		n = size(Z,2)
		if f==1
			Vertices = copy(Z)
			Edges = copy(EW)
			Faces = copy(FW)
			nverts = n
		else
			Vertices = hcat(Vertices,copy(Z))
			Edges = hcat(Edges,copy(EW)+nverts)
			Faces = vcat(Faces,copy(FW)+nverts)
			nverts += n
		end
	end
	""" return the **valid** `LAR` 2-skeleton model `(W,FW,EW)` """
	U,FU,EU = remap(Vertices,Faces,Edges)
	@show length(U),length(FU),length(EU)
end


"""
    wh_learn(X, Y)

Widrow-Hoff Learning.

# Obligatory Arguments
- `test_mode::Symbol`: which test mode, currently supports :train_only, :pre_split, :careful_split and :random_split.

# Optional Arguments
- `eta::Float64=0.1`: the learning rate
- `n_epochs::Int64=1`: the number of epochs to be trained
- `weights::Matrix=nothing`: the initial weights
- `learn_seq::Vector=nothing`: the learning sequence
- `save_history::Bool=false`: if true, a partical training history will be saved
- `history_cols::Vector=nothing`: the list of column indices you want to saved in history, e.g. `[1,32,42]` or `[2]`
- `history_rows::Vector=nothing`: the list of row indices you want to saved in history, e.g. `[1,32,42]` or `[2]`
- `verbose::Bool = false`: if true, more information will be printed out
"""
function wh_learn(
    X,
    Y;
    eta = 0.01,
    n_epochs = 1,
    weights = nothing,
    learn_seq = nothing,
    save_history = false,
    history_cols = nothing,
    history_rows = nothing,
    verbose = false,
    )

    X = Array(X)
    Y = Array(Y)

    if size(X, 1) != size(Y, 1)
        throw(ArgumentError("X($(size(inputs,1))) and Y($(size(outputs,1))) length doesn't match"))
    end

    if isnothing(weights)
        W = zeros(Float64, (size(X, 2), size(Y, 2)))
    else
        W = weights
    end

    # construct learn_seq if nothing
    if isnothing(learn_seq)
        learn_seq = 1:size(X, 1)
    end

    if save_history
        history = zeros(Float64, n_epochs, length(history_rows), length(history_cols))
    end

    inputT = Matrix{Float64}(undef, (size(X, 2), 1))
    pred = Matrix{Float64}(undef, (1, size(Y, 2)))
    deltaW = Matrix{Float64}(undef, (size(X, 2), size(Y, 2)))
    if verbose
        if isnothing(learn_seq)
            pb = Progress(size(X, 1) * n_epochs)
        else
            pb = Progress(size(learn_seq, 1) * n_epochs)
        end
    end
    for j = 1:n_epochs # 100 epochs
        for i in learn_seq # for each events
            # pred = X[i:i, :]*W
            mul!(pred, view(X, i:i, :), W)
            # obsv = Y[i:i, :]-pred
            broadcast!(-, pred, view(Y, i:i, :), pred)
            # inputT = X[i:i, :]'
            transpose!(inputT, view(X, i:i, :))
            # update = inputT*obsv
            mul!(deltaW, inputT, pred)
            # deltaW = eta*update
            rmul!(deltaW, eta)
            # W += deltaW
            broadcast!(+, W, W, deltaW)

            verbose && ProgressMeter.next!(pb)
        end
        # push history
        if save_history
            history[j,:,:] = copy(W[history_rows, history_cols])
        end
    end
    if save_history
        return W, history
    end
    W
end

"""
    make_learn_seq(freq)

Make Widrow-Hoff learning sequence.
"""
function make_learn_seq(freq; random_seed = 314)
    if isnothing(freq)
        return nothing
    end

    learn_seq = [repeat([i], n) for (i, n) in enumerate(freq)]
    learn_seq = collect(Iterators.flatten(learn_seq))
    rng = MersenneTwister(random_seed)
    shuffle(rng, learn_seq)
end


"""
Describes the reservoir bathymetry, i.e. the relationship between the reservoir level and the total storage.
The function is only valid within the bounds of the reservoir (i.e. between its minimum and maximum volumes).
The parameter is the volume and its output is the corresponding reservoir level. The function must be one-way.

`isempty(bathymetry)` allows checking whether the bathymetry is actually available, with `NullReservoirBathymetry` being
a nonexistent bathymetry (i.e. no information available).

`computeLevel(bathymetry, v)` gets the reservoir level from the given volume `v` (10^6 m^3).
`computeVolume(bathymetry, h)` get the volume from the given reservoir level `h` (m).
"""
abstract ReservoirBathymetry

immutable NullReservoirBathymetry <: ReservoirBathymetry end
Base.isempty(g::ReservoirBathymetry) = false
Base.isempty(g::NullReservoirBathymetry) = true

computeVolume(g::ReservoirBathymetry, h::Float64) =
  error("Operation not implemented: volume = f(reservoir level), for bathymetry " * string(typeof(g)))
volume(g::ReservoirBathymetry, h::Float64) = computeVolume(g, h)
computeLevel(g::ReservoirBathymetry, v::Float64) =
  error("Operation not implemented: volume = f(reservoir level), for bathymetry " * string(typeof(g)))
level(g::ReservoirBathymetry, h::Float64) = computeLevel(g, h)

computeVolume(g::ReservoirBathymetry, h::Height) = computeLevel(g, _from_unitful(h)) * 1.m^3
computeLevel(g::ReservoirBathymetry, v::Volume) = computeLevel(g, _from_unitful(v)) * 1.m

"""
This special kind of relationship is completely transparent, and can directly be used by an optimisation solver
(if the mathematical structure is allowable).
"""
abstract WhiteBoxReservoirBathymetry <: ReservoirBathymetry

iswhitebox(g::ReservoirBathymetry) = false
iswhitebox(g::WhiteBoxReservoirBathymetry) = true
isblackbox(g::ReservoirBathymetry) = ! iswhitebox(g)

"""
Among the white-box geometries are polynomial-based ones, which can be represented as follows:
    level(volume) = \sum_{i=0}^N a_{i+1} volume^i

These geometries are thus completely defined by a series of coefficients, which *should* be stored
in the `coefficients` field with increasing order (publicly accessible with the `coefficient` function).
In this case, the functions `coefficients` and `level` are automatically provided (not `volume`, as it requires
solving a polynomial equation).
"""
abstract PolynomialReservoirBathymetry <: WhiteBoxReservoirBathymetry

ispolynomial(g::ReservoirBathymetry) = false
ispolynomial(g::PolynomialReservoirBathymetry) = true

isPolynomial(g::ReservoirBathymetry) = ispolynomial(g)

"""
Retrieves the coefficient of the polynomial in `g` at the given order, i.e. power of the volume.
For example, the constant part of the polynomial has order zero, and the linear one has order one.
"""
coefficient(g::PolynomialReservoirBathymetry, order::Int) = g.coefficients[order + 1]

"""
Evaluates the given polynomial reservoir bathymetry at the given volume(s).
"""
function computeLevel(g::PolynomialReservoirBathymetry, v::Float64)
  if length(g.coefficients) == 0
    return zero(volume)
  end

  # Inefficient implementation of polynomial evaluation. Works for single numbers and vectors.
  level = g.coefficients[1]
  for i = 2:length(g.coefficients)
      level += g.coefficients[i] * v .^ (i - 1)
  end
  return level
end

immutable LinearReservoirBathymetry <: PolynomialReservoirBathymetry
  coefficients::Tuple{Float64, Float64}
  LinearReservoirBathymetry(c::Tuple{Float64, Float64}) = new(c)
  LinearReservoirBathymetry(a::Float64, b::Float64) = new((a, b))
end

getIntercept(g::LinearReservoirBathymetry) = g.coefficients[1]
getSlope(g::LinearReservoirBathymetry) = g.coefficients[2]
islinear(g::ReservoirBathymetry) = false
islinear(g::LinearReservoirBathymetry) = true

computeVolume(g::LinearReservoirBathymetry, h::Float64) = (h - getIntercept(g)) / getSlope(g)

isLinear(g::ReservoirBathymetry) = islinear(g)
intercept(g::LinearReservoirBathymetry) = getIntercept(g)
slope(g::LinearReservoirBathymetry) = getSlope(g)

"""
Quadratic reservoir bathymetry, whose form is:
    level = a + b volume + c volume^2
Both convex and concave polynomials make sense, depending on the position of the inflexion point with respect to the
part of the domain that is used:
        level                     level
          ^                         ^
          |    /                    |  ___
          |   /                     | /
          |_ /                      |/
          |                         |
          +------> vol              +------> vol
          Convex polynomial         Concave polynomial
It is the user's responsibility to ensure that this polynomial makes sense, i.e. is strictly increasing between the
reservoir capacities
"""
immutable QuadraticReservoirBathymetry <: PolynomialReservoirBathymetry
  coefficients::Tuple{Float64, Float64, Float64}
  QuadraticReservoirBathymetry(c::Tuple{Float64, Float64, Float64}) = new(c)
  QuadraticReservoirBathymetry(a::Float64, b::Float64, c::Float64) = new((a, b, c))
end

isquadratic(g::ReservoirBathymetry) = false
isquadratic(g::QuadraticReservoirBathymetry) = true

function computeVolume(g::QuadraticReservoirBathymetry, h::Float64)
  # Solve a quadratic equation:
  #    a v^2 + b v + c == h
  # But the well-known formulae only work for == 0, hence the c.
  a = coefficient(g, 2)
  b = coefficient(g, 1)
  c = coefficient(g, 0) - h
  Δ = b^2 - 4 * a * c

  if Δ < 0.
    error("The bathymetry has no real root for h = " * string(h) * ".")
  end

  v1 = (- b + sqrt(Δ)) / (2 * a)
  v2 = (- b - sqrt(Δ)) / (2 * a)

  if v1 < 0. && v2 < 0.
    error("The bathymetry has no acceptable root for h = " * string(h) * ".")
  elseif v1 != v2 && v1 > 0. && v2 > 0.
    error("The bathymetry has two acceptable roots for h = " * string(h) * ".")
  elseif (v1 == 0. && v2 <= 0.) || (v2 == 0. && v1 <= 0.)
    error("The bathymetry has one root for h = " * string(h) * ", but it is zero.")
  elseif v1 == v2
    return v1
  elseif v1 > 0. && v2 <= 0.
    return v1
  elseif v2 > 0. && v1 <= 0.
    return v2
  end
end

isQuadratic(g::ReservoirBathymetry) = isquadratic(g)

immutable PiecewiseLinearReservoirBathymetry <: WhiteBoxReservoirBathymetry
  control_points::Array{Float64, 1} # Strictly increasing.
  values::Array{Float64, 1} # Strictly increasing (reservoir level increases with volume of water).

  function PiecewiseLinearReservoirBathymetry(control_points::Array{Float64, 1}, values::Array{Float64, 1})
    if length(control_points) < 2
      error("Must provide at least a segment, i.e. two control points.")
    end

    if length(control_points) - 1 > length(values)
      error("Inconsistent set of points: too many control points for the values (exactly one more than the values is needed).")
    elseif length(control_points) - 1 < length(values)
      error("Inconsistent set of points: too few control points for the values (exactly one more than the values is needed).")
    end

    for i in 2:length(control_points)
      if control_points[i] <= control_points[i - 1]
        error("Control points must be strictly increasing: problem found near index " * string(i) * ".")
      end
    end

    for i in 2:length(values)
      if values[i] <= values[i - 1]
        error("Values must be strictly increasing: problem found near index " * string(i) * ".")
      end
    end

    return new(control_points, values)
  end
end

ispiecewiselinear(g::ReservoirBathymetry) = false
ispiecewiselinear(g::PiecewiseLinearReservoirBathymetry) = true

isPiecewiseLinear(g::ReservoirBathymetry) = ispiecewiselinear(g)
isPWL(g::ReservoirBathymetry) = ispiecewiselinear(g)

"""
Among the white-box geometries is the square root, which can be represented as follows:
    level(volume) = a_0 + \sqrt{ a_1 \times volume }
As a consequence, the volume is given by:
    volume(level) = \frac{(level - a_0)^2}{a_1}
"""
immutable SquareRootReservoirBathymetry <: WhiteBoxReservoirBathymetry
  a0::Float64
  a1::Float64
  SquareRootReservoirBathymetry(c::Tuple{Float64, Float64}) = new(c[1], c[2])
  SquareRootReservoirBathymetry(a::Float64, b::Float64) = new(a, b)
end

issquareroot(g::ReservoirBathymetry) = false
issquareroot(g::SquareRootReservoirBathymetry) = true

isSquareRoot(g::ReservoirBathymetry) = issquareroot(g)

computeLevel(g::SquareRootReservoirBathymetry, v::Float64) = g.a0 + sqrt(g.a1 * v)
computeVolume(g::SquareRootReservoirBathymetry, h::Float64) = (h - g.a0)^2 / g.a1


using KernelFunctions
using Test
using Flux

@testset "Params" begin
    ν = 2.0; c = 3.0; d = 2.0; γ = 2.0; α = 2.5
    kc = ConstantKernel(c=c)
    @test all(params(kc) .== params([c]))
    km = MaternKernel(ν=ν)
    @test all(params(km) .== params([ν]))
    kl = LinearKernel(c=c)
    @test all(params(kl) .== params([c]))
    kge = GammaExponentialKernel(γ=γ)
    @test all(params(kge) .== params([γ]))
    kgr = GammaRationalQuadraticKernel(γ=γ, α=α)
    @test all(params(kgr) .== params([α], [γ]))
    kp = PolynomialKernel(c=c, d=d)
    @test all(params(kp) .== params([d], [c]))
    kr = RationalQuadraticKernel(α=α)
    @test all(params(kr) .== params([α]))

    k = km + kc
    @test all(params(k) .== params([k.weights], km, kc))

    k = km * kc
    @test all(params(k) .== params(km, kc))

    s = 2.0
    k = transform(km, s)
    @test all(params(k) .== params([s], km))

    v = [2.0]
    k = transform(kc, v)
    @test all(params(k) .== params(v, kc))

    P = rand(3, 2)
    k = transform(km,LowRankTransform(P))
    @test all(params(k) .== params(P, km))

    k = transform(km, LowRankTransform(P) ∘ ScaleTransform(s))
    @test all(params(k) .== params([s], P, km))

    c = Chain(Dense(3, 2))
    k = transform(km, FunctionTransform(c))
    @test all(params(k) .== params(c, km))

end


"""
This package provides an MIT license, dependency-free implementation of
Algorithm 1 of "Sinkhorn Divergences for Unbalanced Optimal Transport" [SFVTP19
(http://arxiv.org/abs/1910.12958)], which allows calculation of the optimal
transport and Sinkhorn divergence between two positive measures (with possibly
different total mass), where mass creation and destruction is penalized by one
of several possible φ-divergences.
"""
module UnbalancedOptimalTransport

using LinearAlgebra: norm

export DiscreteMeasure,
    OT!, optimal_coupling!, sinkhorn_divergence!, unbalanced_sinkhorn!, cost_matrix

struct DiscreteMeasure{P,LP,S,T}
    density::P
    log_density::LP
    set::S
    dual_potential::Vector{T}
    cache::Vector{T}
end

"""
    DiscreteMeasure(density, [log_density], set) -> DiscreteMeasure

Construct a `DiscreteMeasure` object for use in [`unbalanced_sinkhorn!`](@ref)
and related functions.

* `density` should be strictly positive; zero elements should instead be removed
  from `set`
* `log_density` should be equal to `log.(density)` and can be omitted
  (in which case its computed automatically)
* `set` is a collection so that `density[i]` is the probability of the element
  `set[i]` occurring (where `i ∈ eachindex(density, set)`).

"""
function DiscreteMeasure(density::P, log_density::LP, set::S) where {P,LP,S}
    T = eltype(density)
    n = length(density)
    n == length(log_density) ||
    throw(ArgumentError("`density`, `log_density`, and `set` (if supplied) must have equal length"))
    set !== nothing &&
    length(set) != n &&
    throw(ArgumentError("`density`, `log_density`, and `set` must have equal length"))
    dual_potential = zeros(T, n)
    cache = similar(dual_potential)
    DiscreteMeasure{P,LP,S,T}(density, log_density, set, dual_potential, cache)
end

DiscreteMeasure(density, set = nothing) = DiscreteMeasure(density, log.(density), set)

Base.eltype(::DiscreteMeasure{P,LP,S,T}) where {P,LP,S,T} = T
Base.length(a::DiscreteMeasure) = length(a.density)

include("divergences.jl")
include("sinkhorn.jl")
include("optimized_methods.jl")
include("utilities.jl")

end


@testset "Regularization" begin
    for (constructor, copier) in ((interpolate, identity), (interpolate!, copy))
        isinplace = constructor == interpolate!
        f0(x) = sin((x-3)*2pi/9 - 1)
        f1(x) = 1.0 + 0.1*x + 0.01*x^2 + 0.001*x^3

        xmax = 10
        xi = range(0, stop=xmax, length=11)
        xfine = range(0, stop=xmax, length=101)

        A0 = f0.(xi)
        A1 = f1.(xi)

        for (yi, f) in ((A0, f0), (A1, f1)), λ in (0.01, 100), k in (1, 2, 3, 4)
            it = @inferred(interpolate(yi, BSpline(Cubic(Line(OnCell()))), λ, k))
            itt = @inferred(scale(it, xi))

            if f === f0
                if λ == 0.01
                    # test that interpolation of the interpolating points is close to expected data
                    for x in [2, 3, 6, 7]
                        @test f(x) ≈ itt(x) atol=0.01
                    end
                elseif λ == 100
                    # test that interpolation of the interpolating points is far from expected data
                    for x in [2, 3, 6, 7]
                        @test !(isapprox(f(x), itt(x); atol=0.1))
                    end
                end

            elseif f === f1
                if k==4
                    for x in xfine
                        # test that interpolation is close to expected data when
                        # the smoothing order is higher than the polynomial order
                        @test f(x) ≈ itt(x) atol=0.1
                    end
                end
            end
        end

        # Check fallback to no regularization for λ=0
        itp1  = @inferred(constructor(copier(A0), BSpline(Cubic(Line(OnGrid()))), 0, 1))
        itp1p = @inferred(constructor(copier(A0), BSpline(Cubic(Line(OnGrid())))))
        @test itp1 == itp1p

        f2(x, y) = sin(x/10)*cos(y/6)
        xmax2, ymax2 = 30, 10
        A2 = Float64[f2(x, y) for x in 1:xmax2, y in 1:ymax2]

        # Check fallback to no regularization for non Vectors
        itp2  = @inferred(constructor(copier(A2), BSpline(Cubic(Line(OnGrid()))), 1, 1))
        itp2p = @inferred(constructor(copier(A2), BSpline(Cubic(Line(OnGrid())))))
        @test itp2 == itp2p
    end
end


########## Fold ##########

@enum Fold_type f_x=Int('x') f_y=Int('y')

struct Fold{Fold_type}
    line::Int
end
line(fold::Fold) = fold.line

Fold(entry::String) =
    Fold{Fold_type(Int(entry[12]))}(parse(Int, entry[14:end]))

########## Point ##########

mutable struct Point
    x::Int
    y::Int
end
x(point::Point) = point.x
y(point::Point) = point.y

Point(entry::String) =
    Point(parse.(Int, String.(split(entry, ",")))...)

Base.:(==)(point1::Point, point2::Point)::Bool =
    x(point1) == x(point2) && y(point1) == y(point2)

########## functions ##########

mirror(coord::Int, line::Int)::Int =
    coord - line > 0 ? 2 * line - coord : coord

fold!(point::Point, fold::Fold{f_x}) =
    point.x = mirror(x(point), line(fold))
fold!(point::Point, fold::Fold{f_y}) =
    point.y = mirror(y(point), line(fold))

function Base.unique(points::Vector{Point})
    result = Point[]
    for point in points
        !(point in result) && append!(result, [point])
    end
    return result
end

function fold!(points::Vector{Point}, fold::Fold)
    for point in points
        fold!(point, fold)
    end
    points = unique(points)
end

function code(points::Vector{Point})::Vector{String}
    width = maximum(x.(points))
    height = maximum(y.(points))
    return [join(row) for row in eachrow(
        [exist ? '#' : '.' for exist in
            [Point(x, y) in points for x = 0:width, y = 0:height]'
        ]
    )]
end

########## Script ##########

function main(input_name::String)

    # open file and store content into an input string
    input = open(join(["inputs//", input_name]), "r") do io
        read(io, String)
    end

    # split input into a the points part and the folding instructions part
    data = String.(split(input, "\r\n\r\n"))
    # parse data as a vector of points and a vector of fold instructions
    points = Point.(String.(split(data[1], "\r\n")))
    folds = Fold.(String.(split(data[2], "\r\n")))

    # part 1
    fold!(points, folds[1])
    println("Part 1: ", length(points))

    # part 2
    for fold in folds[2:end]
        fold!(points, fold)
    end
    println("Part 2: ")
    for row in eachrow(code(points))
        println(row)
    end

end

main("day13.txt");


"""
    IndependentMOKernel(k::Kernel) <: Kernel

A Multi-Output kernel which assumes each output is independent of the other.
"""
struct IndependentMOKernel{Tkernel<:Kernel} <: MOKernel
    kernel::Tkernel
end

function (κ::IndependentMOKernel)((x, px)::Tuple{Any, Int}, (y, py)::Tuple{Any, Int})
    if px == py
        return κ.kernel(x, y)
    else
        return 0.0
    end
end

function kernelmatrix(k::IndependentMOKernel, x::MOInput, y::MOInput)
    @assert x.out_dim == y.out_dim
    temp = k.kernel.(x.x, permutedims(y.x))
    return cat((temp for _ in 1:y.out_dim)...; dims=(1,2))
end

function Base.show(io::IO, k::IndependentMOKernel)
    print(io, string("Independent Multi-Output Kernel\n\t", string(k.kernel)))
end


n = 200
m = 100
p = 25

info("Testing ", MOD.kernel)
for kernelobj in (additive_kernels..., composition_kernels...)
    for T in FloatingPointTypes
        x = rand(T,p)
        y = rand(T,p)
        k = convert(Kernel{T}, (kernelobj)())

        if isa(k, PairwiseKernel)
            @test MOD.kernel(k, x[1], y[1]) == MOD.phi(k, x[1], y[1])
            @test MOD.kernel(k, x, y)       == sum(map((x,y) -> MOD.phi(k,x,y), x, y))
        else
            @test MOD.kernel(k, x[1], y[1]) == MOD.phi(k.phi, MOD.phi(k.kappa, x[1], y[1]))
            @test MOD.kernel(k, x, y)       == MOD.phi(k.phi, sum(map((x,y) -> MOD.phi(k.kappa,x,y), x, y)))
        end
    end
end

info("Testing ", MOD.kernelmatrix)
steps = length(additive_kernels) + length(composition_kernels) + 1
counter = 0
info("    Progress:   0.0%")
for kernelobj in (additive_kernels..., composition_kernels..., MLKTest.PairwiseTestKernel)
    for T in FloatingPointTypes
        Set_X = [rand(T,p) for i = 1:n]
        Set_Y = [rand(T,p) for i = 1:m]

        X = transpose(hcat(Set_X...))
        Y = transpose(hcat(Set_Y...))
        k = convert(Kernel{T}, (kernelobj)())
        #println("κ:", k, ", X:", typeof(X), ", Y:", typeof(Y))

        P = [MOD.kernel(k,x,y) for x in Set_X, y in Set_X]
        @test_approx_eq MOD.kernelmatrix!(Val{:row}, Array(T,n,n), k, X)  P
        @test_approx_eq MOD.kernelmatrix!(Val{:col}, Array(T,n,n), k, X') P
        @test_approx_eq MOD.kernelmatrix(Val{:row}, k, X)  P
        @test_approx_eq MOD.kernelmatrix(Val{:col}, k, X') P
        @test_approx_eq MOD.kernelmatrix(k, X) P

        P = [MOD.kernel(k,x,y) for x in Set_X, y in Set_Y]
        @test_approx_eq MOD.kernelmatrix!(Val{:row}, Array(T,n,m), k, X,  Y)  P
        @test_approx_eq MOD.kernelmatrix!(Val{:col}, Array(T,n,m), k, X', Y') P
        @test_approx_eq MOD.kernelmatrix(Val{:row}, k, X,  Y)  P
        @test_approx_eq MOD.kernelmatrix(Val{:col}, k, X', Y') P
        @test_approx_eq MOD.kernelmatrix(k, X,  Y) P
    end
    counter += 1
    info("    Progress: ", @sprintf("%5.1f", counter/steps*100), "%")
end

#=
info("Testing ", centerkernelmatrix)
for T in (Float32, Float64)
    A = T[1 2 3;
          2 3 4;
          3 4 5]

    a = mean(A,1)

    @test_approx_eq centerkernelmatrix(A) ((A .- a) .- a') .+ mean(A)
end
=#


"""
    function integratorHeun_(p, platform)

Adapter for the Heun integration step method as described in [Stochastic Improved Euler](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_method_(SDE)) integrator in Îto prescription.

```math
K₁ = f(x(t),t)*Δt + g(x(t),t)(ΔW-S Δt^{1/2} )
```
```math
K₂ = f(x(t)+K₁,t+Δt)*Δt + g(x(t)+K₁,t+Δt)(ΔW+SΔt^{1/2} )
```
```math
x(t+Δt) = x(t) + (K₁+K₂)/2
```
where ``S`` is a random variable chosen to be ±1 with probability 1/2 and
``ΔW`` is a Wiener process with step proportional to ``Δt^{1/2}``.

The algorithm can be reduced to six kernels invocations performing the following actions:
 - Clean the interaction parameters array
 - Compute interaction parameters at original position v₀  (if there are interaction parameters)
 - Compute K₂ and intermediate v₁
 - Clean the interaction parameters array
 - Compute interaction parameters at intermediate position vᵢₙₜ  (if there are interaction parameters)
 - Compute K₂ parameters and final position v₁
"""
function addIntegratorHeun_!(p::Program_, platform::String)

    if  "Equation" in keys(p.agent.declaredUpdates)

        code = p.agent.declaredUpdates["Equation"]

        #Create first interaction parameter kernel if there is any interaction parameter updated
        if "UpdateInteraction" in keys(p.agent.declaredUpdates)

            k1 = loop_[p.neighbors](p,p.agent.declaredUpdates["UpdateInteraction"],platform)
            k1 = vectorize_(p.agent,k1,p,interaction=true)
            k1 = wrapInFunction_(:interactionStep1_,k1)
            push!(p.declareF.args,k1)

        end

        #Create first integration step kernel
        #Make the substitution dW -> (dW-S) and count number of calls to dW
        m1 = [i for i in split(string(gensym("dW")),"#") if i != ""]
        codeK1 = gensym_ids(postwalk(x->@capture(x,dW) ? :($(gensym("dW"))) : x, code))
        codeK1 = postwalk(x->@capture(x,v_) && (split(string(v),"_")[1] == "dW") ? :(dW[ic1_,$(Meta.parse(split(string(v),"_")[2]))]) : x, codeK1)
        codeK1 = postwalk(x->@capture(x,dW[ic1_,pos_]) ? :(dW[ic1_,$pos]-S_[ic1_,$pos]) : x, codeK1)
        m2 = [i for i in split(string(gensym("dW")),"#") if i != ""]
        m = Meta.parse(m2[2])-Meta.parse(m1[2])-1

        for (i,j) in enumerate(p.agent.declaredSymbols["Local"])
            if j in keys(p.update["Variables"])
                ii = p.update["Local"][j]
                ki = p.update["Variables"][j]
                codeK1 = postwalk(x -> @capture(x,g_(s_)) && g == DIFFSYMBOL && s == j ? :(K₁_[ic1_,$ki]) : x, codeK1)
                #Saves intermediate step vᵢₙₜ in final pos localVCopy
                push!(codeK1.args,:(localVCopy[ic1_,$ii] = localV[ic1_,$i] + K₁_[ic1_,$ki]))
            end
        end
        codeK1 = vectorize_(p.agent,codeK1,p)
        if inexpr(codeK1,:dW) #Add initialisation is necessary
            for i in 1:m
                pushfirst!(codeK1.args,:(dW[ic1_,$i] = Uniform(0,1)*sqrt(dt)))
                pushfirst!(codeK1.args,:(S_[ic1_,$i] = 2*(round(rand())- .5)*sqrt(dt)))
            end
        end
        k2 = simpleFirstLoopWrapInFunction_(platform,:integrationStep1_,codeK1) #First function declaration
        push!(p.declareF.args,k2)
        push!(p.declareVar.args,:(K₁_ = copy(zeros(Float64,nMax,$(length(p.update["Variables"]))))))
        push!(p.args,:K₁_ )

        #Create second interaction parameter kernel using localVCopy if there is any
        if "UpdateInteraction" in keys(p.agent.declaredUpdates)

            k3 = loop_(p,p.agent,space,p.agent.declaredUpdates["UpdateInteraction"],platform)
            for (i,j) in enumerate(p.agent.declaredSymbols["Local"])
                codeK1 = postwalk(x -> @capture(x,$j) ? :(localVCopy[ic1_,$ii]) : x, k3)
            end
            k3 = vectorize_(p.agent,k3,p,interaction=true)
            k3 = wrapInFunction_(:interactionStep2_,k3)
            push!(p.declareF.args,k3)

        end

        #Create second integration step kernel
        codeK2 = gensym_ids(postwalk(x->@capture(x,dW) ? :($(gensym("dW"))) : x, code))
        codeK2 = postwalk(x->@capture(x,v_) && (split(string(v),"_")[1] == "dW") ? :(dW[ic1_,$(Meta.parse(split(string(v),"_")[2]))]) : x, codeK2)
        codeK2 = postwalk(x->@capture(x,dW[ic1_,pos_]) ? :(dW[ic1_,$pos]+S_[ic1_,$pos]) : x, codeK2)
        for (i,j) in enumerate(p.agent.declaredSymbols["Local"])
            if j in keys(p.update["Variables"])
                ii = p.update["Local"][j]
                ki = p.update["Variables"][j]
                codeK2 = postwalk(x -> @capture(x,g_(s_)) && g == DIFFSYMBOL && s == j ? :(K₁[ic1_,$ii]) : x, codeK2) #Save second point in first vector, saves an additional declaration
                codeK2 = postwalk(x -> @capture(x,s_) && s == j ? :((localVCopy_[ic1_,$ki])) : x, codeK2)
                codeK2 = postwalk(x -> @capture(x,t) ? :(t+dt) : x, codeK2)

                #Add old pos, K2 (stored in final pos for now) and K1 to final pos
                push!(codeK2.args,:(localVCopy[ic1_,$ii] = (localVCopy[ic1_,$ii] + localV[ic1_,$i] + K₁_[ic1_,$ki])/2))
            end
        end
        codeK2 = vectorize_(p.agent,codeK2,p)
        k4 = simpleFirstLoopWrapInFunction_(platform,:integrationStep2_,codeK2)
        push!(p.declareF.args,k4)

        #Create a function that puts all kernels together
        if inexpr(codeK1,:dW)
            push!(p.declareVar.args, #declare random variables for Stochastic integration
                :(begin
                    dW = zeros(Float64,nMax,$m)
                    S_ = zeros(Float64,nMax,$m)
                end)
            )

            push!(p.args,:dW,:S_)
        end
        if "UpdateInteraction" in keys(p.agent.declaredUpdates)
            addInteraction1 = [:(@platformAdapt cleanInteraction_!(ARGS_);@platformAdapt interactionStep1_(ARGS_))]
            addInteraction2 = [:(@platformAdapt cleanInteraction_!(ARGS_);@platformAdapt interactionStep2_(ARGS_))]
            push!(p.execInit.args, :(@platformAdapt cleanInteraction_!(ARGS_); @platformAdapt interactionStep1_(ARGS_)))
        else
            addInteraction1 = []
            addInteraction2 = []
        end
        push!(p.declareF.args,
            :(begin
                function integrationStep_!(ARGS_)
                    $(addInteraction1...)
                    @platformAdapt integrationStep1_(ARGS_)
                    $(addInteraction2...)
                    @platformAdapt integrationStep1_(ARGS_)

                    return
                end
            end)
        )

        #Add integration step to the main function
        push!(p.execInit.args,:(integrationStep_!(ARGS_)))
        push!(p.execInloop.args,:(integrationStep_!(ARGS_)))

    end

    return
end


using LPVSpectral
using Test, LinearAlgebra, Statistics, Random, StatsBase, ProximalOperators
using Plots, DSP
Random.seed!(0)
# write your own tests here
function generate_signal(f,w,N, modphase=false)
    x = sort(10rand(N)) # Sample points
    v = range(0, stop=1, length=N) # Scheduling variable

    # generate output signal
    # phase_matrix
    dependence_matrix = Float64[f[(i-1)%length(f)+1].(v) for v in v, i in eachindex(w)] # N x nw
    frequency_matrix = [cos(w*x -0.5modphase*(dependence_matrix[i,j])) for (i,x) in enumerate(x), (j,w) in enumerate(w)] # N x nw
    y = sum(dependence_matrix.*frequency_matrix,dims=2)[:] # Sum over all frequencies
    y += 0.1randn(size(y))
    y,v,x,frequency_matrix, dependence_matrix
end

@testset "LPVSpectral" begin
    @info "Testing LPVSpectral"

    @testset "Windows" begin
        @info "Testing Windows"

        y = 1:100
        t = 1:100
        W = Windows2(y,t,10,0)
        @test length(W) == 10
        @test first(W) == (1:10,1:10)
        res = mapwindows(W) do (y,t)
            -y
        end
        @test res == -1:-1:-100

        W = Windows2(y,t,10,1)
        @test length(W) == 11
        cW = collect(W)
        @test cW[1] == (1:10,1:10)
        @test cW[2] == (10:19,10:19)

        res = mapwindows(W) do (y,t)
            -y
        end
        @test res == -1:-1:-100

        y = 1:100
        t = 1:100
        W = Windows3(y,t,t,10,0)
        @test length(W) == 10
        @test first(W) == (1:10,1:10,1:10)

        W = Windows3(y,t,t,10,1)
        @test length(W) == 11
        cW = collect(W)
        @test cW[1] == (1:10,1:10,1:10)
        @test cW[2] == (10:19,10:19,10:19)

    end

    @testset "Mel" begin
        @info "Testing Mel"

        M = mel(1,256)
        @test size(M,1) == 128
        @test size(M,2) == 256÷2+1
        M = mel(1000,256, fmin=100)
        sum(M[:,1:26]) == 0

        y = randn(1000)
        M = melspectrogram(y)
        @test length(freq(M)) == 128
        @test size(M.power) == (128,14)
        @test length(time(M)) == 14
        plot(M)

        M = mfcc(y)
        @test length(freq(M)) == 20
        @test size(M.mfcc) == (20,14)
        @test length(time(M)) == 14

    end

    @testset "LPV methods" begin
        @info "testing LPV methods"

        N      = 500 # Number of training data points
        f      = [v->2v^2, v->2/(5v+1), v->3exp(-10*(v-0.5)^2),] # Functional dependences on the scheduling variable
        w      = 2pi*[2,10,20] # Frequency vector
        w_test = 2π*collect(2:2:25)
        Y,V,X,frequency_matrix, dependence_matrix = generate_signal(f,w,N,true)

        λ      = 0.02 # Regularization parmater
        normal = true # Use normalized basis functions
        Nv     = 50 # Number of basis functions

        se = ls_spectral_lpv(Y,X,V,w_test,Nv; λ = λ, normalize = normal) # Perform LPV spectral estimation
        @show plot(se, phase=true)
        windowpsd = ls_windowpsd_lpv(Y,X,V,w_test,Nv; λ = λ, normalize = normal)

        spectrum_lpv   = psd(se)

        si = sortperm(spectrum_lpv[:],rev=true)
        @test Set(si[1:3]) == Set([1,5,10])

        si = sortperm(windowpsd[:],rev=true)
        @test Set(si[1:3]) == Set([1,5,10])
    end

    @testset "detrend" begin
        @info "testing detrend"
        tre = [1,2,3]
        @test detrend(tre) == [-1,0,1]
        @test tre == [1,2,3] # Shouldn't have changed
        detrend!(tre)
        @test tre == [-1,0,1] # Should have changed
    end

    ## Test ComplexNormal
    @testset "Complex Normal" begin
        @info "testing Complex Normal"
        n  = 10
        n2 = 5
        a  = randn(n)
        A  = randn(n,n)
        A  = A'A + I
        b  = randn(n2)
        B  = randn(n2,n2)
        B  = B'B + I
        X  = randn(n,n2)
        Y  = randn(n,n2)
        cn = ComplexNormal(X,Y)
        @test size(cn.m) == (n2,)
        @test size(cn.Γ) == (n2,n2)
        @test size(cn.C) == (n2,n2)
        # affine_transform(cn, B, b)
        pdf(cn,b)
        pdf(cn,im*b)

        @test isa(cn_Vxx(A,A), Cholesky)
        @test isa(cn_fVxx(A,A), Matrix)
        @test issymmetric(cn_fVxx(A,A))
        cn_V(A,0.1A)

        cn  = ComplexNormal(a,A)
        cn  = ComplexNormal(im*b,A)

        A   = randn(4,4);
        A   = A'A
        x   = randn(1000,3)
        y   = randn(1000,3)
        cn  = ComplexNormal(x,y)
        z   = rand(cn,1000000);
        cn2 = ComplexNormal(z)
        @test norm(Matrix(cn.Γ)-Matrix(cn2.Γ)) < 0.01
        @test norm(Matrix(cn.C)-Matrix(cn2.C)) < 0.01

    end

    @testset "ls methods" begin
        @info "testing ls methods"
        T = 100
        t = 0:0.1:T-0.1
        f = LPVSpectral.default_freqs(t)
        @test f[1] == 0
        @test f[end] == 5
        @test length(f) == 10T÷2+1

        # f2 = LPVSpectral.default_freqs(t,10)
        # @test f2[1] == 0
        # @test f2[end] == 0.5-1/length(t)/2
        # @test length(f2) == T÷2÷10

        @test LPVSpectral.check_freq(f) == 1
        @test_throws ArgumentError LPVSpectral.check_freq([1,0,2])
        A, z = LPVSpectral.get_fourier_regressor(t,f)
        @test size(A) == (10T,2length(f)-1)

        Base.isapprox(t1::Tuple{Float64,Int64}, t2::Tuple{Float64,Int64}; atol) = all(t -> isapprox(t[1],t[2],atol=atol), zip(t1,t2))
        y = sin.(2pi .* t)
        x,freqs = ls_spectral(y,t)
        @test findmax(abs2.(x)) ≈ (2.0length(freqs), 101) atol=1e-4

        W = ones(length(y))
        x,_ = ls_spectral(y,t,f,W)
        @test findmax(abs2.(x)) ≈ (2.0length(freqs), 101) atol=1e-4

        x,freqs = tls_spectral(y,t)
        @test findmax(abs2.(x)) ≈ (2.0length(freqs), 101) atol=1e-4

        x,freqs = ls_windowpsd(y,t,noverlap=0)
        @test findmax(x)[2] == 13

        x,freqs = ls_windowpsd(y,t,nw=16,noverlap=0)
        @test findmax(abs.(x))[2] ==  7

        x,freqs = ls_windowcsd(y,y,t,noverlap=0)
        @test findmax(abs.(x)) ≈ (2.0length(freqs), 11) atol=1e-4

        x,_ = ls_cohere(y,y,t)
        @test all(x .== 1)

        x,freqs = ls_cohere(y,y .+ 0.5 .*randn.(),t,nw=8,noverlap=-1)
        @show mean(x)
        @test findmax(x) ≈ (1.0, 14) atol=0.15
        @test mean(x) < 0.25

    end

    @testset "plots" begin
        @info "testing plots"

        y = randn(1000)
        @show plot(periodogram(y))
        @show plot(spectrogram(y))
        @show plot(melspectrogram(y))
        @show plot(periodogram(filtfilt(ones(4), [4], y)))
        @show plot(welch_pgram(y))

    end

    @testset "Lasso" begin
        @info "Testing Lasso"
        include("test_lasso.jl")
    end

    @testset "autocov" begin
        @info "Testing autocov"

        y = repeat([1.,0.,-1.],100)
        τ,acf = autocov(1:length(y), y, Inf)
        acf0 = autocov(y, demean=false)
        acfh = [mean(acf[τ.==i]) for i = 0:length(acf0)-1]
        @test acfh ≈ acf0 rtol=0.01
        @test norm(acfh - acf0) < 0.01

        τ,acf = autocor(1:length(y), y, Inf)
        acf0 = autocor(y, demean=false)
        acfh = [mean(acf[τ.==i]) for i = 0:length(acf0)-1]
        @test acfh ≈ acf0 rtol=0.01
        @test norm(acfh - acf0) < 0.1

        y = randn(100)
        τ,acf = autocor(1:length(y), y, Inf)
        acf0 = autocor(y, demean=false)
        acfh = [mean(acf[τ.==i]) for i = 0:length(acf0)-1]
        @test acfh ≈ acf0 rtol=0.01
        @test norm(acfh - acf0) < 0.1

        y = randn(10)
        τ,acf = autocov(1:length(y), y, Inf)
        acf0 = autocov(y, demean=false)
        acfh = [mean(acf[τ.==i]) for i = 0:length(acf0)-1]
        @test acfh ≈ acf0 rtol=0.01
        @test norm(acfh - acf0) < 0.2

        y = randn(10)
        τ,acf = autocor(1:length(y), y, Inf)
        acf0 = autocor(y, demean=false)
        acfh = [mean(acf[τ.==i]) for i = 0:length(acf0)-1]
        @test acfh ≈ acf0 rtol=0.03
        @test norm(acfh - acf0) < 0.2

        y = [randn(10) for _ in 1:10]
        t = reshape(1.0:100,10,10)
        t = [t[:,i] for i in 1:size(t,2)]
        τ,acf = autocov(t, y, Inf)
        acf0 = mean(autocov.(y, demean=false))
        acfh = [mean(acf[τ.==i]) for i = 0:length(acf0)-1]
        @test acfh ≈ acf0 rtol=0.01
        @test norm(acfh - acf0) < 0.2

        τ,acf = autocor(t, y, Inf)
        acf0 = mean(autocor.(y, demean=false))
        acfh = [mean(acf[τ.==i]) for i = 0:length(acf0)-1]
        @test acfh ≈ acf0 rtol=0.01
        @test norm(acfh - acf0) < 0.2

        # See what happens if one series is contant
        y = zeros(10)
        τ,acf = autocor(1:10, y, Inf)
        @test all(acf .== 1)

        using LPVSpectral: _autocov, _autocor, isequidistant

        @test isequidistant(1:5)
        @test isequidistant(1:2:10)
        @test !isequidistant(reverse(1:2:10))
        @test isequidistant(collect(1:5))
        @test isequidistant(collect(1:2:10))
        @test isequidistant(collect(1:0.33:10))

        res = map(1:10) do _
            t = 100rand(100)
            @test !isequidistant(t)
            t0 = 0:99
            y = sin.(0.05 .* t)
            y0 = sin.(0.05 .* t0)
            τ0, acf0 = autocor(t0, y0, Inf, normalize=true)
            τ,acf = autocor(t, y, Inf)
            acff = filtfilt(ones(200),[200], acf)
            @test count(τ .== 0) == length(y)
            # plot(τ[1:10:end],acf[1:10:end])
            # plot!(τ0[1:10:end],acf0[1:10:end])
            # plot!(τ0[1:10:end],acff[1:10:end])
            mean(abs2,acf0-acff) < 0.05
        end
        @test mean(res) > 0.7

        res = map(1:10) do _
            t = 100rand(100)
            @test !isequidistant(t)
            t0 = 0:99
            y = sin.(0.05 .* t)
            y0 = sin.(0.05 .* t0)
            τ0, acf0 = autocov(t0, y0, Inf, normalize=true)
            τ,acf = autocov(t, y, Inf)
            acff = filtfilt(ones(200),[200], acf)
            @test count(τ .== 0) == length(y)
            # plot(τ[1:10:end],acf[1:10:end])
            # plot!(τ0[1:10:end],acf0[1:10:end])
            # plot!(τ0[1:10:end],acff[1:10:end])
            mean(abs2,acf0-acff) < 0.025
        end
        @test mean(res) > 0.7

    end

end


using SparseArrays
using BenchmarkTools
using Test
import StatsBase: sample
##

function sum_over_cols1!(dest::Vector{Float64},M::SparseMatrixCSC)
  _colptr = SparseArrays.getcolptr(M) # column indexing
  Mnz = nonzeros(M) # direct access to weights
  @inbounds @simd for i in 1:length(_colptr)-1
    accu = 0.0
    for k in _colptr[i]:_colptr[i+1]-1
      accu += Mnz[k]
    end
    dest[i] = accu
  end
  return dest
end
function sum_over_cols1B!(dest::Vector{Float64},M::SparseMatrixCSC)
  @assert length(dest) == size(M,2)
  fill!(dest,0.0)
  _colptr = SparseArrays.getcolptr(M) # column indexing
  Mnz = nonzeros(M) # direct access to weights
  @inbounds @simd for i in 1:length(_colptr)-1
    dest[i] += sum(Mnz[_colptr[i]:_colptr[i+1]-1] )
  end
  return dest
end

function sum_over_cols2!(dest::Matrix{Float64},M::SparseMatrixCSC)
  return sum!(dest,M)
end

function sum_over_cols3!(M::SparseMatrixCSC)
  return sum(M;dims=1)
end

function sum_over_cols4!(M::Matrix{Float64})
  return sum(M;dims=1)
end
##

const n = 6000
const spars = 0.3
function domat(_n,_spars)
  return sprand(_n,_n,_spars)
end

## first, test that functions return same vals
Mtest = domat(n,spars)
test1 = let ret = fill(NaN,n)
  sum_over_cols1!(ret,Mtest)
  ret
end
test1B = let ret = fill(NaN,n)
  sum_over_cols1B!(ret,Mtest)
  ret
end
test2 = let ret=fill(NaN,(1,n))
  sum_over_cols2!(ret,Mtest)
  ret[:]
end
test3 = sum_over_cols3!(Mtest)[:]
test4 = sum_over_cols4!(Matrix(Mtest))[:]

@test all(isapprox.(test1,test2))
@test all(isapprox.(test1,test1B))
@test all(isapprox.(test1,test3))
@test all(isapprox.(test1,test4))
# to visualize :
@show extrema(test1 .- test3)

## Now, benchmark them!
@info "Benchmark for function 1"
@benchmark sum_over_cols1!(dest,M) setup=(M=domat(n,spars);dest=Vector{Float64}(undef,n))
##
@info "Benchmark for function 1B"
@benchmark sum_over_cols1B!(dest,M) setup=(M=domat(n,spars);dest=Vector{Float64}(undef,n))
##
@info "Benchmark for function 2"
@benchmark sum_over_cols2!(dest,M) setup=(M=domat(n,spars);dest=Matrix{Float64}(undef,1,n))
##
@info "Benchmark for function 3"
@benchmark sum_over_cols3!(M) setup=(M=domat(n,spars))
##
@info "Benchmark for function 4"
@benchmark sum_over_cols4!(M) setup=(M=Matrix(domat(n,spars)))

##
########################
# Now repeat for row sum

const n = 6000
const spars = 0.3
function domat(_n,_spars)
  return sprand(_n,_n,_spars)
end
Mtest = domat(n,spars)

function sum_over_rows1!(dest::Vector{Float64},M::SparseMatrixCSC)
  return sum!(dest,M)
end
function sum_over_rows2!(M::SparseMatrixCSC)
  return sum(M;dims=2)
end
function sum_over_rows3!(dest::Matrix{Float64},M::SparseMatrixCSC)
  Mtr = permutedims(M)
  return sum!(dest,Mtr)
end
function sum_over_rows4!(dest::Matrix{Float64},M::SparseMatrixCSC)
  Mtr = transpose(M)
  return sum!(dest,Mtr)
end
function sum_over_rows5!(dest::Vector{Float64},M::SparseMatrixCSC)
  fill!(dest,0.0)
  _rowidx = SparseArrays.rowvals(M)
  Mnz = nonzeros(M) # direct access to weights
  @inbounds for (i,r) in enumerate(_rowidx)
    dest[r] += Mnz[i]
  end
  return dest
end
function sum_over_rows6!(M::SparseMatrixCSC)
  dest=zeros(size(M,1))
  _rowidx = SparseArrays.rowvals(M)
  Mnz = nonzeros(M) # direct access to weights
  @inbounds for (i,r) in enumerate(_rowidx)
    dest[r] += Mnz[i]
  end
  return dest
end

##

test1 = let ret = fill(NaN,n)
  sum_over_rows1!(ret,Mtest)
  ret
end
test2 = sum_over_rows2!(Mtest)
test3 = let ret = fill(NaN,(1,n))
  sum_over_rows2!(ret,Mtest)
  ret[:]
end
test4 = let ret = fill(NaN,(1,n))
  sum_over_rows3!(ret,Mtest)
  ret[:]
end
test5 = let ret = fill(NaN,n)
  sum_over_rows4!(ret,Mtest)
  ret
end
test6 = sum_over_rows5!(Mtest)

@test all(isapprox.(test1,test2))
@test all(isapprox.(test1,test3))
@test all(isapprox.(test1,test4))
@test all(isapprox.(test1,test5))
@test all(isapprox.(test1,test6))

##
@info "Benchmark for function 1"
@benchmark sum_over_rows1!(dest,M) setup=(M=domat(n,spars);dest=Vector{Float64}(undef,n))
##
@info "Benchmark for function 2"
@benchmark sum_over_rows2!(M) setup=(M=domat(n,spars))
##
@info "Benchmark for function 3"
@benchmark sum_over_rows3!(dest,M) setup=(M=domat(n,spars);dest=Matrix{Float64}(undef,(1,n)))
##
@info "Benchmark for function 4"
@benchmark sum_over_rows4!(dest,M) setup=(M=domat(n,spars);dest=Matrix{Float64}(undef,(1,n)))
##
@info "Benchmark for function 5"
@benchmark sum_over_rows5!(dest,M) setup=(M=domat(n,spars);dest=Vector{Float64}(undef,n))
##
@info "Benchmark for function 6"
@benchmark sum_over_rows6!(M) setup=(M=domat(n,spars))

##

const n = 6000
const n_sum = 400

function mysumloop(vec::Vector{Float64},idxs)
  ret=0.0
  @inbounds for idx in idxs
    ret += vec[idx]
  end
  return ret
end

##
@benchmark sum(view(vec,idxs)) setup=(vec=randn(n);idxs=sample(1:n,n_sum))
##
@benchmark mapreduce(idx->vec[idx],+,idxs) setup=(vec=randn(n);idxs=sample(1:n,n_sum))
##
@benchmark mysumloop(vec,idxs) setup=(vec=randn(n);idxs=sample(1:n,n_sum))
##
@benchmark sum(view(vec,idxs)) setup=(vec=randn(n);idxs=(1:n_sum))
##
@benchmark mapreduce(idx->vec[idx],+,idxs) setup=(vec=randn(n);idxs=(1:n_sum))
##
@benchmark mysumloop(vec,idxs) setup=(vec=randn(n);idxs=(1:n_sum))

##
# bonus, finding element in array

neus = collect(1:600)

function is_in_all1(i::I,neus::Vector{I}) where I
  return i in neus
end
function is_in_all2(i::I,neus::Vector{I}) where I
  return !isempty(searchsorted(neus,i))
end

@benchmark is_in_all1(i,$neus) setup=(i=rand(1:600))
@benchmark is_in_all2(i,$neus) setup=(i=rand(1:600))


# Copyright 2007, Timothy A. Davis, Univ. of Florida
# Copyright 2017, Andrey Prokopenko, ORNL

# Adapted from Tim Davis' meshnd code
function meshnd(m, n, k = 1)
    """MESHND creation and nested dissection of a regular 2D or 3D mesh.

    G = meshnd(m,n)   constructs a m-by-n 2D mesh G
    G = meshnd(m,n,k) constructs a m-by-n-by-k 3D mesh G

    Example:
    G = meshnd(4,5)

    returns
    G =
    1     2     3     4     5
    6     7     8     9    10
    11    12    13    14    15
    16    17    18    19    20

    With no inputs, a few example meshes are generated and plotted.

    See also nested, numgrid.
    """

    if k == 1
        # create the m-by-n-by-k mesh in "natural" (row-major) order.  This is how
        # a typical 2D mesh is ordered.  A column-major order would be better, since
        # in that case G(:) would equal 1:(m*n) ... but let's stick with tradition.

        # FIXME: this does not work as expected
        # G = reshape(1:(m*n*k), n, m, k)'
        G = zeros(Int64, m, n, k)
        for ii = 1:m
            for jj = 1:n
                G[ii, jj, 1] = (ii-1)*m + jj
            end
        end
    else
        # create the m-by-n-by-k mesh in column-major order. The first m-by-n-by-1
        # slice is in column-major order, followed by all the other slices 2 to k.
        G = reshape(1:(m*n*k), m, n, k)
    end

    return G

end


# ---
# title: 956. Tallest Billboard
# id: problem956
# author: Tian Jun
# date: 2020-10-31
# difficulty: Hard
# categories: Dynamic Programming
# link: <https://leetcode.com/problems/tallest-billboard/description/>
# hidden: true
# ---
#
# You are installing a billboard and want it to have the largest height.  The
# billboard will have two steel supports, one on each side.  Each steel support
# must be an equal height.
#
# You have a collection of `rods` which can be welded together.  For example, if
# you have rods of lengths 1, 2, and 3, you can weld them together to make a
# support of length 6.
#
# Return the largest possible height of your billboard installation.  If you
# cannot support the billboard, return 0.
#
#
#
# **Example 1:**
#
#
#
#     Input: [1,2,3,6]
#     Output: 6
#     Explanation: We have two disjoint subsets {1,2,3} and {6}, which have the same sum = 6.
#
#
# **Example 2:**
#
#
#
#     Input: [1,2,3,4,5,6]
#     Output: 10
#     Explanation: We have two disjoint subsets {2,3,5} and {4,6}, which have the same sum = 10.
#
#
# **Example 3:**
#
#
#
#     Input: [1,2]
#     Output: 0
#     Explanation: The billboard cannot be supported, so we return 0.
#
#
#
#
# **Note:**
#
#   1. `0 <= rods.length <= 20`
#   2. `1 <= rods[i] <= 1000`
#   3. `The sum of rods is at most 5000.`
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


export _At_mul_B

# default tolerance for matrix condition number (see 'isinvertible')
const DEFAULT_COND_TOL = 1e6

# matrix-matrix multiplication and division
@inline _At_mul_B(A, B) = transpose(A) * B
@inline _At_ldiv_B(A, B) = transpose(A) \ B

"""
    dot_zero(x::AbstractVector{N}, y::AbstractVector{N}) where{N<:Real}

Dot product with preference for zero value in the presence of infinity values.

### Input

- `x` -- first vector
- `y` -- second vector

### Output

The dot product of `x` and `y`, but with the rule that `0 * Inf == 0`.
"""
function dot_zero(x::AbstractVector{N}, y::AbstractVector{N}) where{N<:Real}
    res = zero(N)
    for i in 1:length(x)
        if !iszero(x[i]) && !iszero(y[i])
            res += x[i] * y[i]
        end
    end
    return res
end

"""
    sign_cadlag(x::N)::N where {N<:Real}

This function works like the sign function but is ``1`` for input ``0``.

### Input

- `x` -- real scalar

### Output

``1`` if ``x ≥ 0``, ``-1`` otherwise.

### Notes

This is the sign function right-continuous at zero (see
[càdlàg function](https://en.wikipedia.org/wiki/C%C3%A0dl%C3%A0g)).
It can be used with vector-valued arguments via the dot operator.

### Examples

```jldoctest
julia> LazySets.sign_cadlag.([-0.6, 1.3, 0.0])
3-element Array{Float64,1}:
 -1.0
  1.0
  1.0
```
"""
function sign_cadlag(x::N)::N where {N<:Real}
    return x < zero(x) ? -one(x) : one(x)
end

"""
    ispermutation(u::AbstractVector{T}, v::AbstractVector)::Bool where {T}

Check that two vectors contain the same elements up to reordering.

### Input

- `u` -- first vector
- `v` -- second vector

### Output

`true` iff the vectors are identical up to reordering.

### Examples

```jldoctest
julia> LazySets.ispermutation([1, 2, 2], [2, 2, 1])
true

julia> LazySets.ispermutation([1, 2, 2], [1, 1, 2])
false

```
"""
function ispermutation(u::AbstractVector{T}, v::AbstractVector)::Bool where {T}
    if length(u) != length(v)
        return false
    end
    occurrence_map = Dict{T, Int}()
    has_duplicates = false
    for e in u
        if e ∉ v
            return false
        end
        if haskey(occurrence_map, e)
            occurrence_map[e] += 1
            has_duplicates = true
        else
            occurrence_map[e] = 1
        end
    end
    if has_duplicates
        for e in v
            if !haskey(occurrence_map, e) || occurrence_map[e] == 0
                return false
            end
            occurrence_map[e] -= 1
        end
    end
    return true
end

"""
    issquare(M::AbstractMatrix)::Bool

Check whether a matrix is square.

### Input

- `M` -- matrix

### Output

`true` iff the matrix is square.
"""
function issquare(M::AbstractMatrix)::Bool
    m, n = size(M)
    return m == n
end

"""
    isinvertible(M::Matrix; [cond_tol]::Number=DEFAULT_COND_TOL)

A sufficient check of a matrix being invertible (or nonsingular).

### Input

- `M`        -- matrix
- `cond_tol` -- (optional, default: `DEFAULT_COND_TOL`) tolerance of matrix
                condition

### Output

If the result is `true`, `M` is invertible.
If the result is `false`, the matrix is non-square or this function could not
conclude.

### Algorithm

We check whether the matrix is square and whether the
[matrix condition number](https://en.wikipedia.org/wiki/Condition_number#Matrices)
`cond(M)` is below some prescribed tolerance.
"""
function isinvertible(M::Matrix; cond_tol::Number=DEFAULT_COND_TOL)
    return issquare(M) && cond(M) < cond_tol
end

# cond is not available for sparse matrices; see JuliaLang#6485 and related issues
function isinvertible(M::AbstractSparseMatrix;
                      cond_tol::Number=DEFAULT_COND_TOL)
    return issquare(M) && isinvertible(Matrix(M), cond_tol=cond_tol)
end

function isinvertible(M::Diagonal; cond_tol=nothing)
    return !any(iszero, diag(M))
end

"""
    remove_duplicates_sorted!(v::AbstractVector)

Remove duplicate entries in a sorted vector.

### Input

- `v` -- sorted vector

### Output

The input vector without duplicates.
"""
function remove_duplicates_sorted!(v::AbstractVector)
    for i in length(v)-1:-1:1
        if v[i] == v[i+1]
            splice!(v, i+1)
        end
    end
    return v
end

"""
    samedir(u::AbstractVector{N},
            v::AbstractVector{N})::Tuple{Bool, Real} where {N<:Real}

Check whether two vectors point in the same direction.

### Input

- `u` -- first vector
- `v` -- second vector

### Output

`(true, k)` iff the vectors are identical up to a positive scaling factor `k`,
and `(false, 0)` otherwise.

### Examples

```jldoctest
julia> using LazySets: samedir

julia> samedir([1, 2, 3], [2, 4, 6])
(true, 0.5)

julia> samedir([1, 2, 3], [3, 2, 1])
(false, 0)

julia> samedir([1, 2, 3], [-1, -2, -3])
(false, 0)

```
"""
function samedir(u::AbstractVector{N},
                 v::AbstractVector{N}
                )::Tuple{Bool, Real} where {N<:Real}
    @assert length(u) == length(v) "wrong dimension"
    no_factor = true
    factor = 0
    @inbounds for i in 1:length(u)
        if u[i] == 0
            if v[i] != 0
                return (false, 0)
            end
            continue
        elseif v[i] == 0
            return (false, 0)
        end
        if no_factor
            no_factor = false
            factor = u[i] / v[i]
            if factor < 0
                return (false, 0)
            end
        elseif factor != u[i] / v[i]
            return (false, 0)
        end
    end
    if no_factor
        # both vectors are zero
        return (true, 0)
    end
    return (true, factor)
end

"""
    nonzero_indices(v::AbstractVector{N})::Vector{Int} where {N<:Real}

Return the indices in which a vector is non-zero.

### Input

- `v` -- vector

### Output

A vector of ascending indices `i` such that the vector is non-zero in dimension
`i`.
"""
function nonzero_indices(v::AbstractVector{N})::Vector{Int} where {N<:Real}
    n = length(v)
    res = Vector{Int}()
    sizehint!(res, n)
    for i in 1:n
        if v[i] != zero(N)
            push!(res, i)
        end
    end
    return res
end

function nonzero_indices(v::SparseVector{N})::Vector{Int} where {N<:Real}
    return v.nzind
end

"""
    substitute(substitution::Dict{Int, T}, x::AbstractVector{T}) where {T}

Apply a substitution to a given vector.

### Input

- `substitution` -- substitution (a mapping from an index to a new value)
- `x`            -- vector

### Output

A fresh vector corresponding to `x` after `substitution` was applied.
"""
function substitute(substitution::Dict{Int, T}, x::AbstractVector{T}) where {T}
    return substitute!(substitution, copy(x))
end

"""
    substitute!(substitution::Dict{Int, T}, x::AbstractVector{T}) where {T}

Apply a substitution to a given vector.

### Input

- `substitution` -- substitution (a mapping from an index to a new value)
- `x`            -- vector (modified in this function)

### Output

The same (but see the Notes below) vector `x` but after `substitution` was
applied.

### Notes

The vector `x` is modified in-place if it has type `Vector` or `SparseVector`.
Otherwise, we first create a new `Vector` from it.
"""
function substitute!(substitution::Dict{Int, T}, x::AbstractVector{T}) where {T}
    return substitute!(Vector(x), substitution)
end

function substitute!(substitution::Dict{Int, T},
                     x::Union{Vector{T}, SparseVector{T}}) where {T}
    for (index, value) in substitution
        x[index] = value
    end
    return x
end

"""
    reseed(rng::AbstractRNG, seed::Union{Int, Nothing})::AbstractRNG

Reset the RNG seed if the seed argument is a number.

### Input

- `rng`  -- random number generator
- `seed` -- seed for reseeding

### Output

The input RNG if the seed is `nothing`, and a reseeded RNG otherwise.
"""
function reseed(rng::AbstractRNG, seed::Union{Int, Nothing})::AbstractRNG
    if seed != nothing
        return Random.seed!(rng, seed)
    end
    return rng
end

"""
    cross_product(M::AbstractMatrix{N}) where {N<:Real}

Compute the high-dimensional cross product of ``n-1`` ``n``-dimensional vectors.

### Input

- `M` -- ``n × n - 1``-dimensional matrix

### Output

A vector.

### Algorithm

The cross product is defined as follows:

```math
\\left[ \\dots, (-1)^{n+1} \\det(M^{[i]}), \\dots \\right]^T
```
where ``M^{[i]}`` is defined as ``M`` with the ``i``-th row removed.
See *Althoff, Stursberg, Buss: Computing Reachable Sets of Hybrid Systems Using
a Combination of Zonotopes and Polytopes. 2009.*
"""
function cross_product(M::AbstractMatrix{N})::Vector{N} where {N<:Real}
    n = size(M, 1)
    @assert 1 < n == size(M, 2) + 1 "the matrix must be n x (n-1) dimensional"

    v = Vector{N}(undef, n)
    minus = false
    for i in 1:n
        Mi = view(M, 1:n .!= i, :)  # remove i-th row
        d = det(Mi)
        if minus
            v[i] = -d
            minus = false
        else
            v[i] = d
            minus = true
        end
    end
    return v
end

"""
    StrictlyIncreasingIndices

Iterator over the vectors of `m` strictly increasing indices from 1 to `n`.

### Fields

- `n` -- size of the index domain
- `m` -- number of indices to choose (resp. length of the vectors)

### Notes

The vectors are modified in-place.

The iterator ranges over ``\\binom{n}{m}`` (`n` choose `m`) possible vectors.

This implementation results in a lexicographic order with the last index growing
first.

### Examples

```jldoctest
julia> for v in LazySets.StrictlyIncreasingIndices(4, 2)
           println(v)
       end
[1, 2]
[1, 3]
[1, 4]
[2, 3]
[2, 4]
[3, 4]
```
"""
struct StrictlyIncreasingIndices
    n::Int
    m::Int

    function StrictlyIncreasingIndices(n::Int, m::Int)
        @assert n >= m > 0 "require n >= m > 0"
        new(n, m)
    end
end

Base.eltype(::Type{StrictlyIncreasingIndices}) = Vector{Int}
Base.length(sii::StrictlyIncreasingIndices) = binomial(sii.n, sii.m)

# initialization
function Base.iterate(sii::StrictlyIncreasingIndices)
    v = [1:sii.m;]
    return (v, v)
end

# normal iteration
function Base.iterate(sii::StrictlyIncreasingIndices, state::AbstractVector{Int})
    v = state
    i = sii.m
    diff = sii.n
    if i == diff
        return nothing
    end
    while v[i] == diff
        i -= 1
        diff -= 1
    end
    # update vector
    v[i] += 1
    for j in i+1:sii.m
        v[j] = v[j-1] + 1
    end
    # detect termination: first index has maximum value
    if i == 1 && v[1] == (sii.n - sii.m + 1)
        return (v, nothing)
    end
    return (v, v)
end

# termination
function Base.iterate(sii::StrictlyIncreasingIndices, state::Nothing)
    return nothing
end

"""
    subtypes(interface, concrete::Bool)

Return the concrete subtypes of a given interface.

### Input

- `interface` -- an abstract type, usually a set interface
- `concrete`  -- if `true`, seek further the inner abstract subtypes of the given
                 interface, otherwise return only the direct subtypes of `interface`

### Output

A list with the subtypes of the abstract type `interface`, sorted alphabetically.

### Examples

Consider the `AbstractPolytope` interface. If we include the abstract subtypes
of this interface,

```jldoctest subtypes
julia> using LazySets: subtypes

julia> subtypes(AbstractPolytope, false)
4-element Array{Any,1}:
 AbstractCentrallySymmetricPolytope
 AbstractPolygon
 HPolytope
 VPolytope
```

We can use this function to obtain the concrete subtypes of
`AbstractCentrallySymmetricPolytope` and `AbstractPolygon` (further until all
concrete types are obtained), using the `concrete` flag:

```jldoctest subtypes
julia> subtypes(AbstractPolytope, true)
14-element Array{Type,1}:
 Ball1
 BallInf
 HPolygon
 HPolygonOpt
 HPolytope
 Hyperrectangle
 Interval
 LineSegment
 Singleton
 SymmetricIntervalHull
 VPolygon
 VPolytope
 ZeroSet
 Zonotope
```
"""
function subtypes(interface, concrete::Bool)

    subtypes_to_test = subtypes(interface)

    # do not seek the concrete subtypes further
    if !concrete
        return sort(subtypes_to_test, by=string)
    end

    result = Vector{Type}()
    i = 0
    while i < length(subtypes_to_test)
        i += 1
        subtype = subtypes_to_test[i]
        new_subtypes = subtypes(subtype)
        if isempty(new_subtypes)
            # base type found
            push!(result, subtype)
        else
            # yet another interface layer
            append!(subtypes_to_test, new_subtypes)
        end
    end
    return sort(result, by=string)
end

"""
    delete_zero_columns(A::AbstractMatrix)

Remove all columns that only contain zeros from a given matrix.

### Input

- `A`    -- matrix
- `copy` -- (optional, default: `false`) flag to copy the matrix

### Output

A matrix.

If the input matrix `A` does not contain any zero column, we return `A` unless
the option `copy` is set.
If the input matrix contains zero columns, we always return a copy if the option
`copy` is set and otherwise a `SubArray` via `@view`.
"""
function delete_zero_columns(A::AbstractMatrix, copy::Bool=false)
    n = size(A, 2)
    nonzero_columns = Vector{Int}()
    sizehint!(nonzero_columns, n)
    for i in 1:n
        if !iszero(A[:, i])
            push!(nonzero_columns, i)
        end
    end
    if copy
        if length(nonzero_columns) == n
            return copy(A)
        else
            return A[:, nonzero_columns]
        end
    else
        if length(nonzero_columns) == n
            return A
        else
            return @view A[:, nonzero_columns]
        end
    end
end

"""
    inner(x::AbstractVector{N}, A::AbstractMatrix{N}, y::AbstractVector{N}
         ) where {N}

Compute the inner product ``xᵀ A y``.

### Input

- `x` -- vector on the left
- `A` -- matrix
- `y` -- vector on the right

### Output

The (scalar) result of the multiplication.
"""
function inner(x::AbstractVector{N}, A::AbstractMatrix{N}, y::AbstractVector{N}
              ) where {N}
    dot(x, A * y)
end

"""
    rectify(x::AbstractVector{N}) where {N<:Real}

Rectify a vector, i.e., take the element-wise maximum with zero.

### Input

- `x` -- vector

### Output

A copy of the vector where each negative entry is replaced by zero.
"""
function rectify(x::AbstractVector{N}) where {N<:Real}
    return map(xi -> max(xi, zero(N)), x)
end


@testset "bigleaf_constants" begin
    cst = BigleafConstants()
    cst_moon = BigleafConstants(g = 9.81/6)
    f1(;cst = BigleafConstants()) = 2*cst.g
    @test f1() == 2*cst.g
    @test f1(;cst=cst_moon) == 2*cst_moon.g
    #@code_llvm f1()
    #@code_llvm f1(;cst=BigleafConstants(g = 9.81/6))
    #const cst_moon2 = BigleafConstants(g = 9.81/6)
    #@code_llvm f1(;cst=cst_moon2)
    #@code_llvm f1(;cst=BigleafConstants()) # not optimized
    f2(Tair;cst = BigleafConstants()) = Tair*oftype(Tair,cst.g)
    @test f2(2.0) == 2*cst.g
    #@code_llvm f2(Float32(2.0))
    Tair32 = Float32(2.0)
    @test f2(Tair32; cst=cst_moon) == Float32(2*cst_moon.g)
    #@code_llvm f2(Tair32;cst=cst_moon)
end


function selectionsort!(x::AbstractArray)
    n = length(x)
    for i ∈ 1:n
        # mi = minimum index
        mi = i
        for j ∈ (i+1):n
            @inbounds if x[j] ≤ x[mi]
                mi = j
            end
        end
        @inbounds tmp   = x[i]
        @inbounds x[i]  = x[mi]
        @inbounds x[mi] = tmp
    end
    return nothing
end


# Inflación de Percentiles Equiponderados

# InflationPercentileEq.jl - Definición de percentiles equiponderados

"""
    InflationPercentileEq <: InflationFunction
Función de inflación para computar el percentil equiponderado `k`.

## Utilización

    function (inflfn::InflationPercentileEq)(base::VarCPIBase{T}) where T

Define cómo opera `InflationPercentileEq` sobre un objeto de tipo VarCPIBase.

    function (inflfn::InflationPercentileEq)(cs::CountryStructure)

Define cómo opera `InflationPercentileEq` sobre un objeto de tipo CountryStructure.

### Ejemplo
Cálculo del percentil 70 de la distribución de variaciones intermensuales ponderadas

```julia-repl
julia> percEqfn = InflationPercentileEq(70)
(::InflationPercentileEq) (generic function with 5 methods)
julia>percEqfn(gtdata) #gtdata es de tipo UniformCountryStructure
231-element Vector{Float32}:
9.812069
10.271847
10.382021
10.323441
10.427952
⋮
1.9042492
1.9850135
1.9871473
2.0293117
2.1856546
```
"""
Base.@kwdef struct InflationPercentileEq <: InflationFunction
    k::Float32
end

# Métodos para crear funciones de inflación a partir de enteros y flotantes
"""
    InflationPercentileEq(k::Int) = InflationPercentileEq(k = Float32(k) / 100)
    InflationPercentileEq(q::T) where {T <: AbstractFloat}
Permite utilizar valores de `k` que no necesariamente son de tipo de punto flotante, como por ejemplo: enteros, fracciones, Float32 y Float64.

# Ejemplo
Se usa 0.70 en lugar de 70

```julia-repl
julia> percEqfn = InflationPercentileEq(0.70)
(::InflationPercentileEq) (generic function with 5 methods)

julia>percEqfn(gtdata)
231-element Vector{Float32}:
9.812069
10.271847
10.382021
10.323441
10.427952
⋮
1.9042492
1.9850135
1.9871473
2.0293117
2.1856546
```
Se obtienen los mismos resultados.
"""
InflationPercentileEq(k::Int) = InflationPercentileEq(k = Float32(k) / 100)
function InflationPercentileEq(q::T) where {T <: AbstractFloat}
    q < 1.0 && return InflationPercentileEq(convert(Float32, q))
    InflationPercentileEq(convert(Float32, q) / 100)
end

"""
    measure_name(inflfn::InflationPercentileEq)

Indica qué medida se utiliza para una instancia de una función de inflación.

# Ejemplo
```julia-repl
julia> percEqfn = InflationPercentileEq(0.70)
julia> measure_name(percEqfn)
"Percentil equiponderado 70.0"
```
"""
measure_name(inflfn::InflationPercentileEq) = "Percentil equiponderado " * string(round(100inflfn.k, digits=2))

# Parámetro de la función de inflación
params(inflfn::InflationPercentileEq) = (inflfn.k, )

# Las funciones sobre VarCPIBase resumen en variaciones intermensuales
function (inflfn::InflationPercentileEq)(base::VarCPIBase{T}) where T
    # InflationPercentileEq k de la distribución de variaciones intermensuales
    k = inflfn.k

    # Obtener el percentil k de la distribución intermensual
    rows = size(base.v, 1)

    k_interm = Vector{T}(undef, rows)
    Threads.@threads for r in 1:rows
        row = @view base.v[r, :]
        k_interm[r] = quantile(row, k)
    end

    k_interm
end

function InflationPercentileEq(vec::Vector{<:Real})
    x = vec[1]
    return InflationPercentileEq(x)
end


using Gadfly

set_default_plot_size(6inch, 3inch)

n = 10
plot(x=rand(n), y=rand(n), color=sqrt.(rand(n)), Scale.color_sqrt)


module TestCluster
using Test

include("../src/cluster.jl")

@testset "Partition 1112                 " begin
    nums = [1, 1, 1, 2]
    @test all(nums .> 0)
    p = Cluster.PartitionIndex(nums)
    @test length(p) == 2
    @test Set(p[1]) == Set([1, 2, 3])
    @test Set(p[2]) == Set([4])
end

@testset "Test Partition5                " begin
    nums = [2, 3, 1, 1, 3]
    @test all(nums .> 0)
    p = Cluster.PartitionIndex(nums)
    @test length(p) == 3
    for i in 1:3
        @test Set(findall(nums .== i)) == Set(p[i])
    end
end

@testset "Test Cluster                   " begin
    neigh = Dict(1 => [2, 3],
                 2 => [1, 4],
                 3 => [1, 4],
                 4 => [2, 3])
    x = [ 0.0  0.1
         -0.1  1.1]
    @test size(x) == (2,2)
    p = Cluster.cluster(x, neigh, eps=0.2)
    @test length(p) == 2
    @test isperm(p.V)
    @test Set(p[1]) == Set([4])
    @test Set(p[2]) == Set([1,2,3])
end

end


# TODO: fit(D::Distribution, x::Vector{Float}, w::Vector{Float}) --> weighted maximum likelihood estimation...

# Fitting function, add more methods here
function fit!(hmm, o; method=:baum_welch, max_iter=100, tol=1e-5, scaling=true)
	if method == :baum_welch
		return baum_welch!(hmm, o, max_iter, tol, scaling)
	end
end

function baum_welch!(hmm, o, max_iter, tol, scaling)
	# Fit hmm parameters given set of observation sequences
	# Setting tol to NaN will prevent early stopping, resulting in 'max_iter' iterations
	#n_seq = length(sequences)

    # convergence history of the fit, log-liklihood
    ch = (Float64)[]

    for k = 1:max_iter
    	push!(ch,0.0)

		# E step
		log_p_obs,alpha,beta,x,g = calc_stats(hmm,o,scaling)
		ch[end] += log_p_obs #(log_p_obs + log(length(o)))

		# M step (re-estimation)
		re_estimate!(hmm,o,x,g)

		if length(ch)>1 && (ch[end]-ch[end-1] < tol)
			println("Baum-Welch converged, stopping early")
			break
		end
	end

	return ch
end

function re_estimate!(hmm,o,x,g)
	# Update state transition matrix
    for i = 1:hmm.n
        denom = sum(g[1:end-1,i]) # Expected number of transitions from state 'i'
        for j = 1:hmm.n
            # Numerator is the expected number of transitions from state 'i' to 'j'
            hmm.A[i,j] = sum(x[:,i,j]) / denom
        end
    end

	# Update emission probability distributions
	for i = 1:hmm.n
		# weight each observation by probability of being in state i
		hmm.B[i] = fit_mle(typeof(hmm.B[i]), o, g[:,i])
	end

	# Re-estimate hmm.p (initial state probabilities)
	hmm.p = vec(g[1,:])
end

function calc_stats(hmm,o,scaling)
	## Single E-M iteration in the Baum-Welch procedure
	n_obs = length(o)

	# Calculate forward/backward probabilities
	alpha,beta,log_p_obs = forward_backward(hmm,o;scaling=scaling)

	# x[t,i,j] = probability of being in state 'i' at 't' and then in state 'j' at 't+1'
	x = zeros(n_obs-1, hmm.n, hmm.n)
	for t = 1:(n_obs-1)
		for i = 1:hmm.n
			for j = 1:hmm.n
				x[t,i,j] = alpha[t,i] * hmm.A[i,j] * pdf(hmm.B[j],o[t+1]) * beta[t+1,j]
			end
		end
		x[t,:,:] ./= sum(x[t,:,:]) # normalize to achieve probabilities
	end

	# g[t,i] = probability of being in state 'i' at step 't' given all observations
	g = alpha .* beta
	g ./= sum(g,2)   # normalize across states

	return log_p_obs, alpha, beta, x, g
end


######################################################
# functions related to computation of reduced hessian
######################################################

# Q xx' skron yy' Qt
function skron(
  x::AbstractVector,
  y::AbstractVector,
  indRow::AbstractVector,
  indCol::AbstractVector)

  nRow = length(indRow)
  nCol = length(indCol)
  out = zeros( nRow, nCol )
  for ri=1:nRow
    for ci=1:nCol
      out[ri, ci] = skron(x, y, indRow[ri], indCol[ci])
    end
  end
  out
end

function skron(
  x::AbstractVector,
  y::AbstractVector,
  indRow::Int,
  indCol::Int
  )

  p = length(x)
  row1, col1 = ind2subLowerTriangular(p, indRow)
  row2, col2 = ind2subLowerTriangular(p, indCol)

  vrow = (row1 == col1) ? x[row1] * y[row1] : (x[row1] * y[col1] + y[row1] * x[col1]) / sqrt(2.)
  vcol = (row2 == col2) ? x[row2] * y[row2] : (x[row2] * y[col2] + y[row2] * x[col2]) / sqrt(2.)

  vrow * vcol
end

#
# Q Sx skron Sy Qt
function skron(
  Sx::Symmetric,
  Sy::Symmetric)

  p = size(Sx, 1)
  return skron(Sx, Sy, 1:div(p*(p+1),2))
end

function skron(
  Sx::Symmetric,
  Sy::Symmetric,
  indRow::AbstractVector,
  indCol::AbstractVector)

  nRow = length(indRow)
  nCol = length(indCol)
  out = zeros( nRow, nCol )
  for ri=1:nRow
    for ci=1:nCol
      out[ri, ci] = skron(Sx, Sy, indRow[ri], indCol[ci])
    end
  end
  out
end

function skron(
  Sx::Symmetric,
  Sy::Symmetric,
  I::AbstractVector)

  m = length(I)

  out = zeros( m, m )

  for ci = 1:m
    for ri = ci:m
      out[ri, ci] = skron(Sx, Sy, I[ri], I[ci])
      if ri != ci
        out[ci, ri] = out[ri, ci]
      end
    end
  end
  out
end

function skron(
  Sx::Symmetric,
  Sy::Symmetric,
  ind_row::Int,
  ind_col::Int
  )

  p = size(Sx, 1)
  row1, col1 = ind2subLowerTriangular(p, ind_row)
  row2, col2 = ind2subLowerTriangular(p, ind_col)

  if row1 == col1
    if row2 == col2
      return Sx[row1, row2] * Sy[row1, row2]
    else
      return ( Sx[row1, row2] * Sy[row1, col2] + Sx[row1, col2] * Sy[row1, row2] ) / sqrt(2.)
    end
  else
    if row2 == col2
      return ( Sx[row1, row2] * Sy[col1, row2] + Sx[col1, row2] * Sy[row1, row2] ) / sqrt(2.)
    else
      return (Sx[row1, row2] * Sy[col1, col2] +
              Sx[row1, col2] * Sy[col1, row2] +
              Sx[col1, row2] * Sy[row1, col2] +
              Sx[col1, col2] * Sy[row1, row2] ) / 2.
    end
  end
end

#
# Q Sx skron yy' Qt
function skron(
  Sx::Symmetric,
  y::AbstractVector,
  indRow::AbstractVector,
  indCol::AbstractVector)

  nRow = length(indRow)
  nCol = length(indCol)
  out = zeros( nRow, nCol )
  for ri=1:nRow
    for ci=1:nCol
      out[ri, ci] = skron(Sx, y, indRow[ri], indCol[ci])
    end
  end
  out
end

function skron(
  Sx::Symmetric,
  y::AbstractVector,
  ind_row::Int,
  ind_col::Int
  )

  p = size(Sx, 1)
  row1, col1 = ind2subLowerTriangular(p, ind_row)
  row2, col2 = ind2subLowerTriangular(p, ind_col)

  if row1 == col1
    if row2 == col2
      return Sx[row1, row2] * y[row1] * y[row2]
    else
      return ( Sx[row1, row2] * y[row1] * y[col2] + Sx[row1, col2] * y[row1] * y[row2] ) / sqrt(2.)
    end
  else
    if row2 == col2
      return ( Sx[row1, row2] * y[col1] * y[row2] + Sx[col1, row2] * y[row1] * y[row2] ) / sqrt(2.)
    else
      return (Sx[row1, row2] * y[col1] * y[col2] +
              Sx[row1, col2] * y[col1] * y[row2] +
              Sx[col1, row2] * y[row1] * y[col2] +
              Sx[col1, col2] * y[row1] * y[row2] ) / 2.
    end
  end
end

######################################################
# functions related to application of Q to Symmetric matrix
######################################################

# Q(S) == svec(S)

function svec(S::Symmetric)
  p = size(S, 1)
  return svec(S, 1:div(p*(p+1),2))
end

function svec(
  S::Symmetric,
  I::AbstractVector)

  m = length(I)
  out = zeros( m )
  for ri = 1:m
    out[ri] = svec(S, I[ri])
  end
  out
end

function svec(
  S::Symmetric,
  indRow::Int)

  row, col = ind2subLowerTriangular(size(S, 1), indRow)
  (row == col) ? S[row, row] : S[row, col] * sqrt(2.)
end

function svec(
  x::AbstractVector,
  y::AbstractVector,
  I::AbstractVector)

  m = length(I)
  out = zeros( m )
  for ri = 1:m
    out[ri] = svec(x, y, I[ri])
  end
  out
end

function svec(
  x::AbstractVector,
  y::AbstractVector,
  indRow::Int
  )

  row, col = ind2subLowerTriangular(length(x), indRow)

  if row == col
    return x[row] * y[row]
  else
    return (x[row] * y[col] + y[row] * x[col]) / sqrt(2.)
  end
end


# colorFunctions.jl
include("ManagePic.jl")

# auxiliary functions

"""
Get maximum, middle and minimum values of each color.

# Arguments
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
"""
function getValues(rgb)

    r, g, b = rgb
    Cmax = max.(r, g, b)
    Cmin = min.(r, g, b)
    sum = r .+ g .+ b
    middleValue = sum .- Cmax .- Cmin
    return [Cmax, middleValue, Cmin]
end
"""
Change contrast.

# Arguments
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
- `parameter::Float64`: in range [-100, 100], negative values create image with lower contrast.
"""
function changeContrast(rgb, parameter)
    if parameter >= -100 && parameter <= 100
        factor = (102 * (parameter + 100)) ./ (100 * (102 - parameter))
        r, g, b = rgb
        arraySize = size(r)
        newRed = zeros(Float64, arraySize[1], arraySize[2])
        newGreen = zeros(Float64, arraySize[1], arraySize[2])
        newBlue = zeros(Float64, arraySize[1], arraySize[2])

        newRed = factor .* (r .- 0.5) .+ 0.5
        newGreen = factor .* (g .- 0.5) .+ 0.5
        newBlue = factor .* (b .- 0.5) .+ 0.5
    else
        println("Error")
    end
    return [newRed, newGreen, newBlue]
end

"""
Change brightness.

# Arguments
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
- `parameter::Float64`: in range [-100, 100]; negative value create image with lower brightness.
"""
function changeBrightness(rgb, value) # wartosc [-100,100]
    if value <= 100 && value >= -100 && (typeof(value) == Float64 || typeof(value) == Int64)
        point = value / 100
        r, g, b = rgb
        newR = min.(max.(0.5* point .+ r, 0), 1)
        newG =  min.(max.(0.5* point .+ g, 0), 1)
        newB = min.(max.(0.5*point .+ b, 0), 1)
            return [newR, newG,newB]
    else
        return ("Error")
    end
end

"""
Change lightness.

# Arguments
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
- `parameter::Float64`: in range [-100, 100]; negative value create image with lower lightness.
"""
function changeLightness(rgb, value) # wartosc [-100,100]
    if value <= 100 && value >= -100 && (typeof(value) == Float64 || typeof(value) == Int64)
        point = value / 100

        h, s, l = rgb2hsl(rgb)
        newL = l .+ l .* point
        tryplet = h, s, newL
        newR, newG, newB = hsl2rgb(tryplet)
        return [newR, newG,newB]

    else
        return ("Error")
    end
end

"""
Change saturation.

#Arguments
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
- `parameter::Float64`: in range [-100, 100], negative value create image with lower saturation.
"""
function changeSaturation(rgb, value)
    if value <= 100 && value >= -100 && (typeof(value) == Float64 || typeof(value) == Int64)
        point = value / 100
        h, s, l = rgb2hsl(rgb)
        if value >= 0
            complementS = 1 .- s
            newS = complementS .* point .+ s
        else
            newS = s .+ s .* point
        end
        tryplet = h, newS, l
        r, g, b = hsl2rgb(tryplet)
        return [r,g,b]
    else
        return ("Error")
    end
end

##################################################################################
# FILTERS
##################################################################################

# GRAYSCALES

"""
Count average value of each pixel.

# Arguments
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
"""
function average(rgb) # as a average value of pixel

    r, g, b = rgb
    average = (r .+ g .+ b) ./ 3
    return [average, average, average]
end

"""
Get Lightness from HSL and set as a grayscale.

# Arguments
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
"""
function grayscaleLightness(rgb) # as HSL lightness

    l = lightness(rgb)
    return [l, l, l]
end

"""
Gets grayscale by using some ratio.

# Arguments
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
"""
function grayscaleLuminosity(rgb) # calculated by ratio

    r, g, b = rgb
    luminosity = 0.25 .* r .+ 0.75 .* g .+ 0.07 .* b
    return [luminosity, luminosity, luminosity]
end

# OTHER

"""
Create a negative of a image.
- `rgb::Array{Array{Float64,2},1}`: r, g, b matrices list.
"""
function negative(rgb)
    r, g, b = rgb
    newRed = 1 .- r
    newGreen = 1 .- g
    newBlue = 1 .- b
    return [newRed, newGreen, newBlue]
end

# Only
function onlyRed(rgb)
    r, g, b = rgb
    arraySize = size(g)
    g = zeros(Float64, arraySize[1], arraySize[2])
    b = zeros(Float64, arraySize[1], arraySize[2])
    return [r, g, b]
end

function onlyGreen(rgb)
    r, g, b = rgb
    arraySize = size(g)
    r = zeros(Float64, arraySize[1], arraySize[2])
    b = zeros(Float64, arraySize[1], arraySize[2])
    return [r, g, b]
end

function onlyBlue(rgb)
    r, g, b = rgb
    arraySize = size(g)
    g = zeros(Float64, arraySize[1], arraySize[2])
    r = zeros(Float64, arraySize[1], arraySize[2])
    return [r, g, b]
end

# Without
function withoutRed(rgb)
    r, g, b = rgb
    arraySize = size(g)
    r = zeros(Float64, arraySize[1], arraySize[2])
    return [r, g, b]
end

function withoutGreen(rgb)
    r, g, b = rgb
    arraySize = size(g)
    g = zeros(Float64, arraySize[1], arraySize[2])
    return [r, g, b]
end

function withoutBlue(rgb)
    r, g, b = rgb
    arraySize = size(b)
    b = zeros(Float64, arraySize[1], arraySize[2])
    return [r, g, b]
end

# AsGray
function redAsAGrayscale(rgb)
    red, green, blue = rgb
    average = (red .+ green .+ blue) ./ 3
    return [average, green, blue]
end

function greenAsAGrayscale(rgb)
    red, green, blue = rgb
    average = (red .+ green .+ blue) ./ 3
    return [red, average, blue]
end

function blueAsAGrayscale(rgb)
    red, green, blue = rgb
    average = (red .+ green .+ blue) ./ 3
    return [red, green, average]
end

function onlyRedAndGrayscale(rgb)
    red, green, blue = rgb
    average = (red .+ green .+ blue) ./ 3
    return [red, average, average]
end

function onlyGreenAndGrayscale(rgb)
    red, green, blue = rgb
    average = (red .+ green .+ blue) ./ 3
    return [average, green, average]
end

function onlyBlueAndGrayscale(rgb)
    red, green, blue = rgb
    average = (red .+ green .+ blue) ./ 3
    return [average, average, blue]
end

# Double rainbow? what is tHAT? :o

function changeColors(rgb, point) # point is in range [0,360]
    if point <= 360 && point >= 0
        h, s, l = rgb2hsl(rgb)
        Hue = h .+ point
        trypletHSL = Hue, s, l
        newRGB = hsl2rgb(trypletHSL)
        return newRGB
    end
end


using DiffEqBiological
using Random
using Distributions

prokaryote_model = @reaction_network ProkaryoteRN begin
    # order of appearnce: DNA, P₂, DNAxP₂, RNA, P
    θ1, DNA + P₂ --> DNAxP₂
    θ2, DNAxP₂ --> DNA + P₂
    θ3, DNA --> DNA + RNA
    θ4, RNA --> RNA + P
    θ5, 2P --> P₂
    θ6, P₂ --> 2*P
    θ7, RNA --> 0*RNA
    θ8, P --> 0*P
end θ1 θ2 θ3 θ4 θ5 θ6 θ7 θ8

function simulate_data(u0, tt, rates)
    prob = DiscreteProblem(u0, tt, rates)
    jump_prob = JumpProblem(prob, Direct(), prokaryote_model)
    sol = solve(jump_prob, SSAStepper())
    sol
end

u0 = [5,8,5,8,8]
θ = [0.1, 0.7, 0.35, 0.2, 0.1, 0.9, 0.3, 0.1]
tt = (0.0, 100.0)

Random.seed!(4)
sol = simulate_data(u0, tt, θ)
obs_time = collect(tt[1]:1:tt[end])
# permute to obtain (RNA, P, P₂, DNA, DNAxP₂) ordering
XX = map(x->x[[4,5,2,1,3]], sol.(obs_time))

# Let's add noise
#obs = [o[2] + 2*o[3] for o in XX[2:end]] .+ rand(Normal(0.0, 2.0), length(obs)-1)
# Alternative
obs = [[o[1] + rand(Normal(0.0, 1.0)), o[2] + 2*o[3]+rand(Normal(0.0, 2.0))] for o in XX[2:end]]

# save the data
OUT_DIR = joinpath(Base.source_dir(), "..", "..", "output9")

# save the process
open(joinpath(OUT_DIR, "prokaryote_custom.dat"), "w") do f
    write(f, "time RND P P2 DNA DNAxP2\n")
    for i in 1:length(obs_time)
        data = join(obs[i], " ")
        line = join([obs_time[i], " ", data, "\n"])
        write(f, line)
    end
end

# save just partial and noisy Observations
open(joinpath(OUT_DIR, "prokaryote_custom.dat"), "w") do f
    write(f, "time P+2*P2\n")
    for i in 2:length(obs_time)
        data = join(obs[i-1], " ")
        line = join([obs_time[i], " ", data, "\n"])
        write(f, line)
    end
end

# alternative save
open(joinpath(OUT_DIR, "prokaryote_custom.dat"), "w") do f
    write(f, "time RNA P+2*P2\n")
    for i in 2:length(obs_time)
        data = join(obs[i-1], " ")
        line = join([obs_time[i], " ", data, "\n"])
        write(f, line)
    end
end

# Ignore below....

Random.seed!(4)
θˣ = [0.1, 0.7, 0.35, 0.2, 0.1, 0.9, 0.3, 0.1]
K = 10.0
P˟ = Prokaryote(θ_init..., K)
x0, dt, T = ℝ{4}([8.0,8.0,8.0,5.0]), 1/5000, 100.0

XX, _ = simulate_segment(ℝ{8}(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
                        x0, P˟, 0.0:dt:T)

skip = 5000

obs_time, obs_vals = XX.tt[1:skip:end], [o[2] + 2*o[3]+rand(Normal(0.0, 2.0)) for o in XX.yy[1:skip:end]]

obs_time
obs_vals

# save just partial and noisy Observations
open(joinpath(OUT_DIR, "prokaryote_custom.dat"), "w") do f
    write(f, "time P+2*P2\n")
    for i in 2:length(obs_time)
        data = join(obs_vals[i-1], " ")
        line = join([obs_time[i], " ", data, "\n"])
        write(f, line)
    end
end


"""
    Shift(N, [k=1])

Shift operator. Optionally specify the distance to shift (default: 1).

# Fields
- `N::Int`: total number of spins
- `amount::Int`: shift by how many places
"""
struct Shift <: AbstractSymmetry
    N::Int
    amount::Int
    Shift(N, k=1) = new(N, mod(k, N))
end

_order(s::Shift) = div(s.N, gcd(s.N, s.amount))

_mask(k) = (2^k)-1 # mask out the k lowest bits
_roll_bits(N, i, amount) = ((i & _mask(amount)) << (N-amount)) | (i >> amount)
(s::Shift)(index) = _roll_bits(s.N, index, s.amount)

"""
    Flip(positions)
    Flip(system_size)

Spin Flip operator on N spins. Either provide the specific locations to flip or the system's
size to flip them all.

# Fields
- `mask::Int`: encodes the positions to flip in binary
"""
struct Flip <: AbstractSymmetry
    mask::Int
    """
        Flip(positions)
        Flip(system_size)

    Flip the spins at the positions or all if the system's size is given.
    """
    Flip(positions) = new(_compute_mask(positions))
end
Flip(system_size::Int) = Flip(1:system_size)
_compute_mask(inds) = sum(1 .<< (inds .- 1))

_order(::Flip) = 2

(f::Flip)(index)  = xor(index, f.mask)

"""
    Swap(pos1, pos2)

Swaps the 2 specified spins.

# Fields
- `ind1::Int`: Position of the first spin to flip
- `ind2::Int`: Position of the second spin to flip
Positions should be given in the range 1:N.
"""
struct Swap <: AbstractSymmetry
    ind1::Int
    ind2::Int
    function Swap(pos1, pos2)
        pos1 == pos2 && @warn "Swap with identical positions is nonsensical and won't work properly!"
        new(pos1, pos2)
    end
end

_order(::Swap) = 2

function _swap_bits(x, ind1, ind2)
    # compare bits, swap if unequal
    sw = ((x >> ind1) & 1) ⊻ ((x >> ind2) & 1) # xor == 1 if unequal
    return xor(x, (sw << ind1) | (sw << ind2)) # xor(bit, 1) -> flips bit
end

# convert spin positions to bitstring position
(s::Swap)(index) = _swap_bits(index, s.ind1-1, s.ind2-1)

"""
    SpatialReflection(N)

Reflects the whole chain in space.

# Fields
- `N::Int`: Number of spins
"""
struct SpatialReflection <: AbstractSymmetry
    N::Int
    function SpatialReflection(N)
        N == 1 && @warn "SpatialReflection with N=1 is nonsensical and won't work properly!"
        new(N)
    end
end

_order(::SpatialReflection) = 2

# approx 10times faster than the simpler
# parse(Int, string(x; base=2, pad=N)[N:-1:1]; base=2)
# Also note that the former does not account for cases where there are actually more than N
# spins
function _reflect_bits(N, bits)
    for i in 0:div(N,2)-1
        bits = _swap_bits(bits, i, N-1-i)
    end
    return bits
end

(sr::SpatialReflection)(index) = _reflect_bits(sr.N, index)

"""
    GenericSymmetry(f, L)

Allows a user-defined symmetry. Just provide a function to transform a binary basis index
and the order of the symmetry.

# Fields
- `f::Function`: transformation of the indices given in binary representation (0:2^N-1)
- `L::Int`: order of the symmetry, i.e. the smallest positive number such that f^L = identity
"""
struct GenericSymmetry <: AbstractSymmetry
    f::Function
    L::Int
end

_order(gs::GenericSymmetry) = gs.L

(gs::GenericSymmetry)(index) = gs.f(index)