{
  Solutions to the Advent Of Code.
  Copyright (C) 2024  Stefan Müller

  This program is free software: you can redistribute it and/or modify it under
  the terms of the GNU General Public License as published by the Free Software
  Foundation, either version 3 of the License, or (at your option) any later
  version.

  This program is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along with
  this program.  If not, see <http://www.gnu.org/licenses/>.
}

unit UClumsyCrucible;

{$mode ObjFPC}{$H+}

interface

uses
  Classes, SysUtils, Generics.Collections, Math, USolver, UCommon;

type

  { TAxisData }

  TAxisData = record
    // The current minimum total heat loss to get from this node to the end if the first step is on this axis.
    Minimum: Cardinal;
    // True, if and only if the minimum has been set, i.e. a path has been found from this node to the end, with the
    // first step on this axis.
    IsTraversed,
    // True. if a close node was updated (traversed) on the other axis, such that this minimum might be outdated. This
    // means that if this node is on the work list, NeedsUpdate is True for at least one of the axes.
    NeedsUpdate: Boolean;
  end;

  TAxisId = (axHorizontal, axVertical);

const
  CAxisDirections: array[TAxisId] of array[0..1] of PPoint
    = ((@CDirectionRight, @CDirectionLeft), (@CDirectionDown, @CDirectionUp));
  COtherAxes: array[TAxisId] of TAxisId = (axVertical, axHorizontal);

type
  { TNode }

  TNode = record
    Axes: array[TAxisId] of TAxisData;
    LocalHeatLoss: Byte;
  end;
  PNode = ^TNode;

  TNodeArray = array of TNode;
  TNodeArrays = specialize TList<TNodeArray>;

  TWorkQueue = specialize TQueue<TPoint>;

  { TNodeMap }

  TNodeMap = class
  private
    // Each item in FNodes is a horizontal row of nodes.
    FNodes: TNodeArrays;
    FWidth: Integer;
    FMinStraight, FMaxStraight: Integer;
    function GetHeight: Integer;
    function GetNode(APosition: TPoint): TNode;
    function GetPNode(APosition: TPoint): PNode;
    function IsPositionInMap(constref APosition: TPoint): Boolean;
    procedure ClampPositionToMap(var APosition: TPoint);
    procedure InitWorkQueue(constref AWorkQueue: TWorkQueue);
    procedure InvalidateNeighbors(constref AWorkQueue: TWorkQueue; const AAxis: TAxisId; constref APosition: TPoint);
    function FindStepNodeMinimum(const AAxis: TAxisId; constref APosition: TPoint): Cardinal;
  public
    property Width: Integer read FWidth;
    property Height: Integer read GetHeight;
    constructor Create;
    destructor Destroy; override;
    procedure AddNodes(const ALine: string);
    function FindMinimumPathLength(const AMinStraight, AMaxStraight: Integer): Cardinal;
    procedure Reset;
  end;

  { TClumsyCrucible }

  TClumsyCrucible = class(TSolver)
  private
    FMap: TNodeMap;
  public
    constructor Create;
    destructor Destroy; override;
    procedure ProcessDataLine(const ALine: string); override;
    procedure Finish; override;
    function GetDataFileName: string; override;
    function GetPuzzleName: string; override;
  end;

implementation

const
  CMinStraight = 1;
  CMaxStraight = 3;
  CUltraMinStraight = 4;
  CUltraMaxStraight = 10;

{ TNodeMap }

function TNodeMap.GetHeight: Integer;
begin
  Result := FNodes.Count;
end;

function TNodeMap.GetNode(APosition: TPoint): TNode;
begin
  Result := FNodes[APosition.Y][APosition.X];
end;

function TNodeMap.GetPNode(APosition: TPoint): PNode;
begin
  Result := @FNodes[APosition.Y][APosition.X];
end;

function TNodeMap.IsPositionInMap(constref APosition: TPoint): Boolean;
begin
  Result := (0 <= APosition.X) and (APosition.X < Width) and (0 <= APosition.Y) and (APosition.Y < Height);
end;

procedure TNodeMap.ClampPositionToMap(var APosition: TPoint);
begin
  if APosition.X < -1 then
    APosition.X := -1
  else if APosition.X > Width then
    APosition.X := Width;
  if APosition.Y < -1 then
    APosition.Y := -1
  else if APosition.Y > Height then
    APosition.Y := Height;
end;

procedure TNodeMap.InitWorkQueue(constref AWorkQueue: TWorkQueue);
var
  position: TPoint;
  last: PNode;
  axis: TAxisId;
begin
  // Initializes the end node and the work queue with its neighbors.
  position := Point(Width - 1, Height - 1);
  last := GetPNode(position);
  for axis in TAxisId do
  begin
    last^.Axes[axis].Minimum := 0;
    last^.Axes[axis].IsTraversed := True;
  end;
  InvalidateNeighbors(AWorkQueue, axHorizontal, position);
  InvalidateNeighbors(AWorkQueue, axVertical, position);
end;

procedure TNodeMap.InvalidateNeighbors(constref AWorkQueue: TWorkQueue; const AAxis: TAxisId; constref APosition:
  TPoint);
var
  otherAxis: TAxisId;
  nodeMinimum: Cardinal;
  direction: PPoint;
  neighborPos, stop: TPoint;
  neighbor: PNode;
