{
 *****************************************************************************
 *                                                                           *
 *  This file is part of the iPhone Laz Extension                            *
 *                                                                           *
 *  See the file COPYING.modifiedLGPL.txt, included in this distribution,    *
 *  for details about the copyright.                                         *
 *                                                                           *
 *  This program is distributed in the hope that it will be useful,          *
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                     *
 *                                                                           *
 *****************************************************************************
}
unit ideext;

{$mode objfpc}{$H+}

interface

uses
  {$ifdef darwin}BaseUnix, Unix,{$endif}
  process,
  Classes, SysUtils, contnrs,
  Graphics, Controls, Forms, Dialogs, LazFileUtils,
  {Lazarus Interface}
  LazIDEIntf, MenuIntf, ProjectIntf, IDEOptionsIntf, IDEMsgIntf
  ,IDEExternToolIntf

  ,project_iphone_options, xcodetemplate, iphonelog_form,

  iPhoneExtOptions, iPhoneExtStr, iPhoneBundle, lazfilesutils, iphonesimctrl;

procedure Register;

implementation

procedure IDEMsg(const msg: string);
begin
  IDEMessagesWindow.AddCustomMessage(mluProgress, msg);
end;


type
  { TiPhoneExtension }

  TiPhoneExtension = class(TObject)
  protected
    procedure FillBunldeInfo(forSimulator: Boolean; var info: TiPhoneBundleInfo);
    procedure InstallAppToSim; deprecated 'use iphonesimctrl unit routines instead';
    function FixCustomOptions(const Options: String; isRealDevice: Boolean): String;

    function WriteIconTo(const FullName: String): Boolean;

    function ProjectBuilding(Sender: TObject): TModalResult;
    function ProjectOpened(Sender: TObject; AProject: TLazProject): TModalResult;
    //procedure OnProjOptionsChanged(Sender: TObject; Restore: Boolean);

    function GetXcodeProjDirName: string;
  public
    SimPID    : Integer;
    {$ifdef darwin}
    SimLogger : TPTYReader;
    {$endif}
    constructor Create;
    procedure UpdateXcode(Sender: TObject);
    procedure SimRun(Sender: TObject);
    procedure SimTerminate(Sender: TObject);
    procedure CloseProc;
    procedure OnBytesRead(Sender: TObject; const buf: string);
  end;

var
  Extension : TiPhoneExtension = nil;

function GetProjectPlistName(Project: TLazProject): String;
var
  ext : String;
begin
  if Project.LazCompilerOptions.TargetFilename<>'' then
    Result:=ExtractFileName(Project.LazCompilerOptions.TargetFilename)
  else begin
    Result:=ExtractFileName(Project.MainFile.Filename);
    ext:=ExtractFileExt(Result);
    Result:=Copy(Result, 1, length(Result)-length( ext ) );
  end;
  Result:=Result+'.plist';
end;

function GetProjectExeName(Project: TLazProject): String;
var
  ext : String;
begin
  if Project.LazCompilerOptions.TargetFilename<>'' then
    Result:=Project.LazCompilerOptions.TargetFilename
  else begin
    Result:=Project.MainFile.Filename;
    ext:=ExtractFileExt(Result);
    Result:=Copy(Result, 1, length(Result)-length( ext ) );
  end;
  Result := ResolveProjectPath(Result);
end;

{ TiPhoneExtension }

procedure TiPhoneExtension.FillBunldeInfo(forSimulator: Boolean; var info: TiPhoneBundleInfo);
begin
  Info.AppID:=UTF8Decode(ProjOptions.AppID);
  Info.DisplayName:=UTF8Decode(LazarusIDE.ActiveProject.Title);
  Info.iPlatform:=UTF8Decode(EnvOptions.GetSDKName(ProjOptions.SDK, forSimulator));
  Info.SDKVersion:=UTF8Decode(ProjOptions.SDK);
  Info.MainNib:=UTF8Decode(ProjOptions.MainNib);
end;

