push!(LOAD_PATH, "/mnt/g/Python Programme/BALL.jl/src" )
using BALL
using BenchmarkTools

pdb_filepath = "/mnt/g/Python Programme/BALL.jl/1,2-Benzodiazepine.pdb"

comp = System(pdb_filepath)

print(@benchmark MolecularGraph(comp))


"""
$(SIGNATURES)

TrackingResults that hold the correlation results.
"""
struct TrackingResults{
        TS <: TrackingState,
        C <: AbstractCorrelator
    }
    state::TS
    correlator::C
    got_correlator::Bool
    bit_buffer::BitBuffer
    cn0::typeof(1.0dBHz)
end

"""
$(SIGNATURES)

Get state of the tracking result.
"""
@inline get_state(results::TrackingResults) = results.state

"""
$(SIGNATURES)

Get the carrier doppler of the tracking result.
"""
@inline get_carrier_doppler(results::TrackingResults) = get_carrier_doppler(results.state)

"""
$(SIGNATURES)

Get the carrier phase of the tracking result.
"""
@inline get_carrier_phase(results::TrackingResults) = get_carrier_phase(results.state) * 2π

"""
$(SIGNATURES)

Get the code doppler of the tracking result.
"""
@inline get_code_doppler(results::TrackingResults) = get_code_doppler(results.state)

"""
$(SIGNATURES)

Get the code phase of the tracking result.
"""
@inline get_code_phase(results::TrackingResults) = get_code_phase(results.state)

"""
$(SIGNATURES)

Get the correlator of the tracking result.
"""
@inline get_correlator(results::TrackingResults) = results.correlator

"""
$(SIGNATURES)

Get the early of the tracking result.
"""
@inline get_early(results::TrackingResults) = get_early(get_correlator(results))

"""
$(SIGNATURES)

Get the prompt of the tracking result.
"""
@inline get_prompt(results::TrackingResults) = get_prompt(get_correlator(results))

"""
$(SIGNATURES)

Get the late of the tracking result.
"""
@inline get_late(results::TrackingResults) = get_late(get_correlator(results))

"""
$(SIGNATURES)

Get the bits of the tracking result.
"""
@inline get_bits(results::TrackingResults) = get_bits(results.bit_buffer)

"""
$(SIGNATURES)

Get the number of bits of the tracking result.
"""
@inline get_num_bits(results::TrackingResults) = length(results.bit_buffer)

"""
$(SIGNATURES)

Get the carrier to noise density ratio of the tracking result.
"""
@inline get_cn0(results::TrackingResults) = results.cn0

"""
$(SIGNATURES)

Check if the secondary code or bit has been found.
"""
@inline get_secondary_code_or_bit_found(results::TrackingResults) =
    found(get_sc_bit_detector(results.state))


@gen function structure_move_randomness(trace, i::Int, parent_idx)
    num_objects = get_num_objects(trace)
    structure = get_structure(trace)
    floating_to_sliding = isFloating(structure, i)
    world_frame_node_id = num_objects + 1
    if floating_to_sliding

        prev_parent_node = world_frame_node_id
        downstream_nodes = getDownstream(recapitate(structure), Edge(i, prev_parent_node))

        # change to sliding, on top of this object, if possible
        probs = zeros(num_objects)
        for j in downstream_nodes
            if j != nv(structure) + 1
                probs[j] = 1.0
            end
        end

        if isnothing(parent_idx)
            parent_object ~ categorical(probs ./ sum(probs))
        else
            parent_object = parent_idx
        end

        new_parent_node = parent_object
        (parent_face_probs, child_face_probs) =
            structure_move_face_distributions(trace, i, parent_object)
        parent_face ~ labeled_categorical(S.BOX_SURFACE_IDS, parent_face_probs)
        child_face ~ labeled_categorical(S.BOX_SURFACE_IDS, child_face_probs)
    else
        # change to floating
        prev_parent_node = parent(structure, i)
        new_parent_node = world_frame_node_id
    end

    # println(floating_to_sliding, " ", i, " ", prev_parent_node," ", new_parent_node)
    new_tree = replaceEdge(
        recapitate(structure),
        i,
        prev_parent_node,
        new_parent_node,
    )
    return (structure, new_tree, floating_to_sliding)
end

function structure_move_involution(
    trace,
    fwd_choices,
    fwd_ret,
    proposal_args;
    check = false,
)
    i, parent_idx  = proposal_args[1:2]
    (structure, new_tree, floating_to_sliding) = fwd_ret
    scene_graph = get_scene_graph(trace)
    constraints = choicemap()
    bwd_choices = choicemap()

    object_boxes = get_object_boxes(trace)

    local parent_face::Symbol
    local child_face::Symbol

    if isTree(new_tree)

        constraints[structure_addr()] = decapitate(new_tree)
        if floating_to_sliding

            # floating to sliding
            poses = floatingPosesOf(scene_graph)
            pose = poses[i]
            if isnothing(parent_idx)
                parent_object = fwd_choices[:parent_object]
            else
                parent_object = parent_idx
            end

            parent_face = fwd_choices[:parent_face]
            child_face = fwd_choices[:child_face]
            cm = structure_move_get_equivalent_sliding_param(
                scene_graph,
                parent_object,
                i,
                parent_face,
                child_face,
                object_boxes,
            )
            set_submap!(constraints, contact_addr(i), cm)
            jacobian_correction = TO_DIRECTION_AND_PLANE_ROTATION_JACOBIAN_CORRECTION
        else

            # sliding to floating
            parent_object = parent(structure, i)
            (cm, parent_face, child_face) =
                structure_move_get_equivalent_pose(scene_graph, parent_object, i)
            set_submap!(constraints, floating_addr(i), cm)

            if isnothing(parent_idx)
                bwd_choices[:parent_object] = parent_object
            end
            bwd_choices[:parent_face] = parent_face
            bwd_choices[:child_face] = child_face
            jacobian_correction = FROM_DIRECTION_AND_PLANE_ROTATION_JACOBIAN_CORRECTION
        end
    else
        # no-op
        if floating_to_sliding
            if isnothing(parent_idx)
                bwd_choices[:parent_object] = fwd_choices[:parent_object]
            end
            bwd_choices[:parent_face] = fwd_choices[:parent_face]
            bwd_choices[:child_face] = fwd_choices[:child_face]
        end
        jacobian_correction = 0.0
    end
    args = get_args(trace)
    argdiffs = map((_) -> NoChange(), args)
    new_trace, weight, _, _ = Gen.update(trace, args, argdiffs, constraints)
    weight += jacobian_correction
    return (new_trace, bwd_choices, weight)
end

function is_structure_move_valid(trace, i::Int, parent_idx)
    num_objects = get_num_objects(trace)
    structure = get_structure(trace)
    floating_to_sliding = isFloating(structure, i)
    world_frame_node_id = num_objects + 1
    if floating_to_sliding
        prev_parent_node = world_frame_node_id
        downstream_nodes = getDownstream(recapitate(structure), Edge(i, prev_parent_node))
        if isnothing(parent_idx)
            return length(downstream_nodes) > 1
        else
            return (parent_idx in downstream_nodes)
        end
    end
    return true
end

function structure_move(trace, i::Int, parent_idx=nothing; iterations=10)
    acceptances = false
    if is_structure_move_valid(trace, i, parent_idx)
        for _ in 1:iterations
            trace, acc = mh(
                trace,
                structure_move_randomness,
                (i, parent_idx,),
                structure_move_involution,
                check = false,
            )
            acceptances = acc || acceptances
        end
    end
    trace, acceptances
end

export structure_move, is_structure_move_valid, structure_move_randomness, structure_move_involution


using Luxor, FileIO, ImageCore, Statistics, DelimitedFiles, Serialization
fontheight = 80

for font in ["Courier"]
    @info "Rendering $font characters"
    fontdir = joinpath(@__DIR__,"fonts",font)
    !isdir(fontdir) && mkdir(fontdir)
    xFirsts = Int[]
    xLasts = Int[]
    yFirsts = Int[]
    yLasts = Int[]
    for charIDX in 0:255
        char = Char(charIDX)
        file = joinpath(fontdir,"$charIDX.png")
        Drawing(150, 150, file)
        origin()
        background("black")
        fontface(font)
        setcolor("white")
        setopacity(1)
        fontsize(fontheight)
        text(string(char), Point(0,0), halign = :bottom,  valign = :left)
        finish()
        img = load(file)
        pixcol = mean(img, dims=1)[1,:]
        xOccupied = findall(pixcol .!= RGB(0,0,0))
        if length(xOccupied)>2
            push!(xFirsts, Int(xOccupied[1]))
            push!(xLasts, Int(xOccupied[end]))
        end
        pixrow = mean(img, dims=2)[:,1]
        yOccupied = findall(pixrow .!= RGB(0,0,0))
        if length(yOccupied)>2
            push!(yFirsts, Int(yOccupied[1]))
            push!(yLasts, Int(yOccupied[end]))
        end
    end
    x_first_pix = minimum(xFirsts)
    x_last_pix = maximum(xLasts)
    y_first_pix = minimum(yFirsts)
    y_last_pix = maximum(yLasts)

    charIDX = 0
    charStack = Vector{Array{Float64}}(undef,256)

    for charIDX in 0:255
        file = joinpath(fontdir,"$charIDX.png")
        img = load(file)
        charStack[charIDX+1] = round.(UInt8,channelview(img[y_first_pix:y_last_pix, x_first_pix:x_last_pix])[1,:,:] .* 255)
        rm(file)
    end
    Serialization.serialize(joinpath(fontdir,"charStack"), charStack)
end


# MIT license
# Copyright (c) Microsoft Corporation. All rights reserved.
# See LICENSE in the project root for full license information.

module Vis

using ..OpticSim
using ..OpticSim: euclideancontrolpoints, evalcsg, vertex, makiemesh, detector, centroid, lower, upper, intervals, α
using ..OpticSim.Geometry
using ..OpticSim.Repeat

using Unitful
using ImageView
using Images
using ColorTypes
using ColorSchemes
using StaticArrays
using LinearAlgebra
import Makie
import GeometryBasics
import Plots
import Luxor
using FileIO

include("Visualization.jl")
include("Emitters.jl")
include("VisRepeatingStructures.jl")
include("HeadEyeModel.jl")

end # module Vis
export Vis


module DsstWrapper

using JavaCall

const AbsoluteDate = @jimport org.orekit.time.AbsoluteDate
const AbstractGradientConverter = @jimport org.orekit.propagation.integration.AbstractGradientConverter
const AbstractIntegratedPropagator = @jimport org.orekit.propagation.integration.AbstractIntegratedPropagator
const AbstractJacobiansMapper = @jimport org.orekit.propagation.integration.AbstractJacobiansMapper
const AbstractPropagator = @jimport org.orekit.propagation.AbstractPropagator
const AdditionalEquations = @jimport org.orekit.propagation.integration.AdditionalEquations
const AdditionalStateProvider = @jimport org.orekit.propagation.AdditionalStateProvider
const AttitudeProvider = @jimport org.orekit.attitudes.AttitudeProvider
const BoundedPropagator = @jimport org.orekit.propagation.BoundedPropagator
const Class = @jimport java.lang.Class
const Collection = @jimport java.util.Collection
const DSSTForceModel = @jimport org.orekit.propagation.semianalytical.dsst.forces.DSSTForceModel
const DSSTGradientConverter = @jimport org.orekit.propagation.semianalytical.dsst.DSSTGradientConverter
const DSSTJacobiansMapper = @jimport org.orekit.propagation.semianalytical.dsst.DSSTJacobiansMapper
const DSSTPartialDerivativesEquations = @jimport org.orekit.propagation.semianalytical.dsst.DSSTPartialDerivativesEquations
const DSSTPropagator = @jimport org.orekit.propagation.semianalytical.dsst.DSSTPropagator
const EventDetector = @jimport org.orekit.propagation.events.EventDetector
const Field = @jimport org.hipparchus.Field
const FieldAbsoluteDate = @jimport org.orekit.time.FieldAbsoluteDate
const FieldAbstractIntegratedPropagator = @jimport org.orekit.propagation.integration.FieldAbstractIntegratedPropagator
const FieldAbstractPropagator = @jimport org.orekit.propagation.FieldAbstractPropagator
const FieldAdditionalEquations = @jimport org.orekit.propagation.integration.FieldAdditionalEquations
const FieldAdditionalStateProvider = @jimport org.orekit.propagation.FieldAdditionalStateProvider
const FieldBoundedPropagator = @jimport org.orekit.propagation.FieldBoundedPropagator
const FieldDSSTPropagator = @jimport org.orekit.propagation.semianalytical.dsst.FieldDSSTPropagator
const FieldEventDetector = @jimport org.orekit.propagation.events.FieldEventDetector
const FieldODEIntegrator = @jimport org.hipparchus.ode.FieldODEIntegrator
const FieldOrbit = @jimport org.orekit.orbits.FieldOrbit
const FieldOrekitFixedStepHandler = @jimport org.orekit.propagation.sampling.FieldOrekitFixedStepHandler
const FieldOrekitStepHandler = @jimport org.orekit.propagation.sampling.FieldOrekitStepHandler
const FieldSpacecraftState = @jimport org.orekit.propagation.FieldSpacecraftState
const Frame = @jimport org.orekit.frames.Frame
const Gradient = @jimport org.hipparchus.analysis.differentiation.Gradient
const JString = @jimport java.lang.String
const List = @jimport java.util.List
const ODEIntegrator = @jimport org.hipparchus.ode.ODEIntegrator
const Object = @jimport java.lang.Object
const Orbit = @jimport org.orekit.orbits.Orbit
const OrbitType = @jimport org.orekit.orbits.OrbitType
const OrekitFixedStepHandler = @jimport org.orekit.propagation.sampling.OrekitFixedStepHandler
const OrekitStepHandler = @jimport org.orekit.propagation.sampling.OrekitStepHandler
const ParameterDriversList = @jimport org.orekit.utils.ParameterDriversList
const PositionAngle = @jimport org.orekit.orbits.PositionAngle
const PropagationType = @jimport org.orekit.propagation.PropagationType
const RealFieldElement = @jimport org.hipparchus.RealFieldElement
const Set = @jimport java.util.Set
const SpacecraftState = @jimport org.orekit.propagation.SpacecraftState
const TimeStampedFieldPVCoordinates = @jimport org.orekit.utils.TimeStampedFieldPVCoordinates
const TimeStampedPVCoordinates = @jimport org.orekit.utils.TimeStampedPVCoordinates

