# coding: utf-8

# ------------------------------------------------------------------------------
# Copyright (C) 2012-2017 Guillaume Sagnol
# Copyright (C)      2019 Maximilian Stahlberg
#
# This file is part of PICOS.
#
# PICOS is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PICOS is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------

"""Functions for writing optimization problems to a file."""

# ------------------------------------------------------------------------------
# TODO: This is a temporary solution using outdated code. Ideally, writing to
#       a file should be integrated with the reformulation pipeline e.g. by
#       distinguishing a SolutionStrategy and an ExportStrategy.
# ------------------------------------------------------------------------------

from itertools import chain

import cvxopt
import numpy

from ..apidoc import api_end, api_start
from ..constraints import (
    AffineConstraint,
    LMIConstraint,
    RSOCConstraint,
    SOCConstraint,
)

from ..solvers import CVXOPTSolver, CPLEXSolver, MOSEKSolver, GurobiSolver
from ..expressions import IntegerVariable, BinaryVariable, CONTINUOUS_VARTYPES
from ..expressions.vectorizations import FullVectorization

_API_START = api_start(globals())
# -------------------------------


INFINITY = 1e16  #: A number deemed too large to appear in practice.


def write(picos_problem, filename, writer="picos"):
    r"""Write an optimization problem to a file.

    :param ~picos.Problem P: The problem to write.

    :param str filename: Path and name of the output file. The export format
        is inferred from the file extension. Supported extensions and their
        associated format are:

        * ``'.cbf'`` -- Conic Benchmark Format.

          This format is suitable for optimization problems involving second
          order and/or semidefinite cone constraints. This is a standard
          choice for conic optimization problems. Visit the website of
          `The Conic Benchmark Library <http://cblib.zib.de/>`_ or read
          `A benchmark library for conic mixed-integer and continuous
          optimization
          <http://www.optimization-online.org/DB_HTML/2014/03/4301.html>`_
          by Henrik A. Friberg for more information.

        * ``'.lp'`` -- `LP format
          <http://docs.mosek.com/6.0/pyapi/node022.html>`_.

          This format handles only linear constraints, unless the writer
          ``'cplex'`` is used. In the latter case the extended `CPLEX LP format
          <https://www.ibm.com/support/knowledgecenter/SSSA5P_12.10.0/
          ilog.odms.cplex.help/CPLEX/FileFormats/topics/LP.html>`_
          is used instead.

        * ``'.mps'`` -- `MPS format
          <http://docs.mosek.com/6.0/pyapi/node021.html>`_.

          As the writer, you need to choose one of ``'cplex'``, ``'gurobi'``
          or ``'mosek'``.

        * ``'.opf'`` -- `OPF format
          <http://docs.mosek.com/6.0/pyapi/node023.html>`_.

          As the writer, you need to choose ``'mosek'``.

        * ``'.dat-s'`` -- `Sparse SDPA format
          <http://sdpa.indsys.chuo-u.ac.jp/sdpa/download.html#sdpa>`_.

          This format is suitable for semidefinite programs. Second order
          cone constraints are stored as semidefinite constraints on an
          *arrow shaped* matrix.

    :param str writer: The default ``'picos'`` denotes PICOS' internal
        writer, which can export to *LP*, *CBF*, and *Sparse SDPA* formats.
        If CPLEX, Gurobi or MOSEK is installed, you can choose ``'cplex'``,
        ``'gurobi'``, or ``'mosek'``, respectively, to make use of that
        solver's export function and get access to more formats.

    .. warning::

        For problems involving a symmetric matrix variable :math:`X`
        (typically, semidefinite programs), the expressions involving
        :math:`X` are stored in PICOS as a function of :math:`svec(X)`, the
        symmetric vectorized form of :math:`X` (see `Dattorro, ch.2.2.2.1
        <http://meboo.convexoptimization.com/Meboo.html>`_),
        and are also exported in that form. As a result, using an external
        solver on a problem description file exported by PICOS will also
        yield a solution in this symmetric vectorized form.

        The CBF writer tries to write symmetric variables :math:`X` in the
        section ``PSDVAR`` of the .cbf file. However, this is possible only
        if the constraint :math:`X \succeq 0` appears in the problem, and no
        other LMI involves :math:`X`. If these two conditions are not
        satisfied, then the symmetric vectorization of :math:`X` is used as
        a (free) variable of the section ``VAR`` in the .cbf file, as
        explained in the previous paragraph.

    .. warning::

        This function is severly outdated and may fail or not function as
        advertised.
    """
    # HACK: This method abuses the internal problem representation of CVXOPT.
    # TODO: Add a proper method that transforms the problem into the canonical
    #       form required here, and, if applicable, make also CVXOPT use it.

    from .strategy import NoStrategyFound

    # We first prepare the problem so it becomes exportable
    COMMERCIAL_WRITERS = ['cplex', 'mosek', 'gurobi']
    if writer in COMMERCIAL_WRITERS:
        P = picos_problem.prepared(solver=writer)
    else:
        if picos_problem.continuous:
            # This should ensure that all quadratics have been cast as SOC
            # constraints.
            P = picos_problem.prepared(solver='cvxopt', assume_conic=True)
        else:
            try:
                P = picos_problem.prepared(solver=None)
            except NoStrategyFound:
                # FIXME: This will typically happen for integer linear programs
                # if no IP solver is available.
                raise NotImplementedError("You try to export a problem for "
                    "which no solver is available")

            assert 'Linear' in P.type, "only LINEAR integer problems can be " \
                "exported with the default writer"

    # TODO: Need proper way to detect exponential cone constraints. This catches
    #       only the Geometric Programs.
    if P.numberLSEConstraints:
        raise NotImplementedError("It is not possible (yet) to export GPs or "
            "problems with Exponential Cone Constraints")

    # automatic extension recognition
    if not (any(filename.endswith(ext) for ext in
                (".mps", ".opf", ".cbf", ".ptf", ".lp", ".dat-s"))):
        if writer == "gurobi":
            if (P.numberConeConstraints + P.numberQuadConstraints) == 0:
                filename += ".lp"
            else:
                filename += ".mps"
        elif writer == "cplex":
            filename += ".lp"
        elif writer == "mosek":
            if (P.numberConeConstraints + P.numberQuadConstraints
                    + P.numberSDPConstraints) == 0:
                filename += ".lp"
            elif P.numberQuadConstraints == 0:
                filename += ".cbf"
            else:
                filename += ".mps"
        elif writer == "picos":
            assert not P.numberQuadConstraints, \
                "Expected no quadratic constraints after call to 'prepared'."
            if (P.numberConeConstraints + P.numberSDPConstraints) == 0:
                filename += ".lp"
            elif P.numberConeConstraints == 0:
                filename += ".dat-s"
            else:
                filename += ".cbf"
        else:
            raise Exception("unexpected writer")

    if writer == "cplex":
        cpl = CPLEXSolver(P)
        cpl._load_problem()
        cpl.int.write(filename)

    elif writer == "mosek":
        if filename.endswith(".cbf"):
            # This ensures that all quadratics are converted to SOC
            P = picos_problem.prepared(solver='cvxopt')
        msk = MOSEKSolver(P)
        msk._load_problem()
        msk.int.writedata(filename)

    elif writer == "gurobi":
        grb = GurobiSolver(P)
        grb._load_problem()
        grb.int.write(filename)

    elif writer == "picos":
        if filename[-3:] == ".lp":
            _write_lp(P, filename)
        elif filename[-6:] == ".dat-s":
            _write_sdpa(P, filename)
        elif filename[-4:] == ".cbf":
            _write_cbf(P, filename)
        else:
            raise Exception("unexpected file extension")
    else:
        raise Exception("unknown writer")