procedure TiPhoneExtension.InstallAppToSim;
var
  bundlename  : string;
  bundlepath  : WideString;
  exepath     : WideString;
  nm          : String;
  dstpath     : String;
  Space       : WideString;
  RealSpace   : WideString;
  Info        : TiPhoneBundleInfo;
  AProjectFile: TLazProjectFile;

  xiblist : TStringList;
  i       : Integer;
  s       : string;
begin
  Space:=UTF8Decode(ProjOptions.SpaceName); // LazarusIDE.ActiveProject.CustomData.;

  bundleName:=ExtractFileName(LazarusIDE.ActiveProject.ProjectInfoFile);
  bundleName:=Copy(bundleName, 1, length(bundleName)-length(ExtractFileExt(bundleName)));
  if bundlename='' then
    bundlename:='project1';

  nm:=GetProjectExeName(LazarusIDE.ActiveProject);

  FillBunldeInfo(true, Info);

  CreateBundle(UTF8Decode(bundleName), Space
    , UTF8Decode(ExtractFileName(nm))
    , Info, RealSpace, bundlepath, exepath);

  WriteIconTo( UTF8Encode(IncludeTrailingPathDelimiter(bundlepath)+'Icon.png') );

  CopySymLinks(
    ResolveProjectPath(ProjOptions.ResourceDir),
    UTF8Encode(bundlepath),
    // don't copy .xib files, they're replaced by compiled nibs
    '*.xib; '+ ProjOptions.ExcludeMask
  );

  if nm<>'' then begin
    dstpath:=UTF8Encode(exepath);
    {$ifdef darwin}
    FpUnlink(dstpath);
    fpSymlink(PChar(nm), PChar(dstpath));
    {$endif}
  end;

  xiblist := TStringList.Create;
  try
    // Scan for resource-files in the .xib file-format, which are
    // used by the iOSDesigner package
    for i := 0 to LazarusIDE.ActiveProject.FileCount-1 do
      begin
      AProjectFile := LazarusIDE.ActiveProject.Files[i];
      s := ChangeFileExt(AProjectFile.filename,'.xib');
      if (AProjectFile.IsPartOfProject) and FileExistsUTF8(s) then
        xiblist.add(s);
      end;
    EnumFilesAtDir(ResolveProjectPath(ProjOptions.ResourceDir), '*.xib', xiblist);
    for i:=0 to xiblist.Count-1 do begin
      dstpath:=UTF8Encode(IncludeTrailingPathDelimiter(bundlepath))+ChangeFileExt(ExtractFileName(xiblist[i]), '.nib');
      ExecCmdLineNoWait(Format('ibtool --compile "%s" "%s"', [dstpath, xiblist[i]]));
    end;
  finally
    xiblist.free;
  end;
end;

function FindParam(const Source, ParamKey: String; var idx: Integer; var Content: String): Boolean;
var
  i       : Integer;
  quoted  : Boolean;
