unit HashTableGen;

{$I SynEdit.inc}

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls;

type
  TFrmHashTableGen = class(TForm)
    LabelParams: TLabel;
    LabelD: TLabel;
    LabelC: TLabel;
    LabelM: TLabel;
    Label1: TLabel;
    Label2: TLabel;
    LabelPercentage: TLabel;
    ProgressBar1: TProgressBar;
    EditD: TMemo;
    EditC: TMemo;
    EditM: TMemo;
    ButtonFindHash: TButton;
    procedure ButtonFindHashClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    FKeyList: TList;
    FCaseSensitive: Boolean;
  public
    procedure AssignKeyWords(KeyList: TList; CaseSensitive: Boolean);
    function GetHashKeyFunctionSource(ClassName: string): string;
    function GetKeyWordConstantsSource(CaseSensitive: Boolean): string;
    function KeyIndicesCount: Integer;
  end;

const
  MaxTableSize = 100000;

type
  THashKeyList = class
  private
    FMaxHashKey: Integer;
    FHashKeys: array[0..MaxTableSize - 1] of Boolean;
  public
    function Add(HashKey: Integer): Boolean;
    procedure Clear;
  end;

var
  FrmHashTableGen: TFrmHashTableGen;

implementation

{$R *.dfm}

uses
{$IFDEF SYN_COMPILER_6_UP}
  StrUtils,
{$ENDIF}
  SynGenUnit,
  SynUnicode;

{$I primenumbers.inc}

var
  c, d, m: Cardinal;
  FinalC, FinalD, FinalM: Cardinal;
  searching: Boolean;
  KeyWords: array of UnicodeString;
  HashKeyList: THashKeyList;

{$Q-}
function HashKey(const S: UnicodeString): Cardinal;
var
  i: Integer;
begin
  Result := 0;
  for i := 1 to Length(S) do
    Result := Result * c + Ord(S[i]) * d;
  Result := Result mod m;
end;
{$Q+}

{$Q-}
function FinalHashKey(const S: UnicodeString): Cardinal;
var
  i: Integer;
begin
  Result := 0;
  for i := 1 to Length(S) do
    Result := Result * FinalC + Ord(S[i]) * FinalD;
  Result := Result mod FinalM;
end;
{$Q+}

procedure WordWrapAtCol80(Words, Result: TStrings; Indentation: Integer);
var
  WrappedLines: TStringList;
  i: Integer;
  Line: string;
begin
  WrappedLines := TStringList.Create;
  try
    i := 0;

    while i < Words.Count do
    begin
      Line := StringOfChar(' ', Indentation);
      while (i < Words.Count) and (Length(Line) + Length(Words[i]) <= 80) do
      begin
        Line := Line + Words[i] + ' ';
        inc(i);
      end;
      WrappedLines.Add(Line);
    end;

    Result.Assign(WrappedLines);
  finally
    WrappedLines.Free;
  end;
end;

{ TFrmHashTableGen }

procedure TFrmHashTableGen.FormCreate(Sender: TObject);
begin
  HashKeyList := THashKeyList.Create;
  ProgressBar1.Max := 1000;
end;

procedure TFrmHashTableGen.FormDestroy(Sender: TObject);
begin
  HashKeyList.Free;
end;

procedure TFrmHashTableGen.AssignKeyWords(KeyList: TList; CaseSensitive: Boolean);
var
  i: Integer;
  KeyWordsList: TStringList;
begin
  FKeyList := nil;
  FCaseSensitive := CaseSensitive;
  SetLength(KeyWords, 0);
  HashKeyList.Clear;

  KeyWordsList := TStringList.Create;
  try
    KeyWordsList.Sorted := True;
    KeyWordsList.Duplicates := dupIgnore;
    for i := 0 to KeyList.Count - 1 do
      KeyWordsList.Add(TLexKeys(KeyList[i]).KeyName);

    SetLength(KeyWords, KeyWordsList.Count);
    if CaseSensitive then
     for i := 0 to KeyWordsList.Count - 1 do
       KeyWords[i] := KeyWordsList[i]
    else
      for i := 0 to KeyWordsList.Count - 1 do
        KeyWords[i] := SynWideLowerCase(KeyWordsList[i]);

    FKeyList := KeyList;
  finally
    KeyWordsList.Free;
  end;
