unit JSClassXMLRead;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileProcs, OtherIdentifierTree, XMLRead, XMLCfg, DOM,
  avl_tree;

type
  TJSIdentifier = class(TOtherIdentifierTreeNode)
  public
    JSName: string;
    PascalName: string;
  end;

  TJSIUnresolvedIdentifier = class(TJSIdentifier)
  public
    Resolved: TJSIdentifier;
  end;

  TJSIAlias = class(TJSIdentifier)
  public
    PointsTo: TJSIdentifier;
  end;

  TJSIClass = class;

  TJSIParameter = class(TJSIdentifier)
  public
    Typ: TJSIdentifier;
    Optional: boolean;
  end;

  TJSIMethodFlag = (
    jsimStatic,
    jsimOverload
    );
  TJSIMethodFlags = set of TJSIMethodFlag;

  { TJSIMethod }

  TJSIMethod = class(TJSIdentifier)
  public
    Flags: TJSIMethodFlags;
    JSIClass: TJSIClass;
    Params: TFPList; // list of TJSIParameter
    ReturnType: TJSIdentifier;
    destructor Destroy; override;
    procedure ClearParams;
    procedure AddParameter(aParam: TJSIParameter);
  end;

  TJSIPropertyFlag = (
    jsipEnum,
    jsipConfig,
    jsipStatic,
    jsipDefault
    );
  TJSIPropertyFlags = set of TJSIPropertyFlag;

  TJSIProperty = class(TJSIdentifier)
  public
    Flags: TJSIPropertyFlags;
    Typ: TJSIdentifier;
  end;

  TJSIClassFlag = (
    jsicAutoCreated
    );
  TJSIClassFlags = set of TJSIClassFlag;

  { TJSIClass }

  TJSIClass = class(TJSIdentifier)
  public
    Flags: TJSIClassFlags;
    ParentClass: TJSIdentifier;
    Unit_name: TJSIdentifier;
    Simplename: TJSIdentifier;
    Methods: TFPList; // list of TJSIMethod
    Properties: TFPList; // list TJSIProperty
    Classes: TFPList; // list of TJSIClass
    destructor Destroy; override;
    procedure ClearMethods;
    procedure ClearProperties;
    procedure ClearClasses;
    function FindIdentifier(const AJSName: string): TJSIdentifier;
    procedure AddClass(AClass: TJSIClass);
    procedure AddMethod(AMethod: TJSIMethod);
    procedure AddProperty(AProperty: TJSIProperty);
  end;

  { TJavascriptIdentifierTree }

  TJavascriptIdentifierTree = class(TOtherIdentifierTree)
  private
    function FindNode(Doc: TXMLDocument; const APath: String; PathHasValue: boolean): TDomNode;
    function Escape(const s: String): String;
    procedure ReadExtJSNodes(Node: TDOMNode);
    procedure ReadExtJSUnits(UnitsNode: TDOMNode);
    procedure ReadExtJSClasses(ClassesNode: TDOMNode);
    procedure ReadExtJSMethods(ClassNode: TDOMNode; JSIClass: TJSIClass);
    procedure ReadExtJSProperties(ClassNode: TDOMNode; JSIClass: TJSIClass);
    function CreateClass(const Path: string; CreateLast: boolean): TJSIClass;
    function CreateAlias(const Path: string): TJSIAlias;
    function FindGlobal(const aJSName: string): TAVLTreeNode;
    function CreateUnresolved(const aPath: string): TJSIUnresolvedIdentifier;
  public
    Globals: TAVLTree;
    constructor Create;
    destructor Destroy; override;
    procedure LoadFromFile(Filename: string);
    procedure ClearGlobals;
  end;

function CompareJSIdentifiers(Data1, Data2: Pointer): integer;
function CompareJSNameWithJSIdentifier(Key, Data: Pointer): integer;
function IsValidJSName(const Name: string): boolean;

implementation

