# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 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
