{
  Solutions to the Advent Of Code.
  Copyright (C) 2023  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 .
}
unit UPointOfIncidence;
{$mode ObjFPC}{$H+}
interface
uses
  Classes, SysUtils, Generics.Collections, Math, USolver;
type
  TMirrorCompareResult = (mcSame, mcOneDifference, mcDifferent);
  { TMirrorCandidate }
  TMirrorCandidate = record
    Position: Integer;
    HasFix: Boolean;
  end;
  TMirrorCandidates = specialize TList;
  { TPointOfIncidence }
  TPointOfIncidence = class(TSolver)
  private
    FLines: TStringList;
    FHorizontalMirrorCandidates, FVerticalMirrorCandidates: specialize TList;
    FHorizontalFixedMirrorCandidates, FVerticalFixedMirrorCandidates: TMirrorCandidates;
    procedure ProcessPatternLine(const ALine: string);
    procedure VerifyHorizontalCandidates(const ANewLine: string);
    procedure VerifyHorizontalFixedCandidates(const ANewLine: string);
    procedure CheckForNewHorizontalCandidate(const ANewLine: string);
    function HaveSingleDifference(const ALine1, ALine2: string): Boolean;
    procedure VerifyVerticalCandidates(const ALine: string);
    procedure VerifyVerticalFixedCandidates(const ALine: string);
    procedure InitializeNewVerticalCandidates(const AFirstLine: string);
    function IsVerticalMirrorOfLine(const ALine: string; const AMirror: Integer): TMirrorCompareResult;
    procedure FinishPattern;
    function GetMirrorCandidate(const APosition: Integer; const AHasFix: Boolean): TMirrorCandidate;
    procedure SetMirrorCandidateFixed(const AMirrorCandidates: TMirrorCandidates; const AIndex: Integer);
  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
{ TPointOfIncidence }
procedure TPointOfIncidence.ProcessPatternLine(const ALine: string);
begin
  VerifyHorizontalCandidates(ALine);
  VerifyHorizontalFixedCandidates(ALine);
  CheckForNewHorizontalCandidate(ALine);
  if FLines.Count > 0 then
  begin
    VerifyVerticalCandidates(ALine);
    VerifyVerticalFixedCandidates(ALine);
  end
  else
    InitializeNewVerticalCandidates(ALine);
  FLines.Add(ALine);
end;
procedure TPointOfIncidence.VerifyHorizontalCandidates(const ANewLine: string);
var
  i, mirroredIndex: Integer;
begin
  // Checks if the current line fulfills the exisiting horizontal mirror candidates.
  i := FHorizontalMirrorCandidates.Count - 1;
  while i >= 0 do
  begin
    // Calculates the index of the line that the current line would be a duplicate of in regards to the i-th candidate.
    mirroredIndex := 2 * FHorizontalMirrorCandidates[i] - FLines.Count - 1;
    if (mirroredIndex >= 0) and (ANewLine <> FLines[mirroredIndex]) then
      FHorizontalMirrorCandidates.Delete(i);
    Dec(i);
  end;
end;
procedure TPointOfIncidence.VerifyHorizontalFixedCandidates(const ANewLine: string);
var
  i, mirroredIndex: Integer;
begin
  // Checks if the current line fulfills the exisiting horizontal mirror candidates.
  i := FHorizontalFixedMirrorCandidates.Count - 1;
  while i >= 0 do
  begin
    // Calculates the index of the line that the current line would be a duplicate of in regards to the i-th candidate.
    mirroredIndex := 2 * FHorizontalFixedMirrorCandidates[i].Position - FLines.Count - 1;
    if mirroredIndex >= 0 then
    begin
      if ANewLine <> FLines[mirroredIndex] then
      begin
        if not FHorizontalFixedMirrorCandidates[i].HasFix
        and HaveSingleDifference(ANewLine, FLines[mirroredIndex]) then
          SetMirrorCandidateFixed(FHorizontalFixedMirrorCandidates, i)
        else
          FHorizontalFixedMirrorCandidates.Delete(i);
      end;
    end;
    Dec(i);
  end;
end;
procedure TPointOfIncidence.CheckForNewHorizontalCandidate(const ANewLine: string);
begin
  // Checks for new horizontal mirror candidate between ALine and the previous line.
  if FLines.Count > 0 then
  begin
    if ANewLine = FLines[FLines.Count - 1] then
    begin
      FHorizontalMirrorCandidates.Add(FLines.Count);
      FHorizontalFixedMirrorCandidates.Add(GetMirrorCandidate(FLines.Count, False));
    end
    else if HaveSingleDifference(ANewLine, FLines[FLines.Count - 1]) then
      FHorizontalFixedMirrorCandidates.Add(GetMirrorCandidate(FLines.Count, True));
  end;
end;
function TPointOfIncidence.HaveSingleDifference(const ALine1, ALine2: string): Boolean;
var
  i: Integer;
begin
  Result := False;
  for i := 1 to Length(ALine1) do
  begin
    if ALine1[i] <> ALine2[i] then
    begin
      if Result then
      begin
        // Found a second difference between the two lines, return False.
        Result := False;
        Exit;
      end
      else
        // Found a difference between the two lines.
        Result := True;
    end;
  end;
end;
procedure TPointOfIncidence.VerifyVerticalCandidates(const ALine: string);
var
  i: Integer;
