package cn.lihongjie.coal.coal;

import cn.lihongjie.coal.coalBlend.dto.CoalBlendRequest;
import cn.lihongjie.coal.coalBlend.dto.CoalBlendResult;
import cn.lihongjie.coal.exception.BizException;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.ortools.Loader;
import com.google.ortools.sat.*;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
@Transactional
public
class CoalService {

    private static List<IntVar> createPercentVars(CoalBlendRequest request, CpModel model) {
        int index = 0;

        List<IntVar> vars = new ArrayList<>();
        for (CoalInfo coal : request.getCoals()) {
            IntVar x =
                    model.newIntVar(
                            Math.max(coal.getMin(), 0),
                            Math.min(coal.getMax(), 100),
                            "x" + index++);
            vars.add(x);
        }
        return vars;
    }

    /**
     * 配煤
     *
     * @return
     */
    public CoalBlendResult blend(CoalBlendRequest request) {

        if (CollectionUtils.isEmpty(request.getCoals())) {
            throw new BizException("煤不能为空");
        }

        if (CollectionUtils.isEmpty(request.getConstraints())) {
            throw new BizException("参数配置不能为空");
        }

        Loader.loadNativeLibraries();
        CoalBlendResult result = new CoalBlendResult();

        CpModel model = new CpModel();

        List<IntVar> vars = createPercentVars(request, model);

        // 添加各种约束条件
        addConstrains(request, model, vars);

        // 初始化求解器
        CpSolver cpSolver = new CpSolver();
        cpSolver.setLogCallback(x -> log.info(x));
        SatParameters.Builder parameters = cpSolver.getParameters();
        parameters.setEnumerateAllSolutions(false);
        parameters.setMaxTimeInSeconds(request.getMaxTime());
        parameters.setLogSearchProgress(false);
        parameters.setFillAdditionalSolutionsInResponse(true);

        // 开始求最优解

        List<CoalConstraint> sorted =
                request.getConstraints().stream()
                        .filter(x -> x.getPriority() != null)
                        .sorted(Comparator.comparing(CoalConstraint::getPriority))
                        .toList();

        for (CoalConstraint constraint : sorted) {

            log.info("使用约束 {}", constraint);

            LinearExpr weightedSum =
                    LinearExpr.weightedSum(
                            vars.toArray(new IntVar[0]),
                            request.getCoals().stream()
                                    .mapToLong(
                                            x -> (long) (x.getParamVal(constraint.getCode()) * 100))
                                    .toArray());
            if (constraint.getOrder() == -1) {
                model.maximize(weightedSum);

            } else {
                model.minimize(weightedSum);
            }

            CpSolverStatus status = cpSolver.solve(model);

            if (status == CpSolverStatus.FEASIBLE || status == CpSolverStatus.OPTIMAL) {

                double constrainVal = cpSolver.objectiveValue();

                //                model.clearHints();
                //
                //                for (IntVar var : vars) {
                //                    model.addHint(var, cpSolver.value(var));
                //
                //                }
                if (constraint.getOrder() == -1) {
                    model.addLessOrEqual(weightedSum, (long) constrainVal);
                    log.info("添加新的条件 sum({}) <= {}", constraint.getCode(), constrainVal);
                } else {
                    model.addGreaterOrEqual(weightedSum, (long) constrainVal);
                    log.info("添加新的条件 sum({}) >= {}", constraint.getCode(), constrainVal);
                }

                String validate = model.validate();
                log.info(validate);

            } else {
                log.warn("无法求解: " + status);
            }
        }

        // 记录结果

        //
        cpSolver.getParameters().setEnumerateAllSolutions(true);
        CpSolverStatus solverStatus =
                cpSolver.solve(
                        model,
                        new CpSolverSolutionCallback() {
                            @Override
                            public void onSolutionCallback() {

                                CoalInfo solution = new CoalInfo();
                                List<Long> gcdVals = new ArrayList<>();

                                solution.setPercents(new ArrayList<>());
                                solution.setParameters(new ArrayList<>());
                                for (int i = 0; i < vars.size(); i++) {

                                    long percent = this.value(vars.get(i));

                                    CoalInfo coal = request.getCoals().get(i);
                                    // 累加
                                    try {

                                        acc(solution, percent, coal);
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    }

                                    CoalPercent e = new CoalPercent();
                                    e.setId(coal.getId());
                                    e.setName(coal.getName());
                                    e.setPercent(percent);
                                    if (CollectionUtils.isNotEmpty(gcdVals)) {
                                        e.setPercent2(gcdVals.get(i));
                                    }
                                    solution.getPercents().add(e);
                                }

                                // 四舍五入

                                round(solution);

                                result.getCoals().add(solution);
                            }
                        });

        sortAndSelect(request, result);

        return result;
    }