export DSSTGradientConverter
export DSSTJacobiansMapper
export DSSTPartialDerivativesEquations
export DSSTPropagator
export FieldDSSTPropagator
export add_additional_equations
export add_additional_state_provider
export add_event_detector
export add_force_model
export clear_events_detectors
export compute_derivatives
export compute_mean_state
export compute_osculating_state
export equals
export get_additional_state_dimension
export get_additional_state_providers
export get_all_force_models
export get_attitude_provider
export get_basic_dimension
export get_calls
export get_class
export get_events_detectors
export get_field
export get_frame
export get_free_state_parameters
export get_generated_ephemeris
export get_initial_state
export get_managed_additional_states
export get_mapper
export get_mode
export get_mu
export get_name
export get_orbit_type
export get_parameters
export get_parameters_jacobian
export get_position_angle_type
export get_pv_coordinates
export get_satellite_revolution
export get_selected_coefficients
export get_selected_parameters
export get_state
export get_state_jacobian
export hash_code
export init
export initial_is_osculating
export is_additional_state_managed
export notify
export notify_all
export propagate
export remove_force_models
export reset_initial_state
export set_attitude_provider
export set_ephemeris_mode
export set_initial_jacobians
export set_initial_state
export set_interpolation_grid_to_fixed_number_of_points
export set_interpolation_grid_to_max_time_gap
export set_master_mode
export set_mu
export set_reset_at_end
export set_satellite_revolution
export set_selected_coefficients
export set_short_period_jacobians
export set_slave_mode
export to_string
export tolerances
export wait

include("ForcesWrapper/ForcesWrapper.jl")
include("UtilitiesWrapper/UtilitiesWrapper.jl")
include("dsst_gradient_converter.jl")
include("dsst_jacobians_mapper.jl")
include("dsst_partial_derivatives_equations.jl")
include("dsst_propagator.jl")
include("field_dsst_propagator.jl")

end


using DelimitedFiles
using Plots
using LaTeXStrings
pyplot()

wavesInFile = readdlm("src/wavelist_formated.txt")

path_to_thresholds = "src/thresholds_formated.txt"

thresholds = fill(0.0,size(wavesInFile,1))
let path = path_to_thresholds
    if isfile(path) && size(wavesInFile,1)==88
        v = readdlm(path)
        for i in 1:size(v,1)
            global thresholds[Int64(v[i,1])] = v[i,2]
        end
    else
        warn("Do not consider thresholds!")
    end
end

# gr()
# default(fmt = :png)
let mv = 0.5:0.02:2.5
    bar(mv, [sum((thresholds[i]<m) for i in 1:88) for m in mv],
        xlab = L"M_{3π}\,\,\mathrm{bins}", ylab="Number of waves", lab="",
        c=:lightblue, lw=0, yticks=[0,40,61,88])
end
savefig("plots/thresholds.pdf")

# plot(rand(10))

# [sum((thresholds[i]<m) for i in 1:88) for m in 0.5:0.02:2.5]
get_color_palette


"""
    mpifit_hsvgp(get_data, n_parts, n_dims, bounds_low, bounds_high;
    ni_low     = 5,
    ni_high    = 50,
    batch_size = 20,
    n_iters    = 200,
    n_subcycle_ELBO_local  = 20,
    n_subcycle_ELBO_global = 20)

Fit HSVGP to data for distributed computation

    get_data:                function to define how a local partition should read in its data
    ni_low:                  number of local inducing points
    ni_high:                 number of global inducing points
    batch_size:              size of batches for stochastic optimizer
    n_iters:                 number of optimization steps
    n_subcycle_ELBO_local:   subcycles for local  ELBO before MPI communication
    n_subcycle_ELBO_global:  subcycles for global ELBO before MPI communication
    n_batch:                 number of partitions in a local to global communication batch

WARNING: Currently writes out several jld2 files containing local and global GPss
"""
function mpifit_hsvgp(get_data, n_parts, n_dims, bounds_low, bounds_high;
    ni_low     = 5,               # Number of local inducing points
    ni_high    = 50,              # Number of global inducing points
    batch_size = 20,              # Number of points used in updating local ELBO
    n_iters    = 200,             # Total updates in whole cycle
    n_subcycle_ELBO_local  = 20,  # subcycles for local ELBO before comm.
    n_subcycle_ELBO_global = 20,  # subcycles for global ELBO before comm.
    n_batch    = 5)               # Number of partitions selected at each iteration

    #TODO:
    #     Need to clean up saving out of local and global objects at the end.
    #     It's messy now and will get worse with many MPI processes.
    #
    #     Would be nice to write a function for prediction to identify which
    #     partition a new value of X would be in, then direct the prediction to
    #     that local svgp
    #
    #     Need to generalize the interface here. Should have a way to pass in
    #     the structure of data to be worked with. Maybe pass in a get_data
    #     function that returns the x,y data for each partition.
    #
    #     Would like to clean up some of those arguments also. I dont think
    #     we should need to have to pass all that stuff in, but not sure the
    #     best way to give global bounds information without simply specifying
    #     it for cases where all the data cannot be stored on one node.
    #

    grad_clip = 1.e-3

    MPI.Init()
    comm = MPI.COMM_WORLD
    myrank = MPI.Comm_rank(comm)
    mysize = MPI.Comm_size(comm)

    root = 0 # global node

    # Fixing this for Gaussian case for now.
    #     Need to generalize this for the future but that will come in part of a large generalization of the code
    # n_latent = 1

    total_inducing = n_parts * ni_low

    part_split = [[ii] for ii in 1:(mysize-1)]

    if n_parts > mysize - 1
        for ii = mysize:n_parts
            append!(part_split[1 + ((ii-1) % (mysize - 1))], ii)
        end
    end

    ######################## DATA SPLIT ########################################
    # This part need not be done separately. Data will already be stored on nodes

    # Root process is the global node and rest of the processes are local processes
    # datarow goes from 2 to 48601
    # numrowscsv = 48600
    # numrowseachrank = Int(numrowscsv/(mysize-1))

    if myrank != root

        data_array = [get_data(part) for part in part_split[myrank]]
        len        = length(part_split[myrank])

    end # rank != root

    # Initializing "trace" array to store ELBO values
    if myrank == root
        trace_h       = zeros(n_iters)          # To collect the global ELBO value
        store_trace_l = zeros(n_iters,n_parts) # Store trace_l from all proesses
    end
    if myrank != root
        trace_l = [zeros(n_iters) for ii in 1:len] # To collect the local ELBO value
    end

    # Now that each local node has seen the data, it will send xi_low, cov_low,
    # mean_low to global node.
    # Once the global node gets data from local nodes, it will evaluate xi_high,
    # cov_high, mean_high and theta_high
    # Following that, global node will calculate prior_mean and prior_cov and
    # send it back to local nodes.

    ###################### INITIALIZATION ######################################

    if myrank != root
        # Step 1
        # Build local SVGPs ###################
        local_svgps = [SVGP_obj(data_array[ii][1], float(data_array[ii][2]), ni_low) for ii in 1:len]

        for ii in 1:len
            # Step 2
            # Sending mean_factor, xi_low, cov_low, mean_low and N to global #######
            send_inducing_points_to_global(local_svgps[ii].params.inducing_locs, local_svgps[ii].params.inducing_mean, root, comm)
        end

        # Step 3
        # Initialize local optimizers
        lopt_cmean = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075)) for ii in 1:len]
        lopt_lrho  = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075)) for ii in 1:len]
        lopt_lkap  = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075)) for ii in 1:len]
        lopt_lsig  = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075)) for ii in 1:len]
        loptm      = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.01)  ) for ii in 1:len]
        loptS      = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.002) ) for ii in 1:len]
        loptx      = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.01)  ) for ii in 1:len]

    end # myrank != root

    if myrank == root
        wts = StatsBase.AnalyticWeights(ones(n_parts));

        # Step 3
            # Initialize global svgp and optimizers
            # Root process will receive inducing point data
            # Root process will update global parameters
            # Root process will calculate prior_mean prior_cov for local nodes
            # Root process will send prior_mean prior_cov back to local processes

        # Initialize global optimizers
        opt_cmean = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075))
        opt_lrho  = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075))
        opt_lkap  = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075))
        opt_lsig  = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075))
        optm      = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.01)  )
        optS      = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.002) )
        optx      = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.01)  )

        # Initialize global svgp
        # TODO:
        #     Need bounds on input space and some idea of scale for output
        #         to set initial GP variance parameter and
        #         Latin Hypercube for inducing point locations
        #
        #     Assuming we will work in the case that we can't send all inducing point
        #         data to global. Instead should probably get bounds as arguments to original
        #         function or otherwise.
        fake_data_to_init = SVGP_data(
            vcat(bounds_low', bounds_high'),
            randn(4)*10 # sampling 4 points with something like the variance I was to initialize at
        )
        global_svgp = SVGP_obj(ni_high, SVGP_data("global"), SVGP_params(fake_data_to_init, ni_high), "gaussian")

        for part = (root+1):n_parts
            recv_id = 1 + (part - 1) % (mysize - 1)
            # TODO: Would prefer to not do this sequentially by rank number but as communications arrive
            #

            # println("In Process(1): ",recv_id,"\n")

            # Receiving xi_low and yi_low #
            xi_low, yi_low = receive_inducing_points_in_global(ni_low, n_dims, recv_id, comm) ### TODO: IMPORTANT! Need to communicate which partition is being recieved

            # Update global parameters from local data #
            grads  = gradient(gps -> svgp_elbo_global(
                    xi_low,
                    yi_low,
                    gps,
                    global_svgp
                    ), global_svgp.params)[1][]

            Flux.Optimise.update!(opt_cmean, global_svgp.params.const_mean,    -grads.const_mean)
            Flux.Optimise.update!(opt_lrho,  global_svgp.params.log_rho,       -grads.log_rho)
            Flux.Optimise.update!(opt_lkap,  global_svgp.params.log_kappa,     -grads.log_kappa)
            Flux.Optimise.update!(opt_lsig,  global_svgp.params.log_sigma,     -grads.log_sigma)
            Flux.Optimise.update!(optm,      global_svgp.params.inducing_mean, -grads.inducing_mean)
            Flux.Optimise.update!(optS,      global_svgp.params.inducing_L,    -LowerTriangular(grads.inducing_L) )
            Flux.Optimise.update!(optx,      global_svgp.params.inducing_locs, -grads.inducing_locs)
            # Sending prior_mean, prior_cov to local process ###################
            send_prior_to_local( pred_vgp(xi_low, global_svgp.params), recv_id, comm)

        end # for loop
    end

    MPI.Barrier(comm)
    if myrank != root
        #Get prior information from globa
        prior_mean, prior_sd = receive_prior_in_local(
                                                        ni_low,
                                                        root,
                                                        comm)
    end # myrank == root

    ###################### END OF INITIALIZATION ###############################

    ##################### OUTER LOOP ###########################################

    for iter= 1:n_iters
        if mod(iter, 2000) == 0
            print(iter, " ")
        end

        if myrank != root

            for ii in 1:len ## TODO: Clean this up with Kelin to process non-sequentially
                            ## TODO: ENSURE THE MPI PROPERLY ASSOCIATED DATA COMMUNICATED TO CORRECT PARTITION
                # Step 4
                # Each local process will receive prior_mean and prior_cov

                # Receiving prior_mean, prior_cov to local process #################
                # prior_mean, prior_sd = receive_prior_in_local(
                #                                        ni_low,
                #                                        root,
                #                                        comm)
                # println("prior_mean, prior_cov received in local")

                # Step 5
                # Update local svgp using prior_mean prior_cov from global
                nsteps_curr  = rand(Distributions.Poisson(n_subcycle_ELBO_local)) + 3;
                for sub_iter = 1:nsteps_curr
                    inds   = rand(1:size(local_svgps[ii].data.y)[1], batch_size)

                    # Update global parameters from local data #
                    grads  = gradient(gps -> svgp_elbo_local(
                            local_svgps[ii].data.x[inds,:],
                            local_svgps[ii].data.y[inds],
                            gps,
                            local_svgps[ii],
                            (prior_mean, prior_sd)
                            ), local_svgps[ii].params)[1][]

                    Flux.Optimise.update!(lopt_cmean[ii], local_svgps[ii].params.const_mean,    -grads.const_mean)
                    Flux.Optimise.update!(lopt_lrho[ii],  local_svgps[ii].params.log_rho,       -grads.log_rho)
                    Flux.Optimise.update!(lopt_lkap[ii],  local_svgps[ii].params.log_kappa,     -grads.log_kappa)
                    Flux.Optimise.update!(lopt_lsig[ii],  local_svgps[ii].params.log_sigma,     -grads.log_sigma)
                    Flux.Optimise.update!(loptm[ii],      local_svgps[ii].params.inducing_mean, -grads.inducing_mean)
                    Flux.Optimise.update!(loptS[ii],      local_svgps[ii].params.inducing_L,    -LowerTriangular(grads.inducing_L) )
                    Flux.Optimise.update!(loptx[ii],      local_svgps[ii].params.inducing_locs, -grads.inducing_locs)
                    # # Sending prior_mean, prior_cov to local process ###################
                    # send_prior_to_local( pred_vgp(xi_low, global_svgp.params[jj]), recv_id, comm)

                    if sub_iter == nsteps_curr
                        # Collecting local ELBO in an array ###########################
                        trace_l[ii][iter] = svgp_elbo_local(
                            local_svgps[ii].data.x[inds,:],
                            local_svgps[ii].data.y[inds],
                            local_svgps[ii].params,
                            local_svgps[ii],
                            (prior_mean, prior_sd)
                        )
                    end # if sub_iter
                end # for sub_iter
                # println("local ELBO optimized")

                # THIS IS NOW DONE OUTSIDE THE ii LOOP
                # Step 6
                # Sending inducing point data to global
                # for jj in 1:n_latent
                #     send_inducing_points_to_global(local_svgps[ii].params[jj].inducing_locs, local_svgps[ii].params[jj].inducing_mean, root, comm)
                # end
            end # for ii

            # Kelin block for sending inducing point data to global only if you were selected
            is_message, status = MPI.Iprobe(0, 8, comm)
            is_message, status = MPI.Iprobe(0, 8, comm) # Kelin: Silly solution, but try this for now

            msg  = [1];
            msg2 = ones(1);

            if is_message
                MPI.Recv!(msg, 0, 8, comm);
                global msg = convert(Vector{Int64}, msg)
                global msg2 = convert(Vector{Int64}, ones(msg[1]));
                #println("process ", myrank, "recieved first message ", msg);
                MPI.Recv!(msg2, 0, 10+myrank, comm);
                global msg2 = msg2;
                #println("process ", myrank, "recieved second message ", msg2);

                indx = findall(in(part_split[myrank]), msg2)
                for ii in indx
                    send_inducing_points_to_global(local_svgps[ii].params.inducing_locs, local_svgps[ii].params.inducing_mean, root, comm)
                end
               #println("process ", myrank, "sent inducing points")

               #Get prior information from global
               prior_mean, prior_sd = receive_prior_in_local(
                                                        ni_low,
                                                        root,
                                                        comm)
            end # if is_message
        end # if myrank != root

        if myrank == root
            #Select random local partition for batch
            aa = collect(1:n_parts);
            selection = convert(Vector{Int64}, zeros(n_batch));
            StatsBase.efraimidis_ares_wsample_norep!(aa, wts, selection)
            part_split_curr = [intersect(part_split[ii], selection) for ii in 1:(mysize-1)];
            wts[selection] .= 0;
            wts .+= 1;

            for ii in 1:(mysize-1)
               msg_to_send = part_split_curr[ii]
               if length(msg_to_send) > 0
                  MPI.Isend([length(msg_to_send)], ii, 8, comm)
                  MPI.Isend(msg_to_send, ii, 10+ii, comm);
                  #println("sending message ", msg_to_send, "to process ", ii)
               end
            end

            step_elbo = 0.
            for part = 1:n_batch
                status  = MPI.Probe(MPI.MPI_ANY_SOURCE, MPI.MPI_ANY_TAG, comm)
                xi_low  = zeros(ni_low, n_dims)

                rmsg    = MPI.Recv!(xi_low, status.source, 0, comm)
                yi_low  = zeros(ni_low)

                rmsg    = MPI.Recv!(yi_low, status.source, 1, comm)
                recv_id = status.source

                # Update global parameters from local data #
                for sub_iter = 1:n_subcycle_ELBO_global
                    grads  = gradient(gps -> svgp_elbo_global(
                            xi_low,
                            yi_low,
                            gps,
                            global_svgp
                            ), global_svgp.params)[1][]

                    Flux.Optimise.update!(opt_cmean, global_svgp.params.const_mean,    -grads.const_mean)
                    Flux.Optimise.update!(opt_lrho,  global_svgp.params.log_rho,       -grads.log_rho)
                    Flux.Optimise.update!(opt_lkap,  global_svgp.params.log_kappa,     -grads.log_kappa)
                    Flux.Optimise.update!(opt_lsig,  global_svgp.params.log_sigma,     -grads.log_sigma)
                    Flux.Optimise.update!(optm,      global_svgp.params.inducing_mean, -grads.inducing_mean)
                    Flux.Optimise.update!(optS,      global_svgp.params.inducing_L,    -LowerTriangular(grads.inducing_L) )
                    Flux.Optimise.update!(optx,      global_svgp.params.inducing_locs, -grads.inducing_locs)
                end # for sub_iter
                # println("global ELBO optimized")

                # Update step_elbo
                step_elbo += svgp_elbo_global(xi_low, yi_low, global_svgp.params, global_svgp)

                # Sending prior_mean, prior_cov to local process ###################
                send_prior_to_local( pred_vgp(xi_low, global_svgp.params), recv_id, comm) # MAKE SURE COMM. INDICATES WHICH PARTITION IN A PROCESS THIS COMM IS FOR
            end # for part

            trace_h[iter] = step_elbo
        end # myrank == root

        barrier_req = MPI.Ibarrier(comm);
        barrier_flag, barrier_status = MPI.Test!(barrier_req);
        while !barrier_flag
            is_message, status = MPI.Iprobe(0, 8, comm)
            is_message, status = MPI.Iprobe(0, 8, comm) #Silly solution, but try this for now
            msg  = [1];
            msg2 = ones(1);
            #println("process ", myrank, "message recieved? ", is_message)
            if is_message
                MPI.Recv!(msg, 0, 8, comm);
                global msg = convert(Vector{Int64}, msg);
                global msg2 = convert(Vector{Int64}, ones(msg[1]));
                #println("process ", myrank, "recieved first message ", msg);
                MPI.Recv!(msg2, 0, 10+myrank, comm);
                global msg2 = msg2;
                #println("process ", myrank, "recieved second message ", msg2);

                indx = findall(in(part_split[myrank]), msg2)
                #println("process ", myrank, "indx = ", indx);
                for ii in indx
                    send_inducing_points_to_global(local_svgps[ii].params.inducing_locs, local_svgps[ii].params.inducing_mean, root, comm)
                end
                #println("process ", myrank, "sent inducing points")

                #Get prior information from global
                prior_mean, prior_sd = receive_prior_in_local(ni_low, root, comm)
            end
            barrier_flag, barrier_status = MPI.Test!(barrier_req);
        end # while !barrier_flag
    end # outeriter

    ############################################################################
    # Returning trace_h and trace_l to main function
    if myrank != root

        for ii in 1:len
            send_trace(trace_l[ii], root ,comm)
        end

    end #myrank != root

    if myrank == root

        for part = (root+1):n_parts
            recv_id = 1 + (part - 1) % (mysize - 1)
            trace_l = receive_trace(n_iters, recv_id, comm)
            store_trace_l[:,part] = trace_l
        end # recv_id

        return_trace = hcat(trace_h, store_trace_l)
        # println(return_trace)

        # Write trace to a csv file
        df_return_trace = DataFrame(return_trace)
        CSV.write("./return_trace.csv", df_return_trace)

    end # myrank == root
    ############################################################################

    ############################################################################
    # Save svgp_params
    if myrank != root

        # send_svgp_params(local_svgp.params, root ,comm)
        for ii in 1:len
            save(string("local_fitted_parameters",part_split[myrank][ii],"_",mysize-1, ".jld2"), string("local_svgp",part_split[myrank][ii]), local_svgps[ii])
        end

    end #myrank != root

    if myrank == root

        # local_svgp_params = [receive_svgp_params(recv_id, comm) for recv_id = 1:mysize-1]

        save(string("global_fitted_svgp_",mysize-1,".jld2"), "global_svgp", global_svgp)

    end # myrank == root
    ############################################################################

    MPI.Finalize()

