unit uPlugin;
{$INCLUDE 'Filters.inc'}

interface

uses
  Windows, SysUtils, Classes,
  {$INCLUDE 'DelphiFarUnits.inc'} ,
  {$IFDEF DEBUG_LOGS} uLog, {$ENDIF}
  uFilter,
  uFilterListUI,
  uConfig,
  uSystem, uConsts, uMessages;

type
  EFilterPluginError = class(EFarPluginError);

  TPlugin = class(TFarPlugin)
  private
    fPluginInfoAvailable: boolean;
    fPluginInfo: TPluginInfo;
  protected
    function PluginID: TGUID; override;
    function GetPluginTitle(out Description, Author: string): string; override;
    function GetPluginVersion: TVersionInfo; override;
    function GetPluginInfo(out Info: TPluginInfo): boolean; override;
    procedure PluginInit; override;
    function Execute(OpenFrom: DWORD; GUID: PGUID; Data: TIntPtr): THandle; override;
    function GetCommonString(ID: TFarPlugin.TCommonString): string; override;
    procedure DoLog(const Msg: string); override;
    function PluginConfig(GUID: PGUID; MenuItem: TIntPtr): boolean; override;
    function LoadFilterList: TFilterList;
    procedure SaveFilterList(List: TFilterList);
    procedure PopulateDefaultFilterList(List: TFilterList);
    function SelectFilter(const FileName, Content: string): TFilter;
    procedure GetEditorInfo(EditorID: TIntPtr; out EditorInfo: TEditorInfo; out FileName, Content: string);
    function ReplaceAllContent(EditorID: TIntPtr; const Content: string): boolean;
    function ReplaceSelectedContent(EditorID: TIntPtr; const Content: string): boolean;
    function ProcessEditor: boolean; overload;
    function ProcessEditor(Filter: TFilter): boolean; overload;
    function ProcessEditor(EditorID: TIntPtr): boolean; overload;
    function ProcessEditor(EditorID: TIntPtr; Filter: TFilter): boolean; overload;
    function ProcessMacroCall(const Params: array of Variant): boolean;
  public
    constructor Create;
    destructor Destroy; override;
    function CreateFilterList(WithDefaultFilters: boolean): TFilterList;
  end;

implementation

const
  PLUGIN_TITLE = 'Filters';
  PLUGIN_DESCRIPTION = 'Filters';
  PLUGIN_AUTHOR = 'Pepak, http://www.pepak.net';

{ TPlugin }

constructor TPlugin.Create;
begin
  inherited;
  fPluginInfoAvailable := False;
end;

function TPlugin.CreateFilterList(WithDefaultFilters: boolean): TFilterList;
var
  List: TFilterList;
begin
  List := LoadFilterList;
  try
    if WithDefaultFilters and (List.Count <= 0) then
      PopulateDefaultFilterList(List);
    Result := List;
    List := nil;
  finally
    FreeAndNil(List);
  end;
end;

destructor TPlugin.Destroy;
begin
  if fPluginInfoAvailable then begin
    DeletePluginInfo(fPluginInfo);
  end;
  inherited;
end;

procedure TPlugin.DoLog(const Msg: string);
begin
  {$IFDEF DEBUG_LOGS}
  uLog.Log(Msg);
  {$ENDIF}
end;

function TPlugin.Execute(OpenFrom: DWORD; GUID: PGUID; Data: TIntPtr): THandle;
begin
  Result := {$IFDEF FAR3_UP} 0 {$ELSE} INVALID_HANDLE_VALUE {$ENDIF} ;
  if OpenFrom = OPEN_EDITOR then
  begin
    ProcessEditor;
  end;
  {$IFDEF FAR3_UP}
  if OpenFrom = OPEN_FROMMACRO_ then
  begin
    if ProcessMacroCall(GetMacroParams(Data)) then
      Result := 1;
  end;
  {$ENDIF}
end;

function TPlugin.GetCommonString(ID: TFarPlugin.TCommonString): string;
begin
  case ID of
    TFarPlugin.TCommonString.MTitle:
      Result := GetMsg(MTitle);
    TFarPlugin.TCommonString.MExceptionOccurredIn:
      Result := GetMsg(MExceptionOccurredIn);
    TFarPlugin.TCommonString.MOk:
      Result := GetMsg(MOk);
    TFarPlugin.TCommonString.MCancel:
      Result := GetMsg(MCancel);
    TFarPlugin.TCommonString.MYes:
      Result := GetMsg(MYes);
    TFarPlugin.TCommonString.MNo:
      Result := GetMsg(MNo);
    else
      Result := inherited GetCommonString(ID);
  end;
end;

