# -*- coding: utf-8 -*-
"""Provides classes to interface solvers with PyMzn.

PyMzn interfaces with solvers through the ``Solver`` base class. This class
includes the necessary infomation for PyMzn to setup the solver, together with
the ``solve`` method, which respectively take care of the running or
asynchronously starting a process that solves the MiniZinc/FlatZinc model. PyMzn
provides a number of solver implementations out-of-the-box.  PyMzn's default
solver is ``pymzn.gecode``, an instance of `pymzn.Gecode`.

To use a solver that is not provided by PyMzn or to exend an existing one, one
has to subclass the `Solver` class and implement the ``args`` method, which
returns a list of command line arguments for executing the process. This is
generally enough for most solvers, but you can also directly reimplement the
``solve`` method for extra flexibility.

For instance::

    from pymzn import Solver
    from pymzn.process import Process

    class MySolver(Solver):
        def __init__(self, path='path/to/solver', globals_dir='path/to/gobals'):
            super().__init__(globals_dir)
            self.cmd = path

        def args(self, fzn_file, *args, arg1=val1, arg2=val2, **kwargs):
            return [self.cmd, '-arg1', arg1, '-arg2', arg2, fzn_file]

Then it is possible to run the ``minizinc`` function with the custom solver::

    my_solver = MySolver()
    pymzn.minizinc('test.mzn', solver=my_solver, arg1=val1, arg2=val2)
"""

import re
import logging

import pymzn.config as config

from ..process import Process
from subprocess import CalledProcessError


__all__ = [
    'Solver', 'Gecode', 'Chuffed', 'Optimathsat', 'Opturion', 'MIPSolver',
    'Gurobi', 'CBC', 'OscarCBLS', 'ORTools', 'gecode', 'chuffed', 'optimathsat',
    'opturion', 'gurobi', 'cbc', 'oscar_cbls', 'or_tools'
]


class Solver:
    """Abstract solver class.

    Parameter
    ---------
    solver_id : str
        The identifier to use when launching the minizinc command.
    """
    def __init__(self, solver_id):
        self.solver_id = solver_id

    def args(
        self, all_solutions=False, num_solutions=None, free_search=False,
        parallel=None, seed=None, **kwargs
    ):
        """Returns a list of command line arguments for the specified options.

        In all cases, this function should add options to display statistics and
        log messages.

        Parameters
        ----------
        all_solutions : bool
            Whether all the solutions must be returned (default is False).
        num_solutions : int
            The maximum number of solutions to be returned (only used in
            satisfation problems).
        free_search : bool
            Whether the solver should be instructed to perform a free search.
        parallel : int
            The number of parallel threads the solver should use.
        seed : int
            The random number generator seed to pass to the solver.
        """
        args = ['-s', '-v']
        if all_solutions:
            args.append('-a')
        if num_solutions is not None:
            args += ['-n', num_solutions]
        if free_search:
            args.append('-f')
        if parallel is not None:
            args += ['-p', parallel]
        if seed is not None:
            args += ['-r', seed]
        return args

    def parse_out(self, out, err):
        """Parse the output streams for e.g. statistics and log messages.

        Statistics should be wrapped in a dictionary and returned.
        Log messages should be logged using the pymzn logger.

        Parameters
        ----------
        out : str
            The raw output stream of the solver.
        err : str
            The raw error stream of the solver.
        """
        stats = {}
        return out, stats


class Gecode(Solver):
    """Interface to the Gecode solver."""

    def __init__(self, solver_id='gecode'):
        super().__init__(solver_id)


class Chuffed(Solver):
    """Interface to the Chuffed solver."""

    def __init__(self, solver_id='chuffed'):
        super().__init__(solver_id)


class Optimathsat(Solver):
    """Interface to the Optimathsat solver."""

    _line_comm_p = re.compile('%.*\n')
    _rational_p = re.compile('(\d+)\/(\d+)')

    def __init__(self, solver_id='optimathsat'):
        super().__init__(solver_id)

    def parse_out(self, out, err):
        stats = ''.join(self._line_comm_p.findall(out))
        out = self._line_comm_p.sub(out, '')
        for m in self._rational_p.finditer(out):
            n, d = m.groups()
            val = float(n) / float(d)
            out = re.sub('{}/{}'.format(n, d), str(val), out)
        return out, {'statistics': stats}


class Opturion(Solver):
    """Interface to the Opturion CPX solver."""

    def __init__(self, solver_id='opturion'):
        super().__init__(solver_id)


class MIPSolver(Solver):
    """Generic interface to MIP solvers."""


class Gurobi(MIPSolver):
    """Interface to the Gurobi solver.

    Parameters
    ----------
    dll : str
        The string containing the dll of your gurobi installation.
    """

    def __init__(self, solver_id='gurobi', dll=None):
        super().__init__(solver_id)
        self.dll = dll

    def args(self, *args, **kwargs):
        args = super().args(*args, **kwargs)
        if self.dll is not None:
            args += ['--dll', self.dll]
        return args

    def args(
        self, all_solutions=False, num_solutions=None, free_search=False,
        parallel=None, seed=None, **kwargs
    ):
        args = ['-s', '-v']
        if all_solutions:
            args.append('-a')
        if num_solutions is not None:
            args += ['-n', num_solutions]
        if free_search:
            args.append('-f')
        if parallel is not None:
            args += ['-p', parallel]
        if self.dll is not None:
            args += ['--gurobi-dll', self.dll]
        return args


class CBC(MIPSolver):
    """Interface to the COIN-OR CBC solver."""

    def __init__(self, solver_id='osicbc'):
        super().__init__(solver_id)


class OscarCBLS(Solver):
    """Interface to the Oscar/CBLS solver."""

    def __init__(self, solver_id='oscar-cbls'):
        super().__init__(solver_id)


class ORTools(Solver):
    """Interface to the OR-tools solver.

    Parameters
    ----------
    path : str
        The path to the fzn-or-tools executable.
    globals_dir : str
        The path to the directory for global included files. You should either
        copy or link the 'share/minizinc_cp' folder from the or-tools
        distribution into the minizinc library directory (with name 'or-tools')
        or provide the full path here.
    """

    def __init__(self, solver_id='or-tools'):
        super().__init__(solver_id)


#: Default Gecode instance.
gecode = Gecode()

#: Default Chuffed instance.
chuffed = Chuffed()

#: Default Optimathsat instance.
optimathsat = Optimathsat()

#: Default Opturion instance.
opturion = Opturion()

#: Default Gurobi instance.
gurobi = Gurobi()

#: Default CBC instance.
cbc = CBC()

#: Default Oscar/CBLS instance.
oscar_cbls = OscarCBLS()

#: Default ORTools instance.
or_tools = ORTools()


@@ -1466,24 +1466,8 @@
  re%0A
-import logging%0A%0A
 impo
@@ -1497,80 +1497,8 @@
 ig%0A%0A
-from ..process import Process%0Afrom subprocess import CalledProcessError%0A
 %0A%0A__

fix imports