function CompareJSIdentifiers(Data1, Data2: Pointer): integer;
var
  Ident1: TJSIdentifier absolute Data1;
  Ident2: TJSIdentifier absolute Data2;
begin
  Result:=CompareStr(Ident1.JSName,Ident2.JSName);
end;

function CompareJSNameWithJSIdentifier(Key, Data: Pointer): integer;
var
  Ident: TJSIdentifier absolute Data;
  s: String;
begin
  s:=AnsiString(Key);
  Result:=CompareStr(s,Ident.JSName);
end;

function IsValidJSName(const Name: string): boolean;
var
  i: Integer;
begin
  Result:=false;
  if Name='' then exit;
  if length(Name)>255 then exit;
  if not (Name[1] in ['a'..'z','A'..'Z','_']) then exit;
  for i:=1 to Length(Name) do
    if not (Name[i] in ['a'..'z','A'..'Z','_','0'..'9']) then exit;
  Result:=true;
end;

{ TJavascriptIdentifierTree }

function TJavascriptIdentifierTree.FindNode(Doc: TXMLDocument;
  const APath: String; PathHasValue: boolean): TDomNode;
var
  NodePath: String;
  StartPos, EndPos: integer;
  PathLen: integer;
begin
  Result := Doc.DocumentElement;
  debugln(['TJavascriptIdentifierTree.FindNode ',Result.NodeName]);
  PathLen := Length(APath);
  StartPos := 1;
  while Assigned(Result) do
  begin
    EndPos := StartPos;
    while (EndPos <= PathLen) and (APath[EndPos] <> '/') do
      Inc(EndPos);
    if (EndPos > PathLen) and PathHasValue then
      exit;
    if EndPos = StartPos then
      break;
    SetLength(NodePath, EndPos - StartPos);
    Move(APath[StartPos], NodePath[1], Length(NodePath));
    NodePath:=Escape(NodePath);
    debugln(['TJavascriptIdentifierTree.FindNode ',NodePath]);
    Result := Result.FindNode(NodePath);
    StartPos := EndPos + 1;
    if StartPos > PathLen then
      exit;
  end;
  Result := nil;
end;

function TJavascriptIdentifierTree.Escape(const s: String): String;
const
  AllowedChars = ['A'..'Z', 'a'..'z', '0'..'9', '.', '-', '_'];
var
  EscapingNecessary: Boolean;
  i: Integer;
begin
  if Length(s) < 1 then
    raise EXMLConfigError.Create(SMissingPathName);

  if not (s[1] in ['A'..'Z', 'a'..'z', '_']) then
    EscapingNecessary := True
  else
  begin
    EscapingNecessary := False;
    for i := 2 to Length(s) do
      if not (s[i] in AllowedChars) then
      begin
        EscapingNecessary := True;
	exit;
      end;
  end;

  if EscapingNecessary then
  begin
    Result := '_';
    for i := 1 to Length(s) do
      if s[i] in (AllowedChars - ['_']) then
	  Result := Result + s[i]
	else
	  Result := Result + '_' + IntToHex(Ord(s[i]), 2);
  end
  else	// No escaping necessary
    Result := s;
end;

procedure TJavascriptIdentifierTree.ReadExtJSNodes(Node: TDOMNode);
var
  UnitsNode: TDOMNode;
  ClassesNode: TDOMNode;
begin
  debugln(['TJavascriptIdentifierTree.ReadExtJSClasses ',DbgSName(Node)]);

  UnitsNode:=Node.FindNode('units');
  if UnitsNode=nil then begin
    debugln(['TJavascriptIdentifierTree.ReadExtJSClasses no units found']);
    exit;
  end;
  ReadExtJSUnits(UnitsNode);

  ClassesNode:=Node.FindNode('classes');
  if UnitsNode=nil then begin
    debugln(['TJavascriptIdentifierTree.ReadExtJSClasses no classes found']);
    exit;
  end;
  ReadExtJSClasses(ClassesNode);
