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

namespace AsbCloudInfrastructure.Services.DetectOperations.Detectors
{
    public abstract class DetectorAbstract
    {
        private readonly int stepLength = 3;

        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_Custom1 = 10_000;

        public bool TryDetect(int idTelemetry, DetectableTelemetry[] telemetry, int begin, int end, DetectedOperation? previousOperation,
            out OperationDetectorResult? result)
        {
            // Проверка соответствия критерию начала операции
            if (DetectBegin(telemetry, begin, previousOperation))
            {
                // Поиск окончания соответствия критерию
                int idReasonOfEnd = 0;
                var positionEnd = begin;
                while (positionEnd < end)
                {
                    positionEnd += stepLength;
                    if (positionEnd > end)
                        break;

                    idReasonOfEnd = DetectEnd(telemetry, positionEnd, previousOperation);

                    if (idReasonOfEnd != IdReasonOfEnd_NotDetected)
                        break;
                }

                var (Begin, End) = RefineEdges(telemetry, begin, positionEnd);

                if (!IsValidTelemetryRange(telemetry, Begin, End))
                {
                    result = null;
                    return false;
                }

                result = MakeOperationDetectorResult(idTelemetry, telemetry, Begin, End, idReasonOfEnd);

                return IsValidOperationDetectorResult(result);
            }

            result = null;
            return false;
        }

        protected virtual bool IsValidTelemetryRange(DetectableTelemetry[] telemetry, int begin, int end)
            => end - begin > 1;

        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, DetectedOperation? previousOperation);

        protected virtual int DetectEnd(DetectableTelemetry[] telemetry, int position, DetectedOperation? 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 DetectedOperation 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 DetectedOperation
            {
                IdCategory = IdCategory,
                IdTelemetry = idTelemetry,
                IdUsersAtStart = 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;
        }
    }
}