procedure TPlugin.GetEditorInfo(EditorID: TIntPtr; out EditorInfo: TEditorInfo; out FileName, Content: string);
var
  Line: string;
  SelStart, SelLength: TIntPtr;
begin
  if not TFarEditor.GetInfo(EditorID, EditorInfo) then
    raise EFilterPluginError.Create(GetMsg(MFailedToReadEditorInfo));
  if not TFarEditor.GetFileName(EditorID, FileName) then
    raise EFilterPluginError.Create(GetMsg(MFailedToReadEditorFileName));
  if EditorInfo.BlockType = BTYPE_NONE then
  begin
    if not TFarEditor.GetContent(EditorID, Content) then
      raise EFilterPluginError.Create(GetMsg(MFailedToReadEditorContent));
  end
  else
  begin
    if TFarEditor.GetString(EditorID, EditorInfo.BlockStartLine, Line, SelStart, SelLength) and (SelStart > 0) then
      TFarEditor.SetCursorPosition(EditorID, EditorInfo.BlockStartLine, SelStart - 1);
    if not TFarEditor.GetSelection(EditorID, Content) then
      raise EFilterPluginError.Create(GetMsg(MFailedToReadEditorContent));
  end;
end;

function TPlugin.GetPluginInfo(out Info: TPluginInfo): boolean;
var
  fMainMenu: TFarPlugin.TFarPluginMenu;
  fConfigMenu: TFarPlugin.TFarPluginMenu;
  fDiskMenu: TFarPlugin.TFarPluginMenu;
begin
  if not fPluginInfoAvailable then begin
    SetLength(fMainMenu, 1);
    fMainMenu[0].Title := GetMsg(MTitle);
    fMainMenu[0].ID := PLUGIN_MENU_MAIN;
    SetLength(fConfigMenu, 1);
    fConfigMenu[0].Title := GetMsg(MTitle);
    fConfigMenu[0].ID := PLUGIN_MENU_CONFIG;
    SetLength(fDiskMenu, 0);
    Self.fPluginInfo := CreatePluginInfo(
      PF_EDITOR or PF_DISABLEPANELS,
      fMainMenu, fConfigMenu, fDiskMenu,
      ''
    );
    fPluginInfoAvailable := True;
  end;
  Info := Self.fPluginInfo;
  Result := True;
end;

function TPlugin.GetPluginTitle(out Description, Author: string): string;
begin
  Result := PLUGIN_TITLE;
  Description := PLUGIN_DESCRIPTION;
  Author := PLUGIN_AUTHOR;
end;

function TPlugin.GetPluginVersion: TVersionInfo;
var
  FN: string;
  VerHigh, VerLow, VerBuild: LongWord;
begin
  FN := GetModuleName(HInstance);
  if GetFileVersion(FN, VerHigh, VerLow, VerBuild) then
    Result := MakeFarVersion( {$IFDEF FAR3_UP} VerHigh, VerLow, 0, VerBuild, 0 {$ELSE} VerHigh, VerLow, VerBuild {$ENDIF} )
  else
    Result := MakeFarVersion( {$IFDEF FAR3_UP} 0, 0, 0, 0, 0 {$ELSE} 0, 0 ,0 {$ENDIF} );
end;

function TPlugin.LoadFilterList: TFilterList;
var
  FilterKeys: TStringList;
  Config: TConfig;
  FilterConfig: TFilterConfig;
  FailedFilters: TStringList;
  FilterKey: string;
  Filter: TFilter;
  i: Integer;
begin
  Result := TFilterList.Create(True);
  try
    FilterKeys := nil;
    try
      Config := TConfig.Create;
      try
        FilterKeys := Config.GetFilterKeys;
      finally
        FreeAndNil(Config);
      end;
      FilterConfig := TFilterConfig.Create;
      try
        FilterConfig.MustExist := True;
        FailedFilters := TStringList.Create;
        try
          for i := 0 to Pred(FilterKeys.Count) do
          begin
            try
              FilterKey := FilterKeys[i];
              FilterConfig.Key := FilterKey;
              Filter := TFilter.CreateFromConfig(FilterConfig);
              Result.Add(Filter);
            except
              on E: Exception do
              begin
                if FilterKey <> '' then
                  FailedFilters.Add(FilterKey + ': ' + E.ClassName + ': ' + E.Message);
              end;
            end;
          end;
          if FailedFilters.Count > 0 then
          begin
            FailedFilters.Insert(0, GetMsg(MTitle));
            FailedFilters.Insert(1, GetMsg(MFiltersFailedToLoad));
            FailedFilters.Add(GetMsg(MOk));
            ShowMessage(FailedFilters, FMSG_WARNING, 1);
          end;
        finally
          FreeAndNil(FailedFilters);
        end;
      finally
        FreeAndNil(FilterConfig);
      end;
    finally
      FreeAndNil(FilterKeys);
    end;
  except
    FreeAndNil(Result);
    Raise;
  end;
