function chebyshev_weights(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  poly = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i] = chebyshev_polynomial(order[i],normalize_node(nodes[i],domain[:,i]))
  end

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      product = one(T)
      @inbounds for j = 1:N
        product *= poly[j][s[j],i[j]]
      end

      numerator   += f[s]*product
      denominator += product^2

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  n = size(f)

  poly = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i] = chebyshev_polynomial(order[i],normalize_node(nodes[i],domain[:,i]))
  end

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      num = f[s]
      den = one(T)
      @inbounds for j = 1:N
        if s[j] === 1 || s[j] === n[j]
          scale = 0.5
        else
          scale = 1.0
        end
        temp = poly[j][s[j],i[j]]
        num *= temp*scale
        den *= (temp^2)*scale
      end

      numerator   += num
      denominator += den

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  poly       = Array{Array{T,2},1}(undef,N)
  complement = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i]       = chebyshev_polynomial(order[i],normalize_node(nodes[i],domain[:,i]))
    complement[i] = pinv(poly[i])'
  end

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      num = f[s]
      den = one(T)
      @inbounds for j = 1:N
        temp = complement[j][s[j],i[j]]
        num *= temp
        den *= temp*poly[j][s[j],i[j]]
      end

      numerator   += num
      denominator += den

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      product = one(T)
      @inbounds for j = 1:N
        product *= poly[j][s[j],i[j]]
      end

      numerator   += f[s]*product
      denominator += product^2

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  n = size(f)

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      num = f[s]
      den = one(T)
      @inbounds for j = 1:N
        if s[j] == 1 || s[j] == n[j]
          scale = 0.5
        else
          scale = 1.0
        end
        temp = poly[j][s[j],i[j]]
        num *= temp*scale
        den *= (temp^2)*scale
      end

      numerator   += num
      denominator += den

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  complement = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    complement[i] = pinv(poly[i])'
  end

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      num = f[s]
      den = one(T)
      @inbounds for j = 1:N
        temp = complement[j][s[j],i[j]]
        num *= temp
        den *= temp*poly[j][s[j],i[j]]
      end

      numerator   += num
      denominator += den

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  poly = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i] = chebyshev_polynomial(order,normalize_node(nodes[i],domain[:,i]))
  end

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        product = one(T)
        @inbounds for j = 1:N
          product *= poly[j][s[j],i[j]]
        end

        numerator   += f[s]*product
        denominator += product^2

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  n = size(f)

  poly = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i] = chebyshev_polynomial(order,normalize_node(nodes[i],domain[:,i]))
  end

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        num = f[s]
        den = one(T)
        @inbounds for j = 1:N
          if s[j] == 1 || s[j] == n[j]
            scale = 0.5
          else
            scale = 1.0
          end
          temp = poly[j][s[j],i[j]]
          num *= temp*scale
          den *= (temp^2)*scale
        end

        numerator   += num
        denominator += den

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  poly       = Array{Array{T,2},1}(undef,N)
  complement = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i]       = chebyshev_polynomial(order,normalize_node(nodes[i],domain[:,i]))
    complement[i] = pinv(poly[i])'
  end

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        num = f[s]
        den = one(T)
        @inbounds for j = 1:N
          temp = complement[j][s[j],i[j]]
          num *= temp
          den *= temp*poly[j][s[j],i[j]]
        end

        numerator   += num
        denominator += den

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::S) where {T<:AbstractFloat,N,S<:Integer}

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        product = one(T)
        @inbounds for j = 1:N
          product *= poly[j][s[j],i[j]]
        end

        numerator   += f[s]*product
        denominator += product^2

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::S) where {T<:AbstractFloat,N,S<:Integer}

  n = size(f)

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        num = f[s]
        den = one(T)
        @inbounds for j = 1:N
          if s[j] == 1 || s[j] == n[j]
            scale = 0.5
          else
            scale = 1.0
          end
          temp = poly[j][s[j],i[j]]
          num *= temp*scale
          den *= (temp^2)*scale
        end

        numerator   += num
        denominator += den

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::S) where {T<:AbstractFloat,N,S<:Integer}

  complement = Array{Array{T,2},1}(undef,N)
  @inbounds for i = 1:N
    complement[i] = pinv(poly[i])'
  end

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        num = f[s]
        den = one(T)
        @inbounds for j = 1:N
          temp = complement[j][s[j],i[j]]
          num *= temp
          den *= temp*poly[j][s[j],i[j]]
        end

        numerator   += num
        denominator += den

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_threaded(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  poly = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i] = chebyshev_polynomial(order[i],normalize_node(nodes[i],domain[:,i]))
  end

  weights = zeros(Tuple(order.+1))

  @inbounds @sync @qthreads for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      product = one(T)
      @inbounds for j = 1:N
        product *= poly[j][s[j],i[j]]
      end

      numerator   += f[s]*product
      denominator += product^2

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_extrema_threaded(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  n = size(f)

  poly = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i] = chebyshev_polynomial(order[i],normalize_node(nodes[i],domain[:,i]))
  end

  weights = zeros(Tuple(order.+1))

  @inbounds @sync @qthreads for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      num = f[s]
      den = one(T)
      @inbounds for j = 1:N
        if s[j] == 1 || s[j] == n[j]
          scale = 0.5
        else
          scale = 1.0
        end
        temp = poly[j][s[j],i[j]]
        num *= temp*scale
        den *= (temp^2)*scale
      end

      numerator   += num
      denominator += den

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_extended_threaded(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  poly       = Array{Array{T,2},1}(undef,N)
  complement = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i]       = chebyshev_polynomial(order[i],normalize_node(nodes[i],domain[:,i]))
    complement[i] = pinv(poly[i])'
  end

  weights = zeros(Tuple(order.+1))

  @inbounds @sync @qthreads for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      num = f[s]
      den = one(T)
      @inbounds for j = 1:N
        temp = complement[j][s[j],i[j]]
        num *= temp
        den *= temp*poly[j][s[j],i[j]]
      end

      numerator   += num
      denominator += den

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_threaded(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds @sync @qthreads for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      product = one(T)
      @inbounds for j = 1:N
        product *= poly[j][s[j],i[j]]
      end

      numerator   += f[s]*product
      denominator += product^2

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_extrema_threaded(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  n = size(f)

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds @sync @qthreads for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      num = f[s]
      den = one(T)
      @inbounds for j = 1:N
        if s[j] == 1 || s[j] == n[j]
          scale = 0.5
        else
          scale = 1.0
        end
        temp = poly[j][s[j],i[j]]
        num *= temp*scale
        den *= (temp^2)*scale
      end

      numerator   += num
      denominator += den

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_extended_threaded(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  complement = Array{Array{T,2},1}(undef,N)
  @inbounds for i = 1:N
    complement[i] = pinv(poly[i])'
  end

  weights = Array{T,N}(undef,Tuple(order.+1))

  @inbounds @sync @qthreads for i in CartesianIndices(weights)

    numerator   = zero(T)
    denominator = zero(T)

    @inbounds for s in CartesianIndices(f)

      num = f[s]
      den = one(T)
      @inbounds for j = 1:N
        temp = complement[j][s[j],i[j]]
        num *= temp
        den *= temp*poly[j][s[j],i[j]]
      end

      numerator   += num
      denominator += den

    end

    weights[i] = numerator/denominator

  end

  return weights

end

function chebyshev_weights_threaded(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  poly = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i] = chebyshev_polynomial(order,normalize_node(nodes[i],domain[:,i]))
  end

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds @sync @qthreads for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        product = one(T)
        @inbounds for j = 1:N
          product *= poly[j][s[j],i[j]]
        end

        numerator   += f[s]*product
        denominator += product^2

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_extrema_threaded(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  n = size(f)

  poly = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i] = chebyshev_polynomial(order,normalize_node(nodes[i],domain[:,i]))
  end

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds @sync @qthreads for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        num = f[s]
        den = one(T)
        @inbounds for j = 1:N
          if s[j] == 1 || s[j] == n[j]
            scale = 0.5
          else
            scale = 1.0
          end
          temp = poly[j][s[j],i[j]]
          num *= temp*scale
          den *= (temp^2)*scale
        end

        numerator   += num
        denominator += den

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_extended_threaded(f::AbstractArray{T,N},nodes::NTuple{N,Array{T,1}},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  poly       = Array{Array{T,2},1}(undef,N)
  complement = Array{Array{T,2},1}(undef,N)

  @inbounds for i = 1:N
    poly[i]       = chebyshev_polynomial(order,normalize_node(nodes[i],domain[:,i]))
    complement[i] = pinv(poly[i])'
  end

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds @sync @qthreads for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        num = f[s]
        den = one(T)
        @inbounds for j = 1:N
          temp = complement[j][s[j],i[j]]
          num *= temp
          den *= temp*poly[j][s[j],i[j]]
        end

        numerator   += num
        denominator += den

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_threaded(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::S) where {T<:AbstractFloat,N,S<:Integer}

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds @sync @qthreads for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        product = one(T)
        @inbounds for j = 1:N
          product *= poly[j][s[j],i[j]]
        end

        numerator   += f[s]*product
        denominator += product^2

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_extrema_threaded(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::S) where {T<:AbstractFloat,N,S<:Integer}

  n = size(f)

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds @sync @qthreads for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        num = f[s]
        den = one(T)
        @inbounds for j = 1:N
          if s[j] == 1 || s[j] == n[j]
            scale = 0.5
          else
            scale = 1.0
          end
          temp = poly[j][s[j],i[j]]
          num *= temp*scale
          den *= (temp^2)*scale
        end

        numerator   += num
        denominator += den

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights_extended_threaded(f::AbstractArray{T,N},poly::NTuple{N,Array{T,2}},order::S) where {T<:AbstractFloat,N,S<:Integer}

  complement = Array{Array{T,2},1}(undef,N)
  @inbounds for i = 1:N
    complement[i] = pinv(poly[i])'
  end

  ord = (order,)
  for i = 2:N
    ord = (ord...,order)
  end

  weights = Array{T,N}(undef,ord.+1)

  @inbounds @sync @qthreads for i in CartesianIndices(weights)
    if sum(Tuple(i)) <= order+N

      numerator   = zero(T)
      denominator = zero(T)

      @inbounds for s in CartesianIndices(f)

        num = f[s]
        den = one(T)
        @inbounds for j = 1:N
          temp = complement[j][s[j],i[j]]
          num *= temp
          den *= temp*poly[j][s[j],i[j]]
        end

        numerator   += num
        denominator += den

      end

      weights[i] = numerator/denominator

    else
      weights[i] = zero(T)
    end

  end

  return weights

end

function chebyshev_weights(cheb::ChebInterpRoots)

  weights = chebyshev_weights_threaded(cheb.data,cheb.nodes,cheb.order,cheb.domain)

  return weights

end

function chebyshev_weights(cheb::ChebInterpExtrema)

  weights = chebyshev_weights_extrema_threaded(cheb.data,cheb.nodes,cheb.order,cheb.domain)

  return weights

end

function chebyshev_weights(cheb::ChebInterpExtended)

  weights = chebyshev_weights_extended_threaded(cheb.data,cheb.nodes,cheb.order,cheb.domain)

  return weights

end

function chebyshev_weights(cheb::ChebInterpVertesi)

  weights = chebyshev_weights_vertesi_threaded(cheb.data,cheb.nodes,cheb.order,cheb.domain)

  return weights

end

# Functions for the one-variable case where the nodes are a vector

function chebyshev_weights(f::AbstractArray{T,1},nodes::Array{T,1},order::Union{S,Array{S,1}},domain=[one(T);-one(T)]) where {T<:AbstractFloat,S<:Integer}

  weights = chebyshev_weights(f,(nodes,),order,domain)

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,1},nodes::Array{T,1},order::Union{S,Array{S,1}},domain=[one(T);-one(T)]) where {T<:AbstractFloat,S<:Integer}

  weights = chebyshev_weights_extrema(f,(nodes,),order,domain)

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,1},nodes::Array{T,1},order::Union{S,Array{S,1}},domain=[one(T);-one(T)]) where {T<:AbstractFloat,S<:Integer}

  weights = chebyshev_weights_extended(f,(nodes,),order,domain)

  return weights

