namespace ReactorMaintenance.Simulation;

public static class JunctionFlowAnalyzer
{
    private sealed record SourceBranch(GridPosition Position, int? Distance);

    public static IReadOnlyList<JunctionFlow> Analyze(LevelState level)
    {
        var flows = new List<JunctionFlow>();

        for (var y = 0; y < level.Height; y++)
        {
            for (var x = 0; x < level.Width; x++)
            {
                var position = new GridPosition(x, y);
                var prop = level.GetProp(position);
                if (prop.Type != EPropType.Junction)
                    continue;

                flows.Add(AnalyzeJunction(level, position, prop));
            }
        }

        return flows;
    }

    private static JunctionFlow AnalyzeJunction(LevelState level, GridPosition position, PropState prop)
    {
        var errors = new List<string>();
        var carriers = Enum.GetValues<ECarrierType>().Where(carrier => level.GetUnderground(position, carrier).IsPresent).ToArray();
        var carrier = carriers.FirstOrDefault();
        if (carriers.Length != 1)
            errors.Add("Junction must regulate exactly one underground carrier.");

        var branches = carriers.Length == 1
            ? position.Neighbors().Where(level.InBounds).Where(neighbor => level.GetUnderground(neighbor, carrier).CarriesFlow).ToArray()
            : Array.Empty<GridPosition>();

        if (carriers.Length == 1 && branches.Length is not 3 and not 4)
            errors.Add("Junction must have one incoming branch and two or three outgoing branches.");

        var sourceBranches = carriers.Length == 1
            ? branches.Select(branch => new SourceBranch(branch, ShortestDistanceToSource(level, branch, position, carrier)))
                      .Where(branch => branch.Distance.HasValue)
                      .ToArray()
            : Array.Empty<SourceBranch>();

        GridPosition? incomingBranch = null;
        if (sourceBranches.Length > 0)
        {
            var bestDistance = sourceBranches.Min(branch => branch.Distance!.Value);
            var bestBranches = sourceBranches.Where(branch => branch.Distance == bestDistance).ToArray();
            if (bestBranches.Length != 1 || sourceBranches.Length != 1)
                errors.Add("Ambiguous junction flow.");
            else
                incomingBranch = bestBranches[0].Position;
        }

        var outgoingBranches = incomingBranch is null
            ? Array.Empty<GridPosition>()
            : branches.Where(branch => branch != incomingBranch).ToArray();

        return new() {
            Position = position,
            Prop = prop,
            Carrier = carrier,
            Branches = branches,
            IncomingBranch = incomingBranch,
            OutgoingBranches = outgoingBranches,
            Errors = errors
        };
    }

    private static int? ShortestDistanceToSource(LevelState level, GridPosition start, GridPosition blocked, ECarrierType carrier)
    {
        var visited = new HashSet<GridPosition> { blocked, start };
        var open = new Queue<(GridPosition Position, int Distance)>();
        open.Enqueue((start, 0));

        while (open.Count > 0)
        {
            var current = open.Dequeue();
            if (level.GetProp(current.Position) is { Type: EPropType.Flow, Carrier: var sourceCarrier, SwitchState: EPropSwitchState.Enabled } && sourceCarrier == carrier)
                return current.Distance;

            foreach (var next in current.Position.Neighbors().Where(level.InBounds))
            {
                if (!visited.Add(next) || !level.GetUnderground(next, carrier).CarriesFlow)
                    continue;

                open.Enqueue((next, current.Distance + 1));
            }
        }

        return null;
    }
}