end     # mpifit_HSVGP


# This file is a part of Julia. License is MIT: https://julialang.org/license

module CHOLMOD

import Base: (*), convert, copy, eltype, get, getindex, show, size,
             IndexStyle, IndexLinear, IndexCartesian, ctranspose

import Base.LinAlg: (\), A_mul_Bc, A_mul_Bt, Ac_ldiv_B, Ac_mul_B, At_ldiv_B, At_mul_B,
                 cholfact, cholfact!, det, diag, ishermitian, isposdef,
                 issymmetric, ldltfact, ldltfact!, logdet

importall ..SparseArrays

export
    Dense,
    Factor,
    Sparse

import ..SparseArrays: AbstractSparseMatrix, SparseMatrixCSC, increment, indtype

#########
# Setup #
#########

include("cholmod_h.jl")

const CHOLMOD_MIN_VERSION = v"2.1.1"

### These offsets are defined in SuiteSparse_wrapper.c
const common_size = ccall((:jl_cholmod_common_size,:libsuitesparse_wrapper),Int,())

const cholmod_com_offsets = Vector{Csize_t}(19)
ccall((:jl_cholmod_common_offsets, :libsuitesparse_wrapper),
    Void, (Ptr{Csize_t},), cholmod_com_offsets)

## macro to generate the name of the C function according to the integer type
macro cholmod_name(nm, typ)
    string("cholmod_", eval(typ) == SuiteSparse_long ? "l_" : "", nm)
end

function start(a::Vector{UInt8})
    @isok ccall((@cholmod_name("start", SuiteSparse_long), :libcholmod),
        Cint, (Ptr{UInt8},), a)
    return a
end

function finish(a::Vector{UInt8})
    @isok ccall((@cholmod_name("finish", SuiteSparse_long), :libcholmod),
        Cint, (Ptr{UInt8},), a)
    return a
end

function defaults(a::Vector{UInt8})
    @isok ccall((@cholmod_name("defaults", SuiteSparse_long), :libcholmod),
        Cint, (Ptr{UInt8},), a)
    return a
end

common() = commonStruct

const build_version_array = Vector{Cint}(3)
ccall((:jl_cholmod_version, :libsuitesparse_wrapper), Cint, (Ptr{Cint},), build_version_array)
const build_version = VersionNumber(build_version_array...)

function __init__()
    try
        ### Check if the linked library is compatible with the Julia code
        if Libdl.dlsym_e(Libdl.dlopen("libcholmod"), :cholmod_version) != C_NULL
            current_version_array = Vector{Cint}(3)
            ccall((:cholmod_version, :libcholmod), Cint, (Ptr{Cint},), current_version_array)
            current_version = VersionNumber(current_version_array...)
        else # CHOLMOD < 2.1.1 does not include cholmod_version()
            current_version = v"0.0.0"
        end

        if current_version < CHOLMOD_MIN_VERSION
            warn("""

                CHOLMOD version incompatibility

                Julia was compiled with CHOLMOD version $build_version. It is
                currently linked with a version older than
                $(CHOLMOD_MIN_VERSION). This might cause Julia to
                terminate when working with sparse matrix factorizations,
                e.g. solving systems of equations with \\.

                It is recommended that you use Julia with a recent version
                of CHOLMOD, or download the generic binaries
                from www.julialang.org, which ship with the correct
                versions of all dependencies.
            """)
        elseif build_version_array[1] != current_version_array[1]
            warn("""

                CHOLMOD version incompatibility

                Julia was compiled with CHOLMOD version $build_version. It is
                currently linked with version $current_version.
                This might cause Julia to terminate when working with
                sparse matrix factorizations, e.g. solving systems of
                equations with \\.

                It is recommended that you use Julia with the same major
                version of CHOLMOD as the one used during the build, or
                download the generic binaries from www.julialang.org,
                which ship with the correct versions of all dependencies.
            """)
        end

        intsize = Int(ccall((:jl_cholmod_sizeof_long,:libsuitesparse_wrapper),Csize_t,()))
        if intsize != 4length(IndexTypes)
            warn("""

                 CHOLMOD integer size incompatibility

                 Julia was compiled with a version of CHOLMOD that
                 supported $(32length(IndexTypes)) bit integers. It is
                 currently linked with version that supports $(8intsize)
                 integers. This might cause Julia to terminate when
                 working with sparse matrix factorizations, e.g. solving
                 systems of equations with \\.

                 This problem can be fixed by modifying the Julia build
                 configuration or by downloading the OS X or generic
                 Linux binary from www.julialang.org, which include
                 the correct versions of all dependencies.
             """)
        end

        ### Initiate CHOLMOD
        ### The common struct. Controls the type of factorization and keeps pointers
        ### to temporary memory.
        global const commonStruct = fill(0xff, common_size)

        global const common_supernodal =
            convert(Ptr{Cint}, pointer(commonStruct, cholmod_com_offsets[4] + 1))
        global const common_final_ll =
            convert(Ptr{Cint}, pointer(commonStruct, cholmod_com_offsets[7] + 1))
        global const common_print =
            convert(Ptr{Cint}, pointer(commonStruct, cholmod_com_offsets[13] + 1))
        global const common_itype =
            convert(Ptr{Cint}, pointer(commonStruct, cholmod_com_offsets[18] + 1))
        global const common_dtype =
            convert(Ptr{Cint}, pointer(commonStruct, cholmod_com_offsets[19] + 1))
        global const common_nmethods =
            convert(Ptr{Cint}, pointer(commonStruct, cholmod_com_offsets[15] + 1))
        global const common_postorder =
            convert(Ptr{Cint}, pointer(commonStruct, cholmod_com_offsets[17] + 1))

        start(commonStruct)              # initializes CHOLMOD
        set_print_level(commonStruct, 0) # no printing from CHOLMOD by default

        # Register gc tracked allocator if CHOLMOD is new enough
        if current_version >= v"3.0.0"
            cnfg = cglobal((:SuiteSparse_config, :libsuitesparseconfig), Ptr{Void})
            unsafe_store!(cnfg, cglobal(:jl_malloc, Ptr{Void}), 1)
            unsafe_store!(cnfg, cglobal(:jl_calloc, Ptr{Void}), 2)
            unsafe_store!(cnfg, cglobal(:jl_realloc, Ptr{Void}), 3)
            unsafe_store!(cnfg, cglobal(:jl_free, Ptr{Void}), 4)
        end

    catch ex
        Base.showerror_nostdio(ex,
            "WARNING: Error during initialization of module CHOLMOD")
    end
end

function set_print_level(cm::Array{UInt8}, lev::Integer)
    global common_print
    unsafe_store!(common_print, lev)
end

####################
# Type definitions #
####################

abstract type SuiteSparseStruct end

