unit uSplitPDF;
{$I ..\..\..\Lib\PDFiumVcl.inc}

interface

uses
{$IFDEF XE2+}
  Winapi.Windows,
  Winapi.Messages,
  Winapi.ShellAPI,
  System.SysUtils,
  System.UITypes,
  System.Variants,
  System.Classes,
  System.Types,
  System.StrUtils,
  System.Math,
  System.IOUtils,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  Vcl.StdCtrls,
  Vcl.ComCtrls,
  Vcl.ExtCtrls,
  Vcl.FileCtrl,
{$ELSE}
  Windows,
  Messages,
  ShellAPI,
  SysUtils,
  Variants,
  Classes,
  Types,
  StrUtils,
  Math,
  IOUtils,
  Graphics,
  Controls,
  Forms,
  Dialogs,
  StdCtrls,
  ComCtrls,
  ExtCtrls,
  FileCtrl,
{$ENDIF}
  PDFium;

type
  TSplitMode= (smIndividualPages, smPageRange, smBookmarks);

  TPageRange= record
    StartPage: Integer;
    EndPage: Integer;
  end;

  TPageRanges= array of TPageRange;

  TFormMain= class(TForm)
    pnlMain: TPanel;
    grpSource: TGroupBox;
    lblPdfFile: TLabel;
    edtPdfFile: TEdit;
    btnBrowse: TButton;
    lblPageInfo: TLabel;
    lblPageCount: TLabel;
    grpOptions: TGroupBox;
    lblSplitMode: TLabel;
    cmbSplitMode: TComboBox;
    lblPageRange: TLabel;
    edtPageRange: TEdit;
    lblOutputDir: TLabel;
    edtOutputDir: TEdit;
    btnSelectOutputDir: TButton;
    lblNamingPattern: TLabel;
    edtNamingPattern: TEdit;
    lblRangeHelp: TLabel;
    lblPatternHelp: TLabel;
    grpProgress: TGroupBox;
    lblCurrentOperation: TLabel;
    prgProgress: TProgressBar;
    lblProgress: TLabel;
    grpLog: TGroupBox;
    mmoLog: TMemo;
    pnlButtons: TPanel;
    btnSplit: TButton;
    btnCancel: TButton;
    btnClearLog: TButton;
    btnExit: TButton;
    Pdf: TPdf;
    PdfNew: TPdf;
    dlgOpenPdf: TOpenDialog;
    dlgSelectFolder: TFileOpenDialog;
    tmrProgress: TTimer;
    lnkLabel: TLinkLabel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnBrowseClick(Sender: TObject);
    procedure edtPdfFileChange(Sender: TObject);
    procedure cmbSplitModeChange(Sender: TObject);
    procedure btnSelectOutputDirClick(Sender: TObject);
    procedure btnSplitClick(Sender: TObject);
    procedure btnCancelClick(Sender: TObject);
    procedure btnClearLogClick(Sender: TObject);
    procedure btnExitClick(Sender: TObject);
    procedure PdfPageChange(Sender: TObject);
    procedure tmrProgressTimer(Sender: TObject);
    procedure mmoLogChange(Sender: TObject);
    procedure lnkLabelLinkClick(
      Sender    : TObject;
      const Link: string;
      LinkType  : TSysLinkType);
  private
    FProcessing: Boolean;
    FCancelled: Boolean;
    FTotalPages: Integer;
    FCurrentPage: Integer;
    FStartTime: TDateTime;
    FSplitCount: Integer;
    FCurrentOutputFile: string;

    procedure LogMessage(
      const Msg  : string;
      const Level: string= 'INFO');
    procedure UpdateUI;
    procedure UpdateProgress(
      const Operation: string;
      Current, Total : Integer);
    function ValidateSettings: Boolean;
    function ParsePageRanges(const RangeStr: string): TPageRanges;
    function GenerateOutputFileName(
      const Pattern, SourceFile: string;
      PageNum                  : Integer): string;
    procedure ProcessIndividualPages;
    procedure ProcessPageRanges;
    procedure ProcessBookmarks;
    function GetOutputDirectory: string;
    procedure SetProcessingState(Processing: Boolean);
    procedure ShowCompletionSummary;
  public
    { Public declarations }
  end;

