program ExtractPascalInterfaces;

{$APPTYPE CONSOLE}

uses
    System.SysUtils,
    System.IOUtils,
    System.Classes,
    System.Generics.Collections,
    System.Generics.Defaults,
    System.StrUtils,
    System.Types,
    System.Character,
    Winapi.Windows; // For clipboard access

type
    { Forward declaration for TUnitInfoList needed in TUnitInfo }
    TUnitInfo = class;
    TUnitInfoList = TList<TUnitInfo>;

    { TUnitInfo holds information about a single Pascal unit }
    TUnitInfo = class
    private
        FOriginalLines: TStringList;
        procedure ExtractUsesFromClause(const Clause: string);
        function StripCommentsFromUsesBlock(const Block: string): string;
        function FindTerminatingSemicolon(const Block: string): Integer;
        function CheckAndExtractKeywordContent(
            const LineInput: string;
            const KeywordUpperCase: string;
            out ContentAfterKeyword: string
        ): Boolean;
    public
        Name: string;
        NormalizedName: string;
        FilePath: string;
        InterfaceSection: TStringList;
        InterfaceUses: TStringList;

        AdjacencyList: TUnitInfoList;
        InDegree: Integer;
        Processed: Boolean;

        // Fields for the -fc (focus complete) feature
        IsFcStartFile: Boolean;
        FullSource: TStringList;

        constructor Create(const AFilePath: string);
        destructor Destroy; override;
        function ParseUnit: Boolean;
    end;

const
    DefaultExcludedDirNames: array[0..6] of string = ('.svn', '.git', '.hg', '__history', '__recovery', '.vs', '.vscode');

// Helper procedure to set clipboard text using WinAPI
procedure SetClipboardText(const Text: string);
var
    hGlobal: THandle;
    pGlobal: Pointer;
begin
    if not OpenClipboard(0) then
        RaiseLastOSError;
    try
        EmptyClipboard;
        hGlobal := GlobalAlloc(GMEM_MOVEABLE, (Length(Text) + 1) * SizeOf(Char));
        if hGlobal = 0 then
            RaiseLastOSError;

        pGlobal := GlobalLock(hGlobal);
        if pGlobal = nil then
        begin
            GlobalFree(hGlobal);
            RaiseLastOSError;
        end;

        try
            Move(PChar(Text)^, pGlobal^, (Length(Text) + 1) * SizeOf(Char));
        finally
            GlobalUnlock(hGlobal);
        end;

        if SetClipboardData(CF_UNICODETEXT, hGlobal) = 0 then
        begin
            GlobalFree(hGlobal); // We must free it if SetClipboardData fails
            RaiseLastOSError;
        end;
        // On success, the system owns the memory handle.
    finally
        CloseClipboard;
    end;
end;

{ TUnitInfo implementation }
constructor TUnitInfo.Create(const AFilePath: string);
begin
    inherited Create;
    FilePath := AFilePath;
    Name := '';
    NormalizedName := '';
    InterfaceSection := TStringList.Create;
    InterfaceUses := TStringList.Create;
    FOriginalLines := TStringList.Create;
    AdjacencyList := TUnitInfoList.Create;
    InDegree := 0;
    Processed := False;
    IsFcStartFile := False;
    FullSource := TStringList.Create;
end;

destructor TUnitInfo.Destroy;
begin
    InterfaceSection.Free;
    InterfaceUses.Free;
    FOriginalLines.Free;
    AdjacencyList.Free;
    FullSource.Free;
    inherited Destroy;
end;

function TUnitInfo.StripCommentsFromUsesBlock(const Block: string): string;
var
    Builder: TStringBuilder;
    i: Integer;
    L: Integer;
    inLineComment: Boolean;
    inBlockCommentParen: Boolean;
    inBlockCommentBrace: Boolean;
    currentChar, nextChar: Char;
    tempResult: string;