# The three core data types for CHOLMOD: Dense, Sparse and Factor.
# CHOLMOD manages the memory, so the Julia versions only wrap a
# pointer to a struct.  Therefore finalizers should be registered each
# time a pointer is returned from CHOLMOD.

# Dense
struct C_Dense{T<:VTypes} <: SuiteSparseStruct
    nrow::Csize_t
    ncol::Csize_t
    nzmax::Csize_t
    d::Csize_t
    x::Ptr{T}
    z::Ptr{Void}
    xtype::Cint
    dtype::Cint
end

mutable struct Dense{T<:VTypes} <: DenseMatrix{T}
    p::Ptr{C_Dense{T}}
end

# Sparse
struct C_Sparse{Tv<:VTypes} <: SuiteSparseStruct
    nrow::Csize_t
    ncol::Csize_t
    nzmax::Csize_t
    p::Ptr{SuiteSparse_long}
    i::Ptr{SuiteSparse_long}
    nz::Ptr{SuiteSparse_long}
    x::Ptr{Tv}
    z::Ptr{Void}
    stype::Cint
    itype::Cint
    xtype::Cint
    dtype::Cint
    sorted::Cint
    packed::Cint
end

# Corresponds to the exact definition of cholmod_sparse_struct in the library.
# Useful when reading matrices of unknown type from files as in
# cholmod_read_sparse
struct C_SparseVoid <: SuiteSparseStruct
    nrow::Csize_t
    ncol::Csize_t
    nzmax::Csize_t
    p::Ptr{Void}
    i::Ptr{Void}
    nz::Ptr{Void}
    x::Ptr{Void}
    z::Ptr{Void}
    stype::Cint
    itype::Cint
    xtype::Cint
    dtype::Cint
    sorted::Cint
    packed::Cint
end

mutable struct Sparse{Tv<:VTypes} <: AbstractSparseMatrix{Tv,SuiteSparse_long}
    p::Ptr{C_Sparse{Tv}}
    function Sparse{Tv}(p::Ptr{C_Sparse{Tv}}) where Tv<:VTypes
        if p == C_NULL
            throw(ArgumentError("sparse matrix construction failed for " *
                "unknown reasons. Please submit a bug report."))
        end
        new(p)
    end
end
Sparse(p::Ptr{C_Sparse{Tv}}) where {Tv<:VTypes} = Sparse{Tv}(p)

# Factor

if build_version >= v"2.1.0" # CHOLMOD version 2.1.0 or later
    struct C_Factor{Tv<:VTypes} <: SuiteSparseStruct
        n::Csize_t
        minor::Csize_t
        Perm::Ptr{SuiteSparse_long}
        ColCount::Ptr{SuiteSparse_long}
        IPerm::Ptr{SuiteSparse_long}        # this pointer was added in verison 2.1.0
        nzmax::Csize_t
        p::Ptr{SuiteSparse_long}
        i::Ptr{SuiteSparse_long}
        x::Ptr{Tv}
        z::Ptr{Void}
        nz::Ptr{SuiteSparse_long}
        next::Ptr{SuiteSparse_long}
        prev::Ptr{SuiteSparse_long}
        nsuper::Csize_t
        ssize::Csize_t
        xsize::Csize_t
        maxcsize::Csize_t
        maxesize::Csize_t
        super::Ptr{SuiteSparse_long}
        pi::Ptr{SuiteSparse_long}
        px::Ptr{SuiteSparse_long}
        s::Ptr{SuiteSparse_long}
        ordering::Cint
        is_ll::Cint
        is_super::Cint
        is_monotonic::Cint
        itype::Cint
        xtype::Cint
        dtype::Cint
    end
else
    struct C_Factor{Tv<:VTypes} <: SuiteSparseStruct
        n::Csize_t
        minor::Csize_t
        Perm::Ptr{SuiteSparse_long}
        ColCount::Ptr{SuiteSparse_long}
        nzmax::Csize_t
        p::Ptr{SuiteSparse_long}
        i::Ptr{SuiteSparse_long}
        x::Ptr{Tv}
        z::Ptr{Void}
        nz::Ptr{SuiteSparse_long}
        next::Ptr{SuiteSparse_long}
        prev::Ptr{SuiteSparse_long}
        nsuper::Csize_t
        ssize::Csize_t
        xsize::Csize_t
        maxcsize::Csize_t
        maxesize::Csize_t
        super::Ptr{SuiteSparse_long}
        pi::Ptr{SuiteSparse_long}
        px::Ptr{SuiteSparse_long}
        s::Ptr{SuiteSparse_long}
        ordering::Cint
        is_ll::Cint
        is_super::Cint
        is_monotonic::Cint
        itype::Cint
        xtype::Cint
        dtype::Cint
    end
end

mutable struct Factor{Tv} <: Factorization{Tv}
    p::Ptr{C_Factor{Tv}}
    function Factor{Tv}(p::Ptr{C_Factor{Tv}}) where Tv
        if p == C_NULL
            throw(ArgumentError("factorization construction failed for " *
                "unknown reasons. Please submit a bug report."))
        end
        new(p)
    end
end
Factor(p::Ptr{C_Factor{Tv}}) where {Tv<:VTypes} = Factor{Tv}(p)

# Define get similar to get(Nullable) to check pointers. All pointer loads
# should be wrapped in get to make sure that SuiteSparse is not called with
# a C_NULL pointer which could cause a segfault. Pointers are set to null
# when serialized so this can happen when mutiple processes are in use.
function get(p::Ptr{T}) where T<:SuiteSparseStruct
    if p == C_NULL
        throw(ArgumentError("pointer to the $T object is null. This can " *
            "happen if the object has been serialized."))
    else
        return p
    end
end

# FactorComponent, for encoding particular factors from a factorization
mutable struct FactorComponent{Tv,S} <: AbstractMatrix{Tv}
    F::Factor{Tv}

    function FactorComponent{Tv,S}(F::Factor{Tv}) where {Tv,S}
        s = unsafe_load(get(F.p))
        if s.is_ll != 0
            if !(S == :L || S == :U || S == :PtL || S == :UP)
                throw(CHOLMODException(string(S, " not supported for sparse ",
                    "LLt matrices; try :L, :U, :PtL, or :UP")))
            end
        elseif !(S == :L || S == :U || S == :PtL || S == :UP ||
                S == :D || S == :LD || S == :DU || S == :PtLD || S == :DUP)
            throw(CHOLMODException(string(S, " not supported for sparse LDLt ",
                "matrices; try :L, :U, :PtL, :UP, :D, :LD, :DU, :PtLD, or :DUP")))
        end
        new(F)
    end
end
function FactorComponent(F::Factor{Tv}, sym::Symbol) where Tv
    FactorComponent{Tv,sym}(F)
end

Factor(FC::FactorComponent) = Factor(FC.F)

#################
# Thin wrappers #
#################

# Dense wrappers
## Note! Integer type defaults to Cint, but this is actually not necessary, but
## making this a choice would require another type parameter in the Dense type

### cholmod_core_h ###
function allocate_dense(nrow::Integer, ncol::Integer, d::Integer, ::Type{Float64})
    d = Dense(ccall((:cholmod_l_allocate_dense, :libcholmod), Ptr{C_Dense{Float64}},
        (Csize_t, Csize_t, Csize_t, Cint, Ptr{Void}),
        nrow, ncol, d, REAL, common()))
    finalizer(d, free!)
    d
end
function allocate_dense(nrow::Integer, ncol::Integer, d::Integer, ::Type{Complex{Float64}})
    d = Dense(ccall((:cholmod_l_allocate_dense, :libcholmod), Ptr{C_Dense{Complex{Float64}}},
        (Csize_t, Csize_t, Csize_t, Cint, Ptr{Void}),
        nrow, ncol, d, COMPLEX, common()))
    finalizer(d, free!)
    d
end

free_dense!(p::Ptr{C_Dense{T}}) where {T} = ccall((:cholmod_l_free_dense, :libcholmod),
    Cint, (Ref{Ptr{C_Dense{T}}}, Ptr{Void}), p, common())

function zeros(m::Integer, n::Integer, ::Type{T}) where T<:VTypes
    d = Dense(ccall((:cholmod_l_zeros, :libcholmod), Ptr{C_Dense{T}},
        (Csize_t, Csize_t, Cint, Ptr{UInt8}),
         m, n, xtyp(T), common()))
    finalizer(d, free!)
    d
end
zeros(m::Integer, n::Integer) = zeros(m, n, Float64)

function ones(m::Integer, n::Integer, ::Type{T}) where T<:VTypes
    d = Dense(ccall((:cholmod_l_ones, :libcholmod), Ptr{C_Dense{T}},
        (Csize_t, Csize_t, Cint, Ptr{UInt8}),
         m, n, xtyp(T), common()))
    finalizer(d, free!)
    d
end
ones(m::Integer, n::Integer) = ones(m, n, Float64)

function eye(m::Integer, n::Integer, ::Type{T}) where T<:VTypes
    d = Dense(ccall((:cholmod_l_eye, :libcholmod), Ptr{C_Dense{T}},
        (Csize_t, Csize_t, Cint, Ptr{UInt8}),
         m, n, xtyp(T), common()))
    finalizer(d, free!)
    d
end
eye(m::Integer, n::Integer) = eye(m, n, Float64)
eye(n::Integer) = eye(n, n, Float64)

function copy_dense(A::Dense{Tv}) where Tv<:VTypes
    d = Dense(ccall((:cholmod_l_copy_dense, :libcholmod), Ptr{C_Dense{Tv}},
        (Ptr{C_Dense{Tv}}, Ptr{UInt8}),
         get(A.p), common()))
    finalizer(d, free!)
    d
end

function sort!(S::Sparse{Tv}) where Tv<:VTypes
    @isok ccall((:cholmod_l_sort, :libcholmod), SuiteSparse_long,
        (Ptr{C_Sparse{Tv}}, Ptr{UInt8}),
         get(S.p), common())
    return S
end

### cholmod_matrixops.h ###
function norm_dense(D::Dense{Tv}, p::Integer) where Tv<:VTypes
    s = unsafe_load(get(D.p))
    if p == 2
        if s.ncol > 1
            throw(ArgumentError("2 norm only supported when matrix has one column"))
        end
    elseif p != 0 && p != 1
        throw(ArgumentError("second argument must be either 0 (Inf norm), 1, or 2"))
    end
    ccall((:cholmod_l_norm_dense, :libcholmod), Cdouble,
        (Ptr{C_Dense{Tv}}, Cint, Ptr{UInt8}),
          get(D.p), p, common())
end

### cholmod_check.h ###
function check_dense(A::Dense{T}) where T<:VTypes
    ccall((:cholmod_l_check_dense, :libcholmod), Cint,
          (Ptr{C_Dense{T}}, Ptr{UInt8}),
          A.p, common())!=0
end

# Non-Dense wrappers
### cholmod_core.h ###
function allocate_sparse(nrow::Integer, ncol::Integer, nzmax::Integer,
        sorted::Bool, packed::Bool, stype::Integer, ::Type{Float64})
    s = Sparse(ccall((@cholmod_name("allocate_sparse", SuiteSparse_long), :libcholmod),
            Ptr{C_Sparse{Float64}},
                (Csize_t, Csize_t, Csize_t, Cint,
                 Cint, Cint, Cint, Ptr{Void}),
                nrow, ncol, nzmax, sorted,
                packed, stype, REAL, common()))
    finalizer(s, free!)
    s
end
function allocate_sparse(nrow::Integer, ncol::Integer, nzmax::Integer,
        sorted::Bool, packed::Bool, stype::Integer, ::Type{Complex{Float64}})
    s = Sparse(ccall((@cholmod_name("allocate_sparse", SuiteSparse_long), :libcholmod),
            Ptr{C_Sparse{Complex{Float64}}},
                (Csize_t, Csize_t, Csize_t, Cint,
                 Cint, Cint, Cint, Ptr{Void}),
                nrow, ncol, nzmax, sorted,
                packed, stype, COMPLEX, common()))
    finalizer(s, free!)
    s
end
function free_sparse!(ptr::Ptr{C_Sparse{Tv}}) where Tv<:VTypes
    @isok ccall((@cholmod_name("free_sparse", SuiteSparse_long), :libcholmod), Cint,
            (Ptr{Ptr{C_Sparse{Tv}}}, Ptr{UInt8}),
                &ptr, common())
end

function free_sparse!(ptr::Ptr{C_SparseVoid})
    @isok ccall((@cholmod_name("free_sparse", SuiteSparse_long), :libcholmod), Cint,
            (Ptr{Ptr{C_SparseVoid}}, Ptr{UInt8}),
                &ptr, common())
end

function free_factor!(ptr::Ptr{C_Factor{Tv}}) where Tv<:VTypes
    # Warning! Important that finalizer doesn't modify the global Common struct.
    @isok ccall((@cholmod_name("free_factor", SuiteSparse_long), :libcholmod), Cint,
            (Ptr{Ptr{C_Factor{Tv}}}, Ptr{Void}),
                &ptr, common())
end

function aat(A::Sparse{Tv}, fset::Vector{SuiteSparse_long}, mode::Integer) where Tv<:VRealTypes
    s = Sparse(ccall((@cholmod_name("aat", SuiteSparse_long), :libcholmod),
        Ptr{C_Sparse{Tv}},
            (Ptr{C_Sparse{Tv}}, Ptr{SuiteSparse_long}, Csize_t, Cint, Ptr{UInt8}),
                get(A.p), fset, length(fset), mode, common()))
    finalizer(s, free!)
    s
end

function sparse_to_dense(A::Sparse{Tv}) where Tv<:VTypes
    d = Dense(ccall((@cholmod_name("sparse_to_dense", SuiteSparse_long),:libcholmod),
        Ptr{C_Dense{Tv}},
            (Ptr{C_Sparse{Tv}}, Ptr{UInt8}),
                get(A.p), common()))
    finalizer(d, free!)
    d
