##########################
# FunctionCell Interface #	
##########################
"""
	dbscan(eps::Real, minpts::Int64, f::Function)

Constructs an untrained cell that when piped data inside, clusters the data using the
DBSCAN algorithm; this implementation of the  algorithm expects as input a square 
simmetrical distance matrix.

# Arguments
  * `eps` is the radius of observation neighbourhood
  * `minpts` is the minimum number of neighbouring points (including itself) to qualify a point as a density point
  * `f` is a function that is used to calculate a quantity representative of how a point differs/is 
  similar to the cluster seed observations generated by the clustering algorithm.

One could use `f=(x,y)->Distances.pairwise(Distances.Euclidean(),x,y))` where `x` are the 
seed samples of the clusters and `y` is test data. The recovery of the clusters can be
done with `targets(indmin, ...)`.


Read the `Clustering.jl` documentation for more information.  
"""
dbscan(eps::Real, minpts::Int, f::Function) = FunctionCell(dbscan, (eps,minpts,f), ModelProperties(), "DBSCAN (ɛ=$eps, minpts=$minpts)")



############################
# DataCell/Array Interface #	
############################
"""
	dbscan(x, eps::Real, minpts::Int64, f::Function)

The DBSCAN clustering algorithm trainig. `x` is expected to be a distance matrix. Returns a trained
function cell that when piped data into will apply `f` between the seed points of the found
clusters and each of the input observations.
"""
# Training
dbscan(x::T where T<:CellData, eps::Real, minpts::Int, f::Function) = dbscan(getx!(x), eps, minpts, f)
dbscan(x::T where T<:AbstractVector, eps::Real, minpts::Int, f::Function) = dbscan(mat(x, LearnBase.ObsDim.Constant{2}()), eps, minpts, f)
dbscan(x::T where T<:AbstractMatrix, eps::Real, minpts::Int, f::Function) = begin 

	# Perform clustering 
	dbscandata = Clustering.dbscan(x, eps, minpts) 
	
	# Build model properties 
	modelprops = ModelProperties(nvars(x),length(dbscandata.seeds))
	
	FunctionCell(dbscan, Model((f, x[:,sort(dbscandata.seeds)], dbscandata), modelprops), "DBSCAN (ɛ=$eps, minpts=$minpts)");	 
end



# Execution
dbscan(x::T where T<:CellData, model::Model{<:Tuple{<:Function, <:AbstractArray, <:Clustering.DbscanResult}}) =
	datacell(dbscan(getx!(x), model), gety(x)) 	
dbscan(x::T where T<:AbstractVector, model::Model{<:Tuple{<:Function, <:AbstractArray, <:Clustering.DbscanResult}}) =
	dbscan(mat(x, LearnBase.ObsDim.Constant{2}()), model) 	
dbscan(x::T where T<:AbstractMatrix, model::Model{<:Tuple{<:Function, <:AbstractArray, <:Clustering.DbscanResult}}) =
	model.data[1](model.data[2],x) # f(seeds,x)	





"""
	dbscan(radius ,f::Function [;kwargs])

This implementation of the  algorithm expects as input a training data matrix.
`radius` is the radius of an observation neighbourhood and again, `f` is a 
function that is used to calculate a quantity representative of how a point differs/is 
similar to the clusters generated by the training data.

For this implementation of DBSCAN, `f` has implement a distance between a point and 
a set of points. One could define:
`g(d,Ε)= (x,S) -> Ε([d(p, getobs(S,i)) for i in nobs(S)])` and use `f=g(Distances.euclidean, mean)`,
where 'd' is a point-wise distance function, `S` is a cluster of points and Ε is an 
aggregation function that processes all distances between point and cluster.

# Keyword arguments (same as in `Clustering`)
  * `leafsize` number of points binned in each leaf of the KDTree (default `20`)
  * `min_neighbors` minimum number of neighbors to be a core point (default `1`)
  * `min_cluster_size` is the minimum number of points to be a valid cluster (default `1`) 

Read the `Clustering.jl` documentation for more information.  
"""
dbscan(radius::Real, f::Function; kwargs...) = FunctionCell(dbscan, (radius,f), ModelProperties(), kwtitle("DBSCAN (radius=$radius)",kwargs); kwargs...)



############################
# DataCell/Array Interface #	
############################
"""
	dbscan(x, radius::Real [,f::Function] [;kwargs])

The DBSCAN clustering algorithm trainig. `x` is expected to be a data matrix. Returns a trained
function cell that when piped data into will apply `f` between the core points of the found
clusters and each of the input observations.
"""
# Training
dbscan(x::T where T<:CellData, radius::Real, f::Function; kwargs...) = dbscan(getx!(x), radius, f; kwargs...)
dbscan(x::T where T<:AbstractVector, radius::Real, f::Function; kwargs...) = dbscan(mat(x, LearnBase.ObsDim.Constant{2}()), radius, f; kwargs...)
dbscan(x::T where T<:AbstractMatrix, radius::Real, f::Function; kwargs...) = begin	
	
	# Perform clustering 
	dbscandata = Clustering.dbscan(x, radius; kwargs...) 
	
	# Build model properties 
	modelprops = ModelProperties(nvars(x), length(dbscandata))
	core_data = [x[:,dbscandata[i].core_indices] for i in 1:length(dbscandata)]
	FunctionCell(dbscan, Model((f,core_data), modelprops), kwtitle("DBSCAN (radius=$radius)",kwargs)); # model is not stored, all information needed is in modelprops	 
end



# Execution
dbscan(x::T where T<:CellData, model::Model{<:Tuple{<:Function, <:AbstractArray}}) =
	datacell(dbscan(getx!(x), model), gety(x)) 	
dbscan(x::T where T<:AbstractVector, model::Model{<:Tuple{<:Function, <:AbstractArray}}) =
	dbscan(mat(x, LearnBase.ObsDim.Constant{2}()), model) 	
dbscan(x::T where T<:AbstractMatrix, model::Model{<:Tuple{<:Function, <:AbstractArray}}) = begin
	# Pre-allocate
	R = zeros(Float64, model.properties.odim, nobs(x))
	f = model.data[1] 
	@inbounds for k in 1:model.properties.odim
		cluster_data = model.data[2][k]
		@simd for i in 1:nobs(x)
			@fastmath R[k,i] = f(getobs(x,i), cluster_data)
		end
	end
	return R
end