end;

procedure TFrmHashTableGen.FormShow(Sender: TObject);
begin
  c := 0;
  d := 0;
  m := 0;
  FinalC := 0;
  FinalD := 0;
  FinalM := 0;
  ProgressBar1.Position := 0;
  EditD.Lines.Text := '0';
  EditC.Lines.Text := '0';
  EditM.Lines.Text := '0';
  LabelPercentage.Caption := '0%';
  ButtonFindHash.SetFocus;
  ButtonFindHash.Caption := 'Find Hash Params';
end;

procedure TFrmHashTableGen.FormClose(Sender: TObject;
  var Action: TCloseAction);
var
  i: Integer;
begin
  if Assigned(FKeyList) then
  begin
    for i := 0 to FKeyList.Count - 1 do
      with TLexKeys(FKeyList[i]) do
      begin
        if FCaseSensitive then 
          key := FinalHashKey(KeyName)
        else 
          Key := FinalHashKey(SynWideLowerCase(KeyName));
      end;
  end;
end;

procedure TFrmHashTableGen.ButtonFindHashClick(Sender: TObject);
var
  i, j: Integer;
  collided: Boolean;
  Key, smallestM: Cardinal;

  procedure SearchStop;
  begin
    ButtonFindHash.Caption := 'Find Hash Params';
    searching := False;
    Close;
    if FinalM = 0 then
      raise Exception.Create('Cannot build the hashtable as no working hash parameters were found');
  end;

begin
  collided := False;
  
  if searching then
  begin
    SearchStop;
    Exit;
  end
  else
  begin
    ProgressBar1.Position := 0;
    LabelPercentage.Caption := '0%';
    Application.ProcessMessages;

    if Length(KeyWords) = 0 then exit;

    searching := True;
    ButtonFindHash.Caption := 'Stop Search';
  end;

  smallestM := MaxTableSize + 1;

  for d := 1 to 1000 do
  begin
    for c := 1 to 1000 do
      for j := 0 to High(PrimeNumbers) do
      begin
        m := PrimeNumbers[j];
        if m >= smallestM then
        begin
          m := smallestM;
          Break;
        end;
        for i := Low(KeyWords) to High(KeyWords) do
        begin
          Key := HashKey(KeyWords[i]);
          collided := HashKeyList.Add(Key);
          if collided then
          begin
            HashKeyList.Clear;
            break;
          end;
        end;
        if not collided then
        begin
          smallestM := m;
          EditD.Lines.Text := IntToStr(d);
          EditC.Lines.Text := IntToStr(c);
          EditM.Lines.Text := IntToStr(m);
          FinalD := d;
          FinalC := c;
          FinalM := m;
          if m = Cardinal(Length(KeyWords)) then
          begin
            ProgressBar1.Position := ProgressBar1.Max;
            LabelPercentage.Caption := '100%';
            SearchStop;
            Exit;
          end;
        break; // all the following solutions will only have a bigger array
        end;
        Application.ProcessMessages;
        if not searching then
        begin
          SearchStop;
          Exit;
        end;
      end;
    ProgressBar1.Position := d;
    LabelPercentage.Caption := FloatToStr(d / 10) + '%';
    Application.ProcessMessages;
  end;

  SearchStop;
end;