var
  FormMain: TFormMain;

implementation

{$R *.dfm}

const
  LOG_INFO= 'INFO';
  LOG_WARNING= 'WARNING';
  LOG_ERROR= 'ERROR';
  LOG_SUCCESS= 'SUCCESS';

procedure TFormMain.FormCreate(Sender: TObject);
begin
  FProcessing:= False;
  FCancelled:= False;
  FTotalPages:= 0;
  FCurrentPage:= 0;
  FSplitCount:= 0;

  // Set default output directory to same as source
  edtOutputDir.Text:= ExtractFilePath(Application.ExeName);

  LogMessage('Enhanced PDF Splitter initialized');
  LogMessage('PDFiumVCL Demo - Version 1.0');
  LogMessage('Ready to split PDF documents');

  UpdateUI;
end;

procedure TFormMain.FormDestroy(Sender: TObject);
begin
  if Pdf.Active
  then
    Pdf.Active:= False;
  if PdfNew.Active
  then
    PdfNew.Active:= False;
end;

procedure TFormMain.LogMessage(
  const Msg  : string;
  const Level: string= 'INFO');
var
  TimeStamp: string;
  LogLine: string;
begin
  TimeStamp:= FormatDateTime('hh:nn:ss', Now);
  LogLine:= Format('[%s] %s: %s', [TimeStamp, Level, Msg]);

  mmoLog.Lines.Add(LogLine);
  mmoLog.Perform(WM_VSCROLL, SB_BOTTOM, 0);
  Application.ProcessMessages;
end;

procedure TFormMain.UpdateUI;
var
  HasFile: Boolean;
  ValidSettings: Boolean;
begin
  HasFile:= FileExists(edtPdfFile.Text);
  ValidSettings:= HasFile and ValidateSettings;

  btnSplit.Enabled:= ValidSettings and not FProcessing;
  btnCancel.Enabled:= FProcessing;

  // Enable/disable controls during processing
  grpSource.Enabled:= not FProcessing;
  grpOptions.Enabled:= not FProcessing;

  // Update page range edit based on split mode
  edtPageRange.Enabled:= (cmbSplitMode.ItemIndex= 1)and not FProcessing; // Page range mode
end;

procedure TFormMain.btnBrowseClick(Sender: TObject);
begin
  dlgOpenPdf.FileName:= edtPdfFile.Text;
  if dlgOpenPdf.Execute
  then
  begin
    edtPdfFile.Text:= dlgOpenPdf.FileName;
    edtPdfFileChange(nil);
  end;
end;

procedure TFormMain.edtPdfFileChange(Sender: TObject);
var
  FileName: string;
  SR: TSearchRec;
begin
  FileName:= edtPdfFile.Text;

  if FileExists(FileName)
  then
  begin
    try
      if Pdf.Active
      then
        Pdf.Active:= False;

      Pdf.FileName:= FileName;
      Pdf.Active:= True;

      SR.Size:= 0;
      if FindFirst(FileName, faAnyFile, SR) = 0 then
        FindClose(SR);
      lblPageCount.Caption:= Format('%d pages, %s bytes', [Pdf.PageCount, FormatFloat('#,##0', SR.Size)]);
      lblPageCount.Font.Color:= clWindowText;
      lblPageCount.Font.Style:= [];

      // Set default output directory to same as source file
      if edtOutputDir.Text= ExtractFilePath(Application.ExeName)
      then
        edtOutputDir.Text:= ExtractFilePath(FileName);

      LogMessage(Format('Loaded PDF: %s (%d pages)', [ExtractFileName(FileName), Pdf.PageCount]));

    except
      on E: Exception do
      begin
        lblPageCount.Caption:= 'Error loading file: '+ E.Message;
        lblPageCount.Font.Color:= clRed;
        lblPageCount.Font.Style:= [fsItalic];
        LogMessage('Error loading PDF: '+ E.Message, LOG_ERROR);
      end;
    end;
  end
  else
  begin
    lblPageCount.Caption:= 'No file selected';
    lblPageCount.Font.Color:= clGray;
    lblPageCount.Font.Style:= [fsItalic];
    if Pdf.Active
    then
      Pdf.Active:= False;
  end;

  UpdateUI;
