// SPDX-License-Identifier: GPL-3.0-only
unit UCommandline;

{$mode objfpc}{$H+}

interface

uses classes, LazpaintType, uresourcestrings, LCLStrConsts;

{$IFDEF WINDOWS}
  {$DEFINE SHOW_MANUAL_IN_WINDOW}
{$ENDIF}

const Manual: array[0..79] of string = (
'NAME',
'       LazPaint - Image editor',
'',
'SYNOPSIS',
'       lazpaint [INPUT FILE] [OUTPUT FILE]',
'       lazpaint [INPUT FILE] [ACTION]... [OUTPUT FILE]',
'',
'DESCRIPTION',
'       Graphics viewer and editor.',
'',
'       Can  read  layered  files (lzp, ora, pdn, oXo), multi-images (gif, ico,',
'       tiff), flat files (bmp, jpeg,  pcx,  png,  tga,  xpm,  xwd),  vectorial',
'       (svg),  3D  (obj). Has drawing tools, phong shading, curve adjustments,',
'       filters and render some textures.',
'',
'OPTIONS',
'       If supplied, the INPUT FILE is loaded. If the OUTPUT FILE is  supplied,',
'       the image is saved and the program ends. Otherwise, the GUI of the pro‐',
'       gram is displayed.',
'',
'       -scriptbasedir DIRECTORY',
'              set the directory where Python scripts for LazPaint are located.',
'',
'       -script FILENAME',
'              runs the specified Python script. It must have  a  ".py"  exten‐',
'              sion.',
'',
'       -editor default|CONFIGFILE|OPTION1,OPTION2...',
'              shows the image editor with validate and cancel buttons.  If the',
'              validate button is used,  the rest of the commands are executed.',
'              Otherwise the program stops.',
'',
'              Examples:',
'              -editor default',
'              -editor /Users/me/lazpaintCustom.cfg',
'              -editor [Window]ColorWindowVisible=0,LayerWindowVisible=0',
'',
'       -quit',
'              quits the program even if no output file was  provided.  Can  be',
'              useful when only running scripts.',
'',
'       -new WIDTH,HEIGHT',
'              creates an empty image of size WIDTH x HEIGHT.',
'',
'       -screenshot SCREEN|X1,Y1,X2,Y2',
'              takes a screenshot of the screen of index SCREEN  (0 for primary',
'              monitor) or of specified coordinates.',
'',
'       -resample WIDTH,HEIGHT',
'              resamples the image to the size WIDTH x HEIGHT.',
'',
'       -opacity ALPHA',
'              applies the opacity to the image. ALPHA is between 0 and 255.',
'',
'       -gradient R1,G1,B1,A1,R2,G2,B2,A2,TYPE,X1,Y1,X2,Y2',
'              renders  a gradient from point X1,Y1 to point X2,Y2. TYPE can be',
'              linear, reflected, diamond,  radial  or  angular.  The  starting',
'              color is (R1,G1,B1,A1) and final color is (R2,G2,B2,A2).',
'',
'       -horizontalflip',
'              flips selection or image horizontally.',
'',
'       -verticalflip',
'              flips selection or image vertically.',
'',
'       -swapredblue',
'              swap red and blue channels.',
'',
'       -smartzoom3',
'              resample  the  image 3 times bigger with smart detection of bor‐',
'              ders.',
'',
'       -rotatecw',
'              rotates the image clockwise.',
'',
'       -rotateccw',
'              rotates the image counter-clockwise.',
'',
'       -rotate180',
'              rotates the image 180 degrees.');

procedure ProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList; out errorEncountered, fileSaved, quitQuery: boolean);
function ParamStrUTF8(AIndex: integer): string;

implementation

uses
  SysUtils, BGRAUTF8, LazFileUtils, BGRABitmap, BGRABitmapTypes, BGRALayers, Dialogs, uparse,
  UImage, UImageAction, ULayerAction, UScripting, UPython, Forms, Controls,
  UFileSystem, BGRAIconCursor, UGraph
  {$IFDEF SHOW_MANUAL_IN_WINDOW},StdCtrls{$ENDIF};

function ParamStrUTF8(AIndex: integer): string;
begin
  result := SysToUTF8(ParamStr(AIndex)); //not perfect