def _write_lp(P, filename):
    """Write the problem to a file in LP format."""
    # add extension
    if filename[-3:] != ".lp":
        filename += ".lp"
    # check lp compatibility
    if (
            P.numberConeConstraints
            + P.numberQuadConstraints
            + P.numberLSEConstraints
            + P.numberSDPConstraints
    ) > 0:
        raise Exception("the picos LP writer only accepts (MI)LP")
    # open file
    f = open(filename, "w")
    f.write("\\* file " + filename + " generated by picos*\\\n")

    # HACK: This abuses the internal problem representation of CVXOPT.
    if P.continuous:
        localCvxoptInstance = CVXOPTSolver(P)
    else:
        localCvxoptInstance = CVXOPTSolver(P.continuous_relaxation())
    localCvxoptInstance.import_variable_bounds = False
    localCvxoptInstance._load_problem()
    cvxoptVars = localCvxoptInstance.internal_problem()

    # variable names
    varnames = {}
    i = 0
    for name, v in P.variables.items():
        # full vectorization is used, name variables as x, x[j] or x[j,k]
        if isinstance(v._vec, FullVectorization):
            for ind in range(v.dim):
                if v.size == (1, 1):
                    varnames[i] = name
                elif v.size[1] == 1:
                    varnames[i] = name + "(" + str(ind) + ")"
                else:
                    k, j = divmod(ind, v.size[0])
                    varnames[i] = name + "(" + str(j) + "," + str(k) + ")"
                varnames[i] = varnames[i].replace("[", "(")
                varnames[i] = varnames[i].replace("]", ")")
                i += 1
        else:
            for ind in range(v.dim):
                varnames[i] = 'vec_' + name + "(" + str(ind) + ")"
                i += 1

    # affexpr writer
    def affexp_writer(constraint_name, indices, coefs):
        s = ""
        s += constraint_name
        s += " : "
        start = True
        for (i, v) in zip(indices, coefs):
            if v > 0 and not (start):
                s += "+ "
            s += "%.12g" % v
            s += " "
            s += varnames[i]
            # not the first term anymore
            start = False
        if not (coefs):
            s += "0.0 "
            s += varnames[0]
        return s

    print("writing problem in " + filename + "...")

    # objective
    if P.objective.direction == "max":
        f.write("Maximize\n")
        # max handled directly
        cvxoptVars["c"] = -cvxoptVars["c"]
    else:
        f.write("Minimize\n")
    I = cvxopt.sparse(cvxoptVars["c"]).I
    V = cvxopt.sparse(cvxoptVars["c"]).V

    f.write(affexp_writer("obj", I, V))
    f.write("\n")

    f.write("Subject To\n")
    bounds = {}
    # equality constraints:
    Ai, Aj, Av = (cvxoptVars["A"].I, cvxoptVars["A"].J, cvxoptVars["A"].V)
    ijvs = sorted(zip(Ai, Aj, Av))
    del Ai, Aj, Av
    itojv = {}
    lasti = -1
    for (i, j, v) in ijvs:
        if i == lasti:
            itojv[i].append((j, v))
        else:
            lasti = i
            itojv[i] = [(j, v)]
    ieq = 0
    for i, jv in itojv.items():
        J = [jvk[0] for jvk in jv]
        V = [jvk[1] for jvk in jv]
        if len(J) == 1:
            # fixed variable
            b = cvxoptVars["b"][i] / V[0]
            bounds[J[0]] = (b, b)
        else:
            # affine equality
            b = cvxoptVars["b"][i]
            f.write(affexp_writer("eq" + str(ieq), J, V))
            f.write(" = ")
            f.write("%.12g" % b)
            f.write("\n")
            ieq += 1

    # inequality constraints:
    Gli, Glj, Glv = (cvxoptVars["Gl"].I, cvxoptVars["Gl"].J, cvxoptVars["Gl"].V)
    ijvs = sorted(zip(Gli, Glj, Glv))
    del Gli, Glj, Glv
    itojv = {}
    lasti = -1
    for (i, j, v) in ijvs:
        if i == lasti:
            itojv[i].append((j, v))
        else:
            lasti = i
            itojv[i] = [(j, v)]
    iaff = 0
    for i, jv in itojv.items():
        J = [jvk[0] for jvk in jv]
        V = [jvk[1] for jvk in jv]
        b = cvxoptVars["hl"][i]
        f.write(affexp_writer("in" + str(iaff), J, V))
        f.write(" <= ")
        f.write("%.12g" % b)
        f.write("\n")
        iaff += 1

    # variable bounds
    # retrieve as a dictionary {index -> (lo,up)}
    i_var = 0
    for var in P.variables.values():
        for ind, lo in var.bound_dicts[0].items():
            (current_lo, current_up) = bounds.get(
                i_var + ind, (-INFINITY, INFINITY))
            new_lo = max(lo, current_lo)
            bounds[i_var + ind] = (new_lo, current_up)
        for ind, up in var.bound_dicts[1].items():
            (current_lo, current_up) = bounds.get(
                i_var + ind, (-INFINITY, INFINITY))
            new_up = min(up, current_up)
            bounds[i_var + ind] = (current_lo, new_up)
        i_var += var.dim

    f.write("Bounds\n")
    for i in range(P.numberOfVars):
        if i in bounds:
            bl, bu = bounds[i]
        else:
            bl, bu = -INFINITY, INFINITY
        if bl == -INFINITY and bu == INFINITY:
            f.write(varnames[i] + " free")
        elif bl == bu:
            f.write(varnames[i] + (" = %.12g" % bl))
        elif bl < bu:
            if bl == -INFINITY:
                f.write("-inf <= ")
            else:
                f.write("%.12g" % bl)
                f.write(" <= ")
            f.write(varnames[i])
            if bu == INFINITY:
                f.write("<= +inf")
            else:
                f.write(" <= ")
                f.write("%.12g" % bu)
        f.write("\n")

    # general integers
    f.write("Generals\n")
    i_var = 0
    for name, v in P.variables.items():
        if isinstance(v, IntegerVariable):
            for ind in range(v.dim):
                f.write(varnames[i_var + ind] + "\n")
        i_var += v.dim

    # binary variables
    f.write("Binaries\n")
    i_var = 0
    for name, v in P.variables.items():
        if isinstance(v, BinaryVariable):
            for ind in range(v.dim):
                f.write(varnames[i_var + ind] + "\n")
        i_var += v.dim

    f.write("End\n")
    print("done.")
    f.close()


