unit SpellCheck;

{ Simple unit to simplify/OOP-ize pascal-style the aspell interface. Currently
  very limited, will be expanded eventually. Use like you wish. }

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, Aspell;

type
  TSuggestionArray = array of string;
  
  TWordError = record
    Word: string; // the word itself
    Pos: LongWord; // word position in line
    Length: LongWord; // word length
    Suggestions: TSuggestionArray; // suggestions for the given word
  end;
  
  TLineErrors = array of TWordError;
  TLineErrorsArray = array of TLineErrors;

  { TSpellCheck }

  TSpeller = class // abstract class, basis for all checkers
   protected
    FMode: string;
    FEncoding: string;
    FLanguage: string;
    procedure SetEncoding(const AValue: string);
    procedure SetLanguage(const AValue: string);
    procedure SetMode(const AValue: string);
    procedure CreateSpeller; virtual; abstract;
    procedure FreeSpeller; virtual; abstract;
   public
    constructor Create;
    destructor Destroy; override;
   public
    property Mode: string read FMode write SetMode;
    property Encoding: string read FEncoding write SetEncoding;
    property Language: string read FLanguage write SetLanguage;
  end;
  
  { TWordSpeller }

  TWordSpeller = class(TSpeller) // class for simple per-word checking
   private
    FSpeller: PAspellSpeller;
    FLastError: string;
    function DoCreateSpeller(Lang, Enc, aMode: pChar): PAspellSpeller;
   protected
    procedure CreateSpeller; override;
    procedure FreeSpeller; override;
   public
    function SpellCheck(const Word: string): TSuggestionArray; // use to check single words, parsed out by you
  end;
  
  { TDocumentSpeller }

  TDocumentSpeller = class(TWordSpeller)
   private
    FChecker: PAspellDocumentChecker;
    FLineErrors: TLineErrorsArray;
    FNameSuggestions: Boolean;
    function GetLineErrors(i: Integer): TLineErrors;
    function GetLineErrorsCount: Integer;
   protected
    procedure CreateSpeller; override;
    procedure FreeSpeller; override;
    procedure DoNameSuggestions(const Word: string; var aWordError: TWordError);
   public
    constructor Create;
    function CheckLine(const aLine: string): TLineErrors;
    function CheckDocument(const FileName: string): Integer; // returns number of spelling errors found or -1 for error
    function CheckDocument(aStringList: TStringList): Integer; // returns number of spelling errors found or -1 for error
    procedure Reset;
   public
    property LineErrors[i: Integer]: TLineErrors read GetLineErrors;
    property LineErrorsCount: Integer read GetLineErrorsCount;
    property NameSuggestions: Boolean read FNameSuggestions write FNameSuggestions;
  end;

implementation

const
  DEFAULT_ENCODING = 'utf-8';
  DEFAULT_LANGUAGE = 'en';
  DEFAULT_MODE     = '';

function GetDefaultLanguage: string;
begin
  Result := GetEnvironmentVariable('LANG');
  if Length(Result) = 0 then
    Result := DEFAULT_LANGUAGE;
end;

{ TSpeller }

procedure TSpeller.SetEncoding(const AValue: string);
begin
  FEncoding := aValue;
  CreateSpeller;
end;

procedure TSpeller.SetLanguage(const AValue: string);
begin
  FLanguage := aValue;
  CreateSpeller;
end;

procedure TSpeller.SetMode(const AValue: string);
begin
  FMode := aValue;
  CreateSpeller;
end;

constructor TSpeller.Create;
begin
  FEncoding := DEFAULT_ENCODING;
  FLanguage := GetDefaultLanguage;
  FMode := DEFAULT_MODE;

  CreateSpeller;
end;

destructor TSpeller.Destroy;
begin
  FreeSpeller;
end;

{ TWordSpeller }

function TWordSpeller.DoCreateSpeller(Lang, Enc, aMode: pChar): PAspellSpeller;
var
  Error: Paspellcanhaveerror;
begin
  Result := new_aspell_config();

  if Length(FLanguage) > 0 then
    aspell_config_replace(Result, 'lang', Lang);
  if Length(FEncoding) > 0 then
    aspell_config_replace(Result, 'encoding', Enc);
  if Length(FMode) > 0 then
    aspell_config_replace(Result, 'mode', aMode);

  Error := new_aspell_speller(Result);

  delete_aspell_config(Result);

  if aspell_error_number(Error) <> 0 then begin
    FLastError := aspell_error_message(Error);
    delete_aspell_can_have_error(Error);
    Result := nil;
  end else
    Result := to_aspell_speller(Error);
end;