end;

procedure TFormMain.cmbSplitModeChange(Sender: TObject);
begin
  UpdateUI;

  case cmbSplitMode.ItemIndex of
  0:
    LogMessage('Split mode: Individual pages');
  1:
    LogMessage('Split mode: Page ranges');
  2:
    LogMessage('Split mode: By bookmarks');
  end;
end;

procedure TFormMain.btnSelectOutputDirClick(Sender: TObject);
begin
  dlgSelectFolder.DefaultFolder:= edtOutputDir.Text;
  if dlgSelectFolder.Execute
  then
  begin
    edtOutputDir.Text:= dlgSelectFolder.FileName;
    LogMessage('Output directory: '+ dlgSelectFolder.FileName);
  end;
end;

function TFormMain.ValidateSettings: Boolean;
var
  OutputDir: string;
  Ranges: TPageRanges;
begin
  Result:= False;

  // Check if PDF is loaded
  if not Pdf.Active
  then
    Exit;

  // Check output directory
  OutputDir:= GetOutputDirectory;
  if not TDirectory.Exists(OutputDir)
  then
  begin
    try
      TDirectory.CreateDirectory(OutputDir);
    except
      LogMessage('Cannot create output directory: '+ OutputDir, LOG_ERROR);
      Exit;
    end;
  end;

  // Validate page ranges if needed
  if cmbSplitMode.ItemIndex= 1
  then // Page range mode
  begin
    try
      Ranges:= ParsePageRanges(edtPageRange.Text);
      if Length(Ranges)= 0
      then
      begin
        LogMessage('Invalid page range specification', LOG_ERROR);
        Exit;
      end;
    except
      LogMessage('Error parsing page ranges', LOG_ERROR);
      Exit;
    end;
  end;

  Result:= True;
end;

function TFormMain.ParsePageRanges(const RangeStr: string): TPageRanges;
var
  Parts: TStringList;
  I: Integer;
  Part: string;
  DashPos: Integer;
  StartPage, EndPage: Integer;
  Range: TPageRange;
begin
  SetLength(Result, 0);

  if Trim(RangeStr)= ''
  then
    Exit;

  Parts:= TStringList.Create;
  try
    Parts.CommaText:= StringReplace(RangeStr, ' ', '', [rfReplaceAll]);

    for I:= 0 to Parts.Count- 1 do
    begin
      Part:= Trim(Parts[I]);
      if Part= ''
      then
        Continue;

      DashPos:= Pos('-', Part);
      if DashPos> 0
      then
      begin
        // Range like "1-10"
        StartPage:= StrToInt(Copy(Part, 1, DashPos- 1));
        EndPage:= StrToInt(Copy(Part, DashPos+ 1, Length(Part)));
      end
      else
      begin
        // Single page like "5"
        StartPage:= StrToInt(Part);
        EndPage:= StartPage;
      end;

      // Validate ranges
      if (StartPage< 1)or (EndPage< StartPage)or (StartPage> Pdf.PageCount)
      then
        Continue;

      EndPage:= Min(EndPage, Pdf.PageCount);

      Range.StartPage:= StartPage;
      Range.EndPage:= EndPage;
      SetLength(Result, Length(Result)+ 1);
      Result[High(Result)]:= Range;
    end;
  finally
    Parts.Free;
  end;
end;

function TFormMain.GenerateOutputFileName(
  const Pattern, SourceFile: string;
  PageNum                  : Integer): string;
var
  BaseName, Ext: string;