end;

function TPlugin.PluginConfig(GUID: PGUID; MenuItem: TIntPtr): boolean;
var
  List: TFilterList;
  Dlg: TFilterListDialog;
begin
  Result := False;
  List := CreateFilterList(True);
  try
    Dlg := TFilterListDialog.Create;
    try
      Dlg.List := List;
      if Dlg.Execute then
      begin
        SaveFilterList(List);
      end;
    finally
      FreeAndNil(Dlg);
    end;
  finally
    FreeAndNil(List);
  end;
end;

function TPlugin.PluginID: TGUID;
begin
  Result := PLUGIN_GUID;
end;

procedure TPlugin.PluginInit;
begin
  inherited;
end;

procedure TPlugin.PopulateDefaultFilterList(List: TFilterList);
var
  Priority: TFilter.TPriority;
  Filter: TFilter;
  i: integer;
begin
  for Priority := High(Priority) downto Low(Priority) do
  begin
    if Priority = prNone then
      Continue;
    for i := 0 to Pred(TRegisteredFilters.Count) do
    begin
      if TRegisteredFilters.Items[i].Priority = Priority then
      begin
        Filter := TRegisteredFilters.Items[i].Create;
        List.Add(Filter);
      end;
    end;
  end;
end;

function TPlugin.ProcessEditor: boolean;
begin
  Result := ProcessEditor(-1);
end;

function TPlugin.ProcessEditor(Filter: TFilter): boolean;
begin
  Result := ProcessEditor(-1, Filter);
end;

function TPlugin.ProcessEditor(EditorID: TIntPtr): boolean;
begin
  Result := ProcessEditor(EditorID, nil);
end;

function TPlugin.ProcessEditor(EditorID: TIntPtr; Filter: TFilter): boolean;
var
  EditorInfo: TEditorInfo;
  FileName: string;
  Content: string;
  CanUndo: boolean;
  ActiveFilter: TFilter;
begin
  Result := False;
  CanUndo := TFarEditor.BeginUndoBlock(EditorID);
  try
    // Get editor's content
    GetEditorInfo(EditorID, EditorInfo, FileName, Content);
    if EditorInfo.BlockType = BTYPE_COLUMN then
      raise EFilterPluginError.Create(GetMsg(MColumnBlocksNotSupported));
    // Find a suitable filter, if none was pre-selected
    if Filter = nil then
    begin
      ActiveFilter := SelectFilter(FileName, Content);
      if ActiveFilter = nil then
        Exit;
    end
    else
    begin
      ActiveFilter := Filter;
    end;
    ActiveFilter := ActiveFilter.Clone;
    try
      // Allow the user to override the settings, if needed
      if ActiveFilter.EditBeforeUse then
      begin
        if not ActiveFilter.ConfigureBeforeUse then
          Abort;
      end;
      // Apply the ActiveFilter to the content
      Content := ActiveFilter.Execute(FileName, Content);
      // Write the content back into the editor
      try
        if EditorInfo.BlockType = BTYPE_NONE then
        begin
          Result := ReplaceAllContent(EditorID, Content);
        end
        else
        begin
          Result := ReplaceSelectedContent(EditorID, Content);
        end;
      finally
        if CanUndo then
          TFarEditor.EndUndoBlock(EditorID);
      end;
    finally
      FreeAndNil(ActiveFilter);
    end;
  finally
    if CanUndo and (not Result) then
      TFarEditor.Undo(EditorID);
  end;
end;

function TPlugin.ProcessMacroCall(const Params: array of Variant): boolean;
var
  FunctionName: string;
  FilterID: string;
  FilterList: TFilterList;
  FilterClass: TFilterClass;
  FilterName: string;
  Filter: TFilter;
  i: Integer;
begin
  Result := False;
  Filter := nil;
  try
    if Length(Params) > 0 then
    begin
      FunctionName := Params[0];
      if FunctionName = 'execute' then
      begin
        if Length(Params) > 1 then
        begin
          FilterID := Params[1];
          if TRegisteredFilters.Find(FilterID, FilterClass) then
            Filter := FilterClass.Create
          else if FilterID <> '' then
            raise EFilterPluginError.CreateFmt(GetMsg(MUnknownFilter), [FilterID]);
        end;
        if ProcessEditor(Filter) then
          Result := True;
      end
      else if FunctionName = 'execute-by-name' then
      begin
        FilterName := '';
        if Length(Params) > 1 then
        begin
          FilterName := Params[1];
          FilterList := CreateFilterList(True);
          try
            for i := 0 to Pred(FilterList.Count) do
            begin
              if FilterList[i].Description = FilterName then
              begin
                Filter := FilterList[i].Clone;
                Break;
              end;
            end;
          finally
            FreeAndNil(FilterList);
          end;
        end;
        if Filter = nil then
          raise EFilterPluginError.CreateFmt(GetMsg(MUnknownFilter), [FilterName]);
        if ProcessEditor(Filter) then
          Result := True;
      end;
    end;
  finally
    FreeAndNil(Filter);
  end;
