package com.bailuntec.job;

import com.bailuntec.domain.dto.SalesVolumeDTO;
import com.bailuntec.domain.entity.DcAutoSales;
import com.bailuntec.domain.example.DcAutoSalesExample;
import com.bailuntec.domain.pojo.DataNode;
import com.bailuntec.domain.pojo.LinerRegression;
import com.bailuntec.mapper.DcAutoSalesMapper;
import com.bailuntec.service.AutoSalesService;
import com.bailuntec.utils.SessionUtil;
import org.apache.commons.beanutils.BeanUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

public class AutoSalesServiceImpl implements AutoSalesService {

    public DcAutoSales forecastSalesBySalesVolumeDTO(SalesVolumeDTO salesVolumeDTO) throws Exception {
        List<DataNode> dataNodeList = null;
        BigDecimal[] fittingResult = null;
        DataNode dataNode = null;
        DcAutoSales dcAutoSales = new DcAutoSales();
        String historySales = salesVolumeDTO.getHistorySales();
        String[] historySalesArray = historySales.split(",");
        boolean hasZero = false; // 历史销量如果有0 只能生成对数函数
        BigDecimal totalSales = BigDecimal.ZERO;
        dataNodeList = new ArrayList<>();
        BigDecimal maxSales = new BigDecimal(historySalesArray[0]);
        BigDecimal minSales = new BigDecimal(historySalesArray[1]);
        for (int i = 0; i < historySalesArray.length; i++) {
            BigDecimal iDaySales = new BigDecimal(historySalesArray[i]);
            dataNode = new DataNode(BigDecimal.valueOf(i + 1), iDaySales);
            dataNodeList.add(dataNode);
            totalSales = totalSales.add(iDaySales);
            if ("0".equals(historySalesArray[i])) {
                hasZero = true;
            }
            if (iDaySales.compareTo(maxSales) == 1) {
                maxSales = iDaySales;
            }
            if (iDaySales.compareTo(minSales) == -1) {
                minSales = iDaySales;
            }
        }
        BigDecimal avgSales = (totalSales.subtract(maxSales).subtract(minSales)).divide(BigDecimal.valueOf(historySalesArray.length - 2), 3, RoundingMode.HALF_EVEN);

        //根据销量做线性回归
        BigDecimal[] linerRegressionAB = LinerRegression.getAB(dataNodeList);
        BigDecimal alpha = linerRegressionAB[0];
        BigDecimal beta = linerRegressionAB[1];
        BigDecimal r2 = linerRegressionAB[2]; // 拟合优度
        String formula = formatFormula(alpha, beta);
        dcAutoSales.setKVariable(alpha);
        dcAutoSales.setBVariable(beta);
        dcAutoSales.setRVariable(r2);
        dcAutoSales.setForecastFormula(formula);
        BeanUtils.copyProperties(dcAutoSales,salesVolumeDTO );
        //将历史销量详细,存入销量表
        dcAutoSales.setHistorySalesDetails("["+ historySales+ "]");
        dcAutoSales.setAverageSales(avgSales);
        /*
         * 根据不同的历史销量和趋势选择不同的预测算法
         */
        String formulaFitting = null;
        if (hasZero || dcAutoSales.getKVariable().compareTo(BigDecimal.ZERO) == 1) {
            //对数
            fittingResult = logFitting(fittingX(), fittingData(salesVolumeDTO));
            //参数说明  y = aln(x) + b  a=fittingResult[0] b=fittingResult[1]
            formulaFitting = formatFormulaLogFitting(fittingResult);
//                        公式说明 v = fittingResult[0].doubleValue() * Math.log1p(31) + fittingResult[1].doubleValue();
        } else {
            //指数
            fittingResult = expFitting(fittingX(), fittingData(salesVolumeDTO));
            formulaFitting = formatFormulaExpFitting(fittingResult);
            // 参数说明 y = ae^bx   a=fittingResult[0] b=fittingResult[1]
//                       公式说明 v = fittingResult[0].doubleValue() * Math.exp(fittingResult[1].doubleValue() * 31);
        }
        dcAutoSales.setFitForecastFormula(formulaFitting);
        dcAutoSales.setFitAVariable(fittingResult[0]);
        dcAutoSales.setFitBVariable(fittingResult[1]);
        dcAutoSales.setFitRVariable(fittingResult[2]);
        try {
            DcAutoSalesMapper dcAutoSalesMapper = SessionUtil.getSession().getMapper(DcAutoSalesMapper.class);
            int i = dcAutoSalesMapper.updateByExampleSelective(dcAutoSales, DcAutoSalesExample.newAndCreateCriteria().andBailunSkuEqualTo(dcAutoSales.getBailunSku()).andWarehouseCodeEqualTo(dcAutoSales.getWarehouseCode()).example());
            if (i == 0) {
                dcAutoSalesMapper.insertSelective(dcAutoSales);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Mybatis操作dcAutoSalesDB失败", e);
        } finally {
            SessionUtil.closeSession();
        }
        return dcAutoSales;
    }


    /**
     * 格式化
     * 线性回归公式,
     * 方便输出
     */
    public String formatFormula(BigDecimal alpha, BigDecimal beta) {
        alpha = alpha.setScale(5, RoundingMode.HALF_EVEN);
        beta = beta.setScale(5, RoundingMode.HALF_EVEN);
        if (beta.compareTo(BigDecimal.ZERO) == -1) {
            return "y = " + alpha + "x - " + beta.abs();
        }
        return "y = " + alpha + "x + " + beta;
    }


    /**
     * 格式化
     * 指数拟合公式,
     * 方便输出
     */
    private String formatFormulaExpFitting(BigDecimal[] fittingResult) {
        // y = ae^bx
        return "y = " + fittingResult[0] + " * e^(" + fittingResult[1] + " * x)";
    }

    /**
     * 格式化
     * 对数拟合公式,
     * 方便输出
     */
    private String formatFormulaLogFitting(BigDecimal[] fittingResult) {
        //y = y= a*(ln x)+b
        if (fittingResult[1].compareTo(BigDecimal.ZERO) == -1) {
            return "y = " + fittingResult[0] + "ln(x)" + fittingResult[1];
        } else {
            return "y = " + fittingResult[0] + "ln(x) +" + fittingResult[1];
        }
    }


    /**
     * 指数拟合
     * y = b*exp(ax)
     * result[0] = a
     * result[1] = b
     * result[2] 数据点数
     * result[3] 自由度
     * result[4] 确定系数
     *
     * @param x
     * @param y
     * @return
     */
    public BigDecimal[] expFitting(double x[], double y[]) {
        int size = x.length;
        double xmean = 0.0;
        double ymean = 0.0;
        double rss = 0;
        double tss = 0;
        for (int i = 0; i < size; i++) {
            xmean += x[i];
            y[i] = Math.log(y[i]);
            ymean += y[i];
        }
        xmean /= size;
        ymean /= size;

        double sumx2 = 0.0f;
        double sumxy = 0.0f;
        for (int i = 0; i < size; i++) {
            sumx2 += (x[i] - xmean) * (x[i] - xmean);
            sumxy += (y[i] - ymean) * (x[i] - xmean);
        }
        double b = sumxy / sumx2;
        double a = ymean - b * xmean;

        for (int i = 0; i < size; i++) {
            rss += (y[i] - (a + b * x[i])) * (y[i] - (a + b * x[i]));
            tss += (y[i] - ymean) * (y[i] - ymean);
        }
        double r2 = 1 - (rss / (size - 1)) / (tss / (size - 1));
        a = Math.exp(a);
        return new BigDecimal[]{BigDecimal.valueOf(a).setScale(3, RoundingMode.HALF_EVEN), BigDecimal.valueOf(b).setScale(3, RoundingMode.HALF_EVEN), BigDecimal.valueOf(r2).setScale(4, RoundingMode.HALF_EVEN)};
    }


    /**
     * 对数拟合函数,.y= c*(ln x)+b,输出为b,c
     *
     * @param y
     * @param x
     * @return
     */
    public BigDecimal[] logFitting(double[] x, double[] y) {
        double[] lnX = new double[x.length];
        for (int i = 0; i < x.length; i++) {
            if (x[i] == 0 || x[i] < 0) {
                throw new RuntimeException("正对非正数取对数！");
            }
            lnX[i] = Math.log(x[i]);
        }
        double[] ratio = linear(y, lnX);
        BigDecimal a = BigDecimal.valueOf(ratio[1]).setScale(3, RoundingMode.HALF_EVEN);
        BigDecimal b = BigDecimal.valueOf(ratio[0]).setScale(3, RoundingMode.HALF_EVEN);
        BigDecimal r2 = BigDecimal.valueOf(calRSquare_logest(x, y, ratio[0], ratio[1])).setScale(4, RoundingMode.HALF_EVEN);
        return new BigDecimal[]{a, b, r2};
    }


    public double calRSquare_logest(double x[], double[] y, double a,
                                    double b) {

        int num = y.length;
        double[] yLine = new double[num];
        for (int i = 0; i < num; i++) {
            yLine[i] = Math.log(x[i]) * b + a;
        }

        return calRSquare(x, y, yLine, a, b);
    }

    private double calRSquare(double x[], double[] y, double[] yLine,
                              double a, double b) {

        // 1、获取线性函数对应的数组值
        int num = y.length;

        // 2、y平均值
        double sum = 0;
        for (double yi : y) {
            sum = sum + yi;
        }
        double yAverage = sum / num;

        // 3、计算实际值减去预算值的平方和
        double sse = 0;
        for (int i = 0; i < num; i++) {
            sse = sse + (y[i] - yLine[i]) * (y[i] - yLine[i]);
        }

        // 4、计算实际值减去平均值的平方和
        double sst = 0;
        for (int i = 0; i < num; i++) {
            sst = sst + (y[i] - yAverage) * (y[i] - yAverage);
        }
        if (sst == 0) {
            return 1;
        }
        return 1 - sse / sst;
    }


    public double[] linear(double[] y, double[] x) {
        double[] ratio = polyfit(y, x, 1);
        return ratio;
    }

    public double[] polyfit(double[] y, double[] x, int order) {
        double[][] guass = get_Array(y, x, order);

        double[] ratio = cal_Guass(guass, order + 1);

        return ratio;
    }

    private double[] cal_Guass(double[][] guass, int count) {
        double temp;
        double[] x_value;

        for (int j = 0; j < count; j++) {
            int k = j;
            double min = guass[j][j];

            for (int i = j; i < count; i++) {
                if (Math.abs(guass[i][j]) < min) {
                    min = guass[i][j];
                    k = i;
                }
            }

            if (k != j) {
                for (int x = j; x <= count; x++) {
                    temp = guass[k][x];
                    guass[k][x] = guass[j][x];
                    guass[j][x] = temp;
                }
            }

            for (int m = j + 1; m < count; m++) {
                double div = guass[m][j] / guass[j][j];
                for (int n = j; n <= count; n++) {
                    guass[m][n] = guass[m][n] - guass[j][n] * div;
                }
            }
        }
        x_value = get_Value(guass, count);

        return x_value;
    }

    private double[] get_Value(double[][] guass, int count) {
        double[] x = new double[count];
        double[][] X_Array = new double[count][count];

        for (int i = 0; i < count; i++)
            for (int j = 0; j < count; j++)
                X_Array[i][j] = guass[i][j];

        if (2 < count - 1)// 表示有多解
        {
            return null;
        }
        // 回带计算x值
        x[count - 1] = guass[count - 1][count] / guass[count - 1][count - 1];
        for (int i = count - 2; i >= 0; i--) {
            double temp = 0;
            for (int j = i; j < count; j++) {
                temp += x[j] * guass[i][j];
            }
            x[i] = (guass[i][count] - temp) / guass[i][i];
        }

        return x;
    }


    private double[][] get_Array(double[] y, double[] x, int n) {
        double[][] result = new double[n + 1][n + 2];

        if (y.length != x.length) {
            throw new RuntimeException("两个输入数组长度不一！");
        }

        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= n; j++) {
                result[i][j] = cal_sum(x, i + j);
            }
            result[i][n + 1] = cal_multi(y, x, i);
        }