begin
  Result:=false;
  idx:=0;
  for i := 1 to length(Source)-1 do
    if (Source[i]='-') and ((i=1) or (Source[i-1] in [#9,#32,#10,#13])) then
      if Copy(Source, i, 3) = ParamKey then begin
        idx:=i;
        Result:=true;
        Break;
      end;

  if not Result then Exit;

  i:=idx+3;
  quoted:=(i<=length(Source)) and (Source[i]='"');

  if not quoted then begin
    for i:=i to length(Source) do
      if (Source[i] in [#9,#32,#10,#13]) then begin
        Content:=Copy(Source, idx+3, i-idx);
        Exit;
      end;
  end else begin
    i:=i+1;
    for i:=i to length(Source) do
      if (Source[i] = '"') then begin
        Content:=Copy(Source, idx+3, i-idx+1);
        Exit;
      end;
  end;
  Content:=Copy(Source, idx+3, length(Source)-1);
end;

function TiPhoneExtension.FixCustomOptions(const Options: String; isRealDevice: Boolean): String;
var
  prm     : string;
  rawprm  : string;
  idx     : Integer;
  needfix : Boolean;
  sdkuse  : String;
  sdkver  : String;
  st : TStringList;
begin
  sdkver:=ProjOptions.SDK;
  if sdkver='' then begin
    st := TStringList.Create;
    try
      EnvOptions.GetSDKVersions(st);
      if st.Count=0 then
        IDEMsg(strWNoSDK)
      else begin
        sdkver:=st[0];
        ProjOptions.SDK:=sdkver;
      end;
    finally
      st.Free;
    end;
  end;
  sdkuse:=EnvOptions.GetSDKFullPath(sdkver, not isRealDevice);

  Result:=Options;
  if FindParam(Result, '-XR', idx, rawprm) then begin
    if (rawprm='') or(rawprm[1]<>'"')
      then prm:=rawprm
      else prm:=rawprm;

    // there might be -XR option, and it might be the same as selected SDK
    needfix:=sdkuse<>prm;

    // there's -XR option, but it doesn't match the selection options
    if needfix then
      Delete(Result, idx, length(rawprm)+3);

  end else begin
    //there's no -XR string in custom options
    needfix:=true;
    idx:=1;
    sdkuse:=sdkuse+' ';
  end;

  if needfix then Insert('-XR'+sdkuse, Result, idx);
end;

constructor TiPhoneExtension.Create;
begin
  inherited Create;
  LazarusIDE.AddHandlerOnProjectOpened(@ProjectOpened);
  LazarusIDE.AddHandlerOnProjectBuilding(@ProjectBuilding);
  //ProjOptions.OnAfterWrite:=@OnProjOptionsChanged;

  RegisterIDEMenuCommand(itmProjectWindowSection, 'mnuiPhoneSeparator', '-', nil, nil);

  RegisterIDEMenuCommand(itmProjectWindowSection, 'mnuiPhoneToXCode', strStartAtXcode, @UpdateXcode, nil);
  RegisterIDEMenuCommand(itmProjectWindowSection, 'mnuiPhoneRunSim', strRunSimulator, @SimRun, nil);
  RegisterIDEMenuCommand(itmProjectWindowSection, 'mnuiPhoneTermSim', strTermSimulator, @SimTerminate, nil);
end;

function TiPhoneExtension.ProjectBuilding(Sender: TObject): TModalResult;
begin
  Result:=mrOk;
  if not Assigned(LazarusIDE.ActiveProject) or not ProjOptions.isIPhoneApp then Exit;

  LazarusIDE.ActiveProject.LazCompilerOptions.CustomOptions :=
    FixCustomOptions( LazarusIDE.ActiveProject.LazCompilerOptions.CustomOptions, false );

  //const InfoFileName, BundleName, ExeName: WideString; const info: TiPhoneBundleInfo): Boolean;
  {MessageDlg('CustomOptions fixed = '+ LazarusIDE.ActiveProject.LazCompilerOptions.CustomOptions,
    mtInformation, [mbOK], 0);}

  InstallAppToSim;
  Result:=mrOk;
end;

function TiPhoneExtension.ProjectOpened(Sender: TObject; AProject: TLazProject): TModalResult;
begin
  ProjOptions.Reset;
  ProjOptions.Load;
  Result:=mrOk;
end;

function TiPhoneExtension.GetXcodeProjDirName: string;
var
  dir      : string;
  projname : string;
  ext      : string;
begin
  dir:=ResolveProjectPath('xcode');
  dir:=dir+'/';
  projname:=ExtractFileName(LazarusIDE.ActiveProject.MainFile.Filename);
  ext:=ExtractFileExt(projname);
  projname:=Copy(projname, 1, length(projname)-length(ext));
  Result:=dir+projname+'.xcodeproj';
end;

function TiPhoneExtension.WriteIconTo(const FullName: String): Boolean;
var
  icofile : string;
  ico     : TIcon;
  png     : TPortableNetworkGraphic;
  i, idx  : Integer;
const
  iPhoneIconSize=57;
begin
  Result:=false;
  //todo: find a better way of getting the file
  icofile:=ChangeFileExt(LazarusIDE.ActiveProject.MainFile.Filename, '.ico');
  icofile := ResolveProjectPath(icofile);
  if not FileExists(icofile) then begin
    // no icon. it should be deleted!
    DeleteFile(FullName);
    Exit;
  end;

  try
    ico:=TIcon.Create;
    png:=TPortableNetworkGraphic.Create;
    try
      png.Width:=iPhoneIconSize;
      png.Height:=iPhoneIconSize;
      ico.LoadFromFile(icofile);
      idx:=-1;
      for i:=0 to ico.Count- 1 do begin
        ico.Current:=i;
        if (ico.Width=iPhoneIconSize) and (ico.Height=iPhoneIconSize) then begin
          idx:=i;
          Break;
        end;
      end;
      if (idx<0) and (ico.Count>0) then idx:=0;

      ico.Current:=idx;

      if (ico.Width=iPhoneIconSize) and (ico.Height=iPhoneIconSize) then
        png.Assign(ico)
      else begin
        //resize to adjust the image
        png.Canvas.CopyRect( Rect(0,0, iPhoneIconSize, iPhoneIconSize), ico.Canvas, Rect(0,0, ico.Width, ico.Height));
      end;

      png.SaveToFile(FullName);
    finally
      ico.free;
      png.free;
    end;
    Result:=true;
  except
  end;
end;

procedure TiPhoneExtension.UpdateXcode(Sender: TObject);
var
  templates : TStringList;
  build     : TFPStringHashTable;
  dir       : string;
  projdir   : string;
  proj      : TStringList;
  projname  : string;

  ext       : string;
  tname     : string;
  plistname : string;

  Info  : TiPhoneBundleInfo;

  opt   : string;
begin
  // the create .plist would be used by XCode project
  // the simulator .plist in created with InstallAppToSim.
  // they differ with SDKs used

  build := TFPStringHashTable.Create;

  tname:=ExtractFileName( LazarusIDE.ActiveProject.MainFile.Filename);
  tname:=ChangeFileExt(tname, '');
  plistname:='info.plist';

  build.Add('INFOPLIST_FILE','"'+plistname+'"');
  build.Add('PRODUCT_NAME','"'+tname+'"');
  build.Add('SDKROOT',EnvOptions.GetSDKName(ProjOptions.SDK, false));
	build.Add('FPC_COMPILER_PATH','"'+EnvOptions.CompilerPath+'"');
	build.Add('FPC_MAIN_FILE','"'+LazarusIDE.ActiveProject.MainFile.Filename+'"');
  opt:='';

  with LazarusIDE.ActiveProject.LazCompilerOptions do begin
    opt:=opt + ' ' +BreakPathsStringToOption(OtherUnitFiles, '-Fu', '\"');
    opt:=opt + ' ' +BreakPathsStringToOption(IncludePath, '-Fi', '\"');
    opt:=opt + ' ' +BreakPathsStringToOption(ObjectPath, '-Fo', '\"');
    opt:=opt + ' ' +BreakPathsStringToOption(Libraries, '-Fl', '\"');
  end;

  dir:=ResolveProjectPath('xcode');
  dir:=dir+'/';

  // not using executable name. It's driven by product name
  //exename:=ExtractFileName(GetProjectExeName(LazarusIDE.ActiveProject));
  //name:=exename;
  ForceDirectories(dir);

  FillBunldeInfo(false, Info);
  WriteDefInfoList(
    UTF8Decode(dir + plistname)
    , UTF8Decode(tname)
    , UTF8Decode(tname)
    , Info);


  projname:=ExtractFileName(LazarusIDE.ActiveProject.MainFile.Filename);
  ext:=ExtractFileExt(projname);
  projname:=Copy(projname, 1, length(projname)-length(ext));

  projdir:=dir+projname+'.xcodeproj';
  ForceDirectories(projdir);

  projname:=IncludeTrailingPathDelimiter(projdir)+'project.pbxproj';
  proj:=TStringList.Create;
  templates:=nil;
  try
    templates:=TStringList.Create;

    if WriteIconTo( IncludeTrailingPathDelimiter(dir)+'Icon.png') then begin
      templates.Values['icon']:=XCodeProjectTemplateIcon;
      templates.Values['iconid']:=XCodeProjectTemplateIconID;
      templates.Values['iconfile']:=XCodeIconFile;
      templates.Values['iconfileref']:=XCodeIconFileRef;
    end else begin
      templates.Values['icon']:='';
      templates.Values['iconid']:='';
      templates.Values['iconfile']:='';
      templates.Values['iconfileref']:='';
    end;

    //todo:
    templates.Values['bundle']:=tname+'.app';
    templates.Values['plist']:=plistname;
    templates.Values['targetname']:=tname;  // Target and Product name must match
    templates.Values['productname']:=tname;
    templates.Values['mainfile']:=LazarusIDE.ActiveProject.MainFile.Filename;
    templates.Values['projoptions']:=opt;

    PrepareTemplateFile(proj, templates, ProjOptions.ResFiles);
    proj.SaveToFile(projname);
  except
    on e: exception do
      ShowMessage(e.Message);
  end;

  proj.Free;
  templates.Free;
  build.Free;

  IDEMsg(Format(strXcodeUpdated,[projdir]));
end;

procedure SimRunDirect;
var
  t     : TProcess;
  path  : String;
begin
  t :=TProcess.Create(nil);
  try
    path:=IncludeTrailingPathDelimiter(EnvOptions.SimBundle)+'Contents/MacOS/iPhone Simulator';
    EnvOptions.SubstituteMacros(path);
    t.Executable:=path;
    t.CurrentDirectory:=UTF8Encode(GetSandBoxDir(UTF8Decode(ProjOptions.SpaceName)));
    t.Execute;
  except
    on E: Exception do
      MessageDlg(E.Message, mtInformation, [mbOK], 0);
  end;
  t.Free;
end;

function SimRunInstruct(var err: string): Boolean;
begin
  if EnvOptions.DefaultDeviceID='' then begin
    err:='Device type is not specified';
    Result:=false;
    Exit;
  end;
  Result:=true;
  iphonesimctrl.RunSim( EnvOptions.DefaultDeviceID );
end;

procedure TiPhoneExtension.SimRun(Sender: TObject);
var
  err : string;
  pid : integer;
  prj : string;
  s   : string;
begin
  CloseProc;
  UpdateXcode(Sender);
  // install Xcode project
  prj := GetXcodeProjDirName;
  IDEMsg('Build+Install Xcode project (xcodebuild)');

  {$ifndef darwin}
  IDEMsg('Unable to install / run simulator on non Mac OS X platform');
  {$else}
  if not InstallXcodePrj(prj, 'iphonesimulator', EnvOptions.DefaultDeviceID) then begin
    IDEMsg('xcodebuild failed');
    Exit;
  end else
    IDEMsg('xcodebuild successed, project installed');

  IDEMsg('Running Simulator');
  if not SimRunInstruct(err) then begin
    IDEMsg('Unable to run Simulator. '+err);
    Exit;
  end;


  if not Assigned(iphonelogform) then
    iphonelogform:=Tiphonelogform.Create(Application)
  else
    iphonelogform.AddNewSheet;
  SimLogger:=TPTYReader.Create;
  SimLogger.OnBytesRead:=@OnBytesRead;

  IDEMsg('Launching the Application on the Simulator');
  if RunAppOnSim( ProjOptions.AppID, 'booted', true, pid, s) then begin
    SimPID:=pid;
    LLDBRediretIO(SimPID, SimLogger.PTY.FileName);
    iphonelogform.Show;
  end else begin
    IDEMsg('Failed to launch Application.');
    Exit;
  end;
  IDEMsg('Success. Application pid='+IntToStr(pid));
  {$endif}
end;

procedure TiPhoneExtension.SimTerminate(Sender: TObject);
begin
  CloseProc;
end;

procedure TiPhoneExtension.CloseProc;
begin
  if SimPID>0 then begin
    StopProc(SimPID);
    SimPID:=-1;
    {$ifdef darwin}
    SimLogger.Free;
    SimLogger:=nil;
    {$endif}
  end;
end;

procedure TiPhoneExtension.OnBytesRead(Sender: TObject; const buf: string);
begin
  if not Assigned(iphonelogform) then Exit;
  iphonelogform.LogMemo.Append(buf);
end;

procedure Register;
begin
  // IDE integration is done in constructor
  Extension := TiPhoneExtension.Create;
  try
    EnvOptions.Load;
    EnvOptions.RefreshVersions;
  except
  end;
end;

initialization

finalization
  Extension.Free;

end.