using System;
using System.Collections.Generic;
using AsbCloudApp.Data.DetectedOperation;

namespace AsbCloudInfrastructure.Services.DetectOperations.Detectors;

public abstract class DetectorAbstract
{
    protected const int IdReasonOfEnd_NotDetected = 0;
    protected const int IdReasonOfEnd_NotDetectBegin = 1;

    protected const int IdReasonOfEnd_DeltaDepthIsLo = 100;
    protected const int IdReasonOfEnd_DeltaDepthIsHi = 101;
    protected const int IdReasonOfEnd_DeltaDepthOutOfRange = 102;
    protected const int IdReasonOfEnd_WellDepthDeviates = 200;

    protected const int IdReasonOfEnd_PressureIsLo = 300;
    protected const int IdReasonOfEnd_PressureIsHi = 301;
    protected const int IdReasonOfEnd_PressureOutOfRange = 302;
    protected const int IdReasonOfEnd_PressureIsRising = 303;

    protected const int IdReasonOfEnd_RotorSpeedIsLo = 400;
    protected const int IdReasonOfEnd_RotorSpeedIsHi = 401;
    protected const int IdReasonOfEnd_AvgRotorSpeedIsHi = 402;
    protected const int IdReasonOfEnd_AvgRotorSpeedIsLo = 403;

    protected const int IdReasonOfEnd_BlockPositionIsLo = 500;
    protected const int IdReasonOfEnd_BlockPositionIsHi = 501;
    protected const int IdReasonOfEnd_BlockPositionDeviates = 502;

    protected const int IdReasonOfEnd_Drilling = 600;

    protected const int IdReasonOfEnd_ChangeBitDepthAndAxiLoadLessHookWeight = 700;

    protected const int IdReasonOfEnd_DeltaWellDepthAndBitDepthIsLo = 800;

    protected const int IdReasonOfEnd_BitDepthIsLo = 900;

    public bool TryDetect(int idTelemetry, DetectableTelemetry[] telemetry, int begin, int end, DetectedOperationDto? previousOperation,
        out OperationDetectorResult? result)
    {
        // Проверка соответствия критерию начала операции
        if (DetectBegin(telemetry, begin, previousOperation))
        {
            // Поиск окончания соответствия критерию
            int idReasonOfEnd = 0;
            var positionEnd = begin;
            
            while (positionEnd < end)
            {
                positionEnd += 1;
                if (positionEnd > end)
                    break;
                
                //TODO: поиск провалов телеметрий. Следует обсудить, так как алгоритмы теряют в точности.

                idReasonOfEnd = DetectEnd(telemetry, positionEnd, previousOperation);

                if (idReasonOfEnd != IdReasonOfEnd_NotDetected)
                    break;
            }
            
            var (Begin, End) = RefineEdges(telemetry, begin, positionEnd);
            
            result = MakeOperationDetectorResult(idTelemetry, telemetry, Begin, End, idReasonOfEnd);

            return IsValidOperationDetectorResult(result);
        }

        result = null;
        return false;
    }
    
    protected virtual (int Begin, int End) RefineEdges(DetectableTelemetry[] telemetry, int begin, int end)
        => (begin, end);

    protected virtual bool IsValidOperationDetectorResult(OperationDetectorResult operationDetectorResult) 
        => operationDetectorResult.Operation.DateEnd - operationDetectorResult.Operation.DateStart > TimeSpan.FromSeconds(3);

    protected abstract bool DetectBegin(DetectableTelemetry[] telemetry, int position, DetectedOperationDto? previousOperation);

    protected virtual int DetectEnd(DetectableTelemetry[] telemetry, int position, DetectedOperationDto? previousOperation)
        => DetectBegin(telemetry, position, previousOperation)
            ? IdReasonOfEnd_NotDetected
            : IdReasonOfEnd_NotDetectBegin;

    private OperationDetectorResult MakeOperationDetectorResult(
        int idTelemetry,
        DetectableTelemetry[] telemetry, 
        int begin, 
        int end,
        int idReasonOfEnd)
    {
        var operation = MakeDetectedOperation(idTelemetry, telemetry, begin, end);

        operation.ExtraData["IdReasonOfEnd"] = idReasonOfEnd;

        var result = new OperationDetectorResult
        {
            TelemetryBegin = begin,
            TelemetryEnd = end,
            Operation = operation,
        };

        return result;
    }