begin
  BaseName:= ChangeFileExt(ExtractFileName(SourceFile), '');
  Ext:= ExtractFileExt(SourceFile);

  Result:= StringReplace(Pattern, '{filename}', BaseName, [rfReplaceAll, rfIgnoreCase]);
  Result:= StringReplace(Result, '{page}', Format('%d', [PageNum]), [rfReplaceAll, rfIgnoreCase]);
  Result:= StringReplace(Result, '{page:000}', Format('%.3d', [PageNum]), [rfReplaceAll, rfIgnoreCase]);

  if ExtractFileExt(Result)= ''
  then
    Result:= Result+ Ext;
end;

function TFormMain.GetOutputDirectory: string;
begin
  Result:= Trim(edtOutputDir.Text);
  if Result= ''
  then
    Result:= ExtractFilePath(edtPdfFile.Text);
end;

procedure TFormMain.SetProcessingState(Processing: Boolean);
begin
  FProcessing:= Processing;
  UpdateUI;

  if Processing
  then
  begin
    FStartTime:= Now;
    FCancelled:= False;
    FSplitCount:= 0;
    tmrProgress.Enabled:= True;
  end
  else
  begin
    tmrProgress.Enabled:= False;
  end;
end;

procedure TFormMain.UpdateProgress(
  const Operation: string;
  Current, Total : Integer);
begin
  lblCurrentOperation.Caption:= Operation;
  FCurrentPage:= Current;
  FTotalPages:= Total;

  if Total> 0
  then
  begin
    prgProgress.Max:= Total;
    prgProgress.Position:= Current;
    lblProgress.Caption:= Format('%d of %d pages', [Current, Total]);
  end
  else
  begin
    prgProgress.Position:= 0;
    lblProgress.Caption:= 'Initializing...';
  end;

  Application.ProcessMessages;
end;

procedure TFormMain.btnSplitClick(Sender: TObject);
begin
  if not ValidateSettings
  then
  begin
    LogMessage('Please check your settings and try again', LOG_WARNING);
    Exit;
  end;

  LogMessage('Starting PDF split operation...');
  LogMessage(Format('Source: %s', [edtPdfFile.Text]));
  LogMessage(Format('Output directory: %s', [GetOutputDirectory]));
  LogMessage(Format('Naming pattern: %s', [edtNamingPattern.Text]));

  SetProcessingState(True);

  try
    case TSplitMode(cmbSplitMode.ItemIndex) of
    smIndividualPages:
      ProcessIndividualPages;
    smPageRange:
      ProcessPageRanges;
    smBookmarks:
      ProcessBookmarks;
    end;

    if not FCancelled
    then
    begin
      LogMessage('PDF split operation completed successfully!', LOG_SUCCESS);
      ShowCompletionSummary;
    end
    else
    begin
      LogMessage('PDF split operation was cancelled by user', LOG_WARNING);
    end;
  except
    on E: Exception do
    begin
      LogMessage('Error during split operation: '+ E.Message, LOG_ERROR);
      ShowMessage('An error occurred during the split operation:'+ sLineBreak+ E.Message + sLineBreak + 'Copy the DLL to the system or program directory first.');
    end;
  end;

  SetProcessingState(False);
  UpdateProgress('Ready', 0, 0);
end;

procedure TFormMain.ProcessIndividualPages;
var
  I: Integer;
  OutputFile: string;
  OutputDir: string;
begin
  OutputDir:= GetOutputDirectory;
  UpdateProgress('Splitting into individual pages...', 0, Pdf.PageCount);

  for I:= 1 to Pdf.PageCount do
  begin
    if FCancelled
    then
      Break;

    UpdateProgress(Format('Processing page %d...', [I]), I, Pdf.PageCount);

    try
      // Create new document for this page
      if PdfNew.Active
      then
        PdfNew.Active:= False;

      PdfNew.CreateDocument;
      PdfNew.ImportPreferences(Pdf);
      PdfNew.ImportPages(Pdf, IntToStr(I));

      // Generate output filename
      OutputFile:= GenerateOutputFileName(edtNamingPattern.Text, edtPdfFile.Text, I);
      OutputFile:= IncludeTrailingPathDelimiter(OutputDir)+ OutputFile;
      FCurrentOutputFile:= OutputFile;

      // Save the page
      PdfNew.SaveAs(OutputFile);
      Inc(FSplitCount);

      LogMessage(Format('Created: %s', [ExtractFileName(OutputFile)]));

    except
      on E: Exception do
      begin
        LogMessage(Format('Error processing page %d: %s', [I, E.Message]), LOG_ERROR);
        Continue;
      end;
    end;
  end;