end;

procedure TJavascriptIdentifierTree.ReadExtJSUnits(UnitsNode: TDOMNode);
var
  Node: TDOMNode;
  ClassesNode: TDOMNode;
  ClassNode: TDOMNode;
  CurUnitName, PascalClassname: widestring;
begin
  Node:=UnitsNode.FirstChild;
  while Node<>nil do begin
    if (Node.NodeName='unit') and (Node is TDOMElement) then begin
      CurUnitName:=TDOMElement(Node).GetAttribute('name');
      ClassesNode:=Node.FindNode('classes');
      if (ClassesNode<>nil) and (CurUnitName<>'') then begin
        ClassNode:=ClassesNode.FirstChild;
        while ClassNode<>nil do begin
          if ClassNode.NodeName='class' then begin
            if ClassNode is TDOMElement then begin
              PascalClassname:=TDOMElement(ClassNode).GetAttribute('name');
              if PascalClassname<>'' then ;
              //debugln(['TJavascriptIdentifierTree.ReadExtJSUnits ',CurUnitName,' ',PascalClassname]);
            end;
          end;
          ClassNode:=ClassNode.NextSibling;
        end;
      end;
    end;
    Node:=Node.NextSibling;
  end;
end;

procedure TJavascriptIdentifierTree.ReadExtJSClasses(ClassesNode: TDOMNode);
var
  Node: TDOMNode;
  ClassNode: TDOMElement;
  NewClass: TJSIClass;
  JSName: WideString;
  SimpleName: WideString;
  Alias: TJSIAlias;
begin
  Node:=ClassesNode.FirstChild;
  while Node<>nil do begin
    if (Node.NodeName='class') and (Node is TDOMElement) then begin
      ClassNode:=TDOMElement(Node);
      JSName:=ClassNode.GetAttribute('jsname');
      if JSName<>'' then begin
        if JSName='Object' then begin
          debugln(['TJavascriptIdentifierTree.ReadExtJSClasses SKIPPING jsname=Object pasname=',ClassNode.GetAttribute('name')]);
        end else begin
          debugln(['TJavascriptIdentifierTree.ReadExtJSClasses class=',JSName]);
          // create new class
          NewClass:=CreateClass(JSName,true);
          if not (jsicAutoCreated in NewClass.Flags) then
            raise Exception.Create('class redefined: '+JSName);
          Exclude(NewClass.Flags,jsicAutoCreated);
          // pascalname
          NewClass.PascalName:=ClassNode.GetAttribute('name');
          // simplename
          SimpleName:=ClassNode.GetAttribute('simplename');
          if (SimpleName<>'') and (SimpleName<>JSName) then begin
            Alias:=CreateAlias(SimpleName);
            if (Alias.PointsTo<>nil) and (Alias.PointsTo<>NewClass) then
              raise Exception.Create('class simplename redefined: '+JSName+' '+SimpleName);
            NewClass.Simplename:=Alias;
          end;
          // methods
          ReadExtJSMethods(ClassNode,NewClass);
          // properties
          ReadExtJSProperties(ClassNode,NewClass);
        end;
      end;
    end;
    Node:=Node.NextSibling;
  end;
end;

procedure TJavascriptIdentifierTree.ReadExtJSMethods(ClassNode: TDOMNode;
  JSIClass: TJSIClass);
var
  MethodsNode: TDOMNode;
  Node: TDOMNode;
  MethodNode: TDOMElement;
  JSName: WideString;
  NewMethod: TJSIMethod;
  ParamsNode: TDOMNode;
  SubNode: TDOMNode;
  NewParam: TJSIParameter;
  ParamNode: TDOMElement;
  ParamJSName: WideString;
  ReturnType: Widestring;
