module io

export IORestoreSettings
export IOSetParam
export IOSetParamMat
export IOSetParamUMat
export IOSetParamRow
export IOSetParamCol
export IOSetParamURow
export IOSetParamUCol
export IOGetParamBool
export IOGetParamInt
export IOGetParamDouble
export IOGetParamString
export IOGetParamVectorStr
export IOGetParamVectorInt
export IOGetParamMat
export IOGetParamUMat
export IOGetParamCol
export IOGetParamRow
export IOGetParamUCol
export IOGetParamURow
export IOGetParamMatWithInfo
export IOEnableVerbose
export IODisableVerbose
export IOSetPassed

import mlpack_jll
const library = mlpack_jll.libmlpack_julia_util

# Utility function to convert 1d object to 2d.
function convert_to_2d(in::Array{T, 1})::Array{T, 2} where T
  reshape(in, length(in), 1)
end

# Utility function to convert 2d object to 1d.  Fails if the size of one
# dimension is not 1.
function convert_to_1d(in::Array{T, 2})::Array{T, 1} where T
  if size(in, 1) != 1 && size(in, 2) != 1
    throw(ArgumentError("given matrix must be 1-dimensional; but its size is " *
        "$(size(in))"))
  end

  vec(in)
end

# Utility function to convert to and return a matrix.
function to_matrix(input, T::Type)
  if isa(input, Array{T, 1})
    convert_to_2d(input)
  else
    convert(Array{T, 2}, input)
  end
end

# Utility function to convert to and return a vector.
function to_vector(input, T::Type)
  if isa(input, Array{T, 1})
    input
  else
    convert_to_1d(convert(Array{T, 2}, input))
  end
end

function IORestoreSettings(programName::String)
  ccall((:IO_RestoreSettings, library), Nothing, (Cstring,), programName);
end

function IOSetParam(paramName::String, paramValue::Int)
  ccall((:IO_SetParamInt, library), Nothing, (Cstring, Cint), paramName,
      Cint(paramValue));
end

function IOSetParam(paramName::String, paramValue::Float64)
  ccall((:IO_SetParamDouble, library), Nothing, (Cstring, Float64), paramName,
      paramValue);
end

function IOSetParam(paramName::String, paramValue::Bool)
  ccall((:IO_SetParamBool, library), Nothing, (Cstring, Bool), paramName,
      paramValue);
end

function IOSetParam(paramName::String, paramValue::String)
  ccall((:IO_SetParamString, library), Nothing, (Cstring, Cstring), paramName,
      paramValue);
end

function IOSetParamMat(paramName::String,
                        paramValue,
                        pointsAsRows::Bool)
  paramMat = to_matrix(paramValue, Float64)
  ccall((:IO_SetParamMat, library), Nothing, (Cstring, Ptr{Float64}, Csize_t,
      Csize_t, Bool), paramName, Base.pointer(paramMat), size(paramMat, 1),
      size(paramMat, 2), pointsAsRows);
end

function IOSetParamUMat(paramName::String,
                         paramValue,
                         pointsAsRows::Bool)
  paramMat = to_matrix(paramValue, Int)

  # Sanity check.
  if minimum(paramMat) <= 0
    throw(DomainError("Input $(paramName) cannot have 0 or negative values!  " *
        "Must be 1 or greater."))
  end

  m = convert(Array{Csize_t, 2}, paramMat .- 1)
  ccall((:IO_SetParamUMat, library), Nothing, (Cstring, Ptr{Csize_t}, Csize_t,
      Csize_t, Bool), paramName, Base.pointer(m), size(paramValue, 1),
      size(paramValue, 2), pointsAsRows);
end

function IOSetParam(paramName::String,
                     vector::Vector{String})
  # For this we have to set the size of the vector then each string
  # sequentially.  I am not sure if this is fully necessary but I have some
  # reservations about Julia's support for passing arrays of strings correctly
  # as a const char**.
  ccall((:IO_SetParamVectorStrLen, library), Nothing, (Cstring, Csize_t),
      paramName, size(vector, 1));
  for i in 1:size(vector, 1)
    ccall((:IO_SetParamVectorStrStr, library), Nothing, (Cstring, Cstring,
        Csize_t), paramName, vector[i], i .- 1);
  end
end

function IOSetParam(paramName::String,
                     vector::Vector{Int})
  cint_vec = convert(Vector{Cint}, vector)
  ccall((:IO_SetParamVectorInt, library), Nothing, (Cstring, Ptr{Cint},
      Csize_t), paramName, Base.pointer(cint_vec), size(cint_vec, 1));
end