end;

procedure TFormMain.ProcessPageRanges;
var
  Ranges: TPageRanges;
  I, J: Integer;
  OutputFile: string;
  OutputDir: string;
  PageList: string;
begin
  OutputDir:= GetOutputDirectory;
  Ranges:= ParsePageRanges(edtPageRange.Text);

  UpdateProgress('Splitting by page ranges...', 0, Length(Ranges));

  for I:= 0 to High(Ranges) do
  begin
    if FCancelled
    then
      Break;

    UpdateProgress(Format('Processing range %d of %d...', [I+ 1, Length(Ranges)]), I+ 1, Length(Ranges));

    try
      // Create new document for this range
      if PdfNew.Active
      then
        PdfNew.Active:= False;

      PdfNew.CreateDocument;
      PdfNew.ImportPreferences(Pdf);

      // Build page list for this range
      PageList:= '';
      for J:= Ranges[I].StartPage to Ranges[I].EndPage do
      begin
        if PageList<> ''
        then
          PageList:= PageList+ ',';
        PageList:= PageList+ IntToStr(J);
      end;

      PdfNew.ImportPages(Pdf, PageList);

      // Generate output filename
      if Ranges[I].StartPage= Ranges[I].EndPage
      then
        OutputFile:= GenerateOutputFileName(edtNamingPattern.Text, edtPdfFile.Text, Ranges[I].StartPage)
      else
        OutputFile:= GenerateOutputFileName(StringReplace(edtNamingPattern.Text, '{page}', Format('%d-%d', [Ranges[I].StartPage, Ranges[I].EndPage]),
          [rfReplaceAll]), edtPdfFile.Text, Ranges[I].StartPage);

      OutputFile:= IncludeTrailingPathDelimiter(OutputDir)+ OutputFile;
      FCurrentOutputFile:= OutputFile;

      // Save the range
      PdfNew.SaveAs(OutputFile);
      Inc(FSplitCount);

      LogMessage(Format('Created: %s (pages %d-%d)', [ExtractFileName(OutputFile), Ranges[I].StartPage, Ranges[I].EndPage]));

    except
      on E: Exception do
      begin
        LogMessage(Format('Error processing range %d-%d: %s', [Ranges[I].StartPage, Ranges[I].EndPage, E.Message]), LOG_ERROR);
        Continue;
      end;
    end;
  end;
end;

procedure TFormMain.ProcessBookmarks;
var
  I: Integer;
  Bookmarks: TBookmarks;
  OutputFile: string;
  OutputDir: string;
begin
  OutputDir:= GetOutputDirectory;

  try
    Bookmarks:= Pdf.Bookmarks;
    if Length(Bookmarks)= 0
    then
    begin
      LogMessage('No bookmarks found in the PDF', LOG_WARNING);
      // Fallback to individual pages
      LogMessage('Falling back to individual page splitting');
      ProcessIndividualPages;
      Exit;
    end;

    UpdateProgress('Splitting by bookmarks...', 0, Length(Bookmarks));

    for I:= 0 to High(Bookmarks) do
    begin
      if FCancelled
      then
        Break;

      UpdateProgress(Format('Processing bookmark: %s', [Bookmarks[I].Title]), I+ 1, Length(Bookmarks));

      try
        // Create new document for this bookmark
        if PdfNew.Active
        then
          PdfNew.Active:= False;

        PdfNew.CreateDocument;
        PdfNew.ImportPreferences(Pdf);

        // For simplicity, we'll extract the page the bookmark points to
        // In a more advanced implementation, you might extract a range of pages
        PdfNew.ImportPages(Pdf, IntToStr(Bookmarks[I].PageNumber));

        // Generate output filename using bookmark title
        OutputFile:= Format('%s_%s.pdf', [ChangeFileExt(ExtractFileName(edtPdfFile.Text), ''), StringReplace(Bookmarks[I].Title, '/', '_', [rfReplaceAll])]);
        OutputFile:= IncludeTrailingPathDelimiter(OutputDir)+ OutputFile;
        FCurrentOutputFile:= OutputFile;

        // Save the bookmark page
        PdfNew.SaveAs(OutputFile);
        Inc(FSplitCount);

        LogMessage(Format('Created: %s (bookmark: %s)', [ExtractFileName(OutputFile), Bookmarks[I].Title]));

      except
        on E: Exception do
        begin
          LogMessage(Format('Error processing bookmark "%s": %s', [Bookmarks[I].Title, E.Message]), LOG_ERROR);
          Continue;
        end;
      end;
    end;
  except
    on E: Exception do
    begin
      LogMessage('Error accessing bookmarks: '+ E.Message, LOG_ERROR);
      LogMessage('Falling back to individual page splitting');
      ProcessIndividualPages;
    end;
  end;