begin
  MethodsNode:=ClassNode.FindNode('methods');
  if MethodsNode=nil then exit;
  Node:=MethodsNode.FirstChild;
  while Node<>nil do begin
    if (Node.NodeName='method') and (Node is TDOMElement) then begin
      MethodNode:=TDOMElement(Node);
      JSName:=MethodNode.GetAttribute('jsname');
      if copy(JSName,1,length(JSIClass.JSName)+1)=JSIClass.JSName+'.' then
        JSName:=copy(JSName,length(JSIClass.JSName)+2,length(JSName));
      if not IsValidJSName(JSName) then
        raise Exception.Create('invalid method name '+JSName);
      NewMethod:=TJSIMethod.Create;
      NewMethod.JSName:=JSName;
      JSIClass.AddMethod(NewMethod);
      NewMethod.PascalName:=MethodNode.GetAttribute('name');
      if MethodNode.GetAttribute('static')='1' then
        Include(NewMethod.Flags,jsimStatic);
      if MethodNode.GetAttribute('overload')='1' then
        Include(NewMethod.Flags,jsimOverload);
      // return type
      ReturnType:=MethodNode.GetAttribute('return');
      if ReturnType<>'' then
        NewMethod.ReturnType:=CreateUnresolved(ReturnType);
      // parameters
      ParamsNode:=MethodNode.FindNode('params');
      if ParamsNode<>nil then begin
        SubNode:=ParamsNode.FirstChild;
        while SubNode<>nil do begin
          if (SubNode is TDOMElement) and (SubNode.NodeName='param') then begin
            ParamNode:=TDOMElement(SubNode);
            ParamJSName:=ParamNode.GetAttribute('name');
            if not IsValidJSName(ParamJSName) then
              raise Exception.Create('invalid param name '+ParamJSName);
            NewParam:=TJSIParameter.Create;
            NewParam.JSName:=ParamJSName;
            NewParam.PascalName:=ParamJSName;
            NewParam.Optional:=ParamNode.GetAttribute('optional')='1';
            NewParam.Typ:=CreateUnresolved(ParamNode.GetAttribute('type'));
            NewMethod.AddParameter(NewParam);
          end;
          SubNode:=SubNode.NextSibling;
        end;
      end;
    end;
    Node:=Node.NextSibling;
  end;
end;

procedure TJavascriptIdentifierTree.ReadExtJSProperties(ClassNode: TDOMNode;
  JSIClass: TJSIClass);
var
  PropertiesNode: TDOMNode;
  PropertyNode: TDOMElement;
  PropertyJSName: WideString;
  NewProperty: TJSIProperty;
  Node: TDOMNode;
  TypeName: WideString;
begin
  // properties
  PropertiesNode:=ClassNode.FindNode('properties');
  if PropertiesNode=nil then exit;
  Node:=PropertiesNode.FirstChild;
  while Node<>nil do begin
    if (Node is TDOMElement) and (Node.NodeName='property') then begin
      PropertyNode:=TDOMElement(Node);
      PropertyJSName:=PropertyNode.GetAttribute('jsname');
      if not IsValidJSName(PropertyJSName) then
        raise Exception.Create('invalid property name '+PropertyJSName);
      NewProperty:=TJSIProperty.Create;
      NewProperty.JSName:=PropertyJSName;
      NewProperty.PascalName:=PropertyNode.GetAttribute('name');
      if PropertyNode.GetAttribute('enum')='1' then
        Include(NewProperty.Flags,jsipEnum);
      if PropertyNode.GetAttribute('config')='1' then
        Include(NewProperty.Flags,jsipConfig);
      if PropertyNode.GetAttribute('static')='1' then
        Include(NewProperty.Flags,jsipStatic);
      if PropertyNode.GetAttribute('default')='1' then
        Include(NewProperty.Flags,jsipDefault);
      TypeName:=PropertyNode.GetAttribute('type');
      if (jsipEnum in NewProperty.Flags) and (TypeName[1]='(') then
        TypeName:='';
      if TypeName<>'' then
        NewProperty.Typ:=CreateUnresolved(TypeName);
      JSIClass.AddProperty(NewProperty);
    end;
    Node:=Node.NextSibling;
  end;