        return result;
    }

    /**
     * 累加的计算
     *
     * @param input
     * @param order
     * @return
     */
    private double cal_sum(double[] input, int order) {
        double result = 0;
        int length = input.length;

        for (int i = 0; i < length; i++) {
            result += Math.pow(input[i], order);
        }

        return result;
    }

    /**
     * 计算∑(x^j)*y
     *
     * @param y
     * @param x
     * @param order
     * @return
     */
    private double cal_multi(double[] y, double[] x, int order) {
        double result = 0;

        int length = x.length;

        for (int i = 0; i < length; i++) {
            result += Math.pow(x[i], order) * y[i];
        }

        return result;
    }


    public double[] fittingX() {
        double[] doubles = new double[30];
        for (int i = 0; i < 30; i++) {
            doubles[i] = i + 1;
        }
        return doubles;
    }


    public double[] fittingData(SalesVolumeDTO salesVolumeDTO) {
        double[] doubles = new double[30];
        doubles[0] = salesVolumeDTO.getThirtydaySales().doubleValue();
        doubles[1] = salesVolumeDTO.getTwentyNinedaySales().doubleValue();
        doubles[2] = salesVolumeDTO.getTwentyEightdaySales().doubleValue();
        doubles[3] = salesVolumeDTO.getTwentySevenedaySales().doubleValue();
        doubles[4] = salesVolumeDTO.getTwentySixdaySales().doubleValue();
        doubles[5] = salesVolumeDTO.getTwentyFivedaySales().doubleValue();
        doubles[6] = salesVolumeDTO.getTwentyFourthdaySales().doubleValue();
        doubles[7] = salesVolumeDTO.getTwentyThreedaySales().doubleValue();
        doubles[8] = salesVolumeDTO.getTwentyTwodaySales().doubleValue();
        doubles[9] = salesVolumeDTO.getTwentyOnedaySales().doubleValue();
        doubles[10] = salesVolumeDTO.getTwentydaySales().doubleValue();
        doubles[11] = salesVolumeDTO.getNineteendaySales().doubleValue();
        doubles[12] = salesVolumeDTO.getEighteendaySales().doubleValue();
        doubles[13] = salesVolumeDTO.getSeventeendaySales().doubleValue();
        doubles[14] = salesVolumeDTO.getSixteendaySales().doubleValue();
        doubles[15] = salesVolumeDTO.getFifteendaySales().doubleValue();
        doubles[16] = salesVolumeDTO.getFourteendaySales().doubleValue();
        doubles[17] = salesVolumeDTO.getThridteendaySales().doubleValue();
        doubles[18] = salesVolumeDTO.getTwelvedaySales().doubleValue();
        doubles[19] = salesVolumeDTO.getElevendaySales().doubleValue();
        doubles[20] = salesVolumeDTO.getTendaySales().doubleValue();
        doubles[21] = salesVolumeDTO.getNinedaySales().doubleValue();
        doubles[22] = salesVolumeDTO.getEightdaySales().doubleValue();
        doubles[23] = salesVolumeDTO.getSevendaySales().doubleValue();
        doubles[24] = salesVolumeDTO.getSixdaySales().doubleValue();
        doubles[25] = salesVolumeDTO.getFivedaySales().doubleValue();
        doubles[26] = salesVolumeDTO.getFourthdaySales().doubleValue();
        doubles[27] = salesVolumeDTO.getThreedaySales().doubleValue();
        doubles[28] = salesVolumeDTO.getTwodaySales().doubleValue();
        doubles[29] = salesVolumeDTO.getOnedaySales().doubleValue();
        return doubles;
    }
}
