#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2022
#  National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

#
# Class for reading an AMPL *.sol file
#

import re

from pyomo.opt.base import results
from pyomo.opt.base.formats import ResultsFormat
from pyomo.opt import (SolverResults,
                       SolutionStatus,
                       SolverStatus,
                       TerminationCondition)


@results.ReaderFactory.register(str(ResultsFormat.sol))
class ResultsReader_sol(results.AbstractResultsReader):
    """
    Class that reads in a *.sol results file and generates a
    SolverResults object.
    """

    def __init__(self, name=None):
        results.AbstractResultsReader.__init__(self,ResultsFormat.sol)
        if not name is None:
            self.name = name

    def __call__(self, filename, res=None, soln=None, suffixes=[]):
        """
        Parse a *.sol file
        """
        try:
            with open(filename,"r") as f:
                return self._load(f, res, soln, suffixes)
        except ValueError as e:
            with open(filename,"r") as f:
                fdata = f.read()
            raise ValueError(
                "Error reading '%s': %s.\n"
                "SOL File Output:\n%s"
                % (filename, str(e), fdata))

    def _load(self, fin, res, soln, suffixes):

        if res is None:
            res = SolverResults()
        #
        # Some solvers (minto) do not write a message.  We will assume
        # all non-blank lines up the 'Options' line is the message.
        msg = []
        while True:
            line = fin.readline()
            if not line:
                # EOF
                break
            line = line.strip()
            if line == 'Options':
                break
            if line:
                msg.append(line)
        msg = '\n'.join(msg)
        z = []
        if line[:7] == "Options":
            line = fin.readline()
            nopts = int(line)
            need_vbtol = False
            if nopts > 4:           # WEH - when is this true?
                nopts -= 2
                need_vbtol = True
            for i in range(nopts + 4):
                line = fin.readline()
                z += [int(line)]
            if need_vbtol:          # WEH - when is this true?
                line = fin.readline()
                z += [float(line)]
        else:
            raise ValueError("no Options line found")
        n = z[nopts + 3] # variables
        m = z[nopts + 1] # constraints
        x = []
        y = []
        i = 0
        while i < m:
            line = fin.readline()
            y.append(float(line))
            i += 1
        i = 0
        while i < n:
            line = fin.readline()
            x.append(float(line))
            i += 1
        objno = [0,0]
        line = fin.readline()
        if line:                    # WEH - when is this true?
            if line[:5] != "objno":         #pragma:nocover
                raise ValueError("expected 'objno', found '%s'" % (line))
            t = line.split()
            if len(t) != 3:
                raise ValueError("expected two numbers in objno line, "
                                 "but found '%s'" % (line))
            objno = [int(t[1]), int(t[2])]
        res.solver.message = msg.strip()
        res.solver.message = res.solver.message.replace("\n","; ")
        if isinstance(res.solver.message, str):
            res.solver.message = res.solver.message.replace(':', '\\x3a')
        ##res.solver.instanceName = osrl.header.instanceName
        ##res.solver.systime = osrl.header.time
        res.solver.status = SolverStatus.ok
        soln_status = SolutionStatus.unknown
        if (objno[1] >= 0) and (objno[1] <= 99):
            objno_message = "OPTIMAL SOLUTION FOUND!"
            res.solver.termination_condition = TerminationCondition.optimal
            res.solver.status = SolverStatus.ok
            soln_status = SolutionStatus.optimal
        elif (objno[1] >= 100) and (objno[1] <= 199):
            objno_message = "Optimal solution indicated, but ERROR LIKELY!"
            res.solver.termination_condition = TerminationCondition.optimal
            res.solver.status = SolverStatus.warning
            soln_status = SolutionStatus.optimal
        elif (objno[1] >= 200) and (objno[1] <= 299):
            objno_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!"
            res.solver.termination_condition = TerminationCondition.infeasible
            res.solver.status = SolverStatus.warning
            soln_status = SolutionStatus.infeasible
        elif (objno[1] >= 300) and (objno[1] <= 399):
            objno_message = "UNBOUNDED PROBLEM: the objective can be improved without limit!"
            res.solver.termination_condition = TerminationCondition.unbounded
            res.solver.status = SolverStatus.warning
            soln_status = SolutionStatus.unbounded
        elif (objno[1] >= 400) and (objno[1] <= 499):
            objno_message = ("EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver "
                             "was stopped by a limit that you set!")
            res.solver.termination_condition = TerminationCondition.maxIterations
            res.solver.status = SolverStatus.warning
            soln_status = SolutionStatus.stoppedByLimit
        elif (objno[1] >= 500) and (objno[1] <= 599):
            objno_message = ("FAILURE: the solver stopped by an error condition "
                             "in the solver routines!")
            res.solver.termination_condition = TerminationCondition.internalSolverError
            res.solver.status = SolverStatus.error
            soln_status = SolutionStatus.error
        res.solver.id = objno[1]
        ##res.problem.name = osrl.header.instanceName
        if res.solver.termination_condition in [TerminationCondition.unknown,
                        TerminationCondition.maxIterations,
                        TerminationCondition.minFunctionValue,
                        TerminationCondition.minStepLength,
                        TerminationCondition.globallyOptimal,
                        TerminationCondition.locallyOptimal,
                        TerminationCondition.optimal,
                        TerminationCondition.maxEvaluations,
                        TerminationCondition.other,
                        TerminationCondition.infeasible]:

            if soln is None:
                soln = res.solution.add()
            res.solution.status = soln_status
            soln.status_description = objno_message
            soln.message = msg.strip()
            soln.message = res.solver.message.replace("\n","; ")
            soln_variable = soln.variable
            i = 0
            for var_value in x:
                soln_variable["v"+str(i)] = {"Value" : var_value}
                i = i + 1
            soln_constraint = soln.constraint
            if any(re.match(suf,"dual") for suf in suffixes):
                for i in range(0,len(y)):
                    soln_constraint["c"+str(i)] = {"Dual" : y[i]}

            ### Read suffixes ###
            line = fin.readline()
            while line:
                line = line.strip()
                if line == "":
                    continue
                line = line.split()
                if line[0] != 'suffix':
                    # We assume this is the start of a
                    # section like kestrel_option, which
                    # comes after all suffixes.
                    remaining = ""
                    line = fin.readline()
                    while line:
                        remaining += line.strip()+"; "
                        line = fin.readline()
                    res.solver.message += remaining
                    break
                unmasked_kind = int(line[1])
                kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob
                convert_function = int
                if (unmasked_kind & 4) == 4:
                    convert_function = float
                nvalues = int(line[2])
                # namelen = int(line[3])
                # tablen = int(line[4])
                tabline = int(line[5])
                suffix_name = fin.readline().strip()
                if any(re.match(suf,suffix_name) for suf in suffixes):
                    # ignore translation of the table number to string value for now,
                    # this information can be obtained from the solver documentation
                    for n in range(tabline):
                        fin.readline()
                    if kind == 0: # Var
                        for cnt in range(nvalues):
                            suf_line = fin.readline().split()
                            key = "v"+suf_line[0]
                            if key not in soln_variable:
                                soln_variable[key] = {}
                            soln_variable[key][suffix_name] = \
                                convert_function(suf_line[1])
                    elif kind == 1: # Con
                        for cnt in range(nvalues):
                            suf_line = fin.readline().split()
                            key = "c"+suf_line[0]
                            if key not in soln_constraint:
                                soln_constraint[key] = {}
                            # GH: About the comment below: This makes for a
                            # confusing results object and more confusing tests.
                            # We should not muck with the names of suffixes
                            # coming out of the sol file.
                            #
                            #   convert the first letter of the suffix name to upper case,
                            #   mainly for pretty-print / output purposes. these are lower-cased
                            #   when loaded into real suffixes, so it is largely redundant.
                            translated_suffix_name = suffix_name[0].upper() + suffix_name[1:]
                            soln_constraint[key][translated_suffix_name] = \
                                convert_function(suf_line[1])
                    elif kind == 2: # Obj
                        for cnt in range(nvalues):
                            suf_line = fin.readline().split()
                            soln.objective.setdefault("o"+suf_line[0],{})[suffix_name] = \
                                convert_function(suf_line[1])
                    elif kind == 3: # Prob
                        # Skip problem kind suffixes for now. Not sure the
                        # best place to put them in the results object
                        for cnt in range(nvalues):
                            suf_line = fin.readline().split()
                            soln.problem[suffix_name] = convert_function(suf_line[1])
                else:
                    # do not store the suffix in the solution object
                    for cnt in range(nvalues):
                        fin.readline()
                line = fin.readline()

        #
        # This is a bit of a hack to accommodate PICO.  If
        # the PICO parser has parsed the # of constraints, then
        # don't try to read it in from the *.sol file.  The reason
        # is that these may be inconsistent values!
        #
        if res.problem.number_of_constraints == 0:
            res.problem.number_of_constraints = m
        res.problem.number_of_variables = n
        res.problem.number_of_objectives = 1
        return res