end;

procedure InternalProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList;
  out errorEncountered, fileSaved, quitQuery: boolean; AImageActions: TImageActions);
var
  commandPrefix: set of char;
  InputFilename:string;

  i,iStart: integer;
  errPos: integer; //number conversion

  //functions
  CommandStr,LowerCmd:string;
  funcParams: ArrayOfString;
  Filter: TPictureFilter;

  //resample
  w,h: integer;

  //opacity
  opacity: byte;

  //gradient
  c1,c2: TBGRAPixel;
  gt: TGradientType;
  o1,o2: TPointF;
  layerAction: TLayerAction;
  enableScript: Boolean;

  function DoGradient: boolean;
  begin
    //c1, c2: TBGRAPixel; gtype: TGradientType; o1, o2: TPointF;
    funcParams := SimpleParseFuncParam(CommandStr);
    if length(funcParams)<>13 then
    begin
      instance.ShowError('Gradient','"Gradient" '+StringReplace(rsExpectNParameters,'N','13',[])+'red1,green1,blue1,alpha1,red2,green2,blue2,alpha2,type,x1,y1,x2,y2');
      errorEncountered := true;
      exit(false);
    end;
    val(funcParams[0],c1.red,errPos);
    val(funcParams[1],c1.green,errPos);
    val(funcParams[2],c1.blue,errPos);
    val(funcParams[3],c1.alpha,errPos);
    val(funcParams[4],c2.red,errPos);
    val(funcParams[5],c2.green,errPos);
    val(funcParams[6],c2.blue,errPos);
    val(funcParams[7],c2.alpha,errPos);
    gt := StrToGradientType(funcParams[8]);
    val(funcParams[9],o1.x,errPos);
    val(funcParams[10],o1.y,errPos);
    val(funcParams[11],o2.x,errPos);
    val(funcParams[12],o2.y,errPos);
    layerAction := instance.Image.CreateAction(true);
    layerAction.DrawingLayer.GradientFill(0,0,
      instance.Image.Width,instance.Image.Height,
      c1,c2,gt,o1,o2,dmDrawWithTransparency,True,False);
    layerAction.Validate;
    FreeAndNil(layerAction);
    result := true;
  end;

  function DoOpacity: boolean;
  begin
    funcParams := SimpleParseFuncParam(CommandStr);
    if length(funcParams)<>1 then
    begin
      instance.ShowError('Opacity','"Opacity" ' + rsExpect1Parameter+CommandStr);
      errorEncountered := true;
      exit(false);
    end;
    val(funcParams[0],opacity,errPos);
    if (errPos <> 0) then
    begin
      instance.ShowError('Opacity',rsInvalidOpacity+CommandStr);
      errorEncountered := true;
      exit(false);
    end;
    layerAction := instance.Image.CreateAction(true);
    layerAction.DrawingLayer.ApplyGlobalOpacity(opacity);
    layerAction.Validate;
    FreeAndNil(layerAction);
    result := true;
  end;

  function DoResample: boolean;
  begin
    funcParams := SimpleParseFuncParam(CommandStr);
    if length(funcParams)<>2 then
    begin
      instance.ShowError('Resample','"Resample" ' + rsExpect2Parameters+CommandStr);
      errorEncountered := true;
      exit(false);
    end;
    val(funcParams[0],w,errPos);
    val(funcParams[1],h,errPos);
    if (errPos <> 0) or (w <= 0) or (h <= 0) then
    begin
      instance.ShowError('Resample',rsInvalidResampleSize+CommandStr);
      errorEncountered := true;
      exit(false);
    end;
    instance.Image.Resample(w,h,rfHalfCosine);
    result := true;
  end;

  function DoNew: boolean;
  begin
    funcParams := SimpleParseFuncParam(CommandStr);
    if length(funcParams)<>2 then
    begin
      instance.ShowError('New','"New" ' + rsExpect2Parameters+CommandStr);
      errorEncountered := true;
      exit(false);
    end;
    val(funcParams[0],w,errPos);
    val(funcParams[1],h,errPos);
    if (errPos <> 0) or (w <= 0) or (h <= 0) then
    begin
      instance.ShowError('New',rsInvalidSizeForNew+CommandStr);
      errorEncountered := true;
      exit(false);
    end;
    instance.Image.Assign(instance.MakeNewBitmapReplacement(w,h,BGRAPixelTransparent),True,False);
    result := true;
  end;

  function DoScreenshot: boolean;
  var r: TRect;
    screenIndex,errPos: integer;
    invalid: boolean;
  begin
    funcParams := SimpleParseFuncParam(CommandStr);
    if length(funcParams)=1 then
    begin
      val(funcParams[0],screenIndex,errPos);
      if errPos <> 0 then
      begin
        instance.ShowError('Screenshot', '"Screenshot" ' + rsExpect1Parameter+CommandStr);
        errorEncountered := true;
        exit(false);
      end;
      if (screenIndex < 0) or (screenIndex >= Screen.MonitorCount) then
      begin
        instance.ShowError('Screenshot', '"Screenshot" ' +
                                         lclstrconsts.rsListIndexExceedsBounds.Replace('%d', inttostr(screenIndex)));
        errorEncountered := true;
        exit(false);
      end;
      r := Screen.Monitors[screenIndex].BoundsRect;
      r := rect(r.Left*CanvasScale, r.Top*CanvasScale,
                r.Right*CanvasScale, r.Bottom*CanvasScale);
    end else
    if length(funcParams)=4 then
    begin
      r := rect(0,0,0,0);
      invalid := false;
      val(funcParams[0],r.Left,errPos);
      if errPos <> 0 then invalid := true;
      val(funcParams[1],r.Top,errPos);
      if errPos <> 0 then invalid := true;
      val(funcParams[2],r.Right,errPos);
      if errPos <> 0 then invalid := true;
      val(funcParams[3],r.Bottom,errPos);
      if errPos <> 0 then invalid := true;
      if invalid then
      begin
        instance.ShowError('Screenshot', '"Screenshot" ' + StringReplace(rsExpectNParameters,'N','4',[])+CommandStr);
        errorEncountered := true;
        exit(false);
      end;
      if (errPos <> 0) or (r.Width <= 0) or (r.Height <= 0) then
      begin
        instance.ShowError('New',rsInvalidSizeForNew+IntToStr(r.Width)+'x'+IntToStr(r.Height));
        errorEncountered := true;
        exit(false);
      end;
    end else
    begin
      instance.ShowError('Screenshot', '"Screenshot" ' + StringReplace(rsExpectNParameters,'N','4',[])+CommandStr);
      errorEncountered := true;
      exit(false);
    end;
    AImageActions.TakeScreenshot(r);
    result := true;
  end;

  function MakeConfigFromFuncParam: string;
  var
    cfg: TStringList;
    curSection, newSection, p, param: string;
  begin
    cfg := TStringList.Create;
    curSection := '[General]';
    cfg.Add(curSection);
    try
      for p in funcParams do
      begin
        param := p;
        if param.StartsWith('[') then
        begin
          if param.IndexOf(']') <> -1 then
          begin
            newSection := param.Substring(0, param.IndexOf(']')+1);
            param := param.Substring(length(newSection));
          end else
          begin
            newSection := param;
            param := '';
          end;
          if newSection <> curSection then
          begin
            curSection := newSection;
            cfg.Add(curSection);
          end;
        end;
        if param<>'' then
        begin
          if param.IndexOf('=') = -1 then
            raise Exception.Create(SParExpected.Replace('%s', '"="'));
          cfg.Add(param);
        end;
      end;
    finally
      result := cfg.Text;
      cfg.Free;
    end;
  end;

  function DoEditor: boolean;
  var
    iniStream: TStream;
    bmp: TBGRALayeredBitmap;
  begin
    result := false;
    funcParams := SimpleParseFuncParam(CommandStr);
    if (length(funcParams) = 1) and
       ((ExtractFileExt(funcParams[0])='.ini') or (ExtractFileExt(funcParams[0])='.cfg')) then
      iniStream := FileManager.CreateFileStream(funcParams[0], fmOpenRead)
    else if (length(funcParams) = 1) and (funcParams[0] = 'default') then
      iniStream := TMemoryStream.Create
    else
      iniStream := TStringStream.Create(MakeConfigFromFuncParam);

    bmp := instance.Image.CurrentState.GetLayeredBitmapCopy;
    try
      if instance.EditBitmap(bmp, iniStream) then
      begin
        instance.Image.CurrentState.Assign(bmp, true);
        result := true;
      end
      else
      begin
        bmp.Free;
        quitQuery := true;
      end;
    finally
      FileManager.CancelStreamAndFree(iniStream);
    end;
  end;

  function NextAsFuncParam: boolean;
  begin
    inc(i);
    CommandStr := commandsUTF8[i];
    if (length(CommandStr) >= 1) and (CommandStr[1] in commandPrefix) then
    begin
      instance.ShowError('Command line','Expecting parameters but command found');
      exit(false);
    end;
    result := true;
  end;

  procedure DisplayHelp;
  var
    j: Integer;
    {$IFDEF SHOW_MANUAL_IN_WINDOW}
    f: TForm;
    memo: TMemo;
    {$ENDIF}
  begin
    {$IFDEF SHOW_MANUAL_IN_WINDOW}
    f := TForm.Create(nil);
    try
      f.Caption := rsLazPaint;
      f.Position:= poDesktopCenter;
      f.Width := Screen.Width*3 div 4;
      f.Height := Screen.Height*3 div 4;
      memo := TMemo.Create(f);
      memo.Align:= alClient;
      memo.Parent := f;
      memo.Font.Name:= 'monospace';
      memo.ScrollBars := ssVertical;
      memo.Lines.Clear;
      for j := low(manual) to high(manual) do
        memo.Lines.Add(manual[j]);
      f.ShowModal;
    finally
      f.Free;
    end;
    {$ELSE}
    for j := low(manual) to high(manual) do
      writeln(manual[j]);
    {$ENDIF}
  end;

  procedure DoSaveFile(outputFilename: string);
  var
    icoCur: TBGRAIconCursor;
    stream: TStream;
    ext: String;
  begin
    instance.StartSavingImage(outputFilename);
    try
      ext := ExtractFileExt(outputFilename);
      // normally ICO and CUR cannot be saved directly but make an exception
      if (CompareText(ext, '.ico')=0) or (CompareText(ext, '.cur')=0) then
      begin
        icoCur := TBGRAIconCursor.Create;
        try
          if CompareText(ext, '.cur') = 0 then
            icoCur.FileType := ifCur
            else icoCur.FileType := ifIco;
          icoCur.Add(instance.Image.RenderedImage, BGRABitDepthIconCursor(instance.Image.RenderedImage));
          stream := FileManager.CreateFileStream(outputFilename, fmCreate);
          try
            icoCur.SaveToStream(stream);
          finally
            stream.Free;
          end;
        finally
          icoCur.Free;
        end;
      end else
        instance.Image.SaveToFileUTF8(outputFilename)
    except
      on ex: Exception do
      begin
        instance.ShowError(rsSave, rsUnableToSaveFile+outputFilename);
      end;
    end;
    instance.EndSavingImage;
  end;