end;

procedure TFormMain.ShowCompletionSummary;
var
  ElapsedTime: TDateTime;
  Summary: string;
begin
  ElapsedTime:= Now- FStartTime;

  Summary:= Format('Split operation completed successfully!'+ sLineBreak+ sLineBreak+ 'Files created: %d'+ sLineBreak+ 'Elapsed time: %s'+ sLineBreak+
    'Output directory: %s', [FSplitCount, FormatDateTime('hh:nn:ss', ElapsedTime), GetOutputDirectory]);

  LogMessage(Format('Operation completed: %d files created in %s', [FSplitCount, FormatDateTime('hh:nn:ss', ElapsedTime)]), LOG_SUCCESS);

  if MessageDlg(Summary+ sLineBreak+ sLineBreak+ 'Would you like to open the output directory?', mtInformation, [mbYes, mbNo], 0)= mrYes
  then
  begin
    ShellExecute(Handle, 'open', PChar(GetOutputDirectory), nil, nil, SW_SHOWNORMAL);
  end;
end;

procedure TFormMain.btnCancelClick(Sender: TObject);
begin
  if FProcessing
  then
  begin
    FCancelled:= True;
    LogMessage('Cancelling operation...', LOG_WARNING);
    UpdateProgress('Cancelling...', 0, 0);
  end;
end;

procedure TFormMain.btnClearLogClick(Sender: TObject);
begin
  mmoLog.Clear;
  LogMessage('Log cleared');
end;

procedure TFormMain.btnExitClick(Sender: TObject);
begin
  if FProcessing
  then
  begin
    if MessageDlg('An operation is in progress. Are you sure you want to exit?', mtConfirmation, [mbYes, mbNo], 0)<> mrYes
    then
      Exit;
    FCancelled:= True;
  end;

  Close;
end;

procedure TFormMain.PdfPageChange(Sender: TObject);
begin
  // Handle page change events if needed
end;

procedure TFormMain.tmrProgressTimer(Sender: TObject);
var
  ElapsedTime: TDateTime;
  TimeStr: string;
begin
  if FProcessing
  then
  begin
    ElapsedTime:= Now- FStartTime;
    TimeStr:= FormatDateTime('hh:nn:ss', ElapsedTime);

    if FCurrentOutputFile<> ''
    then
      lblCurrentOperation.Caption:= Format('Processing... (%s) - %s', [TimeStr, ExtractFileName(FCurrentOutputFile)])
    else
      lblCurrentOperation.Caption:= Format('Processing... (%s)', [TimeStr]);
  end;
end;

procedure TFormMain.lnkLabelLinkClick(
  Sender    : TObject;
  const Link: string;
  LinkType  : TSysLinkType);
begin
  // Open the link in default browser
  ShellExecute(Handle, 'open', PChar(Link), nil, nil, SW_SHOWNORMAL);
end;

procedure TFormMain.mmoLogChange(Sender: TObject);
begin
  // Event handler for memo log changes - currently no action needed
end;

end.