end
function dense_to_sparse(D::Dense{Tv}, ::Type{SuiteSparse_long}) where Tv<:VTypes
    s = Sparse(ccall((@cholmod_name("dense_to_sparse", SuiteSparse_long),:libcholmod),
        Ptr{C_Sparse{Tv}},
            (Ptr{C_Dense{Tv}}, Cint, Ptr{UInt8}),
                get(D.p), true, common()))
    finalizer(s, free!)
    s
end

function factor_to_sparse!(F::Factor{Tv}) where Tv<:VTypes
    ss = unsafe_load(F.p)
    ss.xtype > PATTERN || throw(CHOLMODException("only numeric factors are supported"))
    s = Sparse(ccall((@cholmod_name("factor_to_sparse", SuiteSparse_long),:libcholmod),
        Ptr{C_Sparse{Tv}},
            (Ptr{C_Factor{Tv}}, Ptr{UInt8}),
                get(F.p), common()))
    finalizer(s, free!)
    s
end

function change_factor!(::Type{Float64}, to_ll::Bool,
        to_super::Bool, to_packed::Bool, to_monotonic::Bool, F::Factor{Tv}) where Tv<:VTypes
    @isok ccall((@cholmod_name("change_factor", SuiteSparse_long),:libcholmod), Cint,
            (Cint, Cint, Cint, Cint, Cint, Ptr{C_Factor{Tv}}, Ptr{UInt8}),
                REAL, to_ll, to_super, to_packed, to_monotonic, get(F.p), common())
    Factor{Float64}(F.p)
end

function change_factor!(::Type{Complex{Float64}}, to_ll::Bool,
        to_super::Bool, to_packed::Bool, to_monotonic::Bool, F::Factor{Tv}) where Tv<:VTypes
    @isok ccall((@cholmod_name("change_factor", SuiteSparse_long),:libcholmod), Cint,
            (Cint, Cint, Cint, Cint, Cint, Ptr{C_Factor{Tv}}, Ptr{UInt8}),
                COMPLEX, to_ll, to_super, to_packed, to_monotonic, get(F.p), common())
    Factor{Complex{Float64}}(F.p)
end

function check_sparse(A::Sparse{Tv}) where Tv<:VTypes
    ccall((@cholmod_name("check_sparse", SuiteSparse_long),:libcholmod), Cint,
          (Ptr{C_Sparse{Tv}}, Ptr{UInt8}),
          get(A.p), common())!=0
end

function check_factor(F::Factor{Tv}) where Tv<:VTypes
    ccall((@cholmod_name("check_factor", SuiteSparse_long),:libcholmod), Cint,
          (Ptr{C_Factor{Tv}}, Ptr{UInt8}),
          get(F.p), common())!=0
end

function nnz(A::Sparse{Tv}) where Tv<:VTypes
    ccall((@cholmod_name("nnz", SuiteSparse_long),:libcholmod), Int,
            (Ptr{C_Sparse{Tv}}, Ptr{UInt8}),
                get(A.p), common())
end

function speye(m::Integer, n::Integer, ::Type{Tv}) where Tv<:VTypes
    s = Sparse(ccall((@cholmod_name("speye", SuiteSparse_long), :libcholmod),
        Ptr{C_Sparse{Tv}},
            (Csize_t, Csize_t, Cint, Ptr{UInt8}),
                m, n, xtyp(Tv), common()))
    finalizer(s, free!)
    s
end

function spzeros(m::Integer, n::Integer, nzmax::Integer, ::Type{Tv}) where Tv<:VTypes
    s = Sparse(ccall((@cholmod_name("spzeros", SuiteSparse_long), :libcholmod),
        Ptr{C_Sparse{Tv}},
            (Csize_t, Csize_t, Csize_t, Cint, Ptr{UInt8}),
             m, n, nzmax, xtyp(Tv), common()))
    finalizer(s, free!)
    s
end

function transpose_(A::Sparse{Tv}, values::Integer) where Tv<:VTypes
    s = Sparse(ccall((@cholmod_name("transpose", SuiteSparse_long),:libcholmod),
        Ptr{C_Sparse{Tv}},
            (Ptr{C_Sparse{Tv}}, Cint, Ptr{UInt8}),
                get(A.p), values, common()))
    finalizer(s, free!)
    s
end

function copy_factor(F::Factor{Tv}) where Tv<:VTypes
    f = Factor(ccall((@cholmod_name("copy_factor", SuiteSparse_long),:libcholmod),
        Ptr{C_Factor{Tv}},
            (Ptr{C_Factor{Tv}}, Ptr{UInt8}),
                get(F.p), common()))
    finalizer(f, free!)
    f
end
function copy_sparse(A::Sparse{Tv}) where Tv<:VTypes
    s = Sparse(ccall((@cholmod_name("copy_sparse", SuiteSparse_long),:libcholmod),
        Ptr{C_Sparse{Tv}},
            (Ptr{C_Sparse{Tv}}, Ptr{UInt8}),
                get(A.p), common()))
    finalizer(s, free!)
    s
end
function copy(A::Sparse{Tv}, stype::Integer, mode::Integer) where Tv<:VRealTypes
    s = Sparse(ccall((@cholmod_name("copy", SuiteSparse_long),:libcholmod),
        Ptr{C_Sparse{Tv}},
            (Ptr{C_Sparse{Tv}}, Cint, Cint, Ptr{UInt8}),
                get(A.p), stype, mode, common()))
    finalizer(s, free!)
    s
end

### cholmod_check.h ###
function print_sparse(A::Sparse{Tv}, name::String) where Tv<:VTypes
    isascii(name) || error("non-ASCII name: $name")
    cm = common()
    set_print_level(cm, 3)
    @isok ccall((@cholmod_name("print_sparse", SuiteSparse_long),:libcholmod), Cint,
            (Ptr{C_Sparse{Tv}}, Ptr{UInt8}, Ptr{UInt8}),
                 get(A.p), name, cm)
    nothing
end
function print_factor(F::Factor{Tv}, name::String) where Tv<:VTypes
    cm = common()
    set_print_level(cm, 3)
    @isok ccall((@cholmod_name("print_factor", SuiteSparse_long),:libcholmod), Cint,
            (Ptr{C_Factor{Tv}}, Ptr{UInt8}, Ptr{UInt8}),
                get(F.p), name, cm)
    nothing
end

### cholmod_matrixops.h ###
function ssmult(A::Sparse{Tv}, B::Sparse{Tv}, stype::Integer,
        values::Bool, sorted::Bool) where Tv<:VRealTypes
    lA = unsafe_load(get(A.p))
    lB = unsafe_load(get(B.p))
    if lA.ncol != lB.nrow
        throw(DimensionMismatch("inner matrix dimensions do not fit"))
    end
    s = Sparse(ccall((@cholmod_name("ssmult", SuiteSparse_long),:libcholmod),
        Ptr{C_Sparse{Tv}},
            (Ptr{C_Sparse{Tv}}, Ptr{C_Sparse{Tv}}, Cint, Cint,
                Cint, Ptr{UInt8}),
             get(A.p), get(B.p), stype, values,
                sorted, common()))
    finalizer(s, free!)
    s
end

function norm_sparse(A::Sparse{Tv}, norm::Integer) where Tv<:VTypes
    if norm != 0 && norm != 1
        throw(ArgumentError("norm argument must be either 0 or 1"))
    end
    ccall((@cholmod_name("norm_sparse", SuiteSparse_long), :libcholmod), Cdouble,
            (Ptr{C_Sparse{Tv}}, Cint, Ptr{UInt8}),
                get(A.p), norm, common())
end

function horzcat(A::Sparse{Tv}, B::Sparse{Tv}, values::Bool) where Tv<:VRealTypes
    s = Sparse(ccall((@cholmod_name("horzcat", SuiteSparse_long), :libcholmod),
        Ptr{C_Sparse{Tv}},
            (Ptr{C_Sparse{Tv}}, Ptr{C_Sparse{Tv}}, Cint, Ptr{UInt8}),
             get(A.p), get(B.p), values, common()))
    finalizer(s, free!)
    s
end

function scale!(S::Dense{Tv}, scale::Integer, A::Sparse{Tv}) where Tv<:VRealTypes
    sS = unsafe_load(get(S.p))
    sA = unsafe_load(get(A.p))
    if sS.ncol != 1 && sS.nrow != 1
        throw(DimensionMismatch("first argument must be a vector"))
    end
    if scale == SCALAR && sS.nrow != 1
        throw(DimensionMismatch("scaling argument must have length one"))
    elseif scale == ROW && sS.nrow*sS.ncol != sA.nrow
        throw(DimensionMismatch("scaling vector has length $(sS.nrow*sS.ncol), " *
            "but matrix has $(sA.nrow) rows."))
    elseif scale == COL && sS.nrow*sS.ncol != sA.ncol
        throw(DimensionMismatch("scaling vector has length $(sS.nrow*sS.ncol), " *
            "but matrix has $(sA.ncol) columns"))
    elseif scale == SYM
        if sA.nrow != sA.ncol
            throw(DimensionMismatch("matrix must be square"))
        elseif sS.nrow*sS.ncol != sA.nrow
            throw(DimensionMismatch("scaling vector has length $(sS.nrow*sS.ncol), " *
                "but matrix has $(sA.ncol) columns and rows"))
        end
    end

    sA = unsafe_load(get(A.p))
    @isok ccall((@cholmod_name("scale",SuiteSparse_long),:libcholmod), Cint,
            (Ptr{C_Dense{Tv}}, Cint, Ptr{C_Sparse{Tv}}, Ptr{UInt8}),
                get(S.p), scale, get(A.p), common())
    A
end

function sdmult!(A::Sparse{Tv}, transpose::Bool,
        α::Number, β::Number, X::Dense{Tv}, Y::Dense{Tv}) where Tv<:VTypes
    m, n = size(A)
    nc = transpose ? m : n
    nr = transpose ? n : m
    if nc != size(X, 1)
        throw(DimensionMismatch("incompatible dimensions, $nc and $(size(X,1))"))
    end
    @isok ccall((@cholmod_name("sdmult", SuiteSparse_long),:libcholmod), Cint,
            (Ptr{C_Sparse{Tv}}, Cint,
             Ref{Complex128}, Ref{Complex128},
             Ptr{C_Dense{Tv}}, Ptr{C_Dense{Tv}}, Ptr{UInt8}),
                get(A.p), transpose, α, β, get(X.p), get(Y.p), common())
    Y
end

function vertcat(A::Sparse{Tv}, B::Sparse{Tv}, values::Bool) where Tv<:VRealTypes
    s = Sparse(ccall((@cholmod_name("vertcat", SuiteSparse_long), :libcholmod),
            Ptr{C_Sparse{Tv}},
            (Ptr{C_Sparse{Tv}}, Ptr{C_Sparse{Tv}}, Cint, Ptr{UInt8}),
                get(A.p), get(B.p), values, common()))
    finalizer(s, free!)
    s
end

function symmetry(A::Sparse{Tv}, option::Integer) where Tv<:VTypes
    xmatched = Ref{SuiteSparse_long}()
    pmatched = Ref{SuiteSparse_long}()
    nzoffdiag = Ref{SuiteSparse_long}()
    nzdiag = Ref{SuiteSparse_long}()
    rv = ccall((@cholmod_name("symmetry", SuiteSparse_long), :libcholmod), Cint,
            (Ptr{C_Sparse{Tv}}, Cint, Ptr{SuiteSparse_long}, Ptr{SuiteSparse_long},
                Ptr{SuiteSparse_long}, Ptr{SuiteSparse_long}, Ptr{UInt8}),
                    get(A.p), option, xmatched, pmatched,
                        nzoffdiag, nzdiag, common())
    rv, xmatched[], pmatched[], nzoffdiag[], nzdiag[]
end

# cholmod_cholesky.h
# For analyze, analyze_p, and factorize_p!, the Common argument must be
# supplied in order to control if the factorization is LLt or LDLt
function analyze(A::Sparse{Tv}, cmmn::Vector{UInt8}) where Tv<:VTypes
    f = Factor(ccall((@cholmod_name("analyze", SuiteSparse_long),:libcholmod),
        Ptr{C_Factor{Tv}},
            (Ptr{C_Sparse{Tv}}, Ptr{UInt8}),
                get(A.p), cmmn))
    finalizer(f, free!)
    f
end
function analyze_p(A::Sparse{Tv}, perm::Vector{SuiteSparse_long},
                   cmmn::Vector{UInt8}) where Tv<:VTypes
    length(perm) != size(A,1) && throw(BoundsError())
    f = Factor(ccall((@cholmod_name("analyze_p", SuiteSparse_long),:libcholmod),
            Ptr{C_Factor{Tv}},
            (Ptr{C_Sparse{Tv}}, Ptr{SuiteSparse_long}, Ptr{SuiteSparse_long},
                Csize_t, Ptr{UInt8}),
                get(A.p), perm, C_NULL, 0, cmmn))
    finalizer(f, free!)
    f
end
function factorize!(A::Sparse{Tv}, F::Factor{Tv}, cmmn::Vector{UInt8}) where Tv<:VTypes
    @isok ccall((@cholmod_name("factorize", SuiteSparse_long),:libcholmod), Cint,
        (Ptr{C_Sparse{Tv}}, Ptr{C_Factor{Tv}}, Ptr{UInt8}),
            get(A.p), get(F.p), cmmn)
    F
end
function factorize_p!(A::Sparse{Tv}, β::Real, F::Factor{Tv}, cmmn::Vector{UInt8}) where Tv<:VTypes
    # note that β is passed as a complex number (double beta[2]),
    # but the CHOLMOD manual says that only beta[0] (real part) is used
    @isok ccall((@cholmod_name("factorize_p", SuiteSparse_long),:libcholmod), Cint,
        (Ptr{C_Sparse{Tv}}, Ref{Complex128}, Ptr{SuiteSparse_long}, Csize_t,
         Ptr{C_Factor{Tv}}, Ptr{UInt8}),
            get(A.p), β, C_NULL, 0, get(F.p), cmmn)
    F
end