def _write_sdpa(P, filename):
    """Write the problem to a file in Sparse SDPA format."""
    # HACK: This abuses the internal problem representation of CVXOPT.
    localCvxoptInstance = CVXOPTSolver(P)
    localCvxoptInstance._load_problem()
    cvxoptVars = localCvxoptInstance.internal_problem()

    dims = {}
    dims["s"] = [int(numpy.sqrt(Gsi.size[0])) for Gsi in cvxoptVars["Gs"]]
    dims["l"] = cvxoptVars["Gl"].size[0]
    dims["q"] = [Gqi.size[0] for Gqi in cvxoptVars["Gq"]]
    G = cvxoptVars["Gl"]
    h = cvxoptVars["hl"]

    # handle the equalities as 2 ineq
    if cvxoptVars["A"].size[0] > 0:
        G = cvxopt.sparse([G, cvxoptVars["A"]])
        G = cvxopt.sparse([G, -cvxoptVars["A"]])
        h = cvxopt.matrix([h, cvxoptVars["b"]])
        h = cvxopt.matrix([h, -cvxoptVars["b"]])
        dims["l"] += 2 * cvxoptVars["A"].size[0]

    for i in range(len(dims["q"])):
        G = cvxopt.sparse([G, cvxoptVars["Gq"][i]])
        h = cvxopt.matrix([h, cvxoptVars["hq"][i]])

    for i in range(len(dims["s"])):
        G = cvxopt.sparse([G, cvxoptVars["Gs"][i]])
        h = cvxopt.matrix([h, cvxoptVars["hs"][i]])

    # Remove the lines in A and b corresponding to 0==0
    JP = list(set(cvxoptVars["A"].I))
    IP = range(len(JP))
    VP = [1] * len(JP)

    # is there a constraint of the form 0==a(a not 0) ?
    if any([b for (i, b) in enumerate(cvxoptVars["b"]) if i not in JP]):
        raise Exception("infeasible constraint of the form 0=a")

    from cvxopt import sparse, spmatrix

    PP = spmatrix(VP, IP, JP, (len(IP), cvxoptVars["A"].size[0]))
    cvxoptVars["A"] = PP * cvxoptVars["A"]
    cvxoptVars["b"] = PP * cvxoptVars["b"]
    c = cvxoptVars["c"]
    # ------------------------------------------------------------#
    # make A,B,and blockstruct.                                  #
    # This code is a modification of the conelp function in SMCP #
    # ------------------------------------------------------------#
    Nl = dims["l"]
    Nq = dims["q"]
    Ns = dims["s"]
    if not Nl:
        Nl = 0

    P_m = G.size[1]

    P_b = -c
    P_blockstruct = []
    if Nl:
        P_blockstruct.append(-Nl)
    for i in Nq:
        P_blockstruct.append(i)
    for i in Ns:
        P_blockstruct.append(i)

    # write data
    # add extension
    if filename[-6:] != ".dat-s":
        filename += ".dat-s"

    # open file
    f = open(filename, "w")
    f.write('"file ' + filename + ' generated by picos"\n')
    if P.options.verbosity >= 1:
        print("writing problem in " + filename + "...")
    f.write(str(P.numberOfVars) + " = number of vars\n")
    f.write(str(len(P_blockstruct)) + " = number of blocs\n")
    # bloc structure
    f.write(str(P_blockstruct).replace("[", "(").replace("]", ")"))
    f.write(" = BlocStructure\n")
    # c vector (objective)
    f.write(str(list(-P_b)).replace("[", "{").replace("]", "}"))
    f.write("\n")
    # coefs
    for k in range(P_m + 1):
        if k != 0:
            v = sparse(G[:, k - 1])
        else:
            v = +sparse(h)

        ptr = 0
        block = 0
        # lin. constraints
        if Nl:
            u = v[:Nl]
            for i, j, value in zip(u.I, u.I, u.V):
                f.write(
                    "{0}\t{1}\t{2}\t{3}\t{4}\n".format(
                        k, block + 1, j + 1, i + 1, -value
                    )
                )
            ptr += Nl
            block += 1

        # SOC constraints
        for nq in Nq:
            u0 = v[ptr]
            u1 = v[ptr + 1:ptr + nq]
            tmp = spmatrix(
                u1.V, [nq - 1 for j in range(len(u1))], u1.I, (nq, nq)
            )
            if not u0 == 0.0:
                tmp += spmatrix(u0, range(nq), range(nq), (nq, nq))
            for i, j, value in zip(tmp.I, tmp.J, tmp.V):
                f.write(
                    "{0}\t{1}\t{2}\t{3}\t{4}\n".format(
                        k, block + 1, j + 1, i + 1, -value
                    )
                )
            ptr += nq
            block += 1

        # SDP constraints
        for ns in Ns:
            u = v[ptr:ptr + ns ** 2]
            for index_k, index in enumerate(u.I):
                j, i = divmod(index, ns)
                if j <= i:
                    f.write(
                        "{0}\t{1}\t{2}\t{3}\t{4}\n".format(
                            k, block + 1, j + 1, i + 1, -u.V[index_k]
                        )
                    )
            ptr += ns ** 2
            block += 1

    f.close()


