using MainShell.Common;
using MainShell.Models.Wafer;
using MainShell.Recipe.BaseBoard.Model;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MainShell.Process
{
    public class DieTransferPathGenerator : IDieTransferPathGenerator
    {
        public DieTransferPathPlan Generate(DieTransferPathRequest request)
        {
            return GenerateByRegion(request);
        }

        public DieTransferPathRegionPlan GenerateByRegion(DieTransferPathRequest request)
        {
            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            DieTransferPlanningContext planningContext = DieTransferPlanningContext.Create(request);
            int effectiveCount = Math.Min(planningContext.AvailablePadCount, planningContext.AvailableDieCount);
            List<PadTransferRow> remainingPadRows;
            List<DieTransferRow> remainingDieRows;
            List<PadTransferRow> activePadRows = DieTransferPlanningContext.TrimPadRows(planningContext.PadRows, effectiveCount, out remainingPadRows);
            List<DieTransferRow> activeDieRows = DieTransferPlanningContext.TrimDieRows(planningContext.DieRows, effectiveCount, out remainingDieRows);
            DieTransferPathRegionPlan pathPlan = GenerateByRows(activePadRows, activeDieRows, planningContext.TransPathType);
            List<Pad> remainingPads = GetOrderedPads(remainingPadRows);
            List<Die> remainingDies = GetOrderedDies(remainingDieRows);

            pathPlan.AvailableDieCount = planningContext.AvailableDieCount;
            pathPlan.AvailablePadCount = planningContext.AvailablePadCount;
            pathPlan.RemainingPads = remainingPads;
            pathPlan.RemainingDies = remainingDies;
            pathPlan.RemainingPadRows = DieTransferPlanningContext.CreatePadRows(remainingPads, planningContext.SubstrateRegion, planningContext.PadRowDirectionStrategy);
            pathPlan.RemainingDieRows = DieTransferPlanningContext.CreateDieRows(remainingDies, planningContext.DieRegion, planningContext.SkipNgDie, planningContext.DieRowDirectionStrategy);
            pathPlan.RemainingSubstrateRegion = CreateRegionFromPads(remainingPads);
            pathPlan.RemainingDieRegion = CreateRegionFromDies(remainingDies);
            return pathPlan;
        }

        public DieTransferPathRegionPlan GenerateByCandidates(
            IReadOnlyCollection<Pad> padCandidates,
            IReadOnlyCollection<Die> dieCandidates,
            TransPathType transPathType,
            bool skipNgDie,
            DieTransferRowTraversalStrategy padRowDirectionStrategy,
            DieTransferRowTraversalStrategy dieRowDirectionStrategy)
        {
            DieTransferPathRequest request = new DieTransferPathRequest();
            request.PadCandidates = padCandidates ?? Array.Empty<Pad>();
            request.DieCandidates = dieCandidates ?? Array.Empty<Die>();
            request.TransPathType = transPathType;
            request.SkipNgDie = skipNgDie;
            request.PadRowDirectionStrategy = padRowDirectionStrategy;
            request.DieRowDirectionStrategy = dieRowDirectionStrategy;

            return GenerateByRegion(request);
        }

        public DieTransferPathRegionPlan GenerateByRows(
            IReadOnlyCollection<PadTransferRow> padRows,
            IReadOnlyCollection<DieTransferRow> dieRows,
            TransPathType transPathType)
        {
            IReadOnlyCollection<PadTransferRow> safePadRows = padRows ?? Array.Empty<PadTransferRow>();
            IReadOnlyCollection<DieTransferRow> safeDieRows = dieRows ?? Array.Empty<DieTransferRow>();
            List<Pad> orderedPads = GetOrderedPads(safePadRows);
            List<Die> orderedDies = GetOrderedDies(safeDieRows);
            List<DieTransferPathStep> steps = CreateSteps(orderedDies, orderedPads, transPathType);
            List<Pad> remainingPads = CreateRemainingPads(orderedPads, steps);
            List<Die> remainingDies = CreateRemainingDies(orderedDies, steps);

            DieTransferPathRegionPlan pathPlan = new DieTransferPathRegionPlan();
            pathPlan.TransPathType = transPathType;
            pathPlan.AvailablePadCount = GetAvailablePadCount(safePadRows);
            pathPlan.AvailableDieCount = GetAvailableDieCount(safeDieRows);
            pathPlan.Steps = steps;
            pathPlan.RemainingPads = remainingPads;
            pathPlan.RemainingDies = remainingDies;
            pathPlan.RemainingPadRows = CreateRemainingPadRows(safePadRows, remainingPads);
            pathPlan.RemainingDieRows = CreateRemainingDieRows(safeDieRows, remainingDies);
            pathPlan.RemainingSubstrateRegion = CreateRegionFromPads(remainingPads);
            pathPlan.RemainingDieRegion = CreateRegionFromDies(remainingDies);
            return pathPlan;
        }

        private static List<DieTransferPathStep> CreateSteps(IReadOnlyList<Die> dies, IReadOnlyList<Pad> pads, TransPathType transPathType)
        {
            return transPathType == TransPathType.Nearest
                ? BuildNearestSteps(dies, pads, transPathType)
                : BuildSequenceSteps(dies, pads, transPathType);
        }

        private static List<DieTransferPathStep> BuildSequenceSteps(IReadOnlyList<Die> dies, IReadOnlyList<Pad> pads, TransPathType transPathType)
        {
            int stepCount = Math.Min(dies.Count, pads.Count);
            List<DieTransferPathStep> steps = new List<DieTransferPathStep>(stepCount);

            for (int i = 0; i < stepCount; i++)
            {
                steps.Add(CreateStep(i + 1, dies[i], pads[i], transPathType));
            }

            return steps;
        }

        private static List<DieTransferPathStep> BuildNearestSteps(IReadOnlyList<Die> dies, IReadOnlyList<Pad> pads, TransPathType transPathType)
        {
            List<Die> remainingDies = (dies ?? Array.Empty<Die>()).Where(die => die != null).ToList();
            List<DieTransferPathStep> steps = new List<DieTransferPathStep>(Math.Min(remainingDies.Count, pads.Count));
            int stepIndex = 1;

            foreach (Pad pad in pads)
            {
                if (remainingDies.Count == 0)
                {
                    break;
                }

                Die nearestDie = SelectNearestDie(pad, remainingDies);
                steps.Add(CreateStep(stepIndex, nearestDie, pad, transPathType));
                remainingDies.Remove(nearestDie);
                stepIndex++;
            }

            return steps;
        }

        private static Die SelectNearestDie(Pad pad, IReadOnlyList<Die> dies)
        {
            Die selectedDie = null;
            double bestDistance = double.MaxValue;

            for (int index = 0; index < dies.Count; index++)
            {
                Die die = dies[index];
                double currentDistance = CalculateSquaredDistance(die.X, die.Y, pad.X, pad.Y);
                if (currentDistance < bestDistance)
                {
                    selectedDie = die;
                    bestDistance = currentDistance;
                }
            }

            return selectedDie;
        }

        private static double CalculateSquaredDistance(double sourceX, double sourceY, double targetX, double targetY)
        {
            double deltaX = sourceX - targetX;
            double deltaY = sourceY - targetY;
            return (deltaX * deltaX) + (deltaY * deltaY);
        }

        private static DieTransferPathStep CreateStep(int stepIndex, Die die, Pad pad, TransPathType transPathType)
        {
            DieTransferPathStep step = new DieTransferPathStep();
            step.StepIndex = stepIndex;
            step.DieRow = die.Row;
            step.DieColumn = die.Column;
            step.PadRow = pad.Row;
            step.PadColumn = pad.Column;
            step.DieX = die.X;
            step.DieY = die.Y;
            step.PadX = pad.X;
            step.PadY = pad.Y;
            step.TransPathType = transPathType;
            return step;
        }

        private static int GetAvailablePadCount(IEnumerable<PadTransferRow> padRows)
        {
            return (padRows ?? Array.Empty<PadTransferRow>())
                .Where(row => row != null)
                .Sum(row => row.AvailableCount);
        }

        private static int GetAvailableDieCount(IEnumerable<DieTransferRow> dieRows)
        {
            return (dieRows ?? Array.Empty<DieTransferRow>())
                .Where(row => row != null)
                .Sum(row => row.AvailableCount);
        }

        private static List<Pad> GetOrderedPads(IEnumerable<PadTransferRow> padRows)
        {
            List<Pad> orderedPads = new List<Pad>();

            foreach (PadTransferRow padRow in (padRows ?? Array.Empty<PadTransferRow>()).Where(row => row != null).OrderBy(row => row.RowIndex))
            {
                orderedPads.AddRange(padRow.GetAvailablePads());
            }

            return orderedPads;
        }

        private static List<Die> GetOrderedDies(IEnumerable<DieTransferRow> dieRows)
        {
            List<Die> orderedDies = new List<Die>();

            foreach (DieTransferRow dieRow in (dieRows ?? Array.Empty<DieTransferRow>()).Where(row => row != null).OrderBy(row => row.RowIndex))
            {
                orderedDies.AddRange(dieRow.GetAvailableDies());
            }

            return orderedDies;
        }

        private static List<PadTransferRow> CreateRemainingPadRows(IEnumerable<PadTransferRow> sourceRows, IReadOnlyCollection<Pad> remainingPads)
        {
            HashSet<string> remainingPadKeys = CreatePointKeys(remainingPads, pad => pad.Row, pad => pad.Column);
            List<PadTransferRow> remainingPadRows = new List<PadTransferRow>();

            foreach (PadTransferRow sourceRow in (sourceRows ?? Array.Empty<PadTransferRow>()).Where(row => row != null).OrderBy(row => row.RowIndex))
            {
                List<Pad> rowPads = (sourceRow.Pads ?? new List<Pad>())
                    .Where(pad => pad != null && remainingPadKeys.Contains(CreatePointKey(pad.Row, pad.Column)))
                    .OrderBy(pad => pad.Column)
                    .ToList();
                if (rowPads.Count == 0)
                {
                    continue;
                }

                PadTransferRow remainingRow = new PadTransferRow();
                remainingRow.RowIndex = sourceRow.RowIndex;
                remainingRow.Direction = sourceRow.Direction;
                remainingRow.Pads = rowPads;
                remainingPadRows.Add(remainingRow);
            }

            return remainingPadRows;
        }

        private static List<DieTransferRow> CreateRemainingDieRows(IEnumerable<DieTransferRow> sourceRows, IReadOnlyCollection<Die> remainingDies)
        {
            HashSet<string> remainingDieKeys = CreatePointKeys(remainingDies, die => die.Row, die => die.Column);
            List<DieTransferRow> remainingDieRows = new List<DieTransferRow>();

            foreach (DieTransferRow sourceRow in (sourceRows ?? Array.Empty<DieTransferRow>()).Where(row => row != null).OrderBy(row => row.RowIndex))
            {
                List<Die> rowDies = (sourceRow.Dies ?? new List<Die>())
                    .Where(die => die != null && remainingDieKeys.Contains(CreatePointKey(die.Row, die.Column)))
                    .OrderBy(die => die.Column)
                    .ToList();
                if (rowDies.Count == 0)
                {
                    continue;
                }

                DieTransferRow remainingRow = new DieTransferRow();
                remainingRow.RowIndex = sourceRow.RowIndex;
                remainingRow.Direction = sourceRow.Direction;
                remainingRow.SkipNgDie = sourceRow.SkipNgDie;
                remainingRow.Dies = rowDies;
                remainingDieRows.Add(remainingRow);
            }

            return remainingDieRows;
        }

        private static HashSet<string> CreatePointKeys<TPoint>(IEnumerable<TPoint> points, Func<TPoint, int> getRow, Func<TPoint, int> getColumn)
            where TPoint : class
        {
            HashSet<string> pointKeys = new HashSet<string>();

            foreach (TPoint point in points ?? Array.Empty<TPoint>())
            {
                if (point == null)
                {
                    continue;
                }

                pointKeys.Add(CreatePointKey(getRow(point), getColumn(point)));
            }

            return pointKeys;
        }

        private static List<Pad> CreateRemainingPads(IReadOnlyList<Pad> orderedPads, IReadOnlyList<DieTransferPathStep> steps)
        {
            HashSet<string> usedPadKeys = new HashSet<string>();
            foreach (DieTransferPathStep step in steps ?? Array.Empty<DieTransferPathStep>())
            {
                usedPadKeys.Add(CreatePointKey(step.PadRow, step.PadColumn));
            }

            return (orderedPads ?? Array.Empty<Pad>())
                .Where(pad => pad != null && !usedPadKeys.Contains(CreatePointKey(pad.Row, pad.Column)))
                .ToList();
        }

        private static List<Die> CreateRemainingDies(IReadOnlyList<Die> orderedDies, IReadOnlyList<DieTransferPathStep> steps)
        {
            HashSet<string> usedDieKeys = new HashSet<string>();
            foreach (DieTransferPathStep step in steps ?? Array.Empty<DieTransferPathStep>())
            {
                usedDieKeys.Add(CreatePointKey(step.DieRow, step.DieColumn));
            }

            return (orderedDies ?? Array.Empty<Die>())
                .Where(die => die != null && !usedDieKeys.Contains(CreatePointKey(die.Row, die.Column)))
                .ToList();
        }

        private static string CreatePointKey(int row, int column)
        {
            return $"{row}:{column}";
        }

        private static DieTransferRegion CreateRegionFromPads(IEnumerable<Pad> pads)
        {
            List<Pad> availablePads = (pads ?? Array.Empty<Pad>()).Where(pad => pad != null).ToList();
            if (availablePads.Count == 0)
            {
                return null;
            }

            DieTransferRegion region = new DieTransferRegion();
            region.StartRow = availablePads.Min(pad => pad.Row);
            region.StartCol = availablePads.Min(pad => pad.Column);
            region.EndRow = availablePads.Max(pad => pad.Row);
            region.EndCol = availablePads.Max(pad => pad.Column);
            return region;
        }

        private static DieTransferRegion CreateRegionFromDies(IEnumerable<Die> dies)
        {
            List<Die> availableDies = (dies ?? Array.Empty<Die>()).Where(die => die != null).ToList();
            if (availableDies.Count == 0)
            {
                return null;
            }

            DieTransferRegion region = new DieTransferRegion();
            region.StartRow = availableDies.Min(die => die.Row);
            region.StartCol = availableDies.Min(die => die.Column);
            region.EndRow = availableDies.Max(die => die.Row);
            region.EndCol = availableDies.Max(die => die.Column);
            return region;
        }
    }
}