procedure TWordSpeller.CreateSpeller;
begin
  FLastError := '';
  FreeSpeller;

  FSpeller := DoCreateSpeller(pChar(FLanguage), pChar(FEncoding), pChar(FMode));
  if not Assigned(FSpeller) then
    FSpeller := DoCreateSpeller(nil, pChar(FEncoding), pChar(FMode));
  if not Assigned(FSpeller) then
    FSpeller := DoCreateSpeller(nil, pChar(FEncoding), nil);
  if not Assigned(FSpeller) then
    FSpeller := DoCreateSpeller(nil, nil, pChar(FMode));
  if not Assigned(FSpeller) then
    FSpeller := DoCreateSpeller(nil, nil, nil);

  if not Assigned(FSpeller) then
    raise Exception.Create('Error on speller creation: ' + FLastError);
end;

procedure TWordSpeller.FreeSpeller;
begin
  if Assigned(FSpeller) then begin
    delete_aspell_speller(FSpeller);
    FSpeller := nil;
  end;
end;

function TWordSpeller.SpellCheck(const Word: string): TSuggestionArray;
var
  sgs: Paspellwordlist;
  elm: Paspellstringenumeration;
  tmp: pChar;
  i: Integer = 0;
begin
  SetLength(Result, 0);

  if aspell_speller_check(FSpeller, pChar(Word), Length(Word)) = 0 then begin
    sgs := aspell_speller_suggest(FSpeller, pChar(Word), Length(Word));
    elm := aspell_word_list_elements(sgs);

    repeat
      if i >= Length(Result) then
        SetLength(Result, Length(Result) + 10);

      tmp := aspell_string_enumeration_next(elm);

      if tmp <> nil then begin
        Result[i] := tmp;
        Inc(i);
      end;
    until tmp = nil;

    SetLength(Result, i);

    delete_aspell_string_enumeration(elm);
  end;
end;

{ TDocumentSpeller }

function TDocumentSpeller.GetLineErrors(i: Integer): TLineErrors;
begin
  Result := FLineErrors[i];
end;

function TDocumentSpeller.GetLineErrorsCount: Integer;
begin
  Result := Length(FLineErrors);
end;

procedure TDocumentSpeller.CreateSpeller;
var
  Error: PAspellCanHaveError;
begin
  inherited CreateSpeller;
  
  Error := new_aspell_document_checker(FSpeller);

  if aspell_error_number(Error) <> 0 then
    raise Exception.Create('Error on checker creation: ' + aspell_error_message(Error))
  else
    FChecker := to_aspell_document_checker(Error);
end;

procedure TDocumentSpeller.FreeSpeller;
begin
  if Assigned(FChecker) then begin
    delete_aspell_document_checker(FChecker);
    FChecker := nil;
  end;

  inherited FreeSpeller;
end;

procedure TDocumentSpeller.DoNameSuggestions(const Word: string;
  var aWordError: TWordError);
begin
  aWordError.Suggestions := SpellCheck(Word);
end;

constructor TDocumentSpeller.Create;
begin
  inherited Create;
  
  FNameSuggestions := True;
end;

function TDocumentSpeller.CheckLine(const aLine: string): TLineErrors;
const
  CHUNK_SIZE = 10;
var
  i, Count: Integer;
  Token: AspellToken;
begin
  aspell_document_checker_process(FChecker, pChar(aLine), Length(aLine));

  SetLength(Result, CHUNK_SIZE);
  i := 0;
  Count := 0;
  repeat
    Token := aspell_document_checker_next_misspelling(FChecker);

    if Token.len > 0 then begin
      if Length(Result) <= i then
        SetLength(Result, Length(Result) + CHUNK_SIZE);

      Result[i].Word := Copy(aLine, Token.offset + 1, Token.len);
      Result[i].Pos := Token.offset + 1; // C goes from 0, we go from 1
      Result[i].Length := Token.len;

      if FNameSuggestions then
        DoNameSuggestions(Copy(aLine, Token.offset + 1, Token.len), Result[i]);
        
      Inc(Count);
    end;

    Inc(i);
  until Token.len = 0;
  
  SetLength(Result, Count);
end;

function TDocumentSpeller.CheckDocument(const FileName: string): Integer;
var
  s: TStringList;
begin
  Result := 0;
  if FileExists(FileName) then try
    s := TStringList.Create;
    s.LoadFromFile(FileName);
    Result := CheckDocument(s);
  finally
    s.Free;
  end;
end;

function TDocumentSpeller.CheckDocument(aStringList: TStringList): Integer;
var
  i: Integer;
begin
  Result := 0;
  SetLength(FLineErrors, aStringList.Count);

  for i := 0 to aStringList.Count - 1 do begin
    FLineErrors[i] := CheckLine(aStringList[i]);
    Inc(Result, Length(FLineErrors[i]));
  end;
end;

procedure TDocumentSpeller.Reset;
begin
  aspell_document_checker_reset(FChecker);
end;

end.