function IOSetParam(paramName::String,
                     matWithInfo::Tuple{Array{Bool, 1}, Array{Float64, 2}},
                     pointsAsRows::Bool)
  ccall((:IO_SetParamMatWithInfo, library), Nothing, (Cstring, Ptr{Bool},
      Ptr{Float64}, Int, Int, Bool), paramName,
      Base.pointer(matWithInfo[1]), Base.pointer(matWithInfo[2]),
      size(matWithInfo[2], 1), size(matWithInfo[2], 2), pointsAsRows);
end

function IOSetParamRow(paramName::String,
                        paramValue)
  paramVec = to_vector(paramValue, Float64)
  ccall((:IO_SetParamRow, library), Nothing, (Cstring, Ptr{Float64}, Csize_t),
      paramName, Base.pointer(paramVec), size(paramVec, 1));
end

function IOSetParamCol(paramName::String,
                        paramValue)
  paramVec = to_vector(paramValue, Float64)
  ccall((:IO_SetParamCol, library), Nothing, (Cstring, Ptr{Float64}, Csize_t),
      paramName, Base.pointer(paramVec), size(paramVec, 1));
end

function IOSetParamURow(paramName::String,
                         paramValue)
  paramVec = to_vector(paramValue, Int)

  # Sanity check.
  if minimum(paramVec) <= 0
    throw(DomainError("Input $(paramName) cannot have 0 or negative values!  " *
        "Must be 1 or greater."))
  end
  m = convert(Array{Csize_t, 1}, paramVec .- 1)

  ccall((:IO_SetParamURow, library), Nothing, (Cstring, Ptr{Csize_t}, Csize_t),
      paramName, Base.pointer(m), size(paramValue, 1));
end

function IOSetParamUCol(paramName::String,
                         paramValue)
  paramVec = to_vector(paramValue, Int)

  # Sanity check.
  if minimum(paramVec) <= 0
    throw(DomainError("Input $(paramName) cannot have 0 or negative values!  " *
        "Must be 1 or greater."))
  end
  m = convert(Array{Csize_t, 1}, paramValue .- 1)

  ccall((:IO_SetParamUCol, library), Nothing, (Cstring, Ptr{Csize_t}, Csize_t),
      paramName, Base.pointer(m), size(paramValue, 1));
end

function IOGetParamBool(paramName::String)
  return ccall((:IO_GetParamBool, library), Bool, (Cstring,), paramName)
end

function IOGetParamInt(paramName::String)
  return Int(ccall((:IO_GetParamInt, library), Cint, (Cstring,), paramName))
end

function IOGetParamDouble(paramName::String)
  return ccall((:IO_GetParamDouble, library), Float64, (Cstring,), paramName)
end

function IOGetParamString(paramName::String)
  return ccall((:IO_GetParamString, library), Cstring, (Cstring,), paramName)
end

function IOGetParamVectorStr(paramName::String)
  local size::Csize_t
  local ptr::Ptr{String}

  # Get the size of the vector, then each element.
  size = ccall((:IO_GetParamVectorStrLen, library), Csize_t, (Cstring,),
      paramName);
  out = Array{String, 1}()
  for i = 1:size
    s = ccall((:IO_GetParamVectorStrStr, library), Cstring, (Cstring, Csize_t),
        paramName, i .- 1)
    push!(out, Base.unsafe_string(s))
  end

  return out
end

function IOGetParamVectorInt(paramName::String)
  local size::Csize_t
  local ptr::Ptr{Cint}

  # Get the size of the vector, then the pointer to it.  We will own the
  # pointer.
  size = ccall((:IO_GetParamVectorIntLen, library), Csize_t, (Cstring,),
      paramName);
  ptr = ccall((:IO_GetParamVectorIntPtr, library), Ptr{Cint}, (Cstring,),
      paramName);

  return convert(Array{Int, 1}, Base.unsafe_wrap(Array{Cint, 1}, ptr, (size),
      own=true))
end

function IOGetParamMat(paramName::String, pointsAsRows::Bool)
  # Can we return different return types?  For now let's restrict to a matrix to
  # make it easy...
  local ptr::Ptr{Float64}
  local rows::Csize_t, cols::Csize_t;
  # I suppose it would be possible to do this all in one call, but this seems
  # easy enough.
  rows = ccall((:IO_GetParamMatRows, library), Csize_t, (Cstring,), paramName);
  cols = ccall((:IO_GetParamMatCols, library), Csize_t, (Cstring,), paramName);
  ptr = ccall((:IO_GetParamMat, library), Ptr{Float64}, (Cstring,), paramName);

  if pointsAsRows
    # In this case we have to transpose, unfortunately.
    m = Base.unsafe_wrap(Array{Float64, 2}, ptr, (rows, cols), own=true)
    return m';
  else
    # Here no transpose is necessary.
    return Base.unsafe_wrap(Array{Float64, 2}, ptr, (rows, cols), own=true);
  end
