1/*
2 * Copyright (c) 2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import fs from 'fs';
17import path from 'path';
18import type { SourceFile } from 'typescript';
19import { SyntaxKind } from 'typescript';
20import type { InterfaceEntity } from '../declaration-node/interfaceDeclaration';
21import { generateCommonMethodSignature } from './generateCommonMethodSignature';
22import { generateIndexSignature } from './generateIndexSignature';
23import { generatePropertySignatureDeclaration } from './generatePropertySignatureDeclaration';
24import { dtsFileList, getApiInputPath, hasBeenImported, specialFiles } from '../common/commonUtils';
25import type { ImportElementEntity } from '../declaration-node/importAndExportDeclaration';
26import type { PropertySignatureEntity } from '../declaration-node/propertySignatureDeclaration';
27
28/**
29 * generate interface
30 * @param interfaceEntity
31 * @param sourceFile
32 * @param isSourceFile
33 * @returns
34 */
35export function generateInterfaceDeclaration(
36  interfaceEntity: InterfaceEntity,
37  sourceFile: SourceFile,
38  isSourceFile: boolean,
39  mockApi: string,
40  currentSourceInterfaceArray: InterfaceEntity[],
41  importDeclarations?: ImportElementEntity[],
42  extraImport?: string[]
43): string {
44  const interfaceName = interfaceEntity.interfaceName;
45  let interfaceBody = '';
46  let interfaceElementSet = new Set<string>();
47  if (interfaceEntity.exportModifiers.length > 0 || isSourceFile) {
48    interfaceBody += `export const ${interfaceName} = { \n`;
49  } else {
50    interfaceBody += `const ${interfaceName} = { \n`;
51  }
52  if (interfaceEntity.interfacePropertySignatures.length > 0) {
53    const isAddExtraImportReturn = isNeedAddExtraImport(interfaceEntity, interfaceBody, interfaceName, mockApi, sourceFile,
54      interfaceElementSet, extraImport, importDeclarations);
55    interfaceElementSet = isAddExtraImportReturn.interfaceElementSet;
56    interfaceBody = isAddExtraImportReturn.interfaceBody;
57  }
58  if (interfaceEntity.interfaceMethodSignature.size > 0) {
59    interfaceEntity.interfaceMethodSignature.forEach(value => {
60      interfaceBody += generateCommonMethodSignature(interfaceName, value, sourceFile, mockApi) + '\n';
61      interfaceElementSet.add(value[0].functionName);
62    });
63  }
64  if (extraImport.length > 0) {
65    for (let i = 0; i < extraImport.length; i++) {
66      if (mockApi.includes(extraImport[i])) {
67        extraImport.splice(i, 1);
68      }
69    }
70  }
71  if (interfaceEntity.indexSignature.length > 0) {
72    interfaceEntity.indexSignature.forEach(value => {
73      interfaceBody += generateIndexSignature(value) + '\n';
74      interfaceElementSet.add(value.indexSignatureKey);
75    });
76  }
77  interfaceBody = assemblyInterface(interfaceEntity, currentSourceInterfaceArray, interfaceBody,
78    sourceFile, interfaceElementSet, mockApi, interfaceName);
79  return interfaceBody;
80}
81
82/**
83 * @param interfaceEntity
84 * @param interfaceBody
85 * @param interfaceName
86 * @param mockApi
87 * @param sourceFile
88 * @param interfaceElementSet
89 * @param extraImport
90 * @param importDeclarations
91 * @returns
92 */
93function isNeedAddExtraImport(
94  interfaceEntity: InterfaceEntity,
95  interfaceBody: string,
96  interfaceName: string,
97  mockApi: string,
98  sourceFile: SourceFile,
99  interfaceElementSet: Set<string>,
100  extraImport: string[],
101  importDeclarations: ImportElementEntity[]
102): { interfaceBody: string; interfaceElementSet: Set<string> } {
103  interfaceEntity.interfacePropertySignatures.forEach(value => {
104    interfaceBody += generatePropertySignatureDeclaration(interfaceName, value, sourceFile, mockApi) + '\n';
105    interfaceElementSet.add(value.propertyName);
106    if (!value.propertyTypeName.includes(' ')) {
107      // Find out whether the value.propertyTypeName was introduced through import.
108      const regex = new RegExp(`import[\\s\n]*?{?[\\s\n]*?${value.propertyTypeName}[,\\s\n]*?`);
109      const results = mockApi.match(regex);
110      if (results) {
111        return;
112      }
113      let temp = false;
114      importDeclarations.forEach(element => {
115        // Determine whether the external variable introduced by import contains value.propertyTypeName.
116        if (
117          element.importPath.startsWith('\'@ohos') &&
118          element.importElements.match(new RegExp(`[\\s\n]*${value.propertyTypeName}[,\\s\n]*`))
119        ) {
120          temp = true;
121        }
122      });
123      if (temp) {
124        return;
125      }
126    }
127    addExtraImport(extraImport, importDeclarations, sourceFile, value);
128  });
129  return {
130    interfaceBody,
131    interfaceElementSet
132  };
133}
134
135function assemblyInterface(
136  interfaceEntity: InterfaceEntity,
137  currentSourceInterfaceArray: InterfaceEntity[],
138  interfaceBody: string,
139  sourceFile: SourceFile,
140  interfaceElementSet: Set<string>,
141  mockApi: string,
142  interfaceName: string
143): string {
144  if (interfaceEntity.heritageClauses.length > 0) {
145    interfaceEntity.heritageClauses.forEach(value => {
146      currentSourceInterfaceArray.forEach(currentInterface => {
147        if (value.types.includes(currentInterface.interfaceName)) {
148          interfaceBody += generateHeritageInterface(currentInterface, sourceFile, interfaceElementSet, mockApi);
149        }
150      });
151    });
152  }
153  interfaceBody += '}\n';
154  if (interfaceEntity.exportModifiers.includes(SyntaxKind.DeclareKeyword)) {
155    interfaceBody += `
156      if (!global.${interfaceName}) {
157        global.${interfaceName} = ${interfaceName};\n
158      }
159    `;
160  }
161  return interfaceBody;
162}
163
164function generateHeritageInterface(
165  interfaceEntity: InterfaceEntity,
166  sourceFile: SourceFile,
167  elements: Set<string>,
168  mockApi: string
169): string {
170  const interfaceName = interfaceEntity.interfaceName;
171  let interfaceBody = '';
172  if (interfaceEntity.interfacePropertySignatures.length > 0) {
173    interfaceEntity.interfacePropertySignatures.forEach(value => {
174      if (!elements.has(value.propertyName)) {
175        interfaceBody += generatePropertySignatureDeclaration(interfaceName, value, sourceFile, mockApi) + '\n';
176      }
177    });
178  }
179
180  if (interfaceEntity.interfaceMethodSignature.size > 0) {
181    interfaceEntity.interfaceMethodSignature.forEach(value => {
182      if (!elements.has(value[0].functionName)) {
183        interfaceBody += generateCommonMethodSignature(interfaceName, value, sourceFile, mockApi) + '\n';
184      }
185    });
186  }
187
188  if (interfaceEntity.indexSignature.length > 0) {
189    interfaceEntity.indexSignature.forEach(value => {
190      if (elements.has(value.indexSignatureKey)) {
191        interfaceBody += generateIndexSignature(value) + '\n';
192      }
193    });
194  }
195  return interfaceBody;
196}
197
198/**
199 * @param extraImport
200 * @param importDeclarations
201 * @param sourceFile
202 * @param value
203 * @returns
204 */
205export function addExtraImport(
206  extraImport: string[],
207  importDeclarations: ImportElementEntity[],
208  sourceFile: SourceFile,
209  value: PropertySignatureEntity
210): void {
211  if (extraImport && importDeclarations) {
212    const propertyTypeName = value.propertyTypeName.split('.')[0].split('|')[0].split('&')[0].replace(/"'/g, '').trim();
213    if (propertyTypeName.includes('/')) {
214      return;
215    }
216    if (hasBeenImported(importDeclarations, propertyTypeName)) {
217      return;
218    }
219    const specialFilesList = [
220      ...specialFiles.map(specialFile => path.join(getApiInputPath(), ...specialFile.split('/')))
221    ];
222    if (!specialFilesList.includes(sourceFile.fileName)) {
223      specialFilesList.unshift(sourceFile.fileName);
224    }
225    searchHasExtraImport(specialFilesList, propertyTypeName, sourceFile, extraImport);
226  }
227}
228
229/**
230 * @param specialFilesList
231 * @param propertyTypeName
232 * @param sourceFile
233 * @param extraImport
234 * @returns
235 */
236function searchHasExtraImport(
237  specialFilesList: string[],
238  propertyTypeName: string,
239  sourceFile: SourceFile,
240  extraImport: string[]
241): void {
242  for (let i = 0; i < specialFilesList.length; i++) {
243    const specialFilePath = specialFilesList[i];
244    if (!fs.existsSync(specialFilePath)) {
245      continue;
246    }
247    let specialFileContent = fs.readFileSync(specialFilePath, 'utf-8');
248    const removeNoteRegx = /\/\*[\s\S]*?\*\//g;
249    specialFileContent = specialFileContent.replace(removeNoteRegx, '');
250    const regex = new RegExp(`\\s${propertyTypeName}\\s*(<|{|=|extends)`);
251    const results = specialFileContent.match(regex);
252    if (!results) {
253      continue;
254    }
255    if (sourceFile.fileName === specialFilePath) {
256      return;
257    }
258    let specialFileRelatePath = path.relative(path.dirname(sourceFile.fileName), path.dirname(specialFilePath));
259    if (!specialFileRelatePath.startsWith('./') && !specialFileRelatePath.startsWith('../')) {
260      specialFileRelatePath = './' + specialFileRelatePath;
261    }
262    if (!dtsFileList.includes(specialFilePath)) {
263      dtsFileList.push(specialFilePath);
264    }
265    specialFileRelatePath = specialFileRelatePath.split(path.sep).join('/');
266    const importStr = `import { ${propertyTypeName} } from '${specialFileRelatePath}${
267      specialFileRelatePath.endsWith('/') ? '' : '/'
268    }${path.basename(specialFilePath).replace('.d.ts', '').replace('.d.ets', '')}'\n`;
269    if (extraImport.includes(importStr)) {
270      return;
271    }
272    extraImport.push(importStr);
273    return;
274  }
275  if (propertyTypeName.includes('<') || propertyTypeName.includes('[')) {
276    return;
277  }
278  console.log(sourceFile.fileName, 'propertyTypeName', propertyTypeName);
279  return;
280}
281