# This is an ugly function from a former version of picos
# It separates a linear constraint between 'plain' vars and matrix 'bar'
# variables J and V denote the sparse indices/values of the constraints for the
# whole (s-)vectorized vector (without offset)
def _separate_linear_cons_plain_bar_vars(J, V, idx_sdp_vars):
    # sparse values of the constraint for 'plain' variables
    jj = []
    vv = []
    # sparse values of the constraint for the next svec bar variable
    js = []
    vs = []
    mats = []
    offset = 0
    if idx_sdp_vars:
        idxsdpvars = [ti for ti in idx_sdp_vars]
        nextsdp = idxsdpvars.pop()
    else:
        return J, V, []
    for (j, v) in zip(J, V):
        if j < nextsdp[0]:
            jj.append(j - offset)
            vv.append(v)
        elif j < nextsdp[1]:
            js.append(j - nextsdp[0])
            vs.append(v)
        else:
            while j >= nextsdp[1]:
                mats.append(
                    devectorize(
                        cvxopt.spmatrix(
                            vs,
                            js,
                            [0] * len(js),
                            (nextsdp[1] - nextsdp[0], 1)
                        )).T)
                js = []
                vs = []
                offset += (nextsdp[1] - nextsdp[0])
                try:
                    nextsdp = idxsdpvars.pop()
                except IndexError:
                    nextsdp = (float('inf'), float('inf'))
            if j < nextsdp[0]:
                jj.append(j - offset)
                vv.append(v)
            elif j < nextsdp[1]:
                js.append(j - nextsdp[0])
                vs.append(v)
    while len(mats) < len(idx_sdp_vars):
        mats.append(
            devectorize(
                cvxopt.spmatrix(
                    vs,
                    js,
                    [0] * len(js),
                    (nextsdp[1] - nextsdp[0], 1)
                )).T)
        js = []
        vs = []
        nextsdp = (0, 1)  # doesnt matter, it will be an empt matrix anyway
    return jj, vv, mats