    private void sortAndSelect(CoalBlendRequest request, CoalBlendResult result) {

        List<CoalConstraint> list =
                request.getConstraints().stream()
                        .filter(x -> x.getPriority() != null)
                        .sorted(Comparator.comparing(x -> x.getPriority()))
                        .collect(Collectors.toList());

        Comparator<CoalInfo> c = Comparator.comparing(x -> 1);
        if (!list.isEmpty()) {

            for (CoalConstraint constraint : list) {

                if (constraint.getOrder() == null || constraint.getOrder() == 1) {
                    c =
                            c.thenComparing(
                                    Comparator.comparing(
                                            x -> x.getParamVal(constraint.getCode())));
                } else {

                    c =
                            c.thenComparing(
                                    Comparator.<CoalInfo, Double>comparing(
                                                    x -> x.getParamVal(constraint.getCode()))
                                            .reversed());
                }
            }
        }
        result.setCoals(
                result.getCoals().stream()
                        .sorted(c)
                        .limit(request.getCount())
                        .collect(Collectors.toList()));

        result.getCoals().forEach(x -> x.calPercent2());
    }

    private void round(CoalInfo solution) {

        solution.getParameters()
                .forEach(
                        x ->
                                x.setValue(
                                        x.getValue() == null
                                                ? x.getValue()
                                                : BigDecimal.valueOf(x.getValue())
                                                        .setScale(2, RoundingMode.HALF_UP)
                                                        .doubleValue()));
    }

    private void acc(CoalInfo solution, long percent, CoalInfo coal) {

        for (CoalParameter parameter : coal.getParameters()) {

            boolean found = false;

            for (CoalParameter solutionParameter : solution.getParameters()) {

                if (StringUtils.equalsIgnoreCase(
                        parameter.getCode(), solutionParameter.getCode())) {

                    solutionParameter.setValue(
                            solutionParameter.getValue()
                                    + (parameter.getValue() * percent / 100.0));
                    found = true;
                    break;
                }
            }

            if (!found) {
                CoalParameter e = new CoalParameter();
                e.setCode(parameter.getCode());
                e.setValue(parameter.getValue() * percent / 100.0);
                solution.getParameters().add(e);
            }
        }
    }

    private void addConstrains(CoalBlendRequest request, CpModel model, List<IntVar> vars) {
        // 各种煤比例之和为 100

        model.addEquality(LinearExpr.sum(vars.toArray(new LinearArgument[0])), 100);

        if (request.getType() == 2) {

            IntVar gcd = model.newIntVar(1, 100, "gcd");

            List<IntVar> varGcdList = new ArrayList<>();
            for (IntVar var : vars) {

                model.addModuloEquality(model.newConstant(0), var, gcd);

                // 定义一个变量,表示 percent /  gcd
                IntVar varGcd = model.newIntVar(1, 100, var.getName() + "_gcd");
                // 约束这个变量
                model.addDivisionEquality(varGcd, var, gcd);
                varGcdList.add(varGcd);
            }

            // 约束 sum(percent / gcd) < request.getPercent2Sum()
            model.addLessOrEqual(
                    LinearExpr.sum(varGcdList.toArray(new IntVar[0])), request.getPercent2Sum());
        }

        for (CoalConstraint constrain : request.getConstraints()) {

            if (StringUtils.isBlank(constrain.getCode())) {
                continue;
            }

            if (constrain.getMin() == null && constrain.getMax() == null) {
                continue;
            }

            long[] paramsOfEachCoal = new long[request.getCoals().size()];
            int index = 0;
            for (CoalInfo coal : request.getCoals()) {

                boolean found = false;

                for (CoalParameter parameter : coal.getParameters()) {

                    if (StringUtils.equalsIgnoreCase(parameter.getCode(), constrain.getCode())) {

                        paramsOfEachCoal[index++] = (long) (parameter.getValue() * 100);
                        found = true;
                        break;
                    }
                }

                if (!found) {

                    throw new RuntimeException(
                            String.format(
                                    "煤 %s 没有找到指标 %s, 但是存在条件 %s <= %s <= %s",
                                    coal.getName(),
                                    constrain.getCode(),
                                    constrain.getMin(),
                                    constrain.getCode(),
                                    constrain.getMax()));
                }
            }

            if (constrain.getMin() != null) {

                model.addGreaterOrEqual(
                        LinearExpr.weightedSum(
                                vars.toArray(new LinearArgument[0]), paramsOfEachCoal),
                        (long) ((constrain.getMin()) * 10000));
            }

            if (constrain.getMax() != null) {

                model.addLessOrEqual(
                        LinearExpr.weightedSum(
                                vars.toArray(new LinearArgument[0]), paramsOfEachCoal),
                        (long) ((constrain.getMax()) * 10000));
            }
        }
    }

    @SneakyThrows
    public List<CoalParameterDef> paramDefs() {

        ClassPathResource classPathResource =
                new ClassPathResource("/config/CoalParameterDef.json");

        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        List<CoalParameterDef> coalParameterDefs;
        try (InputStream inputStream = classPathResource.getInputStream()) {
            coalParameterDefs =
                    mapper.readValue(inputStream, new TypeReference<List<CoalParameterDef>>() {});
        }

        return coalParameterDefs;
    }
}