begin
  fileSaved := True;
  quitQuery:= false;
  errorEncountered := false;
  enableScript:= false;
  if commandsUTF8.count = 0 then exit;

  commandPrefix := ['-'];
  {$WARNINGS OFF}
  if PathDelim<>'/' then commandPrefix += ['/'];
  {$WARNINGS ON}
  InputFilename:= commandsUTF8[0];
  iStart := 0;
  if InputFilename <> '' then
  begin
    if not (InputFilename[1] in commandPrefix) then
    begin
      iStart := 1;
      if not FileManager.FileExists(ExpandFileNameUTF8(InputFilename)) then
      begin
        instance.ShowError(rsOpen, rsFileNotFound);
        errorEncountered := true;
        exit;
      end else
      if not instance.TryOpenFileUTF8(ExpandFileNameUTF8(InputFilename), true) then
      begin
        instance.ShowError(rsOpen, rsUnableToLoadFile+InputFilename);
        errorEncountered := true;
        exit;
      end;
    end;
  end;

  fileSaved := false;
  i := iStart-1;
  while i < commandsUTF8.count-1 do
  begin
    inc(i);

    CommandStr := commandsUTF8[i];
    if (length(CommandStr) >= 1) and (CommandStr[1] in commandPrefix) then
    begin
      if (commandStr[1] = '-') and (length(commandStr)>=2) and (commandStr[2] = '-') then
        delete(commandStr,1,2)
        else Delete(CommandStr,1,1);
      Filter := StrToPictureFilter(CommandStr);
      if Filter <> pfNone then
      begin
        if instance.ExecuteFilter(Filter,True) <> srOk then
        begin
          instance.ShowError(CommandStr, rsUnableToApplyFilter+CommandStr);
          errorEncountered := true;
          exit;
        end;
      end else
      begin
        LowerCmd := UTF8LowerCase(CommandStr);
        if (LowerCmd='help') or (LowerCmd = 'h') or (LowerCmd = '?') then
        begin DisplayHelp; quitQuery := true; exit; end else
        if LowerCmd='horizontalflip' then AImageActions.HorizontalFlip(foAuto) else
        if LowerCmd='verticalflip' then AImageActions.VerticalFlip(foAuto) else
        if LowerCmd='swapredblue' then instance.Image.SwapRedBlue else
        if LowerCmd='smartzoom3' then AImageActions.SmartZoom3 else
        if LowerCmd='rotatecw' then AImageActions.RotateCW else
        if LowerCmd='rotateccw' then AImageActions.RotateCCW else
        if LowerCmd='rotate180' then AImageActions.Rotate180 else
        if copy(lowerCmd,1,9)='gradient(' then begin if not DoGradient then exit end else
        if lowerCmd = 'gradient' then begin if not NextAsFuncParam or not DoGradient then exit end else
        if copy(lowerCmd,1,8)='opacity(' then begin if not DoOpacity then exit end else
        if lowerCmd = 'opacity' then begin if not NextAsFuncParam or not DoOpacity then exit end else
        if copy(lowerCmd,1,9)='resample(' then begin if not DoResample then exit end else
        if lowerCmd = 'resample' then begin if not NextAsFuncParam or not DoResample then exit end else
        if copy(lowerCmd,1,4)='new(' then begin if not DoNew then exit end else
        if lowerCmd = 'new' then begin if not NextAsFuncParam or not DoNew then exit end else
        if lowerCmd.StartsWith('screenshot(') then begin if not DoScreenShot then exit end else
        if lowerCmd = 'screenshot' then begin if not NextAsFuncParam or not DoScreenShot then exit end else
        if lowerCmd.StartsWith('editor(') then begin if not DoEditor then exit end else
        if lowerCmd = 'editor' then begin if not NextAsFuncParam or not DoEditor then exit end else
        if lowerCmd = 'script' then
        begin
          enableScript := true;
        end else
        if lowerCmd = 'scriptbasedir' then
        begin
          if not NextAsFuncParam then exit;
          CustomScriptDirectory:= ChompPathDelim(ExpandFileNameUTF8(commandStr));
        end else
        if lowerCmd = 'quit' then
        begin
          quitQuery:= true;
          exit;
        end else
        if Copy(CommandStr,1,4) <> 'psn_' then //ignore mac parameter
        begin
          instance.ShowError('Command line', rsUnknownCommand+CommandStr);
          errorEncountered := true;
          exit;
        end;
      end;
    end else
    if enableScript then
    begin
      ForcePathDelims(commandStr);
      if (CompareText(ExtractFileExt(CommandStr), '.py') <> 0) then
      begin
        instance.ShowError('Command line', rsFileExtensionNotSupported + ' ('+ExtractFileExt(commandStr)+')');
        errorEncountered:= true;
        exit;
      end;
      if not FileExistsUTF8(commandStr) and
        (pos(PathDelim, commandStr) = 0) then
        commandStr := TPythonScript.DefaultScriptDirectory + PathDelim + commandStr;
      if not instance.RunScript(commandStr) then exit;
      enableScript := false;
    end else
    begin
      ForcePathDelims(CommandStr);
      DoSaveFile(commandStr);
      fileSaved:= true;
      exit;
    end;
  end;

end;

procedure ProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList;
  out errorEncountered, fileSaved, quitQuery: boolean);
begin
  InternalProcessCommands(instance, commandsUTF8, errorEncountered, fileSaved, quitQuery, TImageActions(instance.ImageAction));
end;

end.