def devectorize(vec):
    """Create a matrix from a symmetric vectorization."""
    from ..expressions.vectorizations import SymmetricVectorization
    v = vec.size[0]
    n = int((1 + 8 * v) ** 0.5 - 1) // 2
    return SymmetricVectorization((n, n)).devectorize(vec)


def _write_cbf(P, filename, uptri=False):
    """Write the problem to a file in Sparse SDPA format.

    :param bool uptri: Whether upper triangular elements of symmetric
        matrices are specified.
    """
    # write data
    # add extension
    if filename[-4:] != ".cbf":
        filename += ".cbf"

    # parse variables
    semidef_vars = set()
    for cons in P.constraints:
        cs = P.constraints[cons]
        if isinstance(cs, LMIConstraint):
            if cs.semidefVar:
                semidef_vars.add(cs.semidefVar)

    NUMVAR_SCALAR = int(
        sum(
            [var.dim
             for var in P.variables.values()
             if var not in semidef_vars]
        )
    )

    ind = 0
    indices = []
    start_indices = {}
    for v in P.variables.values():
        indices.append((ind, ind + v.dim, v))
        start_indices[v] = ind
        ind += v.dim

    indices = sorted(indices)
    idxsdpvars = [
        (si, ei) for (si, ei, v) in indices[::-1] if v in semidef_vars]
    # search if some semidef vars are implied in other semidef constraints
    PSD_not_handled = []
    for c in P.constraints.values():
        if isinstance(c, LMIConstraint) and c not in semidef_vars:
            for v in (c.lhs - c.rhs)._coefs:
                if v in semidef_vars:
                    start_index = start_indices[v]
                    idx = (start_index, start_index + v.dim)
                    if idx in idxsdpvars:
                        PSD_not_handled.append(v)
                        NUMVAR_SCALAR += idx[1] - idx[0]
                        idxsdpvars.remove(idx)

    barvars = bool(idxsdpvars)

    # find integer variables, retrieve bounds and put 0-1 bounds on binaries
    ints = []
    ind = 0
    varbounds_lo = {}
    varbounds_up = {}

    for k, var in P.variables.items():
        if isinstance(var, BinaryVariable):
            for relind, absind in enumerate(range(ind, ind + var.dim)):
                ints.append(absind)
                clb = var.bound_dicts[0].get(relind, -INFINITY)
                cub = var.bound_dicts[1].get(relind, INFINITY)
                varbounds_lo[absind] = max(0.0, clb)
                varbounds_up[absind] = min(1.0, cub)

        elif (isinstance(var, IntegerVariable) or
              isinstance(var, CONTINUOUS_VARTYPES)):
            for relind, absind in enumerate(range(ind, ind + var.dim)):
                if isinstance(var, IntegerVariable):
                    ints.append(absind)
                clb = var.bound_dicts[0].get(relind, -INFINITY)
                cub = var.bound_dicts[1].get(relind, INFINITY)
                varbounds_lo[absind] = clb
                varbounds_up[absind] = cub

        else:
            raise Exception("variable type not handled by _write_cbf()")
        ind += var.dim

    if barvars:
        ints, _, mats = _separate_linear_cons_plain_bar_vars(
            ints, [0.0] * len(ints), idxsdpvars
        )
        if any([bool(mat) for mat in mats]):
            raise Exception(
                "semidef vars with integer elements are not supported"
            )

    # open file
    f = open(filename, "w")
    f.write("#file " + filename + " generated by picos\n")
    print("writing problem in " + filename + "...")

    f.write("VER\n")
    f.write("1\n\n")

    f.write("OBJSENSE\n")
    if P.objective.direction == "max":
        f.write("MAX\n\n")
    else:
        f.write("MIN\n\n")

    # VARIABLEs

    if barvars:
        f.write("PSDVAR\n")
        f.write(str(len(idxsdpvars)) + "\n")
        for si, ei in idxsdpvars:
            ni = int(((8 * (ei - si) + 1) ** 0.5 - 1) / 2.0)
            f.write(str(ni) + "\n")
        f.write("\n")

    # bounds
    cones = []
    conecons = []
    Acoord = []
    Bcoord = []
    iaff = 0
    offset = 0
    for si, ei, v in indices:
        if v in semidef_vars and not (v in PSD_not_handled):
            offset += ei - si
        else:
            if all(varbounds_lo[ind] == 0 for ind in range(si, ei)):
                cones.append(("L+", ei - si))
            elif all(varbounds_up[ind] == 0 for ind in range(si, ei)):
                cones.append(("L-", ei - si))
            else:
                cones.append(("F", ei - si))
            if any(varbounds_lo[ind] != 0 for ind in range(si, ei)):
                for ind in range(si, ei):
                    l = varbounds_lo[ind]
                    if l - 1 > -INFINITY:
                        Acoord.append((iaff, ind - offset, 1.0))
                        Bcoord.append((iaff, -l))
                        iaff += 1
            if any(varbounds_up[ind] != 0 for ind in range(si, ei)):
                for ind in range(si, ei):
                    u = varbounds_up[ind]
                    if u + 1 < INFINITY:
                        Acoord.append((iaff, ind - offset, -1.0))
                        Bcoord.append((iaff, u))
                        iaff += 1
    if iaff:
        conecons.append(("L+", iaff))

    f.write("VAR\n")
    f.write(str(NUMVAR_SCALAR) + " " + str(len(cones)) + "\n")
    for tp, n in cones:
        f.write(tp + " " + str(n) + "\n")

    f.write("\n")

    # integers
    if ints:
        f.write("INT\n")
        f.write(str(len(ints)) + "\n")
        for i in ints:
            f.write(str(i) + "\n")
        f.write("\n")

    # constraints
    psdcons = []
    isdp = 0
    Fcoord = []
    Hcoord = []
    Dcoord = []
    ObjAcoord = []
    ObjBcoord = []
    ObjFcoord = []

    # dummy constraint for the objective
    dummy_cons = P.objective.function >= 0
    setattr(dummy_cons, "dummycon", None)

    for cons in chain((dummy_cons,), P.constraints.values()):
        if isinstance(cons, LMIConstraint):
            v = cons.semidefVar
            if v is not None and v not in PSD_not_handled:
                continue

        # get sparse indices
        if isinstance(cons, AffineConstraint):
            expcone = cons.lhs - cons.rhs
            if hasattr(cons, "dummycon"):
                conetype = "0"  # Dummy type for the objective function.
            elif cons.is_equality():
                conetype = "L="
            elif cons.is_increasing():
                conetype = "L-"
            elif cons.is_decreasing():
                conetype = "L+"
            else:
                assert False, "Unexpected constraint relation."
        elif isinstance(cons, SOCConstraint):
            expcone = (cons.ub) // (cons.ne[:])
            conetype = "Q"
        elif isinstance(cons, RSOCConstraint):
            expcone = (cons.ub1) // (0.5 * cons.ub2) // (cons.ne[:])
            conetype = "QR"
        elif isinstance(cons, LMIConstraint):
            if cons.is_increasing():
                expcone = cons.rhs - cons.lhs
                conetype = None
            elif cons.is_decreasing():
                expcone = cons.lhs - cons.rhs
                conetype = None
            else:
                assert False, "Unexpected constraint relation."
        else:
            assert False, "Unexpected constraint type."

        ijv = []
        for var, fact in expcone._coefs.items():
            if not isinstance(fact, cvxopt.base.spmatrix):
                fact = cvxopt.sparse(fact)
            sj = start_indices[var]
            ijv.extend(zip(fact.I, fact.J + sj, fact.V))
        ijvs = sorted(ijv)

        itojv = {}
        lasti = -1
        for (i, j, v) in ijvs:
            if i == lasti:
                itojv[i].append((j, v))
            else:
                lasti = i
                itojv[i] = [(j, v)]

        if conetype:
            if conetype != "0":
                dim = expcone.size[0] * expcone.size[1]
                conecons.append((conetype, dim))
        else:
            dim = expcone.size[0]
            psdcons.append(dim)

        if conetype:
            for i, jv in itojv.items():
                J = [jvk[0] for jvk in jv]
                V = [jvk[1] for jvk in jv]
                J, V, mats = _separate_linear_cons_plain_bar_vars(
                    J, V, idxsdpvars)
                for j, v in zip(J, V):
                    if conetype != "0":
                        Acoord.append((iaff + i, j, v))
                    else:
                        ObjAcoord.append((j, v))
                for k, mat in enumerate(mats):
                    for row, col, v in zip(mat.I, mat.J, mat.V):
                        if conetype != "0":
                            Fcoord.append((iaff + i, k, row, col, v))
                        else:
                            ObjFcoord.append((k, row, col, v))
                        if uptri and row != col:
                            if conetype != "0":
                                Fcoord.append((iaff + i, k, col, row, v))
                            else:
                                ObjFcoord.append((k, col, row, v))
            constant = expcone._const
            if not (constant is None):
                constant = cvxopt.sparse(constant)
                for i, v in zip(constant.I, constant.V):
                    if conetype != "0":
                        Bcoord.append((iaff + i, v))
                    else:
                        ObjBcoord.append(v)
        else:
            for i, jv in itojv.items():
                col, row = divmod(i, dim)
                if not (uptri) and row < col:
                    continue
                J = [jvk[0] for jvk in jv]
                V = [jvk[1] for jvk in jv]
                J, V, mats = _separate_linear_cons_plain_bar_vars(
                    J, V, idxsdpvars)
                if any([bool(m) for m in mats]):
                    raise Exception("SDP cons should not depend on PSD var")
                for j, v in zip(J, V):
                    Hcoord.append((isdp, j, row, col, v))

            constant = expcone._const
            if not (constant is None):
                constant = cvxopt.sparse(constant)
                for i, v in zip(constant.I, constant.V):
                    col, row = divmod(i, dim)
                    if row < col:
                        continue
                    Dcoord.append((isdp, row, col, v))

        if conetype:
            if conetype != "0":
                iaff += dim
        else:
            isdp += 1

    if iaff > 0:
        f.write("CON\n")
        f.write(str(iaff) + " " + str(len(conecons)) + "\n")
        for tp, n in conecons:
            f.write(tp + " " + str(n))
            f.write("\n")

        f.write("\n")

    if isdp > 0:
        f.write("PSDCON\n")
        f.write(str(isdp) + "\n")
        for n in psdcons:
            f.write(str(n) + "\n")
        f.write("\n")

    if ObjFcoord:
        f.write("OBJFCOORD\n")
        f.write(str(len(ObjFcoord)) + "\n")
        for (k, row, col, v) in ObjFcoord:
            f.write("{0} {1} {2} {3}\n".format(k, row, col, v))
        f.write("\n")

    if ObjAcoord:
        f.write("OBJACOORD\n")
        f.write(str(len(ObjAcoord)) + "\n")
        for (j, v) in ObjAcoord:
            f.write("{0} {1}\n".format(j, v))
        f.write("\n")

    if ObjBcoord:
        f.write("OBJBCOORD\n")
        v = ObjBcoord[0]
        f.write("{0}\n".format(v))
        f.write("\n")

    if Fcoord:
        f.write("FCOORD\n")
        f.write(str(len(Fcoord)) + "\n")
        for (i, k, row, col, v) in Fcoord:
            f.write("{0} {1} {2} {3} {4}\n".format(i, k, row, col, v))
        f.write("\n")

    if Acoord:
        f.write("ACOORD\n")
        f.write(str(len(Acoord)) + "\n")
        for (i, j, v) in Acoord:
            f.write("{0} {1} {2}\n".format(i, j, v))
        f.write("\n")

    if Bcoord:
        f.write("BCOORD\n")
        f.write(str(len(Bcoord)) + "\n")
        for (i, v) in Bcoord:
            f.write("{0} {1}\n".format(i, v))
        f.write("\n")

    if Hcoord:
        f.write("HCOORD\n")
        f.write(str(len(Hcoord)) + "\n")
        for (i, j, row, col, v) in Hcoord:
            f.write("{0} {1} {2} {3} {4}\n".format(i, j, row, col, v))
        f.write("\n")

    if Dcoord:
        f.write("DCOORD\n")
        f.write(str(len(Dcoord)) + "\n")
        for (i, row, col, v) in Dcoord:
            f.write("{0} {1} {2} {3}\n".format(i, row, col, v))
        f.write("\n")

    print("done.")
    f.close()


# --------------------------------------
__all__ = api_end(_API_START, globals())