end

function chebyshev_weights(f::AbstractArray{T,1},poly::Array{T,2},order::Union{S,Array{S,1}}) where {T<:AbstractFloat,S<:Integer}

  weights = chebyshev_weights(f,(poly,),order)

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,1},poly::Array{T,2},order::Union{S,Array{S,1}}) where {T<:AbstractFloat,S<:Integer}

  weights = chebyshev_weights_extrema(f,(poly,),order)

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,1},poly::Array{T,2},order::Union{S,Array{S,1}}) where {T<:AbstractFloat,S<:Integer}

  weights = chebyshev_weights_extended(f,(poly,),order)

  return weights

end

# Functions that allow the nodes to be in an array of arrays

function chebyshev_weights(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extrema(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extended(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extrema(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extended(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extrema(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extended(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::S) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_extrema(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::S) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extrema(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_extended(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::S) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extended(f,tuple(poly...),order)

  return weights

end

# Threaded functions

function chebyshev_weights_threaded(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_threaded(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_extrema_threaded(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extrema_threaded(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_extended_threaded(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::Array{S,1},domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extended_threaded(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_threaded(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_threaded(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_extrema_threaded(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extrema_threaded(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_extended_threaded(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::Array{S,1}) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extended_threaded(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_threaded(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_threaded(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_extrema_threaded(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extrema_threaded(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_extended_threaded(f::AbstractArray{T,N},nodes::Array{Array{T,1},1},order::S,domain=[ones(T,1,N);-ones(T,1,N)]) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extended_threaded(f,tuple(nodes...),order,domain)

  return weights

end

function chebyshev_weights_threaded(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::S) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_threaded(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_extrema_threaded(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::S) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extrema_threaded(f,tuple(poly...),order)

  return weights

end

function chebyshev_weights_extended_threaded(f::AbstractArray{T,N},poly::Array{Array{T,2},1},order::S) where {T<:AbstractFloat,N,S<:Integer}

  weights = chebyshev_weights_extended_threaded(f,tuple(poly...),order)

  return weights

end

const chebyshev_weights_vertesi          = chebyshev_weights_extended
const chebyshev_weights_vertesi_threaded = chebyshev_weights_extended_threaded


"""
type-II discrete cosine transform (DCT)
"""
function dct!(data::Array{T, 3}) where {T <:AbstractFloat}
	dctmat = genmat(size(data, 1))
	dctmat1 = genmat(size(data, 2))'
	@views @inbounds @fastmath for i in 1:size(data, 3)
		data[:, :, i] .= dctmat * data[:, :, i] * dctmat1
	end

	dctmat = genmat(size(data, 3))
	@views @inbounds @fastmath for j in 1:size(data, 2), i in 1:size(data, 1)
		data[i, j, :] .= dctmat * data[i, j, :]
	end
end

"""
inverse discrete cosine transform (DCT)
"""
function idct!(data::Array{T, 3}) where {T <:AbstractFloat}
	dctmat = genmat(size(data, 3))'
	@views @inbounds @fastmath for j in 1:size(data, 2), i in 1:size(data, 1)
		data[i, j, :] .= dctmat * data[i, j, :]
	end

	dctmat = genmat(size(data, 1))'
	dctmat1 = genmat(size(data, 2))
	@views @inbounds @fastmath for i in 1:size(data, 3)
		data[:, :, i] .= dctmat * data[:, :, i] * dctmat1
	end
end

function genmat(h::Int)::Array{Float64, 2}
	mat = Array{Float64, 2}(undef, h, h)
	mat[1, :] .= sqrt(1 / h)
	@inbounds @fastmath for j in 1:h
		@simd for i in 2:h
			mat[i, j] = cos(π * (i - 1) * (j - 0.5) / h) * sqrt(2 / h)
		end
	end
	mat
end


using DrWatson
@quickactivate "StatReth"

# %%
# Bayes: Pr(Earth|Land) = Pr(Land|Earth) * Pr(Earth) / Pr(Land)

p_land_earth = 1 - 0.7
p_land_mars = 1.0
p_earth = 0.5
p_land = p_land_earth * p_earth + p_land_mars * (1 - p_earth)

p_earth_land = p_land_earth * p_earth / p_land


using DelimitedFiles

input = readdlm(ARGS[1], ' ', Int, '\n');

# Part 1
fuel_required(mass) = (mass ÷ 3) - 2;
part1 = reduce(+, map(fuel_required, input))
println("Part 1: ", part1)

# Part 2
function fuel_required_2(mass)
    fuel = fuel_required(mass)
    return (fuel > 0) ? fuel + fuel_required_2(fuel) : 0;
end
part2 = reduce(+, map(fuel_required_2, input))
println("Part 2: ", part2)

# Nothing too crazy here for those familiar with recursion, although reading the instructions carefully always helps.
# Notice something interesting: the part 2 function does not distribute. That is, you cannot compute
#   fuel_required_2(part1) + part1
# to get part2.
# https://twitter.com/wjholdentech/status/1201023263633219585


# Auxiliary functions for natural cubic spline interpolation

# Stores parameters for fitted spline polynomials
mutable struct Spline{T}
    x::Vector{T} # (x, y) are the original points
    y::Vector{Float64}
    params::Vector{Float64} # stores parameters for polynomials. [ a1, b1, c1, d1, a2, b2, c2, d2 ...]

    function Spline(x::Vector{T}, y::Vector{Float64}, params::Vector{Float64}) where {T}
        polynms_count = length(x) - 1
        @assert length(params) == polynms_count * 4 "params length $(length(params)) does not conform to the expected number of polynomials ($(polynms_count))" # each polynomial has 4 parameters
        return new{T}(x, y, params)
    end
end

# Given a polinomial index, returns the indexer of the last parameter before the first parameter of the referenced polinomial
_base_param_index_(poly_index::Int) = (poly_index-1)*4

# Performs natural cubic spline interpolation
function splineint(s::Spline{T}, x_out::Number) where {T<:Number}
    local poly_index::Int = 1
    local base_idx::Int

    if x_out > s.x[end]
        # Extrapolation after last point
        poly_index = length(s.x) - 1
        base_idx = _base_param_index_(poly_index)
        return (x_out - s.x[end])*(s.params[base_idx + 2] + 2*s.params[base_idx + 3]*s.x[end] + 3*s.params[base_idx + 4]*(s.x[end]^2)) + s.y[end]
    elseif x_out < s.x[1]
        # Extrapolation before first point
        base_idx = 0
        return (x_out - s.x[1])*(s.params[base_idx + 2] + 2*s.params[base_idx + 3]*s.x[1] + 3*s.params[base_idx + 4]*(s.x[1]^2)) + s.y[1]
    else
        # Interplation
        while x_out > s.x[poly_index+1]
            poly_index += 1
        end

        base_idx = _base_param_index_(poly_index)

        #P1                       P2                    P3                     ...
        #1   2    3      4        5   6    7      8     9   10   11     12
        #a + bx + cx^2 + dx^3     a + bx + cx^2 + dx^3  a + bx + cx^2 + dx^3   ...
        return s.params[base_idx + 1] + s.params[base_idx + 2]*x_out +
            s.params[base_idx + 3]*(x_out^2) + s.params[base_idx + 4]*(x_out^3)
    end
end

# Performs natural cubic spline interpolation
function splineint(s::Spline{T1}, x_out::Vector{T2}) :: Vector{Float64} where {T1, T2}
    len = length(x_out)
    y_out = Vector{Float64}(undef, len)
    for i in 1:len
        y_out[i] = splineint(s, x_out[i])
    end
    return y_out
end

# Build a Spline object by fitting 3rd order polynomials around points (x_in, y_in)
function splinefit(x_in::Vector{T}, y_in::Vector{Float64}) :: Spline{T} where {T}
    #
    # TODO: optimize. See http://www.math.ntnu.no/emner/TMA4215/2008h/cubicsplines.pdf
    #
    points_count = length(x_in)
    @assert points_count == length(y_in) "x_in and y_in doesn't conform on sizes."
    matrix_n = 4*(points_count-1) # the main matrix is a square matrix matrix_n by matrix_n

    A = zeros(matrix_n, matrix_n)
    b = zeros(matrix_n)

    # Known values for polys

    # First Point
    A[1,1] = 1.0
    A[1,2] = x_in[1]
    A[1,3] = x_in[1]^2
    A[1,4] = x_in[1]^3
    b[1] = y_in[1]

    # Last Point
    base_idx = 4*(points_count - 2)
    A[2, base_idx + 1] = 1.0
    A[2, base_idx + 2] = x_in[points_count]
    A[2, base_idx + 3] = x_in[points_count]^2
    A[2, base_idx + 4] = x_in[points_count]^3
    b[2] = y_in[points_count]

    # Inner Points
    row = 3
    for i in 2:(points_count-1)
        # Connecting to left poly
        A[row, (i-2)*4 + 1] = 1.0
        A[row, (i-2)*4 + 2] = x_in[i]
        A[row, (i-2)*4 + 3] = x_in[i]^2
        A[row, (i-2)*4 + 4] = x_in[i]^3
        b[row] = y_in[i]

        row += 1

        # Connecting to right poly
        A[row, (i-1)*4 + 1] = 1.0
        A[row, (i-1)*4 + 2] = x_in[i]
        A[row, (i-1)*4 + 3] = x_in[i]^2
        A[row, (i-1)*4 + 4] = x_in[i]^3
        b[row] = y_in[i]

        row += 1

        # Conditions on first order derivatives
        A[row, (i-2)*4 + 2] = 1.0
        A[row, (i-2)*4 + 3] = 2.0*x_in[i]
        A[row, (i-2)*4 + 4] = 3.0*(x_in[i]^2)
        A[row, (i-1)*4 + 2] = -1.0
        A[row, (i-1)*4 + 3] = -2.0*x_in[i]
        A[row, (i-1)*4 + 4] = -3.0*(x_in[i]^2)

        row += 1

        # Conditions on second order derivatives
        A[row, (i-2)*4 + 3] = 2.0
        A[row, (i-2)*4 + 4] = 6.0 * x_in[i]
        A[row, (i-1)*4 + 3] = -2.0
        A[row, (i-1)*4 + 4] = -6.0 * x_in[i]

        row += 1
    end

    # Conditions for natural cubic spline
    A[row, 3] = 2.0
    A[row, 4] = 6.0 * x_in[1]

    row += 1

    A[row, (points_count-2)*4 + 3] = 2.0
    A[row, (points_count-2)*4 + 4] = 6.0 * x_in[points_count]

    return Spline(x_in, y_in, A \ b)
end


# Copyright (c) 2021 Leonardo Banchi <leonardo.banchi@unifi.it>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

using LinearAlgebra, BlockDiagonals

" symplectic matrix "
omega(n::Int) = BlockDiagonal([[0 1.; -1. 0] for k=1:n])
omega(A::AbstractMatrix) = omega(Int(size(A,1)/2))

" arrange diagonal elements in a block form "
wdiag(D) = BlockDiagonal([ Diagonal([d,d]) for d in D])

" check if a matrix is symplectic "
issymplectic(S) = S*omega(S)*S'≈omega(S)

" generate a random symplectic matrix "
function rand_symplectic(n)
	A = randn(2n,2n)
	exp(Hermitian(A)*omega(n))
end

" generate a random covariance matrix "
function rand_covmat(n,σ=1)
	v = 1/2 .+ σ*rand(n)
	S = rand_symplectic(n)
	Symmetric(S*wdiag(v)*S')
end

function _will_init(V)
	@assert	size(V,1) == size(V,2) && iseven(size(V,1))
	Int(size(V,1)/2), omega(V)
end

"""
	D,S = williamson(V)

	find the Williamson decomposition such that
	S'*wdiag(D)*S ≈ V
"""
function williamson(V)
	n, Ω = _will_init(V)
	iv12 = inv(sqrt(Symmetric(V)))
	J = iv12*Ω*iv12
	f = schur((J-J')/2)
	T, Q = f.Schur, f.vectors
	P = BlockDiagonal([ sign(T[2k-1,2k]) > 0 ?
					   [1. 0; 0 1.] : [0 1.; 1. 0] for k=1:n])
	D = inv.(diag(P*T*P,1)[1:2:2n])
	iS = wdiag(sqrt.(D))*P*Q'*iv12
	D, Ω*iS*Ω
end

"""
	D,S = williamsonNew(V)

	find the Williamson decomposition such that
	S'*wdiag(D)*S ≈ V
	new algorithm.
"""
function williamsonNew(V)
	n, Ω = _will_init(V)
	λ = sort(real(eigvals(V*Ω*1im)))[n+1:2n]
	S = zeros(2n,2n)
    for m=1:n
	    denom = λ[m]*prod(λ[k]^2-λ[m]^2 for k=1:n if k!=m)
        Vλ = complex.(V)-(1im*λ[m])*Ω
	    minorden(k) = det(view(Vλ,setdiff(1:2n,2m),setdiff(1:2n,k)))/denom
        fam = real(minorden(2m))
	    for k=1:n
            faeven = k==m ? sqrt(fam) : minorden(2k)/sqrt(fam)
            faodd = -minorden(2k-1)/sqrt(fam)
            S[2m-1,2k] 	= -real(faodd)
            S[2m,2k] = imag(faodd)
            S[2m-1,2k-1] = real(faeven)
            S[2m,2k-1] = -imag(faeven)
        end
	end
	λ, S
end