function TFrmHashTableGen.GetHashKeyFunctionSource(ClassName: string): string;
begin
  Result := '{$Q-}'#13#10;
  Result := Result + Format('function %s.HashKey(Str: PWideChar): Cardinal;', [ClassName]) + #13#10;
  Result := Result + 'begin'#13#10;
  Result := Result + '  Result := 0;'#13#10;
  Result := Result + '  while IsIdentChar(Str^) do'#13#10;
  Result := Result + '  begin'#13#10;
  if (FinalC = 1) and (FinalD = 1) then
    Result := Result + '    Result := Result + Ord(Str^);'#13#10
  else if FinalC = 1 then
    Result := Result + Format('    Result := Result + Ord(Str^) * %d;', [FinalD]) + #13#10
  else if FinalD = 1 then
    Result := Result + Format('    Result := Result * %d + Ord(Str^);', [FinalC]) + #13#10
  else
    Result := Result + Format('    Result := Result * %d + Ord(Str^) * %d;', [FinalC, FinalD]) + #13#10;
  Result := Result + '    inc(Str);'#13#10;
  Result := Result + '  end;'#13#10;
  Result := Result + '  Result := Result mod ' + IntToStr(FinalM) + ';'#13#10;
  Result := Result + '  fStringLen := Str - fToIdent;'#13#10;
  Result := Result + 'end;'#13#10;
  Result := Result + '{$Q+}'#13#10;
end;

{$IFNDEF SYN_COMPILER_6_UP}
function DupeString(const AText: string; ACount: Integer): string;
var
  P: PChar;
  C: Integer;
begin
  C := Length(AText);
  SetLength(Result, C * ACount);
  P := Pointer(Result);
  if P = nil then Exit;
  while ACount > 0 do
  begin
    Move(Pointer(AText)^, P^, C);
    Inc(P, C);
    Dec(ACount);
  end;
end;
{$ENDIF}

function TFrmHashTableGen.GetKeyWordConstantsSource(CaseSensitive: Boolean): string;
var
  i: Integer;
  sl: TStringList;
  LastItem: string;
begin
  // write KeyWords
  if not CaseSensitive then
    Result := Result + '  // as this language is case-insensitive keywords *must* be in lowercase'#13#10;
  Result := Result + Format('  KeyWords: array[0..%d] of UnicodeString = (', [High(KeyWords)]) + #13#10;
  sl := TStringList.Create;
  try
    for i := Low(KeyWords) to High(KeyWords) do
      sl.Add(#39 + KeyWords[i] + #39 + ',');

    if sl.Count > 0 then
    begin
      // remove comma from last line
      LastItem := sl[sl.Count - 1];
      Delete(LastItem, Length(LastItem), 1);
      sl[sl.Count - 1] := LastItem;

      WordWrapAtCol80(sl, sl, 4);

      Result := Result + sl.Text;
    end;
  finally
    sl.Free;
  end;
  Result := Result + '  );'#13#10;

  Result := Result + #13#10;

  // write KeyIndices
  Result := Result + Format('  KeyIndices: array[0..%d] of Integer = (', [FinalM - 1]) + #13#10;
  sl := TStringList.Create;
  try
    sl.Text := DupeString('-1,'#13#10, FinalM);
    for i := Low(KeyWords) to High(KeyWords) do
      sl[FinalHashKey(KeyWords[i])] := IntToStr(i) + ',';

    if sl.Count > 0 then
    begin
      // remove comma from last line
      LastItem := Trim(sl[sl.Count - 1]);
      Delete(LastItem, Length(LastItem), 1);
      sl[sl.Count - 1] := LastItem;

      WordWrapAtCol80(sl, sl, 4);

      Result := Result + sl.Text;
    end;
  finally
    sl.Free;
  end;
  Result := Result + '  );'#13#10;
end;

function TFrmHashTableGen.KeyIndicesCount: Integer;
begin
  Result := FinalM;
end;

{ THashKeyList }

function THashKeyList.Add(HashKey: Integer): Boolean;
begin
  if HashKey > FMaxHashKey then
    FMaxHashKey := HashKey;
  Result := FHashKeys[HashKey];
  FHashKeys[HashKey] := True;
end;

procedure THashKeyList.Clear;
begin
  FillChar(FHashKeys, FMaxHashKey + 1, 0);
  FMaxHashKey := 0;
end;

end.