end;

function TJavascriptIdentifierTree.CreateClass(const Path: string; CreateLast: boolean
  ): TJSIClass;
var
  p: Integer;
  StartPos: Integer;
  AName: String;
  AVLNode: TAVLTreeNode;
  Identifier: TJSIdentifier;
  Parent: TJSIClass;
  IsLast: Boolean;
  PropType: TJSIdentifier;
begin
  Result:=nil;
  p:=1;
  repeat
    StartPos:=p;
    while (p<=length(Path))  and (Path[p]<>'.') do inc(p);
    AName:=copy(Path,StartPos,p-StartPos);
    if not IsValidJSName(AName) then
      raise Exception.Create('invalid javascript class path: '+Path);
    IsLast:=p>Length(Path);
    if IsLast and not CreateLast then exit;
    // search class
    if Result=nil then begin
      AVLNode:=FindGlobal(AName);
      if AVLNode=nil then begin
        // create new global class
        debugln(['TJavascriptIdentifierTree.CreateClass new global class: ',AName]);
        Result:=TJSIClass.Create;
        Include(Result.Flags,jsicAutoCreated);
        Result.JSName:=AName;
        Globals.Add(Result);
      end else begin
        // class already exists
        if not (TObject(AVLNode.Data) is TJSIClass) then
          raise Exception.Create('path is not class: '+AName);
        Result:=TJSIClass(AVLNode.Data);
      end;
    end else begin
      Identifier:=Result.FindIdentifier(AName);
      if Identifier=nil then begin
        // create new sub class
        debugln(['TJavascriptIdentifierTree.CreateClass new sub class: ',AName,' of ',Result.JSName]);
        Parent:=Result;
        Result:=TJSIClass.Create;
        Include(Result.Flags,jsicAutoCreated);
        Result.JSName:=AName;
        Result.ParentClass:=Parent;
        Parent.AddClass(Result);
      end else if Identifier is TJSIClass then begin
        // sub class already exists
        Result:=TJSIClass(Identifier);
      end else if Identifier is TJSIProperty then begin
        // resolve property
        PropType:=TJSIProperty(Identifier).Typ;
        if PropType is TJSIClass then
          Result:=TJSIClass(PropType)
        else
          raise Exception.Create('path is not class: '+AName+' is '+Identifier.ClassName);
      end else begin
        raise Exception.Create('path is not class: '+AName+' is '+Identifier.ClassName);
      end;
    end;
    // skip point
    inc(p);
  until p>Length(Path);
end;

function TJavascriptIdentifierTree.CreateAlias(const Path: string): TJSIAlias;
var
  Context: TJSIClass;
  AVLNode: TAVLTreeNode;
  JSName: String;
begin
  Context:=CreateClass(Path,false);
  if Context<>nil then
    raise Exception.Create('nested alias not spported yet: '+Path);
  JSName:=Path;
  AVLNode:=FindGlobal(JSName);
  if AVLNode=nil then begin
    // create new alias
    debugln(['TJavascriptIdentifierTree.CreateAlias new alias: ',JSName]);
    Result:=TJSIAlias.Create;
    Result.JSName:=JSName;
  end else begin
    // alias already exists
    if not (TObject(AVLNode.Data) is TJSIAlias) then
      raise Exception.Create('path not an alias: '+Path);
    Result:=TJSIAlias(AVLNode.Data);
  end;
end;

function TJavascriptIdentifierTree.FindGlobal(const aJSName: string
  ): TAVLTreeNode;
begin
  Result:=Globals.FindKey(Pointer(aJSName),@CompareJSNameWithJSIdentifier);
end;

function TJavascriptIdentifierTree.CreateUnresolved(const aPath: string
  ): TJSIUnresolvedIdentifier;