end

function IOGetParamUMat(paramName::String, pointsAsRows::Bool)
  # Can we return different return types?  For now let's restrict to a matrix to
  # make it easy...
  local ptr::Ptr{Csize_t}
  local rows::Csize_t, cols::Csize_t;
  # I suppose it would be possible to do this all in one call, but this seems
  # easy enough.
  rows = ccall((:IO_GetParamUMatRows, library), Csize_t, (Cstring,), paramName);
  cols = ccall((:IO_GetParamUMatCols, library), Csize_t, (Cstring,), paramName);
  ptr = ccall((:IO_GetParamUMat, library), Ptr{Csize_t}, (Cstring,), paramName);

  if pointsAsRows
    # In this case we have to transpose, unfortunately.
    m = Base.unsafe_wrap(Array{Csize_t, 2}, ptr, (rows, cols), own=true);
    return convert(Array{Int, 2}, m' .+ 1)  # Add 1 because these are indexes.
  else
    # Here no transpose is necessary.
    m = Base.unsafe_wrap(Array{Csize_t, 2}, ptr, (rows, cols), own=true);
    return convert(Array{Int, 2}, m .+ 1)
  end
end

function IOGetParamCol(paramName::String)
  local ptr::Ptr{Float64};
  local rows::Csize_t;

  rows = ccall((:IO_GetParamColRows, library), Csize_t, (Cstring,), paramName);
  ptr = ccall((:IO_GetParamCol, library), Ptr{Float64}, (Cstring,), paramName);

  return Base.unsafe_wrap(Array{Float64, 1}, ptr, rows, own=true);
end

function IOGetParamRow(paramName::String)
  local ptr::Ptr{Float64};
  local cols::Csize_t;

  cols = ccall((:IO_GetParamRowCols, library), Csize_t, (Cstring,), paramName);
  ptr = ccall((:IO_GetParamRow, library), Ptr{Float64}, (Cstring,), paramName);

  return Base.unsafe_wrap(Array{Float64, 1}, ptr, cols, own=true);
end

function IOGetParamUCol(paramName::String)
  local ptr::Ptr{Csize_t};
  local rows::Csize_t;

  rows = ccall((:IO_GetParamUColRows, library), Csize_t, (Cstring,), paramName);
  ptr = ccall((:IO_GetParamUCol, library), Ptr{Csize_t}, (Cstring,), paramName);

  m = Base.unsafe_wrap(Array{Csize_t, 1}, ptr, rows, own=true);
  return convert(Array{Int, 1}, m .+ 1)
end

function IOGetParamURow(paramName::String)
  local ptr::Ptr{Csize_t};
  local cols::Csize_t;

  cols = ccall((:IO_GetParamURowCols, library), Csize_t, (Cstring,), paramName);
  ptr = ccall((:IO_GetParamURow, library), Ptr{Csize_t}, (Cstring,), paramName);

  m = Base.unsafe_wrap(Array{Csize_t, 1}, ptr, cols, own=true);
  return convert(Array{Int, 1}, m .+ 1)
end

function IOGetParamMatWithInfo(paramName::String, pointsAsRows::Bool)
  local ptrBool::Ptr{Bool};
  local ptrData::Ptr{Float64};
  local rows::Csize_t;
  local cols::Csize_t;

  rows = ccall((:IO_GetParamMatWithInfoRows, library), Csize_t, (Cstring,),
      paramName);
  cols = ccall((:IO_GetParamMatWithInfoCols, library), Csize_t, (Cstring,),
      paramName);
  ptrBool = ccall((:IO_GetParamMatWithInfoBoolPtr, library), Ptr{Bool},
      (Cstring,), paramName);
  ptrMem = ccall((:IO_GetParamMatWithInfoPtr, library), Ptr{Float64},
      (Cstring,), paramName);

  types = Base.unsafe_wrap(Array{Bool, 1}, ptrBool, (rows), own=true)
  if pointsAsRows
    # In this case we have to transpose, unfortunately.
    m = Base.unsafe_wrap(Array{Float64, 2}, ptr, (rows, cols), own=true)
    return (types, m');
  else
    # Here no transpose is necessary.
    return (types, Base.unsafe_wrap(Array{Float64, 2}, ptr, (rows, cols),
        own=true));
  end
end

function IOEnableVerbose()
  ccall((:IO_EnableVerbose, library), Nothing, ());
end

function IODisableVerbose()
  ccall((:IO_DisableVerbose, library), Nothing, ());
end

function IOSetPassed(paramName::String)
  ccall((:IO_SetPassed, library), Nothing, (Cstring,), paramName);
end

end # module io