begin
  otherAxis := COtherAxes[AAxis];
  nodeMinimum := GetNode(APosition).Axes[otherAxis].Minimum;

  for direction in CAxisDirections[AAxis] do
  begin
    neighborPos := Point(APosition.X + direction^.X * FMinStraight, APosition.Y + direction^.Y * FMinStraight);
    if IsPositionInMap(neighborPos) then
    begin
      stop := Point(APosition.X + direction^.X * (FMaxStraight + 1), APosition.Y + direction^.Y * (FMaxStraight + 1));
      ClampPositionToMap(stop);
      while neighborPos <> stop do
      begin
        neighbor := GetPNode(neighborPos);
        if not neighbor^.Axes[AAxis].NeedsUpdate
        and (not neighbor^.Axes[AAxis].IsTraversed or (neighbor^.Axes[AAxis].Minimum > nodeMinimum)) then
        begin
          neighbor^.Axes[AAxis].NeedsUpdate := True;
          if not neighbor^.Axes[otherAxis].NeedsUpdate then
            AWorkQueue.Enqueue(neighborPos);
        end;
        neighborPos := neighborPos + direction^;
      end;
    end;
  end;
end;

function TNodeMap.FindStepNodeMinimum(const AAxis: TAxisId; constref APosition: TPoint): Cardinal;
var
  otherAxis: TAxisId;
  direction: PPoint;
  acc: Cardinal;
  neighborPos, start, stop: TPoint;
  isStartReached: Boolean;
  neighbor: TNode;
begin
  otherAxis := COtherAxes[AAxis];
  Result := Cardinal.MaxValue;

  for direction in CAxisDirections[AAxis] do
  begin
    acc := 0;
    isStartReached := False;
    neighborPos := APosition + direction^;
    start := Point(APosition.X + direction^.X * FMinStraight, APosition.Y + direction^.Y * FMinStraight);
    if IsPositionInMap(start) then
    begin
      stop := Point(APosition.X + direction^.X * (FMaxStraight + 1), APosition.Y + direction^.Y * (FMaxStraight + 1));
      ClampPositionToMap(stop);
      while neighborPos <> stop do
      begin
        if neighborPos = start then
          isStartReached := True;
        neighbor := GetNode(neighborPos);
        Inc(acc, neighbor.LocalHeatLoss);
        if isStartReached and neighbor.Axes[otherAxis].IsTraversed then
          Result := Min(Result, neighbor.Axes[otherAxis].Minimum + acc);
        neighborPos := neighborPos + direction^;
      end;
    end;
  end;
end;

constructor TNodeMap.Create;
begin
  FNodes := TNodeArrays.Create;
end;

destructor TNodeMap.Destroy;
begin
  FNodes.Free;
  inherited Destroy;
end;

procedure TNodeMap.AddNodes(const ALine: string);
var
  i: Integer;
  nodes: TNodeArray;
  axis: TAxisId;
begin
  FWidth := Length(ALine);
  SetLength(nodes, FWidth);
  for i := 0 to FWidth - 1 do
  begin
    nodes[i].LocalHeatLoss := StrToInt(ALine[i + 1]);
    for axis in TAxisId do
    begin
      nodes[i].Axes[axis].IsTraversed := False;
      nodes[i].Axes[axis].NeedsUpdate := False;
    end;
  end;
  FNodes.Add(nodes);
end;

function TNodeMap.FindMinimumPathLength(const AMinStraight, AMaxStraight: Integer): Cardinal;
var
  queue: TWorkQueue;
  position: TPoint;
  node: PNode;
  axis: TAxisId;
  start: TNode;
  newMinimum: Cardinal;
begin
  FMinStraight := AMinStraight;
  FMaxStraight := AMaxStraight;

  queue := TWorkQueue.Create;
  InitWorkQueue(queue);

  // Processes work queue.
  while queue.Count > 0 do
  begin
    position := queue.Dequeue;
    node := GetPNode(position);

    for axis in TAxisId do
      if node^.Axes[axis].NeedsUpdate then
      begin
        node^.Axes[axis].NeedsUpdate := False;
        // Finds minimum for one step from this node along this axis.
        newMinimum := FindStepNodeMinimum(axis, position);
        if not node^.Axes[axis].IsTraversed or (node^.Axes[axis].Minimum > newMinimum) then
        begin
          // Updates this axis minimum and queues update for neighbors on the other axis.
          node^.Axes[axis].IsTraversed := True;
          node^.Axes[axis].Minimum := newMinimum;
          InvalidateNeighbors(queue, COtherAxes[axis], position);
        end;
      end;
  end;

  queue.Free;

  start := GetNode(Point(0, 0));
  Result := Min(start.Axes[axHorizontal].Minimum, start.Axes[axVertical].Minimum);
end;

procedure TNodeMap.Reset;
var
  i, j: Integer;
  axis: TAxisId;
begin
  for i := 0 to Width - 1 do
    for j := 0 to Height - 1 do
      for axis in TAxisId do
      begin
        FNodes[j][i].Axes[axis].IsTraversed := False;
        FNodes[j][i].Axes[axis].NeedsUpdate := False;
      end;
end;

{ TClumsyCrucible }

constructor TClumsyCrucible.Create;
begin
  FMap := TNodeMap.Create;
end;

destructor TClumsyCrucible.Destroy;
begin
  FMap.Free;
  inherited Destroy;
end;

procedure TClumsyCrucible.ProcessDataLine(const ALine: string);
begin
  FMap.AddNodes(ALine);
end;

procedure TClumsyCrucible.Finish;
begin
  FPart1 := FMap.FindMinimumPathLength(CMinStraight, CMaxStraight);
  FMap.Reset;
  FPart2 := FMap.FindMinimumPathLength(CUltraMinStraight, CUltraMaxStraight);
end;

function TClumsyCrucible.GetDataFileName: string;
begin
  Result := 'clumsy_crucible.txt';
end;

function TClumsyCrucible.GetPuzzleName: string;
begin
  Result := 'Day 17: Clumsy Crucible';
end;

end.