begin
  // Checks if the current line fulfills the exisiting vertical mirror candidates.
  i := FVerticalMirrorCandidates.Count - 1;
  while i >= 0 do
  begin
    if IsVerticalMirrorOfLine(ALine, FVerticalMirrorCandidates[i]) <> mcSame then
      FVerticalMirrorCandidates.Delete(i);
    Dec(i);
  end;
end;
procedure TPointOfIncidence.VerifyVerticalFixedCandidates(const ALine: string);
var
  i: Integer;
begin
  i := FVerticalFixedMirrorCandidates.Count - 1;
  while i >= 0 do
  begin
    case IsVerticalMirrorOfLine(ALine, FVerticalFixedMirrorCandidates[i].Position) of
      mcOneDifference:
        if FVerticalFixedMirrorCandidates[i].HasFix then
          FVerticalFixedMirrorCandidates.Delete(i)
        else
          SetMirrorCandidateFixed(FVerticalFixedMirrorCandidates, i);
      mcDifferent:
        FVerticalFixedMirrorCandidates.Delete(i);
    end;
    Dec(i);
  end;
end;
procedure TPointOfIncidence.InitializeNewVerticalCandidates(const AFirstLine: string);
var
  i: Integer;
begin
  // Checks the line for vertical mirror candidates, i.e. mirrors that work for this line.
  for i := 1 to Length(AFirstLine) - 1 do
    case IsVerticalMirrorOfLine(AFirstLine, i) of
      mcSame: begin
        FVerticalMirrorCandidates.Add(i);
        FVerticalFixedMirrorCandidates.Add(GetMirrorCandidate(i, False));
      end;
      mcOneDifference:
        FVerticalFixedMirrorCandidates.Add(GetMirrorCandidate(i, True));
    end;
end;
function TPointOfIncidence.IsVerticalMirrorOfLine(const ALine: string; const AMirror: Integer): TMirrorCompareResult;
var
  i : Integer;
begin
  Result := mcSame;
  for i := 0 to Min(AMirror - 1, Length(ALine) - AMirror - 1) do
  begin
    if ALine[AMirror - i] <> ALine[AMirror + i + 1] then
    begin
      if Result = mcSame then
        Result := mcOneDifference
      else begin
        Result := mcDifferent;
        Break;
      end;
    end;
  end;
end;
procedure TPointOfIncidence.FinishPattern;
var
  i: Integer;
begin
  for i := 0 to FHorizontalMirrorCandidates.Count - 1 do
    Inc(FPart1, FHorizontalMirrorCandidates[i] * 100);
  for i := 0 to FVerticalMirrorCandidates.Count - 1 do
    Inc(FPart1, FVerticalMirrorCandidates[i]);
  for i := 0 to FHorizontalFixedMirrorCandidates.Count - 1 do
    if FHorizontalFixedMirrorCandidates[i].HasFix then
      Inc(FPart2, FHorizontalFixedMirrorCandidates[i].Position * 100);
  for i := 0 to FVerticalFixedMirrorCandidates.Count - 1 do
    if FVerticalFixedMirrorCandidates[i].HasFix then
      Inc(FPart2, FVerticalFixedMirrorCandidates[i].Position);
  FLines.Clear;
  FHorizontalMirrorCandidates.Clear;
  FVerticalMirrorCandidates.Clear;
  FHorizontalFixedMirrorCandidates.Clear;
  FVerticalFixedMirrorCandidates.Clear;
end;
function TPointOfIncidence.GetMirrorCandidate(const APosition: Integer; const AHasFix: Boolean): TMirrorCandidate;
begin
  Result.Position := APosition;
  Result.HasFix := AHasFix;
end;
procedure TPointOfIncidence.SetMirrorCandidateFixed(const AMirrorCandidates: TMirrorCandidates; const AIndex: Integer);
var
  mirrorCandidate: TMirrorCandidate;
begin
  mirrorCandidate := AMirrorCandidates[AIndex];
  mirrorCandidate.HasFix := True;
  AMirrorCandidates[AIndex] := mirrorCandidate;
end;
constructor TPointOfIncidence.Create;
begin
  FLines := TStringList.Create;
  FHorizontalMirrorCandidates := specialize TList.Create;
  FVerticalMirrorCandidates := specialize TList.Create;
  FHorizontalFixedMirrorCandidates := TMirrorCandidates.Create;
  FVerticalFixedMirrorCandidates := TMirrorCandidates.Create;
end;
destructor TPointOfIncidence.Destroy;
begin
  FLines.Free;
  FHorizontalMirrorCandidates.Free;
  FVerticalMirrorCandidates.Free;
  FHorizontalFixedMirrorCandidates.Free;
  FVerticalFixedMirrorCandidates.Free;
  inherited Destroy;
end;
procedure TPointOfIncidence.ProcessDataLine(const ALine: string);
begin
  if ALine <> '' then
    ProcessPatternLine(ALine)
  else
    FinishPattern;
end;
procedure TPointOfIncidence.Finish;
begin
  FinishPattern;
end;
function TPointOfIncidence.GetDataFileName: string;
begin
  Result := 'point_of_incidence.txt';
end;
function TPointOfIncidence.GetPuzzleName: string;
begin
  Result := 'Day 13: Point of Incidence';
end;
end.