    private DetectedOperationDto MakeDetectedOperation(int idTelemetry, DetectableTelemetry[] telemetry, int begin, int end)
    {
        var pBegin = telemetry[begin];
        var pEnd = telemetry[end];
        var (IdCategory, ExtraData) = GetSpecificInformation(telemetry, begin, end);
        var operation = new DetectedOperationDto
        {
            IdCategory = IdCategory,
            IdTelemetry = idTelemetry,
            IdUserAtStart = pBegin.IdUser ?? -1,
            DateStart = pBegin.DateTime,
            DateEnd = pEnd.DateTime,
            DepthStart = (double)pBegin.WellDepth,
            DepthEnd = (double)pEnd.WellDepth,
            ExtraData = ExtraData,
            Value = CalcValue(telemetry, begin, end),
            EnabledSubsystems = DetectEnabledSubsystems(telemetry, begin, end, ExtraData)
        };

        return operation;
    }

    /// <summary>
    /// Получение информации специфичной для конкретного детектора
    /// IdCategory - одна из констант WellOperationCategory
    /// ExtraData - дополнительная информация для отладки алгоритмов авто определения
    /// </summary>
    /// <returns></returns>
    protected abstract (int IdCategory, IDictionary<string, object> ExtraData) GetSpecificInformation(DetectableTelemetry[] telemetry, int begin, int end);

    /// <summary>
    /// Расчет ключевого параметра операции
    /// </summary>
    /// <param name="telemetry"></param>
    /// <param name="begin"></param>
    /// <param name="end"></param>
    /// <returns></returns>
    protected abstract double CalcValue(DetectableTelemetry[] telemetry, int begin, int end);
    
    /// <summary>
    /// Определение включенных подсистем во время выполнения операции
    /// </summary>
    /// <param name="telemetry"></param>
    /// <param name="begin"></param>
    /// <param name="end"></param>
    /// <param name="extraData"></param>
    /// <returns></returns>
    private static int DetectEnabledSubsystems(DetectableTelemetry[] telemetry, int begin, int end, IDictionary<string, object> extraData)
    {
        var enabledSubsystems = 0;

        if (extraData.TryGetValue(DetectorDrilling.ExtraDataKeyHasOscillation, out var hasOscillation)
            && hasOscillation is true)
            enabledSubsystems |= (int)EnabledSubsystemsFlags.AutoOscillation;
        
        for (var i = begin; i < end; i += 2)
        {
            var mode = telemetry[i].Mode;

            if(mode == 1)
                enabledSubsystems |= (int)EnabledSubsystemsFlags.AutoRotor;

            if (mode == 3)
                enabledSubsystems |= (int)EnabledSubsystemsFlags.AutoSlide;

            if (mode == 2)
                enabledSubsystems |= (int)EnabledSubsystemsFlags.AutoConditionig;

            if (mode == 4)
                enabledSubsystems |= (int)EnabledSubsystemsFlags.AutoSinking;

            if (mode == 5)
                enabledSubsystems |= (int)EnabledSubsystemsFlags.AutoLifting;

            if (mode == 6)
                enabledSubsystems |= (int)EnabledSubsystemsFlags.AutoLiftingWithConditionig;

            if (mode == 10)
                enabledSubsystems |= (int)EnabledSubsystemsFlags.AutoBlocknig;
        }

        return enabledSubsystems;
    }

    /// <summary>
    /// расчет продолжительности операции
    /// </summary>
    /// <param name="telemetry"></param>
    /// <param name="begin"></param>
    /// <param name="end"></param>
    /// <returns></returns>
    protected static double CalcDeltaMinutes(DetectableTelemetry[] telemetry, int begin, int end)
    {
        var pBegin = telemetry[begin];
        var pEnd = telemetry[end];
        var result = (pEnd.DateTime - pBegin.DateTime).TotalMinutes;
        return result;
    }

    /// <summary>
    /// часто используемый предикат для определения отсутствия изменения глубины ствола скважины
    /// </summary>
    /// <param name="telemetry"></param>
    /// <param name="begin"></param>
    /// <param name="end"></param>
    /// <returns></returns>
    protected static bool IsValidByWellDepthDoesNotChange(DetectableTelemetry[] telemetry, int begin, int end)
    {
        var pBegin = telemetry[begin];
        var pEnd = telemetry[end];
        if (Math.Abs((double)(pBegin.WellDepth - pEnd.WellDepth)) > 0.01)
            return false;
        return true;
    }

    protected static bool IsValidByWellDepthIncreasing(DetectableTelemetry[] telemetry, int begin, int end)
    {
        var pBegin = telemetry[begin];
        var pEnd = telemetry[end];
        if (pBegin.WellDepth >= pEnd.WellDepth)
            return false;
        return true;
    }

    protected static double CalcRop(DetectableTelemetry[] telemetry, int begin, int end)
    {
        var pBegin = telemetry[begin];
        var pEnd = telemetry[end];
        var result = (double)(pEnd.WellDepth - pBegin.WellDepth) / (pEnd.DateTime - pBegin.DateTime).TotalHours;
        return result;
    }