function solve(sys::Integer, F::Factor{Tv}, B::Dense{Tv}) where Tv<:VTypes
    if size(F,1) != size(B,1)
        throw(DimensionMismatch("LHS and RHS should have the same number of rows. " *
            "LHS has $(size(F,1)) rows, but RHS has $(size(B,1)) rows."))
    end
    d = Dense(ccall((@cholmod_name("solve", SuiteSparse_long),:libcholmod), Ptr{C_Dense{Tv}},
            (Cint, Ptr{C_Factor{Tv}}, Ptr{C_Dense{Tv}}, Ptr{UInt8}),
                sys, get(F.p), get(B.p), common()))
    finalizer(d, free!)
    d
end

function spsolve(sys::Integer, F::Factor{Tv}, B::Sparse{Tv}) where Tv<:VTypes
    if size(F,1) != size(B,1)
        throw(DimensionMismatch("LHS and RHS should have the same number of rows. " *
            "LHS has $(size(F,1)) rows, but RHS has $(size(B,1)) rows."))
    end
    s = Sparse(ccall((@cholmod_name("spsolve", SuiteSparse_long),:libcholmod),
        Ptr{C_Sparse{Tv}},
            (Cint, Ptr{C_Factor{Tv}}, Ptr{C_Sparse{Tv}}, Ptr{UInt8}),
                sys, get(F.p), get(B.p), common()))
    finalizer(s, free!)
    s
end

# Autodetects the types
function read_sparse(file::Libc.FILE, ::Type{SuiteSparse_long})
    ptr = ccall((@cholmod_name("read_sparse", SuiteSparse_long), :libcholmod),
        Ptr{C_SparseVoid},
            (Ptr{Void}, Ptr{UInt8}),
                file.ptr, common())
    if ptr == C_NULL
        throw(ArgumentError("sparse matrix construction failed. Check that input file is valid."))
    end
    s = Sparse(ptr)
    finalizer(s, free!)
    s
end

function read_sparse(file::IO, T)
    cfile = Libc.FILE(file)
    try return read_sparse(cfile, T)
    finally close(cfile)
    end
end

function get_perm(F::Factor)
    s = unsafe_load(get(F.p))
    p = unsafe_wrap(Array, s.Perm, s.n, false)
    p+1
end
get_perm(FC::FactorComponent) = get_perm(Factor(FC))

#########################
# High level interfaces #
#########################

# Convertion/construction
function convert(::Type{Dense{T}}, A::StridedVecOrMat) where T<:VTypes
    d = allocate_dense(size(A, 1), size(A, 2), stride(A, 2), T)
    s = unsafe_load(d.p)
    for i in eachindex(A)
        unsafe_store!(s.x, A[i], i)
    end
    d
end
function convert(::Type{Dense}, A::StridedVecOrMat)
    T = promote_type(eltype(A), Float64)
    return convert(Dense{T}, A)
end
convert(::Type{Dense}, A::Sparse) = sparse_to_dense(A)

# This constructior assumes zero based colptr and rowval
function Sparse(m::Integer, n::Integer,
        colptr0::Vector{SuiteSparse_long}, rowval0::Vector{SuiteSparse_long},
        nzval::Vector{Tv}, stype) where Tv<:VTypes
    # checks
    ## length of input
    if length(colptr0) <= n
        throw(ArgumentError("length of colptr0 must be at least n + 1 = $(n + 1) but was $(length(colptr0))"))
    end
    if colptr0[n + 1] > length(rowval0)
        throw(ArgumentError("length of rowval0 is $(length(rowval0)) but value of colptr0 requires length to be at least $(colptr0[n + 1])"))
    end
    if colptr0[n + 1] > length(nzval)
        throw(ArgumentError("length of nzval is $(length(nzval)) but value of colptr0 requires length to be at least $(colptr0[n + 1])"))
    end
    ## columns are sorted
    iss = true
    for i = 2:length(colptr0)
        if !issorted(view(rowval0, colptr0[i - 1] + 1:colptr0[i]))
            iss = false
            break
        end
    end

    o = allocate_sparse(m, n, colptr0[n + 1], iss, true, stype, Tv)
    s = unsafe_load(o.p)

    unsafe_copy!(s.p, pointer(colptr0), n + 1)
    unsafe_copy!(s.i, pointer(rowval0), colptr0[n + 1])
    unsafe_copy!(s.x, pointer(nzval) , colptr0[n + 1])

    @isok check_sparse(o)

    return o
end

function Sparse(m::Integer, n::Integer,
        colptr0::Vector{SuiteSparse_long},
        rowval0::Vector{SuiteSparse_long},
        nzval::Vector{<:VTypes})
    o = Sparse(m, n, colptr0, rowval0, nzval, 0)

    # sort indices
    sort!(o)

    # check if array is symmetric and change stype if it is
    if ishermitian(o)
        change_stype!(o, -1)
    end
    o
end

function Sparse(A::SparseMatrixCSC{Tv,SuiteSparse_long}, stype::Integer) where Tv<:VTypes
    ## Check length of input. This should never fail but see #20024
    if length(A.colptr) <= A.n
        throw(ArgumentError("length of colptr must be at least size(A,2) + 1 = $(A.n + 1) but was $(length(A.colptr))"))
    end
    if nnz(A) > length(A.rowval)
        throw(ArgumentError("length of rowval is $(length(A.rowval)) but value of colptr requires length to be at least $(nnz(A))"))
    end
    if nnz(A) > length(A.nzval)
        throw(ArgumentError("length of nzval is $(length(A.nzval)) but value of colptr requires length to be at least $(nnz(A))"))
    end

    o = allocate_sparse(A.m, A.n, nnz(A), true, true, stype, Tv)
    s = unsafe_load(o.p)
    for i = 1:(A.n + 1)
        unsafe_store!(s.p, A.colptr[i] - 1, i)
    end
    for i = 1:nnz(A)
        unsafe_store!(s.i, A.rowval[i] - 1, i)
    end
    unsafe_copy!(s.x, pointer(A.nzval), nnz(A))

    @isok check_sparse(o)

    return o
end

# convert SparseVectors into CHOLMOD Sparse types through a mx1 CSC matrix
convert(::Type{Sparse}, A::SparseVector{<:VTypes,SuiteSparse_long}) =
    convert(Sparse, convert(SparseMatrixCSC, A))
function convert(::Type{Sparse}, A::SparseMatrixCSC{<:VTypes,<:ITypes})
    o = Sparse(A, 0)
    # check if array is symmetric and change stype if it is
    if ishermitian(o)
        change_stype!(o, -1)
    end
    o
end
convert(::Type{Sparse}, A::SparseMatrixCSC{Complex{Float32},<:ITypes}) =
    convert(Sparse, convert(SparseMatrixCSC{Complex{Float64},SuiteSparse_long}, A))
convert(::Type{Sparse}, A::Symmetric{Float64,SparseMatrixCSC{Float64,SuiteSparse_long}}) =
    Sparse(A.data, A.uplo == 'L' ? -1 : 1)
convert(::Type{Sparse}, A::Hermitian{Tv,SparseMatrixCSC{Tv,SuiteSparse_long}}) where {Tv<:VTypes} =
    Sparse(A.data, A.uplo == 'L' ? -1 : 1)
function convert(::Type{Sparse},
                 A::Union{SparseMatrixCSC{BigFloat,Ti},
                          Symmetric{BigFloat,SparseMatrixCSC{BigFloat,Ti}},
                          Hermitian{Complex{BigFloat},SparseMatrixCSC{Complex{BigFloat},Ti}}},
                 args...) where Ti<:ITypes
    throw(MethodError(convert, (Sparse, A)))
end
function convert(::Type{Sparse},
    A::Union{SparseMatrixCSC{T,Ti},
             Symmetric{T,SparseMatrixCSC{T,Ti}},
             Hermitian{T,SparseMatrixCSC{T,Ti}}},
    args...) where T where Ti<:ITypes
    return Sparse(convert(AbstractMatrix{promote_type(Float64, T)}, A), args...)
end

# Useful when reading in files, but not type stable
function convert(::Type{Sparse}, p::Ptr{C_SparseVoid})
    if p == C_NULL
        throw(ArgumentError("sparse matrix construction failed for " *
            "unknown reasons. Please submit a bug report."))
    end

    s = unsafe_load(p)

    # Check integer type
    if s.itype == INT
        free_sparse!(p)
        throw(CHOLMODException("the value of itype was $s.itype. " *
            "Only integer type of $SuiteSparse_long is supported."))
    elseif s.itype == INTLONG
        free_sparse!(p)
        throw(CHOLMODException("the value of itype was $s.itype. This combination " *
            "of integer types shouldn't happen. Please submit a bug report."))
    elseif s.itype != LONG # must be s.itype == LONG
        free_sparse!(p)
        throw(CHOLMODException("illegal value of itype: $s.itype"))
    end

    # Check for double or single precision
    if s.dtype == DOUBLE
        Tv = Float64
    elseif s.dtype == SINGLE
        # Tv = Float32 # this should be supported at some point
        free_sparse!(p)
        throw(CHOLMODException("single precision not supported yet"))
    else
        free_sparse!(p)
        throw(CHOLMODException("illegal value of dtype: $s.dtype"))
    end

    # Check for real or complex
    if s.xtype == COMPLEX
        Tv = Complex{Tv}
    elseif s.xtype != REAL
        free_sparse!(p)
        throw(CHOLMODException("illegal value of xtype: $s.xtype"))
    end

    return Sparse(convert(Ptr{C_Sparse{Tv}}, p))
end

convert(::Type{Sparse}, A::Dense) = dense_to_sparse(A, SuiteSparse_long)
convert(::Type{Sparse}, L::Factor) = factor_to_sparse!(copy(L))
function (::Type{Sparse})(filename::String)
    open(filename) do f
        return read_sparse(f, SuiteSparse_long)
    end
end

## convertion back to base Julia types
function convert(::Type{Matrix{T}}, D::Dense{T}) where T
    s = unsafe_load(D.p)
    a = Matrix{T}(s.nrow, s.ncol)
    copy!(a, D)
end

Base.copy!(dest::Base.PermutedDimsArrays.PermutedDimsArray, src::Dense) = _copy!(dest, src) # ambig
Base.copy!(dest::Dense{T}, D::Dense{T}) where {T<:VTypes} = _copy!(dest, D)
Base.copy!(dest::AbstractArray{T}, D::Dense{T}) where {T<:VTypes} = _copy!(dest, D)
Base.copy!(dest::AbstractArray{T,2}, D::Dense{T}) where {T<:VTypes} = _copy!(dest, D)
Base.copy!(dest::AbstractArray, D::Dense) = _copy!(dest, D)

function _copy!(dest::AbstractArray, D::Dense)
    s = unsafe_load(D.p)
    n = s.nrow*s.ncol
    n <= length(dest) || throw(BoundsError(dest, n))
    if s.d == s.nrow && isa(dest, Array)
        unsafe_copy!(pointer(dest), s.x, s.d*s.ncol)
    else
        k = 0
        for j = 1:s.ncol
            for i = 1:s.nrow
                dest[k+=1] = unsafe_load(s.x, i + (j - 1)*s.d)
            end
        end
    end
    dest
end
convert(::Type{Matrix}, D::Dense{T}) where {T} = convert(Matrix{T}, D)
function convert(::Type{Vector{T}}, D::Dense{T}) where T
    if size(D, 2) > 1
        throw(DimensionMismatch("input must be a vector but had $(size(D, 2)) columns"))
    end
    copy!(Vector{T}(size(D, 1)), D)
end
convert(::Type{Vector}, D::Dense{T}) where {T} = convert(Vector{T}, D)

function convert(::Type{SparseMatrixCSC{Tv,SuiteSparse_long}}, A::Sparse{Tv}) where Tv
    s = unsafe_load(A.p)
    if s.stype != 0
        throw(ArgumentError("matrix has stype != 0. Convert to matrix " *
            "with stype == 0 before converting to SparseMatrixCSC"))
    end

    B = SparseMatrixCSC(s.nrow, s.ncol,
        increment(unsafe_wrap(Array, s.p, (s.ncol + 1,), false)),
        increment(unsafe_wrap(Array, s.i, (s.nzmax,), false)),
        copy(unsafe_wrap(Array, s.x, (s.nzmax,), false)))

    if s.sorted == 0
        return SparseArrays.sortSparseMatrixCSC!(B)
    else
        return B
    end
end
function convert(::Type{Symmetric{Float64,SparseMatrixCSC{Float64,SuiteSparse_long}}}, A::Sparse{Float64})
    s = unsafe_load(A.p)
    if !issymmetric(A)
        throw(ArgumentError("matrix is not symmetric"))
    end

    B = Symmetric(SparseMatrixCSC(s.nrow, s.ncol,
        increment(unsafe_wrap(Array, s.p, (s.ncol + 1,), false)),
        increment(unsafe_wrap(Array, s.i, (s.nzmax,), false)),
        copy(unsafe_wrap(Array, s.x, (s.nzmax,), false))), s.stype > 0 ? :U : :L)

    if s.sorted == 0
        return SparseArrays.sortSparseMatrixCSC!(B.data)
    else
        return B
    end
end
function convert(::Type{Hermitian{Tv,SparseMatrixCSC{Tv,SuiteSparse_long}}}, A::Sparse{Tv}) where Tv<:VTypes
    s = unsafe_load(A.p)
    if !ishermitian(A)
        throw(ArgumentError("matrix is not Hermitian"))
    end

    B = Hermitian(SparseMatrixCSC(s.nrow, s.ncol,
        increment(unsafe_wrap(Array, s.p, (s.ncol + 1,), false)),
        increment(unsafe_wrap(Array, s.i, (s.nzmax,), false)),
        copy(unsafe_wrap(Array, s.x, (s.nzmax,), false))), s.stype > 0 ? :U : :L)

    if s.sorted == 0
        return SparseArrays.sortSparseMatrixCSC!(B.data)
    else
        return B
    end
