# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# http://www.apache.org/licenses/
# (c) Copyright IBM Corp. 2015, 2016
# --------------------------------------------------------------------------

import os
import time

# docplex
from docplex.mp.model import Model
from docplex.mp.utils import DOcplexException, MockIterable
from docplex.mp.environment import Environment

from docplex.mp.params.cplex_params import get_params_from_cplex_version
from docplex.mp.constants import ComparisonType
from docplex.mp.constr import LinearConstraint

from docplex.mp.cplex_adapter import CplexAdapter
from docplex.mp.cplex_engine import CplexEngine

from docplex.mp.quad import VarPair


class ModelReaderError(DOcplexException):
    pass


class _CplexReaderFileContext(object):
    def __init__(self, filename, read_method=None):
        self._cplex = None
        self._filename = filename
        self._read_method = read_method or ["read"]

    def __enter__(self):
        self.cpx_adapter = CplexAdapter()
        cpx = self.cpx_adapter.cpx
        # no output from CPLEX
        cpx.set_results_stream(None)
        cpx.set_log_stream(None)
        cpx.set_warning_stream(None)
        cpx.set_error_stream(None)
        self_read_fn = cpx
        for m in self._read_method:
            self_read_fn = self_read_fn.__getattribute__(m)

        try:
            self_read_fn(self._filename)
            self._cplex = cpx
            return self.cpx_adapter

        except self.cpx_adapter.CplexError as cpx_e:  # pragma: no cover
            # delete cplex instance
            del cpx
            raise ModelReaderError("*CPLEX error {0!s} reading file {1} - exiting".format(cpx_e, self._filename))

    # noinspection PyUnusedLocal
    def __exit__(self, exc_type, exc_val, exc_tb):
        cpx = self._cplex
        if cpx is not None:
            del cpx
            self._cplex = None


# def compute_full_dotf(mdl, coefs, constant=0):
#     def coef_fn(dvx):
#         return coefs[dvx]
#
#     lfactory = mdl._lfactory
#     terms_dict = lfactory._new_term_dict()
#     for dv in mdl.iter_variables():
#         coef = coef_fn(dv.index)
#         if coef:
#             terms_dict[dv] = coef
#     linear_expr = lfactory.linear_expr(terms_dict, constant=constant, safe=True)
#     return linear_expr


def compute_full_dot(mdl, coefs, constant=0):
    # compute a scalar product, with possible zeros as coefs
    lfactory = mdl._lfactory
    terms_dict = lfactory._new_term_dict()
    for dv, k in zip(mdl.iter_variables(), coefs):
        if k:
            terms_dict[dv] = k
    linear_expr = lfactory.linear_expr(terms_dict, constant=constant, safe=True)
    return linear_expr