begin
    Builder := TStringBuilder.Create;
    L := Length(Block);
    i := 1;
    inLineComment := False;
    inBlockCommentParen := False;
    inBlockCommentBrace := False;
    while i <= L do
    begin
        currentChar := Block[i];
        if i < L then
            nextChar := Block[i + 1]
        else
            nextChar := #0;
        if inLineComment then
        begin
            if (currentChar = #13) or (currentChar = #10) then
            begin
                inLineComment := False;
                if (Builder.Length > 0) and (Builder[Builder.Length - 1] <> ' ') and (Builder[Builder.Length - 1] <> ',') then
                    Builder.Append(' ');
            end;
            Inc(i);
            Continue;
        end;
        if inBlockCommentParen then
        begin
            if (currentChar = '*') and (nextChar = ')') then
            begin
                inBlockCommentParen := False;
                Inc(i, 2);
            end
            else
                Inc(i);
            Continue;
        end;
        if inBlockCommentBrace then
        begin
            if currentChar = '}' then
            begin
                inBlockCommentBrace := False;
                Inc(i);
            end
            else
                Inc(i);
            Continue;
        end;
        if (currentChar = '/') and (nextChar = '/') then
        begin
            inLineComment := True;
            Inc(i, 2);
            Continue;
        end;
        if (currentChar = '(') and (nextChar = '*') then
        begin
            inBlockCommentParen := True;
            Inc(i, 2);
            Continue;
        end;
        if currentChar = '{' then
        begin
            inBlockCommentBrace := True;
            Inc(i);
            Continue;
        end;
        if (currentChar = #13) or (currentChar = #10) or (currentChar = #9) then
        begin
            if (Builder.Length = 0) or ((Builder.Length > 0) and (Builder[Builder.Length - 1] <> ' ')) then
                Builder.Append(' ');
            Inc(i);
        end
        else
        begin
            Builder.Append(currentChar);
            Inc(i);
        end;
    end;
    tempResult := Trim(Builder.ToString);
    Builder.Free;
    while Pos('  ', tempResult) > 0 do
        tempResult := StringReplace(tempResult, '  ', ' ', [rfReplaceAll]);
    tempResult := StringReplace(tempResult, ' ,', ',', [rfReplaceAll]);
    tempResult := StringReplace(tempResult, ', ', ',', [rfReplaceAll]);
    Result := tempResult;
end;

procedure TUnitInfo.ExtractUsesFromClause(const Clause: string);
var
    parts: TArray<string>;
    partName: string;
begin
    if Trim(Clause) = '' then
        Exit;
    parts := Clause.Split([','], TStringSplitOptions.ExcludeEmpty);
    for partName in parts do
    begin
        var pn := Trim(partName);
        if pn <> '' then
            InterfaceUses.Add(System.SysUtils.LowerCase(pn));
    end;
end;

function TUnitInfo.FindTerminatingSemicolon(const Block: string): Integer;
var
    i: Integer;
    L: Integer;
    inLineComment: Boolean;
    inBlockCommentParen: Boolean;
    inBlockCommentBrace: Boolean;
    currentChar: Char;
    nextChar: Char;
begin
    Result := 0;
    L := Length(Block);
    i := 1;
    inLineComment := False;
    inBlockCommentParen := False;
    inBlockCommentBrace := False;
    while i <= L do
    begin
        currentChar := Block[i];
        if i < L then
            nextChar := Block[i + 1]
        else
            nextChar := #0;
        if inLineComment then
        begin
            if (currentChar = #13) or (currentChar = #10) then
                inLineComment := False;
            Inc(i);
            Continue;
        end;
        if inBlockCommentParen then
        begin
            if (currentChar = '*') and (nextChar = ')') then
            begin
                inBlockCommentParen := False;
                Inc(i, 2);
            end
            else
                Inc(i);
            Continue;
        end;
        if inBlockCommentBrace then
        begin
            if currentChar = '}' then
            begin
                inBlockCommentBrace := False;
                Inc(i);
            end
            else
                Inc(i);
            Continue;
        end;
        if (currentChar = '/') and (nextChar = '/') then
        begin
            inLineComment := True;
            Inc(i, 2);
            Continue;
        end;
        if (currentChar = '(') and (nextChar = '*') then
        begin
            inBlockCommentParen := True;
            Inc(i, 2);
            Continue;
        end;
        if currentChar = '{' then
        begin
            inBlockCommentBrace := True;
            Inc(i);
            Continue;
        end;
        if currentChar = ';' then
        begin
            Result := i;
            Exit;
        end;
        Inc(i);
    end;
end;

function TUnitInfo.CheckAndExtractKeywordContent(
    const LineInput: string;
    const KeywordUpperCase: string;
    out ContentAfterKeyword: string
): Boolean;
var
    trimmedLine: string;
    keywordLen: Integer;
    charAfterKeyword: Char;
begin
    Result := False;
    ContentAfterKeyword := '';
    trimmedLine := Trim(LineInput);
    keywordLen := Length(KeywordUpperCase);
    if Length(trimmedLine) >= keywordLen then
    begin
        if System.SysUtils.UpperCase(Copy(trimmedLine, 1, keywordLen)) = KeywordUpperCase then
        begin
            if Length(trimmedLine) = keywordLen then
            begin
                Result := True;
                ContentAfterKeyword := '';
            end
            else
            begin
                charAfterKeyword := trimmedLine[keywordLen + 1];
                if not charAfterKeyword.IsLetterOrDigit then
                begin
                    Result := True;
                    ContentAfterKeyword := Trim(Copy(trimmedLine, keywordLen + 1, MaxInt));
                end;
            end;
        end;
    end;
end;

function TUnitInfo.ParseUnit: Boolean;
var
    i: Integer;
    LineValue: string;
    upperLine: string;
    inInterface: Boolean;
    inUsesBlock: Boolean;
    usesBuilder: TStringBuilder;
    potentialUnitNameLine: string;
    nameExtracted: Boolean;
    contentAfterKeyword: string;
    accumulatedContent: string;
    trueEndOfUsesPos: Integer;
    contentToClean: string;
    cleanedContent: string;
begin
    Result := False;
    if not TFile.Exists(FilePath) then
        exit;
    FOriginalLines.LoadFromFile(FilePath);
    inInterface := False;
    inUsesBlock := False;
    nameExtracted := False;
    usesBuilder := TStringBuilder.Create;
    try
        for i := 0 to FOriginalLines.Count - 1 do
        begin
            LineValue := FOriginalLines[i];
            upperLine := System.SysUtils.UpperCase(Trim(LineValue));

            if not nameExtracted and CheckAndExtractKeywordContent(LineValue, 'UNIT', contentAfterKeyword) then
            begin
                potentialUnitNameLine := contentAfterKeyword;
                if Pos(';', potentialUnitNameLine) > 0 then
                    Name := Trim(Copy(potentialUnitNameLine, 1, Pos(';', potentialUnitNameLine) - 1))
                else
                    Name := Trim(potentialUnitNameLine);
                if Name <> '' then
                begin
                    NormalizedName := System.SysUtils.LowerCase(Name);
                    nameExtracted := True;
                end
                else
                begin
                    WriteLn('Warning: Could not parse unit name from: ' + LineValue + ' in file ' + FilePath);
                    exit;
                end;
            end;

            if not inInterface and upperLine.StartsWith('INTERFACE') then
            begin
                inInterface := True;
            end;

            if inInterface then
            begin
                if upperLine.StartsWith('IMPLEMENTATION') or upperLine.StartsWith('END.') then
                begin
                    if inUsesBlock and (usesBuilder.Length > 0) then
                    begin
                        accumulatedContent := usesBuilder.ToString;
                        trueEndOfUsesPos := FindTerminatingSemicolon(accumulatedContent);
                        if trueEndOfUsesPos > 0 then
                            contentToClean := Trim(Copy(accumulatedContent, 1, trueEndOfUsesPos - 1))
                        else
                            contentToClean := Trim(accumulatedContent);
                        if contentToClean <> '' then
                        begin
                            cleanedContent := StripCommentsFromUsesBlock(contentToClean);
                            if Trim(cleanedContent) <> '' then
                                ExtractUsesFromClause(cleanedContent);
                        end;
                    end;
                    Result := True;
                    exit;
                end;

                var lineIsForUses := inUsesBlock;
                if not inUsesBlock and CheckAndExtractKeywordContent(LineValue, 'USES', contentAfterKeyword) then
                begin
                    usesBuilder.Clear;
                    inUsesBlock := True;
                    usesBuilder.AppendLine(contentAfterKeyword); // Use AppendLine to preserve structure for comment stripping
                    lineIsForUses := True;
                end
                else if inUsesBlock then
                begin
                    usesBuilder.AppendLine(Trim(LineValue));
                    lineIsForUses := True;
                end;

                if not upperLine.StartsWith('INTERFACE') and not lineIsForUses then
                begin
                    InterfaceSection.Add(LineValue);
                end;

                if inUsesBlock then
                begin
                    accumulatedContent := usesBuilder.ToString;
                    trueEndOfUsesPos := FindTerminatingSemicolon(accumulatedContent);
                    if trueEndOfUsesPos > 0 then
                    begin
                        contentToClean := Trim(Copy(accumulatedContent, 1, trueEndOfUsesPos - 1));
                        usesBuilder.Clear;
                        inUsesBlock := False;
                        if contentToClean <> '' then
                        begin
                            cleanedContent := StripCommentsFromUsesBlock(contentToClean);
                            if Trim(cleanedContent) <> '' then
                                ExtractUsesFromClause(cleanedContent);
                        end;
                    end;
                end;
            end;
        end;
        if inInterface then
        begin
            Result := True;
        end;
    finally
        usesBuilder.Free;
    end;
end;

procedure CollectPasFilesRecursively(const Path: string; Files: TStringList; const ExcludedDirPatterns: TStrings);
var
    dirName: string;
    foundPasFiles: TStringDynArray;
    subDirs: TStringDynArray;
    i: Integer;
    currentSubDir: string;
    isExcluded: Boolean;
    exclusionPattern: string;
begin
    try
        foundPasFiles := TDirectory.GetFiles(Path, '*.pas', TSearchOption.soTopDirectoryOnly);
        for i := Low(foundPasFiles) to High(foundPasFiles) do
            Files.Add(foundPasFiles[i]);
    except
        on E: EDirectoryNotFoundException do
        begin
            WriteLn('Warning: Could not access path ' + Path + ' while scanning files: ' + E.Message);
            Exit;
        end;
        on E: EInOutError do
        begin
            WriteLn('Warning: IO error accessing path ' + Path + ' while scanning files: ' + E.Message);
            Exit;
        end;
    end;
    try
        subDirs := TDirectory.GetDirectories(Path);
    except
        on E: EDirectoryNotFoundException do
        begin
            WriteLn('Warning: Could not access path ' + Path + ' while scanning subdirectories: ' + E.Message);
            Exit;
        end;
        on E: EInOutError do
        begin
            WriteLn('Warning: IO error accessing path ' + Path + ' while scanning subdirectories: ' + E.Message);
            Exit;
        end;
    end;
    for currentSubDir in subDirs do
    begin
        dirName := ExtractFileName(currentSubDir);
        isExcluded := False;
        for exclusionPattern in ExcludedDirPatterns do
        begin
            if SameText(dirName, exclusionPattern) then
            begin
                isExcluded := True;
                Break;
            end;
        end;
        if not isExcluded then
            CollectPasFilesRecursively(currentSubDir, Files, ExcludedDirPatterns);
    end;
end;

var
    allUnits: TDictionary<string, TUnitInfo>;
    sortedUnits: TList<TUnitInfo>;
    sourceDirectories: TList<string>;
    processingQueue: TQueue<TUnitInfo>;
    excludedDirectoryPatternsList: TStringList;
    allFoundPasFilesList: TStringList;
    item: TUnitInfo;
    outputLineText: string;
    recursiveSearch: Boolean;
    outputFileName: string;
    writeToClipboard: Boolean;
    currentParamIndex: Integer;
    focusMode: Boolean;
    fcMode: Boolean;
    focusFilesParameter: string;
    initialFocusFiles: TStringList;
    filesToExcludeFromOutput: TStringList;

begin
    recursiveSearch := False;
    outputFileName := '';
    writeToClipboard := False;
    sourceDirectories := nil;
    allUnits := nil;
    sortedUnits := nil;
    processingQueue := nil;
    excludedDirectoryPatternsList := nil;
    allFoundPasFilesList := nil;
    initialFocusFiles := nil;
    filesToExcludeFromOutput := nil;
    exitcode := 0;
    focusMode := False;
    fcMode := False;
    focusFilesParameter := '';

    try
        var usageStr := 'Usage: ExtractPascalInterfaces.exe [-r] [-dirs <file>] [-f|-fx|-fc <files>] [-o <file> | -c] <SrcDir1> ...';

        WriteLn('Delphi Interface Extractor');
        sourceDirectories := TList<string>.Create;
        excludedDirectoryPatternsList := TStringList.Create;
        initialFocusFiles := TStringList.Create;
        filesToExcludeFromOutput := TStringList.Create;
        for var excludedName in DefaultExcludedDirNames do
            excludedDirectoryPatternsList.Add(excludedName);

        currentParamIndex := 1;
        while currentParamIndex <= ParamCount do
        begin
            var currentParam := ParamStr(currentParamIndex);

            if SameText(currentParam, '-r') then
            begin
                recursiveSearch := True;
                Inc(currentParamIndex);
            end
            else if SameText(currentParam, '-c') then
            begin
                writeToClipboard := True;
                Inc(currentParamIndex);
            end
            else if SameText(currentParam, '-o') then
            begin
                if currentParamIndex + 1 <= ParamCount then
                begin
                    outputFileName := ParamStr(currentParamIndex + 1);
                    Inc(currentParamIndex, 2);
                end
                else
                begin
                    WriteLn('Error: Missing filename after -o option.');
                    WriteLn(usageStr);
                    exitcode := 1;
                    exit;
                end;
            end
            else if SameText(currentParam, '-dirs') then
            begin
                if currentParamIndex + 1 <= ParamCount then
                begin
                    var dirsFileName := ParamStr(currentParamIndex + 1);
                    if TFile.Exists(dirsFileName) then
                    begin
                        var dirsFromFile := TStringList.Create;
                        try
                            dirsFromFile.LoadFromFile(dirsFileName);
                            for var dirPath in dirsFromFile do
                                sourceDirectories.Add(dirPath);
                        finally
                            dirsFromFile.Free;
                        end;
                    end
                    else
                        WriteLn('Warning: File specified with -dirs not found: ' + dirsFileName);
                    Inc(currentParamIndex, 2);
                end
                else
                begin
                    WriteLn('Error: Missing filename after -dirs option.');
                    WriteLn(usageStr);
                    exitcode := 1;
                    exit;
                end;
            end
            else if SameText(currentParam, '-f') or SameText(currentParam, '-fx') or SameText(currentParam, '-fc') then
            begin
                if focusMode then
                begin
                    WriteLn('Error: Focus options (-f, -fx, -fc) cannot be used together.');
                    WriteLn(usageStr);
                    exitcode := 1;
                    exit;
                end;

                if currentParamIndex + 1 <= ParamCount then
                begin
                    focusMode := True;
                    fcMode := SameText(currentParam, '-fc');
                    focusFilesParameter := ParamStr(currentParamIndex + 1);
                    var patternsList := TStringList.Create;
                    var expandedFocusFiles := TStringList.Create;
                    try
                        patternsList.Text := StringReplace(focusFilesParameter, ';', sLineBreak, [rfReplaceAll]);
                        for var pattern in patternsList do
                        begin
                            if (pattern.Contains('*')) or (pattern.Contains('?')) then
                            begin
                                var searchPath := ExtractFilePath(pattern);
                                var searchMask := ExtractFileName(pattern);
                                if searchPath = '' then
                                    searchPath := TDirectory.GetCurrentDirectory;
                                try
                                    expandedFocusFiles.AddStrings(TDirectory.GetFiles(searchPath, searchMask));
                                except
                                    on E: EDirectoryNotFoundException do
                                        WriteLn('Warning: Directory for pattern not found, skipping: ' + pattern);
                                end;
                            end
                            else
                            begin
                                expandedFocusFiles.Add(pattern);
                            end;
                        end;

                        initialFocusFiles.Clear;
                        initialFocusFiles.AddStrings(expandedFocusFiles);

                        if SameText(currentParam, '-fx') then
                            filesToExcludeFromOutput.Text := initialFocusFiles.Text;
                    finally
                        patternsList.Free;
                        expandedFocusFiles.Free;
                    end;
                    Inc(currentParamIndex, 2);
                end
                else
                begin
                    WriteLn('Error: Missing file list after ' + currentParam + ' option.');
                    WriteLn(usageStr);
                    exitcode := 1;
                    exit;
                end;
            end
            else
            begin
                sourceDirectories.Add(currentParam);
                inc(currentParamIndex);
            end;
        end;

        var i := sourceDirectories.Count - 1;
        while i >= 0 do
        begin
            var trimmedPath := Trim(sourceDirectories[i]);
            if (trimmedPath = '') or (not TDirectory.Exists(trimmedPath)) then
            begin
                if trimmedPath <> '' then
                    WriteLn('Warning: Directory "' + trimmedPath + '" not found or is not a directory. Skipping.');
                sourceDirectories.Delete(i);
            end
            else
                sourceDirectories[i] := trimmedPath;
            Dec(i);
        end;

        if writeToClipboard and (outputFileName <> '') then
        begin
            WriteLn('Error: -o (output to file) and -c (output to clipboard) cannot be used together.');
            exitcode := 1;
            exit;
        end;

        if sourceDirectories.Count = 0 then
        begin
            WriteLn('Error: No source directories specified.');
            WriteLn(usageStr);
            exitcode := 1;
            exit;
        end;

        allFoundPasFilesList := TStringList.Create;
        WriteLn(IfThen(recursiveSearch, 'Recursive search enabled.', 'Non-recursive search.'));

        for var currentScanDir in sourceDirectories do
        begin
            WriteLn('Scanning directory: ' + currentScanDir + IfThen(recursiveSearch, ' (recursively)', ''));
            if recursiveSearch then
                CollectPasFilesRecursively(currentScanDir, allFoundPasFilesList, excludedDirectoryPatternsList)
            else
            begin
                var filesInDir := TDirectory.GetFiles(currentScanDir, '*.pas', TSearchOption.soTopDirectoryOnly);
                for var fName in filesInDir do
                    allFoundPasFilesList.Add(fName);
            end;
        end;

        if focusMode then
        begin
            for var focusFile in initialFocusFiles do
            begin
                if not TFile.Exists(focusFile) then
                begin
                    WriteLn('Warning: Focus file not found, skipping: ' + focusFile);
                    Continue;
                end;
                if allFoundPasFilesList.IndexOf(focusFile) = -1 then
                    allFoundPasFilesList.Add(focusFile);
            end;
        end;

        if allFoundPasFilesList.Count = 0 then
        begin
            WriteLn('No .pas files found in any specified directory (respecting exclusions).');
            exit;
        end;

        allUnits := TDictionary<string, TUnitInfo>.Create(TStringComparer.Ordinal);
        sortedUnits := TList<TUnitInfo>.Create;
        processingQueue := TQueue<TUnitInfo>.Create;

        WriteLn('Found ' + IntToStr(allFoundPasFilesList.Count) + ' .pas files in total. Parsing...');
        for var filePathKey in allFoundPasFilesList do
        begin
            var unitInfo := TUnitInfo.Create(filePathKey);
            if unitInfo.ParseUnit then
            begin
                if unitInfo.Name <> '' then
                begin
                    if not allUnits.ContainsKey(unitInfo.NormalizedName) then
                        allUnits.Add(unitInfo.NormalizedName, unitInfo)
                    else
                    begin
                        WriteLn('Warning: Duplicate unit name "' + unitInfo.Name + '" found.');
                        WriteLn('  Existing: ' + allUnits[unitInfo.NormalizedName].FilePath);
                        WriteLn('  Skipped:  ' + unitInfo.FilePath);
                        unitInfo.Free;
                    end;
                end
                else
                begin
                    WriteLn('Warning: Could not determine unit name for file: ' + filePathKey + '. Skipping.');
                    unitInfo.Free;
                end;
            end
            else
            begin
                WriteLn('Warning: Failed to parse unit: ' + filePathKey + '. Skipping.');
                unitInfo.Free;
            end;
        end;

        if allUnits.Count = 0 then
        begin
            WriteLn('No units successfully parsed.');
            exit;
        end;

        if focusMode then
        begin
            WriteLn('Focus mode active. Finding dependency cone for files: ' + focusFilesParameter);
            var workQueue := TQueue<TUnitInfo>.Create;
            var requiredUnits := TDictionary<string, TUnitInfo>.Create(TStringComparer.Ordinal);
            var unitsToRemove := TStringList.Create;
            try
                for var unitInfo in allUnits.Values do
                begin
                    if initialFocusFiles.IndexOf(unitInfo.FilePath) > -1 then
                    begin
                        if not requiredUnits.ContainsKey(unitInfo.NormalizedName) then
                        begin
                            workQueue.Enqueue(unitInfo);
                            requiredUnits.Add(unitInfo.NormalizedName, unitInfo);
                        end;
                    end;
                end;

                if requiredUnits.Count = 0 then
                    WriteLn('Warning: None of the initial focus files could be parsed successfully.');

                while workQueue.Count > 0 do
                begin
                    var currentUnit := workQueue.Dequeue;
                    for var usedUnitName in currentUnit.InterfaceUses do
                    begin
                        var dependencyUnit: TUnitInfo;
                        if allUnits.TryGetValue(usedUnitName, dependencyUnit) and (not requiredUnits.ContainsKey(usedUnitName)) then
                        begin
                            requiredUnits.Add(dependencyUnit.NormalizedName, dependencyUnit);
                            workQueue.Enqueue(dependencyUnit);
                        end;
                    end;
                end;

                WriteLn('Found ' + requiredUnits.Count.ToString + ' required units in total.');
                for var unitInfo in allUnits.Values do
                    if not requiredUnits.ContainsKey(unitInfo.NormalizedName) then
                        unitsToRemove.Add(unitInfo.NormalizedName);

                if unitsToRemove.Count > 0 then
                begin
                    WriteLn('Filtering out ' + unitsToRemove.Count.ToString + ' unneeded units...');
                    for var unitNameToRemove in unitsToRemove do
                    begin
                        allUnits[unitNameToRemove].Free;
                        allUnits.Remove(unitNameToRemove);
                    end;
                end;

            finally
                workQueue.Free;
                requiredUnits.Free;
                unitsToRemove.Free;
            end;
        end;

        if fcMode then
        begin
            // After filtering, identify the start files and store their full source
            for var unitInfo in allUnits.Values do
            begin
                if initialFocusFiles.IndexOf(unitInfo.FilePath) > -1 then
                begin
                    unitInfo.IsFcStartFile := True;
                    unitInfo.FullSource.AddStrings(unitInfo.FOriginalLines);
                end;
            end;
        end;

        WriteLn('Building dependency graph...');
        for var currentUnit in allUnits.Values do
            for var usedUnitName in currentUnit.InterfaceUses do
            begin
                var usedUnitInfo: TUnitInfo;
                if allUnits.TryGetValue(usedUnitName, usedUnitInfo) then
                begin
                    currentUnit.InDegree := currentUnit.InDegree + 1;
                    usedUnitInfo.AdjacencyList.Add(currentUnit);
                end;
            end;

        WriteLn('');
        WriteLn('--------------------------------------------------------------------------------');
        WriteLn('Dependency Graph (Project Units Only):');
        WriteLn('--------------------------------------------------------------------------------');
        if allUnits.Count = 0 then
            WriteLn('(No units to display in graph)')
        else
        begin
            var tempUnitListForDisplay: TList<TUnitInfo>;
            tempUnitListForDisplay := TList<TUnitInfo>.Create(allUnits.Values);
            try
                tempUnitListForDisplay.Sort(
                    TComparer<TUnitInfo>
                        .Construct(function(const L, R: TUnitInfo): Integer begin Result := CompareText(L.Name, R.Name); end)
                );
                for var unitToDisplay in tempUnitListForDisplay do
                begin
                    var usesClauseText := '';
                    var projectDependenciesFound := False;
                    if unitToDisplay.InterfaceUses.Count > 0 then
                    begin
                        var usedUnitDisplayNames := TStringList.Create;
                        try
                            for var usedNormalizedName in unitToDisplay.InterfaceUses do
                            begin
                                var usedUnitActualInfo: TUnitInfo;
                                if allUnits.TryGetValue(usedNormalizedName, usedUnitActualInfo) then
                                begin
                                    usedUnitDisplayNames.Add(usedUnitActualInfo.Name);
                                    projectDependenciesFound := True;
                                end;
                            end;
                            if projectDependenciesFound then
                                usesClauseText := usedUnitDisplayNames.CommaText
                            else
                                usesClauseText := '(none)';
                        finally
                            usedUnitDisplayNames.Free;
                        end;
                    end
                    else
                        usesClauseText := '(none)';
                    WriteLn(Format('  %-40s -> %s', [unitToDisplay.Name, usesClauseText]));
                end;
            finally
                tempUnitListForDisplay.Free;
            end;
        end;
        WriteLn('--------------------------------------------------------------------------------');
        WriteLn('');

        WriteLn('Performing topological sort...');
        for item in allUnits.Values do
            if item.InDegree = 0 then
                processingQueue.Enqueue(item);
        while processingQueue.Count > 0 do
        begin
            var u := processingQueue.Dequeue;
            sortedUnits.Add(u);
            u.Processed := True;
            for var v in u.AdjacencyList do
            begin
                v.InDegree := v.InDegree - 1;
                if (v.InDegree = 0) and (not v.Processed) then
                    processingQueue.Enqueue(v);
            end;
        end;

        if sortedUnits.Count < allUnits.Count then
        begin
            WriteLn('Error: Circular dependency detected among units. Output generation aborted.');
            WriteLn('Units not processed:');
            for item in allUnits.Values do
                if not item.Processed then
                    WriteLn('- ' + item.Name + ' (InDegree: ' + item.InDegree.ToString + ', File: ' + item.FilePath + ')');
            exitcode := 1;
        end
        else
        begin
            var outputContent := TStringList.Create;
            var externalUses := TDictionary<string, boolean>.Create;
            try
                outputContent.Add('// Combined interfaces from directories:');
                for var dirPath in sourceDirectories do
                    outputContent.Add('// - ' + dirPath + IfThen(recursiveSearch, ' (recursive)', ''));
                if focusMode then
                begin
                    var modeStr: string;
                    if fcMode then
                        modeStr := '-fc'
                    else if filesToExcludeFromOutput.Count > 0 then
                        modeStr := '-fx'
                    else
                        modeStr := '-f';
                    outputContent.Add('// Focus mode (' + modeStr + ') active for files: ' + focusFilesParameter);
                end;
                outputContent.Add('// Generated on: ' + DateTimeToStr(Now));
                outputContent.Add('');

                for item in sortedUnits do
                begin
                    if filesToExcludeFromOutput.IndexOf(item.FilePath) > -1 then
                        Continue;

                    for var usedUnit in item.InterfaceUses do
                        if not allUnits.ContainsKey(usedUnit) then
                            externalUses.AddOrSetValue(usedUnit, True);
                end;

                if externalUses.Count > 0 then
                begin
                    var externalUsesList := TStringList.Create;
                    try
                        for var extUse in externalUses.Keys do
                            externalUsesList.Add(extUse);
                        externalUsesList.Sort;
                        outputContent.Add('uses');
                        for var j := 0 to externalUsesList.Count - 1 do
                        begin
                            if j = externalUsesList.Count - 1 then
                                outputContent.Add('  ' + externalUsesList[j] + ';')
                            else
                                outputContent.Add('  ' + externalUsesList[j] + ',');
                        end;
                        outputContent.Add('');
                    finally
                        externalUsesList.Free;
                    end;
                end;

                for item in sortedUnits do
                begin
                    if filesToExcludeFromOutput.IndexOf(item.FilePath) > -1 then
                        Continue;

                    if item.IsFcStartFile then
                    begin
                        // For -fc start files, add the complete source code
                        outputContent
                            .Add('//==================================================================================================');
                        outputContent.Add('//== FULL UNIT START: ' + item.Name + ' (from ' + ExtractFileName(item.FilePath) + ')');
                        outputContent
                            .Add('//==================================================================================================');
                        outputContent.AddStrings(item.FullSource);
                        outputContent
                            .Add('//==================================================================================================');
                        outputContent.Add('//== FULL UNIT END: ' + item.Name);
                        outputContent
                            .Add('//==================================================================================================');
                        outputContent.Add('');
                    end
                    else if item.InterfaceSection.Count > 0 then
                    begin
                        // For all other units, add the interface section only
                        outputContent
                            .Add('//==================================================================================================');
                        outputContent.Add('//== INTERFACE START: ' + item.Name + ' (from ' + ExtractFileName(item.FilePath) + ')');
                        outputContent
                            .Add('//==================================================================================================');
                        outputContent.AddStrings(item.InterfaceSection);
                        outputContent.Add('//== INTERFACE END: ' + item.Name);
                        outputContent
                            .Add('//==================================================================================================');
                        outputContent.Add('');
                    end;
                end;

                if writeToClipboard then
                begin
                    SetClipboardText(outputContent.Text);
                    WriteLn('Output successfully copied to clipboard.');
                end
                else if outputFileName <> '' then
                begin
                    outputContent.SaveToFile(outputFileName);
                    WriteLn('Output successfully written to: ' + outputFileName);
                end
                else
                begin
                    for outputLineText in outputContent do
                        WriteLn(outputLineText);
                end;
            finally
                outputContent.Free;
                externalUses.Free;
            end;
        end;
    except
        on E: Exception do
        begin
            WriteLn('An error occurred: ' + E.ClassName + ': ' + E.Message);
            exitcode := 1;
        end;
    end;
    if Assigned(filesToExcludeFromOutput) then
        filesToExcludeFromOutput.Free;
    if Assigned(initialFocusFiles) then
        initialFocusFiles.Free;
    if Assigned(excludedDirectoryPatternsList) then
        excludedDirectoryPatternsList.Free;
    if Assigned(allFoundPasFilesList) then
        allFoundPasFilesList.Free;
    if Assigned(sourceDirectories) then
        sourceDirectories.Free;
    if Assigned(allUnits) then
    begin
        for item in allUnits.Values do
            item.Free;
        allUnits.Free;
    end;
    if Assigned(sortedUnits) then
        sortedUnits.Free;
    if Assigned(processingQueue) then
        processingQueue.Free;
    if exitcode <> 0 then
        WriteLn('Extraction failed.');
end.