end
function sparse(A::Sparse{Float64}) # Notice! Cannot be type stable because of stype
    s = unsafe_load(A.p)
    if s.stype == 0
        return convert(SparseMatrixCSC{Float64,SuiteSparse_long}, A)
    end
    return convert(Symmetric{Float64,SparseMatrixCSC{Float64,SuiteSparse_long}}, A)
end
function sparse(A::Sparse{Complex{Float64}}) # Notice! Cannot be type stable because of stype
    s = unsafe_load(A.p)
    if s.stype == 0
        return convert(SparseMatrixCSC{Complex{Float64},SuiteSparse_long}, A)
    end
    return convert(Hermitian{Complex{Float64},SparseMatrixCSC{Complex{Float64},SuiteSparse_long}}, A)
end
function sparse(F::Factor)
    s = unsafe_load(F.p)
    if s.is_ll != 0
        L = Sparse(F)
        A = sparse(L*L')
    else
        LD = sparse(F[:LD])
        L, d = getLd!(LD)
        A = (L * Diagonal(d)) * L'
    end
    SparseArrays.sortSparseMatrixCSC!(A)
    p = get_perm(F)
    if p != [1:s.n;]
        pinv = Vector{Int}(length(p))
        for k = 1:length(p)
            pinv[p[k]] = k
        end
        A = A[pinv,pinv]
    end
    A
end

sparse(D::Dense) = sparse(Sparse(D))

function sparse(FC::FactorComponent{Tv,:L}) where Tv
    F = Factor(FC)
    s = unsafe_load(F.p)
    if s.is_ll == 0
        throw(CHOLMODException("sparse: supported only for :LD on LDLt factorizations"))
    end
    sparse(Sparse(F))
end
sparse(FC::FactorComponent{Tv,:LD}) where {Tv} = sparse(Sparse(Factor(FC)))

# Calculate the offset into the stype field of the cholmod_sparse_struct and
# change the value
let offset = fieldoffset(C_Sparse{Float64}, findfirst(name -> name === :stype, fieldnames(C_Sparse{Float64})))
    global change_stype!
    function change_stype!(A::Sparse, i::Integer)
        unsafe_store!(convert(Ptr{Cint}, A.p), i, div(offset, 4) + 1)
        return A
    end
end

free!(A::Dense) = free_dense!(A.p)
free!(A::Sparse) = free_sparse!(A.p)
free!(F::Factor) = free_factor!(F.p)

eltype(::Type{Dense{T}}) where {T<:VTypes} = T
eltype(::Type{Factor{T}}) where {T<:VTypes} = T
eltype(::Type{Sparse{T}}) where {T<:VTypes} = T

nnz(F::Factor) = nnz(Sparse(F))

function show(io::IO, F::Factor)
    println(io, typeof(F))
    showfactor(io, F)
end

function show(io::IO, FC::FactorComponent)
    println(io, typeof(FC))
    showfactor(io, Factor(FC))
end

function showfactor(io::IO, F::Factor)
    s = unsafe_load(get(F.p))
    @printf(io, "type: %12s\n", s.is_ll!=0 ? "LLt" : "LDLt")
    @printf(io, "method: %10s\n", s.is_super!=0 ? "supernodal" : "simplicial")
    @printf(io, "maxnnz: %10d\n", Int(s.nzmax))
    @printf(io, "nnz: %13d\n", nnz(F))
end

# getindex not defined for these, so don't use the normal array printer
show(io::IO, ::MIME"text/plain", FC::FactorComponent) = show(io, FC)
show(io::IO, ::MIME"text/plain", F::Factor) = show(io, F)

isvalid(A::Dense) = check_dense(A)
isvalid(A::Sparse) = check_sparse(A)
isvalid(A::Factor) = check_factor(A)

copy(A::Dense) = copy_dense(A)
copy(A::Sparse) = copy_sparse(A)
copy(A::Factor) = copy_factor(A)

function size(A::Union{Dense,Sparse})
    s = unsafe_load(get(A.p))
    return (Int(s.nrow), Int(s.ncol))
end
function size(F::Factor, i::Integer)
    if i < 1
        throw(ArgumentError("dimension must be positive"))
    end
    s = unsafe_load(get(F.p))
    if i <= 2
        return Int(s.n)
    end
    return 1
end
size(F::Factor) = (size(F, 1), size(F, 2))

IndexStyle(::Dense) = IndexLinear()

size(FC::FactorComponent, i::Integer) = size(FC.F, i)
size(FC::FactorComponent) = size(FC.F)

ctranspose(FC::FactorComponent{Tv,:L}) where {Tv} = FactorComponent{Tv,:U}(FC.F)
ctranspose(FC::FactorComponent{Tv,:U}) where {Tv} = FactorComponent{Tv,:L}(FC.F)
ctranspose(FC::FactorComponent{Tv,:PtL}) where {Tv} = FactorComponent{Tv,:UP}(FC.F)
ctranspose(FC::FactorComponent{Tv,:UP}) where {Tv} = FactorComponent{Tv,:PtL}(FC.F)
ctranspose(FC::FactorComponent{Tv,:D}) where {Tv} = FC
ctranspose(FC::FactorComponent{Tv,:LD}) where {Tv} = FactorComponent{Tv,:DU}(FC.F)
ctranspose(FC::FactorComponent{Tv,:DU}) where {Tv} = FactorComponent{Tv,:LD}(FC.F)
ctranspose(FC::FactorComponent{Tv,:PtLD}) where {Tv} = FactorComponent{Tv,:DUP}(FC.F)
ctranspose(FC::FactorComponent{Tv,:DUP}) where {Tv} = FactorComponent{Tv,:PtLD}(FC.F)

function getindex(A::Dense, i::Integer)
    s = unsafe_load(get(A.p))
    0 < i <= s.nrow*s.ncol || throw(BoundsError())
    unsafe_load(s.x, i)
end

IndexStyle(::Sparse) = IndexCartesian()
function getindex(A::Sparse{T}, i0::Integer, i1::Integer) where T
    s = unsafe_load(get(A.p))
    !(1 <= i0 <= s.nrow && 1 <= i1 <= s.ncol) && throw(BoundsError())
    s.stype < 0 && i0 < i1 && return conj(A[i1,i0])
    s.stype > 0 && i0 > i1 && return conj(A[i1,i0])

    r1 = Int(unsafe_load(s.p, i1) + 1)
    r2 = Int(unsafe_load(s.p, i1 + 1))
    (r1 > r2) && return zero(T)
    r1 = Int(searchsortedfirst(unsafe_wrap(Array, s.i, (s.nzmax,), false),
        i0 - 1, r1, r2, Base.Order.Forward))
    ((r1 > r2) || (unsafe_load(s.i, r1) + 1 != i0)) ? zero(T) : unsafe_load(s.x, r1)
end

function getindex(F::Factor, sym::Symbol)
    sym == :p && return get_perm(F)
    FactorComponent(F, sym)
end

function getLd!(S::SparseMatrixCSC)
    d = Vector{eltype(S)}(size(S, 1))
    fill!(d, 0)
    col = 1
    for k = 1:nnz(S)
        while k >= S.colptr[col+1]
            col += 1
        end
        if S.rowval[k] == col
            d[col] = S.nzval[k]
            S.nzval[k] = 1
        end
    end
    S, d
end

## Multiplication
(*)(A::Sparse, B::Sparse) = ssmult(A, B, 0, true, true)
(*)(A::Sparse, B::Dense) = sdmult!(A, false, 1., 0., B, zeros(size(A, 1), size(B, 2)))
(*)(A::Sparse, B::VecOrMat) = (*)(A, Dense(B))

function A_mul_Bc(A::Sparse{Tv}, B::Sparse{Tv}) where Tv<:VRealTypes
    cm = common()

    if A !== B
        aa1 = transpose_(B, 2)
        ## result of ssmult will have stype==0, contain numerical values and be sorted
        return ssmult(A, aa1, 0, true, true)
    end

    ## The A*A' case is handled by cholmod_aat. This routine requires
    ## A->stype == 0 (storage of upper and lower parts). If neccesary
    ## the matrix A is first converted to stype == 0
    s = unsafe_load(A.p)
    if s.stype != 0
        aa1 = copy(A, 0, 1)
        return aat(aa1, SuiteSparse_long[0:s.ncol-1;], 1)
    else
        return aat(A, SuiteSparse_long[0:s.ncol-1;], 1)
    end
end

function Ac_mul_B(A::Sparse, B::Sparse)
    aa1 = transpose_(A, 2)
    if A === B
        return A_mul_Bc(aa1, aa1)
    end
    ## result of ssmult will have stype==0, contain numerical values and be sorted
    return ssmult(aa1, B, 0, true, true)
end

Ac_mul_B(A::Sparse, B::Dense) = sdmult!(A, true, 1., 0., B, zeros(size(A, 2), size(B, 2)))
Ac_mul_B(A::Sparse, B::VecOrMat) =  Ac_mul_B(A, Dense(B))

## Factorization methods

## Compute that symbolic factorization only
function fact_(A::Sparse{<:VTypes}, cm::Array{UInt8};
    perm::AbstractVector{SuiteSparse_long}=SuiteSparse_long[],
    postorder::Bool=true, userperm_only::Bool=true)

    sA = unsafe_load(get(A.p))
    sA.stype == 0 && throw(ArgumentError("sparse matrix is not symmetric/Hermitian"))

    if !postorder
        unsafe_store!(common_postorder, 0)
    end

    if isempty(perm)
        F = analyze(A, cm)
    else # user permutation provided
        if userperm_only # use perm even if it is worse than AMD
            unsafe_store!(common_nmethods, 1)
        end
        F = analyze_p(A, SuiteSparse_long[p-1 for p in perm], cm)
    end

    return F
end

function cholfact!(F::Factor{Tv}, A::Sparse{Tv}; shift::Real=0.0) where Tv
    cm = common()

    # Makes it an LLt
    unsafe_store!(common_final_ll, 1)

    # Compute the numerical factorization
    factorize_p!(A, shift, F, cm)

    s = unsafe_load(get(F.p))
    s.minor < size(A, 1) && throw(Base.LinAlg.PosDefException(s.minor))
    return F
end

"""
    cholfact!(F::Factor, A; shift = 0.0) -> CHOLMOD.Factor

Compute the Cholesky (``LL'``) factorization of `A`, reusing the symbolic
factorization `F`. `A` must be a `SparseMatrixCSC`, `Symmetric{SparseMatrixCSC}`,
or `Hermitian{SparseMatrixCSC}`. Note that even if `A` doesn't
have the type tag, it must still be symmetric or Hermitian.

See also [`cholfact`](@ref).

!!! note
    This method uses the CHOLMOD library from SuiteSparse, which only supports
    doubles or complex doubles. Input matrices not of those element types will
    be converted to `SparseMatrixCSC{Float64}` or `SparseMatrixCSC{Complex128}`
    as appropriate.
"""
cholfact!(F::Factor, A::Union{SparseMatrixCSC{T},
        SparseMatrixCSC{Complex{T}},
        Symmetric{T,SparseMatrixCSC{T,SuiteSparse_long}},
        Hermitian{Complex{T},SparseMatrixCSC{Complex{T},SuiteSparse_long}},
        Hermitian{T,SparseMatrixCSC{T,SuiteSparse_long}}};
    shift = 0.0) where {T<:Real} =
    cholfact!(F, Sparse(A); shift = shift)

function cholfact(A::Sparse; shift::Real=0.0,
    perm::AbstractVector{SuiteSparse_long}=SuiteSparse_long[])

    cm = defaults(common())
    set_print_level(cm, 0)

    # Compute the symbolic factorization
    F = fact_(A, cm; perm = perm)

    # Compute the numerical factorization
    cholfact!(F, A; shift = shift)

    s = unsafe_load(get(F.p))
    s.minor < size(A, 1) && throw(Base.LinAlg.PosDefException(s.minor))
    return F
end

"""
    cholfact(A; shift = 0.0, perm = Int[]) -> CHOLMOD.Factor

Compute the Cholesky factorization of a sparse positive definite matrix `A`.
`A` must be a `SparseMatrixCSC`, `Symmetric{SparseMatrixCSC}`, or
`Hermitian{SparseMatrixCSC}`. Note that even if `A` doesn't
have the type tag, it must still be symmetric or Hermitian.
A fill-reducing permutation is used.
`F = cholfact(A)` is most frequently used to solve systems of equations with `F\\b`,
but also the methods [`diag`](@ref), [`det`](@ref), and
[`logdet`](@ref) are defined for `F`.
You can also extract individual factors from `F`, using `F[:L]`.
However, since pivoting is on by default, the factorization is internally
represented as `A == P'*L*L'*P` with a permutation matrix `P`;
using just `L` without accounting for `P` will give incorrect answers.
To include the effects of permutation,
it's typically preferable to extract "combined" factors like `PtL = F[:PtL]`
(the equivalent of `P'*L`) and `LtP = F[:UP]` (the equivalent of `L'*P`).

Setting the optional `shift` keyword argument computes the factorization of
`A+shift*I` instead of `A`. If the `perm` argument is nonempty,
it should be a permutation of `1:size(A,1)` giving the ordering to use
(instead of CHOLMOD's default AMD ordering).

!!! note
    This method uses the CHOLMOD library from SuiteSparse, which only supports
    doubles or complex doubles. Input matrices not of those element types will
    be converted to `SparseMatrixCSC{Float64}` or `SparseMatrixCSC{Complex128}`
    as appropriate.

    Many other functions from CHOLMOD are wrapped but not exported from the
    `Base.SparseArrays.CHOLMOD` module.
"""
cholfact(A::Union{SparseMatrixCSC{T}, SparseMatrixCSC{Complex{T}},
    Symmetric{T,SparseMatrixCSC{T,SuiteSparse_long}},
    Hermitian{Complex{T},SparseMatrixCSC{Complex{T},SuiteSparse_long}},
    Hermitian{T,SparseMatrixCSC{T,SuiteSparse_long}}};
    kws...) where {T<:Real} = cholfact(Sparse(A); kws...)

function ldltfact!(F::Factor{Tv}, A::Sparse{Tv}; shift::Real=0.0) where Tv
    cm = common()

    # Compute the numerical factorization
    factorize_p!(A, shift, F, cm)

    s = unsafe_load(get(F.p))
    s.minor < size(A, 1) && throw(Base.LinAlg.ArgumentError("matrix has one or more zero pivots"))
    return F
end

"""
    ldltfact!(F::Factor, A; shift = 0.0) -> CHOLMOD.Factor

Compute the ``LDL'`` factorization of `A`, reusing the symbolic factorization `F`.
`A` must be a `SparseMatrixCSC`, `Symmetric{SparseMatrixCSC}`, or
`Hermitian{SparseMatrixCSC}`. Note that even if `A` doesn't
have the type tag, it must still be symmetric or Hermitian.

See also [`ldltfact`](@ref).

!!! note
    This method uses the CHOLMOD library from SuiteSparse, which only supports
    doubles or complex doubles. Input matrices not of those element types will
    be converted to `SparseMatrixCSC{Float64}` or `SparseMatrixCSC{Complex128}`
    as appropriate.
"""
ldltfact!(F::Factor, A::Union{SparseMatrixCSC{T},
    SparseMatrixCSC{Complex{T}},
    Symmetric{T,SparseMatrixCSC{T,SuiteSparse_long}},
    Hermitian{Complex{T},SparseMatrixCSC{Complex{T},SuiteSparse_long}},
    Hermitian{T,SparseMatrixCSC{T,SuiteSparse_long}}};
    shift = 0.0) where {T<:Real} =
    ldltfact!(F, Sparse(A), shift = shift)

function ldltfact(A::Sparse; shift::Real=0.0,
    perm::AbstractVector{SuiteSparse_long}=SuiteSparse_long[])

    cm = defaults(common())
    set_print_level(cm, 0)

    # Makes it an LDLt
    unsafe_store!(common_final_ll, 0)
    # Really make sure it's an LDLt by avoiding supernodal factorization
    unsafe_store!(common_supernodal, 0)

    # Compute the symbolic factorization
    F = fact_(A, cm; perm = perm)

    # Compute the numerical factorization
    ldltfact!(F, A; shift = shift)

    s = unsafe_load(get(F.p))
    if s.minor < size(A, 1)
        throw(Base.LinAlg.ArgumentError("matrix has one or more zero pivots"))
    end
    return F
end

"""
    ldltfact(A; shift = 0.0, perm=Int[]) -> CHOLMOD.Factor

Compute the ``LDL'`` factorization of a sparse matrix `A`.
`A` must be a `SparseMatrixCSC`, `Symmetric{SparseMatrixCSC}`, or
`Hermitian{SparseMatrixCSC}`. Note that even if `A` doesn't
have the type tag, it must still be symmetric or Hermitian.
A fill-reducing permutation is used. `F = ldltfact(A)` is most frequently
used to solve systems of equations `A*x = b` with `F\\b`. The returned
factorization object `F` also supports the methods [`diag`](@ref),
[`det`](@ref), [`logdet`](@ref), and [`inv`](@ref).
You can extract individual factors from `F` using `F[:L]`.
However, since pivoting is on by default, the factorization is internally
represented as `A == P'*L*D*L'*P` with a permutation matrix `P`;
using just `L` without accounting for `P` will give incorrect answers.
To include the effects of permutation, it is typically preferable to extract
"combined" factors like `PtL = F[:PtL]` (the equivalent of
`P'*L`) and `LtP = F[:UP]` (the equivalent of `L'*P`).
The complete list of supported factors is `:L, :PtL, :D, :UP, :U, :LD, :DU, :PtLD, :DUP`.

Setting the optional `shift` keyword argument computes the factorization of
`A+shift*I` instead of `A`. If the `perm` argument is nonempty,
it should be a permutation of `1:size(A,1)` giving the ordering to use
(instead of CHOLMOD's default AMD ordering).

!!! note
    This method uses the CHOLMOD library from SuiteSparse, which only supports
    doubles or complex doubles. Input matrices not of those element types will
    be converted to `SparseMatrixCSC{Float64}` or `SparseMatrixCSC{Complex128}`
    as appropriate.

    Many other functions from CHOLMOD are wrapped but not exported from the
    `Base.SparseArrays.CHOLMOD` module.
"""
ldltfact(A::Union{SparseMatrixCSC{T},SparseMatrixCSC{Complex{T}},
    Symmetric{T,SparseMatrixCSC{T,SuiteSparse_long}},
    Hermitian{Complex{T},SparseMatrixCSC{Complex{T},SuiteSparse_long}},
    Hermitian{T,SparseMatrixCSC{T,SuiteSparse_long}}};
    kws...) where {T<:Real} = ldltfact(Sparse(A); kws...)

## Solvers

for (T, f) in ((:Dense, :solve), (:Sparse, :spsolve))
    @eval begin
        # Solve Lx = b and L'x=b where A = L*L'
        function (\)(L::FactorComponent{T,:L}, B::$T) where T
            ($f)(CHOLMOD_L, Factor(L), B)
        end
        function (\)(L::FactorComponent{T,:U}, B::$T) where T
            ($f)(CHOLMOD_Lt, Factor(L), B)
        end
        # Solve PLx = b and L'P'x=b where A = P*L*L'*P'
        function (\)(L::FactorComponent{T,:PtL}, B::$T) where T
            F = Factor(L)
            ($f)(CHOLMOD_L, F, ($f)(CHOLMOD_P, F, B)) # Confusingly, CHOLMOD_P solves P'x = b
        end
        function (\)(L::FactorComponent{T,:UP}, B::$T) where T
            F = Factor(L)
            ($f)(CHOLMOD_Pt, F, ($f)(CHOLMOD_Lt, F, B))
        end
        # Solve various equations for A = L*D*L' and A = P*L*D*L'*P'
        function (\)(L::FactorComponent{T,:D}, B::$T) where T
            ($f)(CHOLMOD_D, Factor(L), B)
        end
        function (\)(L::FactorComponent{T,:LD}, B::$T) where T
            ($f)(CHOLMOD_LD, Factor(L), B)
        end
        function (\)(L::FactorComponent{T,:DU}, B::$T) where T
            ($f)(CHOLMOD_DLt, Factor(L), B)
        end
        function (\)(L::FactorComponent{T,:PtLD}, B::$T) where T
            F = Factor(L)
            ($f)(CHOLMOD_LD, F, ($f)(CHOLMOD_P, F, B))
        end
        function (\)(L::FactorComponent{T,:DUP}, B::$T) where T
            F = Factor(L)
            ($f)(CHOLMOD_Pt, F, ($f)(CHOLMOD_DLt, F, B))
        end
    end
end

SparseVecOrMat{Tv,Ti} = Union{SparseVector{Tv,Ti}, SparseMatrixCSC{Tv,Ti}}

function (\)(L::FactorComponent, b::Vector)
    reshape(convert(Matrix, L\Dense(b)), length(b))
end
function (\)(L::FactorComponent, B::Matrix)
    convert(Matrix, L\Dense(B))
end
function (\)(L::FactorComponent, B::SparseVecOrMat)
    sparse(L\Sparse(B,0))
end

Ac_ldiv_B(L::FactorComponent, B) = ctranspose(L)\B
Ac_ldiv_B(L::FactorComponent, B::RowVector) = ctranspose(L)\B # ambiguity

(\)(L::Factor{T}, B::Dense{T}) where {T<:VTypes} = solve(CHOLMOD_A, L, B)
# Explicit typevars are necessary to avoid ambiguities with defs in linalg/factorizations.jl
# Likewise the two following explicit Vector and Matrix defs (rather than a single VecOrMat)
(\)(L::Factor{T}, B::Vector{Complex{T}}) where {T<:Float64} = complex.(L\real(B), L\imag(B))
(\)(L::Factor{T}, B::Matrix{Complex{T}}) where {T<:Float64} = complex.(L\real(B), L\imag(B))
(\)(L::Factor{T}, b::StridedVector) where {T<:VTypes} = Vector(L\convert(Dense{T}, b))
(\)(L::Factor{T}, B::StridedMatrix) where {T<:VTypes} = Matrix(L\convert(Dense{T}, B))
(\)(L::Factor, B::Sparse) = spsolve(CHOLMOD_A, L, B)
# When right hand side is sparse, we have to ensure that the rhs is not marked as symmetric.
(\)(L::Factor, B::SparseVecOrMat) = sparse(spsolve(CHOLMOD_A, L, Sparse(B, 0)))

Ac_ldiv_B(L::Factor, B::Dense) = solve(CHOLMOD_A, L, B)
Ac_ldiv_B(L::Factor, B::VecOrMat) = convert(Matrix, solve(CHOLMOD_A, L, Dense(B)))
Ac_ldiv_B(L::Factor, B::Sparse) = spsolve(CHOLMOD_A, L, B)
Ac_ldiv_B(L::Factor, B::SparseVecOrMat) = Ac_ldiv_B(L, Sparse(B))

for f in (:\, :Ac_ldiv_B)
    @eval function ($f)(A::Union{Symmetric{Float64,SparseMatrixCSC{Float64,SuiteSparse_long}},
                          Hermitian{Float64,SparseMatrixCSC{Float64,SuiteSparse_long}},
                          Hermitian{Complex{Float64},SparseMatrixCSC{Complex{Float64},SuiteSparse_long}}}, B::StridedVecOrMat)
        try
            return ($f)(cholfact(A), B)
        catch e
            isa(e, LinAlg.PosDefException) || rethrow(e)
            return ($f)(ldltfact(A) , B)
        end
    end
end

## Other convenience methods
function diag(F::Factor{Tv}) where Tv
    f = unsafe_load(get(F.p))
    fsuper = f.super
    fpi = f.pi
    res = Base.zeros(Tv, Int(f.n))
    xv  = f.x
    if f.is_super!=0
        px = f.px
        pos = 1
        for i in 1:f.nsuper
            base = unsafe_load(px, i) + 1
            res[pos] = unsafe_load(xv, base)
            pos += 1
            for j in 1:unsafe_load(fsuper, i + 1) - unsafe_load(fsuper, i) - 1
                res[pos] = unsafe_load(xv, base + j*(unsafe_load(fpi, i + 1) -
                    unsafe_load(fpi, i) + 1))
                pos += 1
            end
        end
    else
        c0 = f.p
        r0 = f.i
        xv = f.x
        for j in 1:f.n
            jj = unsafe_load(c0, j) + 1
            assert(unsafe_load(r0, jj) == j - 1)
            res[j] = unsafe_load(xv, jj)
        end
    end
    res
end

function logdet(F::Factor{Tv}) where Tv<:VTypes
    f = unsafe_load(get(F.p))
    res = zero(Tv)
    for d in diag(F); res += log(abs(d)) end
    f.is_ll!=0 ? 2res : res
end

det(L::Factor) = exp(logdet(L))

function isposdef(A::SparseMatrixCSC{<:VTypes,SuiteSparse_long})
    if !ishermitian(A)
        return false
    end
    try
        f = cholfact(A)
    catch e
        isa(e, LinAlg.PosDefException) || rethrow(e)
        return false
    end
    true
end

function ishermitian(A::Sparse{Float64})
    s = unsafe_load(A.p)
    if s.stype != 0
        return true
    else
        i = symmetry(A, 1)[1]
        if i < 0
            throw(CHOLMODException("negative value returned from CHOLMOD's symmetry function. This
                is either because the indices are not sorted or because of a memory error"))
        end
        return i == MM_SYMMETRIC || i == MM_SYMMETRIC_POSDIAG
    end
end
function ishermitian(A::Sparse{Complex{Float64}})
    s = unsafe_load(A.p)
    if s.stype != 0
        return true
    else
        i = symmetry(A, 1)[1]
        if i < 0
            throw(CHOLMODException("negative value returned from CHOLMOD's symmetry function. This
                is either because the indices are not sorted or because of a memory error"))
        end
        return i == MM_HERMITIAN || i == MM_HERMITIAN_POSDIAG
    end
end

(*)(A::Symmetric{Float64,SparseMatrixCSC{Float64,Ti}},
    B::SparseVecOrMat{Float64,Ti}) where {Ti} = sparse(Sparse(A)*Sparse(B))
(*)(A::Hermitian{Complex{Float64},SparseMatrixCSC{Complex{Float64},Ti}},
    B::SparseVecOrMat{Complex{Float64},Ti}) where {Ti} = sparse(Sparse(A)*Sparse(B))
(*)(A::Hermitian{Float64,SparseMatrixCSC{Float64,Ti}},
    B::SparseVecOrMat{Float64,Ti}) where {Ti} = sparse(Sparse(A)*Sparse(B))

(*)(A::SparseVecOrMat{Float64,Ti},
    B::Symmetric{Float64,SparseMatrixCSC{Float64,Ti}}) where {Ti} = sparse(Sparse(A)*Sparse(B))
(*)(A::SparseVecOrMat{Complex{Float64},Ti},
    B::Hermitian{Complex{Float64},SparseMatrixCSC{Complex{Float64},Ti}}) where {Ti} = sparse(Sparse(A)*Sparse(B))
(*)(A::SparseVecOrMat{Float64,Ti},
    B::Hermitian{Float64,SparseMatrixCSC{Float64,Ti}}) where {Ti} = sparse(Sparse(A)*Sparse(B))

end #module


seed!(42)
L = 3
D = 10
N = 20
nSamples = 10
b = 5
x = rand(N, D)
y = rand(N)

@testset "NumericalVI" begin
    @test NumericalVI(:quad) isa QuadratureVI
    @test NumericalVI(:mc) isa MCIntegrationVI
    @test_throws ErrorException NumericalVI(:blah)
    @test NumericalSVI(b, :quad) isa QuadratureVI
    @test NumericalSVI(b, :mc) isa MCIntegrationVI
    @test_throws ErrorException NumericalSVI(b, :blah)
    @test repr(NumericalVI(:quad)) == "Numerical Inference by Quadrature"
    @test repr(NumericalVI(:mc)) ==
          "Numerical Inference by Monte Carlo Integration"
end