end;

function TPlugin.ReplaceAllContent(EditorID: TIntPtr; const Content: string): boolean;
begin
  TFarEditor.SetCursorPosition(EditorID, 0, 0);
  if not TFarEditor.SelectAll(EditorID) then
    raise EFilterPluginError.Create(GetMsg(MFailedToSelectAllContent));
  Result := ReplaceSelectedContent(EditorID, Content);
end;

function TPlugin.ReplaceSelectedContent(EditorID: TIntPtr; const Content: string): boolean;
var
  EditorInfo: TEditorInfo;
  Line, TopLine: integer;
begin
  if not TFarEditor.GetInfo(EditorID, EditorInfo) then
    raise EFilterPluginError.Create(GetMsg(MFailedToReadEditorInfo));
  if EditorInfo.BlockType = BTYPE_COLUMN then
    raise EFilterPluginError.Create(GetMsg(MColumnBlocksNotSupported));
  if EditorInfo.BlockType <> BTYPE_NONE then
    if not TFarEditor.DeleteSelection(EditorID) then
      raise EFilterPluginError.Create(GetMsg(MFailedToDeleteOldContent));
  if not TFarEditor.InsertText(EditorID, Content) then
    raise EFilterPluginError.Create(GetMsg(MFailedToInsertNewContent));
  if EditorInfo.BlockType = BTYPE_NONE then
  begin
    Line := EditorInfo.CurLine;
    TopLine := EditorInfo.TopScreenLine;
  end
  else
  begin
    Line := EditorInfo.BlockStartLine;
    if Line >= 5 then
      TopLine := Line - 5
    else
      TopLine := 0;
  end;
  TFarEditor.SetCursorPosition(EditorID, Line, EditorInfo.CurPos, EditorInfo.CurTabPos, TopLine, EditorInfo.LeftPos, EditorInfo.Overtype);
  Result := True;
end;

procedure TPlugin.SaveFilterList(List: TFilterList);
var
  Config: TConfig;
  FilterConfig: TFilterConfig;
  i: Integer;
begin
  Config := TConfig.Create;
  try
    Config.DeleteAllFilters;
  finally
    FreeAndNil(Config);
  end;
  FilterConfig := TFilterConfig.Create;
  try
    FilterConfig.MustExist := False;
    for i := 0 to Pred(List.Count) do
    begin
      FilterConfig.Key := Format('%6.6d', [i]);
      List[i].SaveToConfig(FilterConfig);
    end;
  finally
    FreeAndNil(FilterConfig);
  end;
end;

function TPlugin.SelectFilter(const FileName, Content: string): TFilter;
var
  FilterList: TFilterList;
  Menu: TStringList;
  ItemIndex: integer;
  i: integer;
begin
  Result := nil;
  FilterList := CreateFilterList(True);
  try
    // Keep only the supported filters
    for i := Pred(FilterList.Count) downto 0 do
    begin
      if not FilterList[i].IsSupported(FileName, Content) then
      begin
        FilterList.Delete(i);
      end;
    end;
    // If no supported filter is found, report an error
    if FilterList.Count <= 0 then
      raise EFilterPluginError.Create(GetMsg(MNoApplicableFilterFound));
    // If only one filter remains, use it
    if FilterList.Count = 1 then
    begin
      Result := FilterList[0].Clone;
    end
    // Else let user choose a filter
    else
    begin
      Menu := TStringList.Create;
      try
        for i := 0 to Pred(FilterList.Count) do
        begin
          if FilterList[i].Description <> '' then
            Menu.Add(FilterList[i].Description)
          else
            Menu.Add(FilterList[i].Name);
        end;
        if Self.ShowMenu(PLUGIN_MENU_FILTERS, GetMsg(MSelectFilter), '', Menu, ItemIndex) then begin
          Result := FilterList[ItemIndex].Clone;
        end;
      finally
        FreeAndNil(Menu);
      end;
    end;
  finally
    FreeAndNil(FilterList);
  end;
end;

initialization
  TFarPlugin.InitInstance(TPlugin.Create);

end.