# noinspection PyArgumentList
class ModelReader(object):
    """ This class is used to read models from CPLEX files (e.g.  SAV, LP, MPS)
    This class requires CPLEX runtime, otherwise an exception is raised.

    Example:
        Use class method ``read`` to read a model file.

        >>> m = ModelReader.read('mymodel.lp', ignore_names=True)

        reads a model while ignoring all names.

        Global function ``read_model`` is a synonym for ``ModelReader.read``,
        and is the preferred way to read model files.

    """

    @staticmethod
    def _build_linear_expr_from_sparse_pair(lfactory, var_map, cpx_sparsepair):
        terms = {var_map[ix]: k for ix, k in zip(cpx_sparsepair.ind, cpx_sparsepair.val)}
        expr = lfactory.linear_expr(arg=terms, safe=True)
        return expr

    _sense2comp_dict = {'L': ComparisonType.LE, 'E': ComparisonType.EQ, 'G': ComparisonType.GE}

    # noinspection PyDefaultArgument
    @classmethod
    def parse_sense(cls, cpx_sense, sense_dict=_sense2comp_dict):
        return sense_dict.get(cpx_sense)

    @classmethod
    def read_prm(cls, filename):
        """ Reads a CPLEX PRM file.

        Reads a CPLEX parameters file and returns a DOcplex parameter group
        instance. This parameter object can be used in a solve().

        Args:
            filename: a path string

        Returns:
            A `RootParameterGroup object`, if the read operation succeeds, else None.
        """
        # TODO: Clean up - now creating an adapter raise importError if CPLEX not found
        # if not Cplex:  # pragma: no cover
        #    raise RuntimeError("ModelReader.read_prm() requires CPLEX runtime.")
        with _CplexReaderFileContext(filename, read_method=["parameters", "read_file"]) as adapter:
            cpx = adapter.cpx
            if cpx:
                # raw parameters
                params = get_params_from_cplex_version(cpx.get_version())
                for param in params:
                    try:
                        cpx_value = cpx._env.parameters._get(param.cpx_id)
                        if cpx_value != param.default_value:
                            param.set(cpx_value)

                    except adapter.CplexError:  # pragma: no cover
                        pass
                return params
            else:  # pragma: no cover
                return None

    @staticmethod
    def _safe_call_get_names(cpx_adapter, get_names_fn, fallback_names=None):
        # cplex crashes when calling get_names on some files (e.g. SAV)
        # in this case filter out error 1219
        # and return a fallback list with None or ""
        try:
            names = get_names_fn()
            return names
        # except TypeError:
        #     print("** type error ignored in call to {0}".format(get_names_fn.__name__))
        #     return fallback_names or []

        except cpx_adapter.CplexSolverError as cpxse:  # pragma: no cover
            errcode = cpxse.args[2]
            # when all indicators have no names, cplex raises this error
            # CPLEX Error  1219: No names exist.
            if errcode == 1219:
                return fallback_names or []
            else:
                # this is something else
                raise

    @classmethod
    def _read_cplex(cls, filename, datacheck=None, silent=True):
        cpx_adapter = CplexAdapter()
        cpx = cpx_adapter.cpx
        # no warnings
        if silent:
            cpx.set_results_stream(None)
            cpx.set_log_stream(None)
            cpx.set_warning_stream(None)
            cpx.set_error_stream(None)  # remove messages about names
        try:
            if datacheck is not None:
                assert datacheck in {0,1,2}
                cpx.parameters.read.datacheck.set(datacheck)
            t0 = time.time()
            cpx.read(filename)
            t1 = time.time() - t0
            if Model.is_docplex_debug() and t1 >= 0.1:
                print(f"-- cplex read time={t1:.1f}s")
            return cpx_adapter
        except cpx_adapter.CplexError as cpx_e:
            raise ModelReaderError("*CPLEX error {0!s} reading file {1} - exiting".format(cpx_e, filename))

    @classmethod
    def _make_expr_from_varmap_coefs_dict(cls, lfactory, varmap, var_indices, coefs, offset=0):
        terms_dict = {varmap.get(dvx): k for dvx, k in zip(var_indices, coefs)}
        return lfactory.linear_expr(arg=terms_dict, constant=offset, safe=True)

    @classmethod
    def _make_expr_from_var_coef_seq_dict(cls, lfactory, varmap, var_coefs, constant=0):
        terms_dict = {varmap.get(dvx): k for dvx, k in var_coefs}
        return lfactory.linear_expr(arg=terms_dict, constant=constant, safe=True)

    @classmethod
    def _make_expr_from_varmap_coefs_nodict(cls, lfactory, varmap, var_indices, coefs, offset=0):
        terms_dict = lfactory._new_term_dict()
        for dvx, k in zip(var_indices, coefs):
            dv = varmap.get(dvx)
            if dv is not None:
                terms_dict[dv] = k
        return lfactory.linear_expr(arg=terms_dict, constant=offset, safe=True)

    @classmethod
    def _make_expr_from_var_coef_seq_nodict(cls, lfactory, varmap, var_coefs, constant=0):
        terms_dict = lfactory._new_term_dict()
        for dvx, k in var_coefs:
            dv = varmap.get(dvx)
            if dv is not None:
                terms_dict[dv] = k
        return lfactory.linear_expr(arg=terms_dict, constant=constant, safe=True)

    @classmethod
    def _make_expr_from_varmap_coefs(cls, lfactory, varmap, var_indices, coefs, offset=0):
        if Environment.env_is_python36:
            terms_dict = {varmap.get(dvx): k for dvx, k in zip(var_indices, coefs)}
        else:
            terms_dict = lfactory._new_term_dict()
            for dvx, k in zip(var_indices, coefs):
                dv = varmap.get(dvx)
                if dv is not None:
                    terms_dict[dv] = k
        return lfactory.linear_expr(arg=terms_dict, constant=offset, safe=True)

    @classmethod
    def read(cls, filename, model_name=None, verbose=False, model_class=None, **kwargs):
        """ Reads a model from a CPLEX export file.

        Accepts all formats exported by CPLEX: LP, SAV, MPS.

        If an error occurs while reading the file, the message of the exception
        is printed and the function returns None.

        Args:
            filename: The file to read.
            model_name: An optional name for the newly created model. If None,
                the model name will be the path basename.
            verbose: An optional flag to print informative messages, default is False.
            model_class: An optional class type; must be a subclass of Model.
                The returned model is built using this model_class and the keyword arguments kwargs, if any.
                By default, the model class is :class:`docplex.mp.model.Model`.
            kwargs: A dict of keyword-based arguments that are used when creating the modelpassed to the
                model constructor.

        Example:
            `m = read_model("c:/temp/foo.mps", model_name="docplex_foo", ignore_names=True)`

            reads a ,odel from file "c:/temp/foo.mps", sets its name to "docplex_foo", and discard all names.

        Returns:
            An instance of Model, or None if an exception is raised.

        See Also:
            :class:`docplex.mp.model.Model`

        """
        if not os.path.exists(filename):
            raise IOError("* file not found: {0}".format(filename))

        # extract basename
        if model_name:
            name_to_use = model_name
        else:
            basename = os.path.basename(filename)
            if '.' not in filename:  # pragma: no cover
                raise RuntimeError('ModelReader.read_model(): path has no extension: {}'.format(filename))
            dotpos = basename.find(".")
            if dotpos > 0:
                name_to_use = basename[:dotpos]
            else:  # pragma: no cover
                name_to_use = basename

        model_class = model_class or Model

        if 0 == os.stat(filename).st_size:
            print("* file is empty: {0} - exiting".format(filename))
            return model_class(name=name_to_use, **kwargs)

        data_check = kwargs.pop('datacheck', None)
        if verbose:
            print("-> CPLEX starts reading file: {0}".format(filename))
        cpx_adapter = cls._read_cplex(filename, datacheck=data_check)
        cpx = cpx_adapter.cpx
        if verbose:
            print("<- CPLEX finished reading file: {0}".format(filename))

        if not cpx:  # pragma: no cover
            return None

        final_output_level = kwargs.get("output_level", "info")
        debug_read = kwargs.get("debug", False)
        final_agent = kwargs.get('agent', 'cplex')
        use_new = kwargs.pop('use_new', True)


        try:
            # force no tck
            if 'checker' in kwargs:
                final_checker = kwargs['checker']
            else:
                final_checker = 'default'
            # build the model with no checker, then restore final_checker in the end.
            kwargs['checker'] = 'off'
            kwargs['agent'] = 'zero'

            ignore_names = kwargs.get('ignore_names', False)
            # -------------
            if Environment.env_is_python36:
                make_terms_fn = cls._make_expr_from_varmap_coefs_dict
                make_terms_seq_fn = cls._make_expr_from_var_coef_seq_dict
            else:
                make_terms_fn = cls._make_expr_from_varmap_coefs_nodict
                make_terms_seq_fn = cls._make_expr_from_var_coef_seq_nodict

            mdl = model_class(name=name_to_use, **kwargs)
            if data_check is not None:
                mdl.parameters.read.datacheck = data_check
            mdl._provenance = filename
            lfactory = mdl._lfactory
            qfactory = mdl._qfactory
            mdl.set_quiet()  # output level set to ERROR
            vartype_cont = mdl.continuous_vartype
            vartype_map = {'B': mdl.binary_vartype,
                           'I': mdl.integer_vartype,
                           'C': mdl.continuous_vartype,
                           'S': mdl.semicontinuous_vartype}

            def cpx_type_to_docplex_type(cpxt):
                return vartype_map.get(cpxt, vartype_cont)

            # 1 upload variables
            cpx_nb_vars = cpx.variables.get_num()

            if verbose:
                print("-- uploading {0} variables...".format(cpx_nb_vars))

            cpx_env_e = cpx._env._e
            cpx_lp = cpx._lp
            has_non_default_lb = cpx_adapter.has_non_default_lb
            has_non_default_ub = cpx_adapter.has_non_default_ub
            has_name = cpx_adapter.has_name

            cpx_var_names = []
            if not ignore_names:
                if not has_name or has_name(cpx_env_e, cpx_lp, 0, cpx_nb_vars - 1):
                    cpx_var_names = cls._safe_call_get_names(cpx_adapter, cpx.variables.get_names)

            # check whether all varianbles have same type, it's worth it.
            unique_type = None
            docplex_vartypes = None
            if cpx._is_MIP():
                cpx_types = cpx.variables.get_types()
                first_cpxtype = cpx_types[0]
                if all(cpxt == first_cpxtype for cpxt in cpx_types):
                    unique_type = cpx_type_to_docplex_type(first_cpxtype)
                else:
                    # docplex_vartypes = [cpx_type_to_docplex_type(cpxt) for cpxt in cpx_types]
                    def kth_vartype(k):
                        return cpx_type_to_docplex_type(cpx_types[k])
                    docplex_vartypes = MockIterable(size=cpx_nb_vars, fn_k=kth_vartype)
            else:
                unique_type = vartype_cont

            if use_new and has_non_default_lb and not has_non_default_lb(cpx_env_e, cpx_lp, 0, cpx_nb_vars - 1):
                cpx_var_lbs = []
            else:
                # either we have no fast predicate or we dont use it.
                cpx_var_lbs = cpx.variables.get_lower_bounds()
                # we have no predicate, so we might try to avoid building a large list??
                if not has_non_default_lb and not any(cpx_var_lbs):  # all equal 0
                    cpx_var_lbs = []

            if use_new and has_non_default_ub and not has_non_default_ub(cpx_env_e, cpx_lp, 0, cpx_nb_vars - 1):
                cpx_var_ubs = []
            else:
                cpx_var_ubs = cpx.variables.get_upper_bounds()

                if not has_non_default_ub and all(cpxu >= 1e+20 for cpxu in cpx_var_ubs):
                    cpx_var_ubs = []

            # if no names, None is fine.
            model_varnames = cpx_var_names or None

            model_lbs = cpx_var_lbs
            model_ubs = cpx_var_ubs

            # vars
            if unique_type:
                model_vars = lfactory.new_var_list(var_container=None,
                                                   key_seq=range(cpx_nb_vars),
                                                   vartype=unique_type,
                                                   lb=model_lbs,
                                                   ub=model_ubs,
                                                   name=model_varnames,
                                                   _safe_bounds=True,
                                                   _safe_names=True
                                                   )
            else:
                assert docplex_vartypes
                model_vars = lfactory.new_multitype_var_list(cpx_nb_vars,
                                                             docplex_vartypes,
                                                             model_lbs,
                                                             model_ubs,
                                                             model_varnames)

            # inverse map from indices to docplex vars
            cpx_var_index_to_docplex = {v: model_vars[v] for v in range(cpx_nb_vars)}

            # 2. upload linear constraints and ranges (mixed in cplex)
            cpx_linearcts = cpx.linear_constraints
            nb_linear_cts = cpx_linearcts.get_num()

            all_rhs = cpx_linearcts.get_rhs()
            all_senses = cpx_adapter.getsense(cpx_env_e, cpx_lp, 0, nb_linear_cts - 1)
            if 'R' in all_senses:
                all_range_values = cpx_linearcts.get_range_values()
            else:
                # do not query ranges if no R in senses...
                all_range_values = []
            cpx_ctnames = [] if ignore_names else cls._safe_call_get_names(cpx_adapter,
                                                                           cpx_linearcts.get_names)

            def make_constant_expr(k):
                return lfactory.constant_expr(k, safe_number=True)

            if verbose:
                print("-- uploading {0} linear constraints...".format(nb_linear_cts))

            def upload_rows(row_iter):
                cts_ = []
                for c, (indices, coefs) in enumerate(row_iter):
                    sense = all_senses[c]
                    rhs = all_rhs[c]

                    ctname = cpx_ctnames[c] if cpx_ctnames else None

                    expr = make_terms_fn(lfactory, cpx_var_index_to_docplex, indices, coefs)

                    if sense == 'R':
                        # no use of querying range vars if no R constraints
                        range_val = all_range_values[c]
                        # rangeval can be negative !!! issue 52
                        if range_val >= 0:
                            range_lb = rhs
                            range_ub = rhs + range_val
                        else:
                            range_ub = rhs
                            range_lb = rhs + range_val

                        rgct = lfactory.new_range_constraint(lb=range_lb, ub=range_ub, expr=expr, name=ctname,
                                                             check_feasible=False)
                        cts_.append(rgct)
                    else:
                        op = cls.parse_sense(sense)
                        rhs_expr = make_constant_expr(rhs)

                        ct = LinearConstraint(mdl, expr, op, rhs_expr, ctname)
                        cts_.append(ct)

                return cts_
            if nb_linear_cts:
                fast_get_rows_tuple = cpx_adapter.fast_get_row_tuples
                if fast_get_rows_tuple:
                    with fast_get_rows_tuple(cpx_env_e, cpx_lp) as rowgen:
                        deferred_cts = upload_rows(rowgen)
                else:
                    all_rows = cpx_adapter.fast_get_rows(cpx)
                    deferred_cts = upload_rows(all_rows)
                # add constraint as a block
                lfactory._post_constraint_block(posted_cts=deferred_cts)

            # 3. upload Quadratic constraints
            cpx_quadraticcts = cpx.quadratic_constraints
            nb_quadratic_cts = cpx_quadraticcts.get_num()
            if nb_quadratic_cts:
                all_rhs = cpx_quadraticcts.get_rhs()
                all_linear_nb_non_zeros = cpx_quadraticcts.get_linear_num_nonzeros()
                all_linear_components = cpx_quadraticcts.get_linear_components()
                all_quadratic_nb_non_zeros = cpx_quadraticcts.get_quad_num_nonzeros()
                all_quadratic_components = cpx_quadraticcts.get_quadratic_components()
                all_senses = cpx_quadraticcts.get_senses()
                cpx_ctnames = [] if ignore_names else cls._safe_call_get_names(cpx_adapter,
                                                                               cpx_quadraticcts.get_names)

                for c in range(nb_quadratic_cts):
                    rhs = all_rhs[c]
                    linear_nb_non_zeros = all_linear_nb_non_zeros[c]
                    linear_component = all_linear_components[c]
                    quadratic_nb_non_zeros = all_quadratic_nb_non_zeros[c]
                    quadratic_component = all_quadratic_components[c]
                    sense = all_senses[c]
                    ctname = cpx_ctnames[c] if cpx_ctnames else None

                    if linear_nb_non_zeros > 0:
                        indices, coefs = linear_component.unpack()
                        # linexpr = mdl._aggregator._scal_prod((cpx_var_index_to_docplex[idx] for idx in indices), coefs)
                        linexpr = make_terms_fn(lfactory, cpx_var_index_to_docplex, indices, coefs)
                    else:
                        linexpr = None

                    if quadratic_nb_non_zeros > 0:
                        qfactory = mdl._qfactory
                        ind1, ind2, coefs = quadratic_component.unpack()
                        quads = qfactory.term_dict_type()
                        for idx1, idx2, coef in zip(ind1, ind2, coefs):
                            quads[VarPair(cpx_var_index_to_docplex[idx1], cpx_var_index_to_docplex[idx2])] = coef

                    else:  # pragma: no cover
                        # should not happen, but who knows
                        quads = None

                    quad_expr = mdl._aggregator._quad_factory.new_quad(quads=quads, linexpr=linexpr, safe=True)
                    op = ComparisonType.cplex_ctsense_to_python_op(sense)
                    ct = op(quad_expr, rhs)
                    mdl.add_constraint(ct, ctname)

            # 4. upload indicators
            cpx_indicators = cpx.indicator_constraints
            nb_indicators = cpx_indicators.get_num()
            if nb_indicators:
                all_ind_names = [] if ignore_names else cls._safe_call_get_names(cpx_adapter,
                                                                                 cpx_indicators.get_names)

                all_ind_bvars = cpx_indicators.get_indicator_variables()
                all_ind_rhs = cpx_indicators.get_rhs()
                all_ind_linearcts = cpx_indicators.get_linear_components()
                all_ind_senses = cpx_indicators.get_senses()
                all_ind_complemented = cpx_indicators.get_complemented()
                all_ind_types = cpx_indicators.get_types()
                ind_equiv_type = 3

                for i in range(nb_indicators):
                    ind_bvar = all_ind_bvars[i]
                    ind_name = all_ind_names[i] if all_ind_names else None
                    ind_rhs = all_ind_rhs[i]
                    ind_linear = all_ind_linearcts[i]  # SparsePair(ind, val)
                    ind_sense = cls.parse_sense(all_ind_senses[i])
                    ind_complemented = all_ind_complemented[i]
                    ind_type = all_ind_types[i]
                    # 1 . check the bvar is ok
                    ind_bvar = cpx_var_index_to_docplex[ind_bvar]
                    # each var appears once
                    ind_linexpr = cls._build_linear_expr_from_sparse_pair(lfactory, cpx_var_index_to_docplex,
                                                                          ind_linear)
                    ind_lct = lfactory._new_binary_constraint(ind_linexpr, ind_sense, ind_rhs)
                    if ind_type == ind_equiv_type:
                        logct = lfactory.new_equivalence_constraint(
                            ind_bvar, ind_lct, true_value=1 - ind_complemented, name=ind_name)
                    else:
                        logct = lfactory.new_indicator_constraint(
                            ind_bvar, ind_lct, true_value=1 - ind_complemented, name=ind_name)
                    mdl.add(logct)

            # 5. upload Piecewise linear constraints
            try:
                cpx_pwl = cpx.pwl_constraints
                cpx_pwl_defs = cpx_pwl.get_definitions()
                pwl_fallback_names = [""] * cpx_pwl.get_num()
                cpx_pwl_names = pwl_fallback_names if ignore_names else cls._safe_call_get_names(cpx_adapter,
                                                                                                 cpx_pwl.get_names,
                                                                                                 pwl_fallback_names)
                for (vary_idx, varx_idx, preslope, postslope, breakx, breaky), pwl_name in zip(cpx_pwl_defs,
                                                                                                cpx_pwl_names):
                    varx = cpx_var_index_to_docplex.get(varx_idx, None)
                    vary = cpx_var_index_to_docplex.get(vary_idx, None)
                    breakxy = [(brkx, brky) for brkx, brky in zip(breakx, breaky)]
                    pwl_func = mdl.piecewise(preslope, breakxy, postslope, name=pwl_name)
                    pwl_expr = mdl._lfactory.new_pwl_expr(pwl_func, varx, 0, resolve=False)
                    pwl_expr._f_var = vary
                    pwl_expr._ensure_resolved()

            except AttributeError:  # pragma: no cover
                pass  # Do not check for PWLs if Cplex version does not support them

            # 6. upload objective

            # noinspection PyPep8
            try:
                cpx_multiobj = cpx.multiobj
            except AttributeError:  # pragma: no cover
                # pre-12.9 version
                cpx_multiobj = None

            if cpx_multiobj is None or cpx_multiobj.get_num() <= 1:
                cpx_obj = cpx.objective
                cpx_sense = cpx_obj.get_sense()
                fast_getobj = cpx_adapter.fast_getobj
                if fast_getobj:
                    with fast_getobj(cpx_env_e, cpx_lp, 0, cpx_nb_vars-1) as var_coefs_tuple:
                        obj_expr = make_terms_seq_fn(lfactory, cpx_var_index_to_docplex, var_coefs_tuple)
                else:
                    cpx_all_lin_obj_coeffs = cpx_obj.get_linear()
                    obj_expr = compute_full_dot(mdl, cpx_all_lin_obj_coeffs)

                if cpx_obj.get_num_quadratic_variables() > 0:
                    cpx_all_quad_cols_coeffs = cpx_obj.get_quadratic()
                    quads = qfactory.term_dict_type()
                    for v, col_coefs in zip(cpx_var_index_to_docplex, cpx_all_quad_cols_coeffs):
                        var1 = cpx_var_index_to_docplex[v]
                        indices, coefs = col_coefs.unpack()
                        for idx, coef in zip(indices, coefs):
                            vp = VarPair(var1, cpx_var_index_to_docplex[idx])
                            quads[vp] = quads.get(vp, 0) + coef / 2

                    obj_expr += qfactory.new_quad(quads=quads, linexpr=None)

                obj_expr += cpx.objective.get_offset()
                is_maximize = cpx_sense == cpx_adapter.cplex_module._internal._subinterfaces.ObjSense.maximize

                if is_maximize:
                    mdl.maximize(obj_expr)
                else:
                    mdl.minimize(obj_expr)
            else:
                # we have multiple objective

                nb_multiobjs = cpx_multiobj.get_num()
                exprs = [0] * nb_multiobjs
                priorities = [1] * nb_multiobjs
                weights = [1] * nb_multiobjs
                abstols = [0] * nb_multiobjs
                reltols = [0] * nb_multiobjs
                if ignore_names:
                    names = ["Goal_{0}".format(g) for g in range(1, nb_multiobjs + 1)]
                else:
                    names = cpx_multiobj.get_names()

                fast_multiobj = cpx_adapter.fast_multiobj_getobj is not None
                if fast_multiobj:
                    def get_kth_multiobj(k):
                        weight = cpx_adapter.fast_multiobj_getweight(cpx_env_e, cpx_lp, k)
                        offset = cpx_adapter.fast_multiobj_getoffset(cpx_env_e, cpx_lp, k)
                        priority = cpx_adapter.fast_multiobj_getprio(cpx_env_e, cpx_lp, k)
                        abstol = cpx_adapter.fast_multiobj_getabstol(cpx_env_e, cpx_lp, k)
                        reltol = cpx_adapter.fast_multiobj_getreltol(cpx_env_e, cpx_lp, k)
                        with cpx_adapter.fast_multiobj_getobj(cpx_env_e, cpx_lp, k, 0, cpx_nb_vars-1) as objk_tuples:
                            obj_expr = make_terms_seq_fn(lfactory, cpx_var_index_to_docplex, objk_tuples, constant=offset)
                        return obj_expr, weight, priority, abstol, reltol
                else:
                    def get_kth_multiobj(k):
                        (obj_coeffs, obj_offset, weight, prio, abstol, reltol) = cpx_multiobj.get_definition(k)
                        obj_expr = compute_full_dot(mdl, obj_coeffs, obj_offset)
                        return obj_expr, weight, prio, abstol, reltol

                for m in range(nb_multiobjs):
                    (obj_expr, weight, prio, abstol, reltol) = get_kth_multiobj(m)
                    exprs[m] = obj_expr
                    priorities[m] = prio
                    weights[m] = weight
                    abstols[m] = abstol
                    reltols[m] = reltol
                sense = cpx_multiobj.get_sense()
                mdl.set_multi_objective(sense, exprs, priorities, weights, abstols, reltols, names)

            # upload sos
            cpx_sos = cpx.SOS
            cpx_sos_num = cpx_sos.get_num()
            if cpx_sos_num > 0:
                cpx_sos_types = cpx_sos.get_types()
                cpx_sos_indices = cpx_sos.get_sets()
                cpx_sos_names = cpx_sos.get_names()
                if not cpx_sos_names:
                    cpx_sos_names = [None] * cpx_sos_num
                for sostype, sos_sparse, sos_name in zip(cpx_sos_types, cpx_sos_indices, cpx_sos_names):
                    sos_var_indices = sos_sparse.ind
                    sos_weights = sos_sparse.val
                    isostype = int(sostype)
                    sos_vars = [cpx_var_index_to_docplex[var_ix] for var_ix in sos_var_indices]
                    mdl.add_sos(dvars=sos_vars, sos_arg=isostype, name=sos_name, weights=sos_weights)

            # upload lazy constraints
            cpx_linear_advanced = cpx.linear_constraints.advanced
            cpx_lazyct_num = cpx_linear_advanced.get_num_lazy_constraints()
            if cpx_lazyct_num:
                print("WARNING: found {0} lazy constraints that cannot be uploaded to DOcplex".format(cpx_lazyct_num))

            mdl.output_level = final_output_level
            if final_checker:
                # need to restore checker
                mdl.set_checker(final_checker)

        except cpx_adapter.CplexError as cpx_e:  # pragma: no cover
            print("* CPLEX error: {0!s} reading file {1}".format(cpx_e, filename))
            mdl = None
            if debug_read:
                raise

        except ModelReaderError as mre:  # pragma: no cover
            print("! Model reader error: {0!s} while reading file {1}".format(mre, filename))
            mdl = None
            if debug_read:
                raise

        except DOcplexException as doe:  # pragma: no cover
            print("! Internal DOcplex error: {0!s} while reading file {1}".format(doe, filename))
            mdl = None
            if debug_read:
                raise

        # except Exception as any_e:  # pragma: no cover
        #     print("Internal exception raised: {0} msg={1!s} while reading file '{2}'".format(type(any_e), any_e, filename))
        #     mdl = None
        #     if debug_read:
        #         raise

        finally:
            # clean up CPLEX instance...
            # cpx.end()
            pass
        if mdl:
            # reset engine, if necessary
            if final_agent == 'zero':
                pass  # nothing to do
            if final_agent == 'cplex':
                mdl._set_engine(CplexEngine(mdl, _cplex=cpx_adapter))
                # mdl._check_scope_indices()
            else:
                # set a new non-cplex agent
                mdl._set_new_engine_from_agent(final_agent)
            mdl._set_solver_agent(final_agent)

        return mdl

    @classmethod
    def read_model(cls, filename, model_name=None, verbose=False, model_class=None, **kwargs):
        """ This method is a synonym of `read` for compatibility.

        """
        import warnings
        warnings.warn("ModelReader.read_model is deprecated, use class method ModelReader.read()", DeprecationWarning)
        return cls.read(filename, model_name, verbose, model_class, **kwargs)


def read_model(filename, model_name=None, verbose=False, **kwargs):
    """ Reads a model from a CPLEX export file.

    Accepts all formats exported by CPLEX: LP, SAV, SAV.gz, MPS.

    If an error occurs while reading the file, the message of the exception
    is printed and the function returns None.

    Args:
        filename: The model file to read.
        model_name: An optional name for the newly created model. If None,
            the model name will be the path basename.
        verbose: An optional flag to print informative messages, default is False.
        kwargs: A dict of keyword-based arguments that are passed to the model contructor.

    Note:
        This function requires CPLEX runtime, otherwise an exceotion is raised.

    Example:
        `m = read_model("c:/temp/foo.mps", model_name="docplex_foo", solver_agent="local", output_level=100)`

    Returns:
        An instance of Model, or None if an exception is raised.



    See Also:
        :class:`docplex.mp.model.Model`

    """
    return ModelReader.read(filename, model_name=model_name, verbose=verbose, **kwargs)