begin
  if not IsValidJSName(aPath) then
    raise Exception.Create('invalid type name '+aPath);
  Result:=TJSIUnresolvedIdentifier.Create;
  Result.JSName:=aPath;
end;

constructor TJavascriptIdentifierTree.Create;
begin
  inherited Create;
  Globals:=TAVLTree.Create(@CompareJSIdentifiers);
end;

destructor TJavascriptIdentifierTree.Destroy;
begin
  ClearGlobals;
  FreeAndNil(Globals);
  inherited Destroy;
end;

procedure TJavascriptIdentifierTree.LoadFromFile(Filename: string);
var
  Doc: TXMLDocument;
begin
  debugln(['TJavascriptIdentifierTree.LoadFromFile ',Filename]);
  ClearNodes;
  Doc:=nil;
  try
    ReadXMLFile(Doc,Filename);
    if (Doc.DocumentElement<>nil) and (Doc.DocumentElement.NodeName='ExtJSClasses') then
      ReadExtJSNodes(Doc.DocumentElement)
    else
      raise Exception.Create('ExtJSClasses not found in file '+Filename);
  finally
    Doc.Free;
  end;
end;

procedure TJavascriptIdentifierTree.ClearGlobals;
begin
  Globals.FreeAndClear;
end;

{ TJSIMethod }

destructor TJSIMethod.Destroy;
begin
  ClearParams;
  inherited Destroy;
end;

procedure TJSIMethod.ClearParams;
var
  i: Integer;
begin
  if Params<>nil then
    for i:=0 to Params.Count-1 do TObject(Params[i]).Free;
  FreeAndNil(Params);
end;

procedure TJSIMethod.AddParameter(aParam: TJSIParameter);
begin
  if Params=nil then
    Params:=TFPList.Create;
  Params.Add(aParam);
end;

{ TJSIClass }

destructor TJSIClass.Destroy;
begin
  ClearClasses;
  ClearMethods;
  ClearProperties;
  inherited Destroy;
end;

procedure TJSIClass.ClearMethods;
var
  i: Integer;
begin
  if Methods<>nil then
    for i:=0 to Methods.Count-1 do TObject(Methods[i]).Free;
  FreeAndNil(Methods);
end;

procedure TJSIClass.ClearProperties;
var
  i: Integer;
begin
  if Properties<>nil then
    for i:=0 to Properties.Count-1 do TObject(Properties[i]).Free;
  FreeAndNil(Properties);
end;

procedure TJSIClass.ClearClasses;
var
  i: Integer;
begin
  if Classes<>nil then
    for i:=0 to Classes.Count-1 do TObject(Classes[i]).Free;
  FreeAndNil(Classes);
end;

function TJSIClass.FindIdentifier(const AJSName: string): TJSIdentifier;

  function Find(List: TFPList): TJSIdentifier;
  var
    i: Integer;
  begin
    if List=nil then exit(nil);
    for i:=0 to List.Count-1 do begin
      Result:=TJSIdentifier(List[i]);
      if CompareStr(AJSName,Result.JSName)=0 then exit;
    end;
    Result:=nil;
  end;

begin
  Result:=Find(Classes);
  if Result<>nil then exit;
  Result:=Find(Properties);
  if Result<>nil then exit;
  Result:=Find(Methods);
  if Result<>nil then exit;
end;

procedure TJSIClass.AddClass(AClass: TJSIClass);
begin
  if Classes=nil then
    Classes:=TFPList.Create;
  Classes.Add(AClass);
end;

procedure TJSIClass.AddMethod(AMethod: TJSIMethod);
begin
  if Methods=nil then
    Methods:=TFPList.Create;
  Methods.Add(AMethod);
end;

procedure TJSIClass.AddProperty(AProperty: TJSIProperty);
begin
  if Properties=nil then
    Properties:=TFPList.Create;
  Properties.Add(AProperty);
end;

end.