    /// <summary>
    /// Расчет статистики по массиву данных за интервал
    /// </summary>
    /// <param name="telemetry"></param>
    /// <param name="getter"></param>
    /// <param name="begin"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    protected static (double min, double max, double sum, int count) CalcStat(
        DetectableTelemetry[] telemetry,
        Func<DetectableTelemetry, double> getter,
        int begin,
        int count)
    {
        var sum = 0d;
        var min = double.MaxValue;
        var max = double.MinValue;
        var end = begin + count;
        end = end < telemetry.Length ? end : telemetry.Length;

        for (var i = begin; i < end; i++)
        {
            var item = telemetry[i];
            var itemValue = getter(item);
            if (min > itemValue)
                min = itemValue;
            if (max < itemValue)
                max = itemValue;
            sum += itemValue;
        }

        return (min, max, sum, end - begin);
    }

    /// <summary>
    /// Максимальное отклонение от среднего за интервал
    /// </summary>
    /// <param name="telemetry"></param>
    /// <param name="getter"></param>
    /// <param name="begin"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    protected static double CalcMaxDeviation(
        DetectableTelemetry[] telemetry,
        Func<DetectableTelemetry, double> getter,
        int begin,
        int count)
    {
        var stat = CalcStat(telemetry, getter, begin, count);
        var avg = stat.sum / stat.count;
        var dev1 = avg - stat.min;
        var dev2 = stat.max - avg;
        var dev = dev1 > dev2 ? dev1 : dev2;
        return dev;
    }

    /// <summary>
    /// Определяет наличие разброса значений в интервале большего указанного значения.
    /// </summary>
    /// <param name="telemetry"></param>
    /// <param name="getter"></param>
    /// <param name="begin"></param>
    /// <param name="count"></param>
    /// <param name="deviation"></param>
    /// <returns></returns>
    protected static bool ContainsDeviation(
        DetectableTelemetry[] telemetry,
        Func<DetectableTelemetry, double> getter,
        int begin,
        int count,
        double deviation)
    {
        var min = double.MaxValue;
        var max = double.MinValue;
        var end = begin + count;
        end = end < telemetry.Length ? end : telemetry.Length;

        for (var i = begin; i < end; i++)
        {
            var item = telemetry[i];
            var itemValue = getter(item);
            if (min > itemValue)
                min = itemValue;
            if (max < itemValue)
                max = itemValue;
            if (max - min > deviation)
                return true;
        }

        return false;
    }

    /// <summary>
    /// Определяет наличие разброса значений в интервале большего указанного значения. По нескольким значениям из интервала.
    /// </summary>
    /// <param name="telemetry"></param>
    /// <param name="getter"></param>
    /// <param name="begin"></param>
    /// <param name="count"></param>
    /// <param name="deviation"></param>
    /// <returns></returns>
    protected static bool ContainsDeviationApprox(
        DetectableTelemetry[] telemetry,
        Func<DetectableTelemetry, double> getter,
        int begin,
        int count,
        double deviation)
    {
        var min = double.MaxValue;
        var max = double.MinValue;
        var end = begin + count;
        end = end < telemetry.Length ? end : telemetry.Length;
        var step = count > 15 ? count / 5 : count > 3 ? 3 : 1;
        for (var i = begin; i < end; i += step)
        {
            var item = telemetry[i];
            var itemValue = getter(item);
            if (min > itemValue)
                min = itemValue;
            if (max < itemValue)
                max = itemValue;
            if (max - min > deviation)
                return true;
        }

        return false;
    }

    protected static bool DeviatesFromBegin(
        DetectableTelemetry[] telemetry,
        Func<DetectableTelemetry, double> getter,
        int begin,
        int count,
        double deviation)
    {
        var beginPointValue = getter(telemetry[begin]);
        var end = begin + count;
        end = end < telemetry.Length ? end : telemetry.Length;
        var step = count > 15 ? count / 5 : count > 3 ? 3 : 1;
        for (var i = begin; i < end; i += step)
        {
            var item = telemetry[i];
            var itemValue = getter(item);
            if (Math.Abs(beginPointValue - itemValue) > deviation)
                return true;
        }

        return false;
    }

    protected static bool RisesFromBegin(
        DetectableTelemetry[] telemetry,
        Func<DetectableTelemetry, double> getter,
        int begin,
        int count,
        double deviation)
    {
        var beginPointValue = getter(telemetry[begin]);
        var end = begin + count;
        end = end < telemetry.Length ? end : telemetry.Length;
        var step = count > 15 ? count / 5 : count > 3 ? 3 : 1;
        for (var i = begin; i < end; i += step)
        {
            var item = telemetry[i];
            var itemValue = getter(item);
            if (itemValue - beginPointValue > deviation)
                return true;
        }

        return false;
    }
}