/*
 * Copyright (c) 2026 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import ts from 'typescript';
import { projectConfig } from '../main';
import { RUNTIME_OS_OH } from './fast_build/system_api/api_check_define';

const MOCK_FUNCTION_NAME = '__mockApiAvailable';
const TARGET_FUNCTION_NAME = 'apiAvailable';

/**
 * Process API Available calls and references, converting them to mock function calls or closures.
 * @param node Node to process
 * @returns Transformed node if it's an API Available statement, undefined otherwise
 */
export function processAvailableStatement(node: ts.Node): ts.Node | undefined {
  if (ts.isCallExpression(node)) {
    return transformCallExpression(node);
  }
  if (ts.isPropertyAccessExpression(node) && node.name.text === TARGET_FUNCTION_NAME) {
    return transformReference(node);
  }
  return undefined;
}

function transformCallExpression(node: ts.CallExpression): ts.Node | undefined {
  if (!isApiAvailableExpression(node.expression)) {
    return undefined;
  }

  const chain = extractCallChain(node.expression);
  if (!chain) {
    return undefined;
  }

  return ts.factory.createCallExpression(
    ts.factory.createIdentifier(MOCK_FUNCTION_NAME),
    undefined,
    [chain, ...node.arguments]
  );
}

function isUnderTypeOfExpression(node: ts.Node): boolean {
  let current = node.parent;
  while (current) {
    if (ts.isTypeOfExpression(current)) {
      return true;
    }
    if (ts.isParenthesizedExpression(current) || ts.isNonNullExpression(current) ||
      ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
      current = current.parent;
      continue;
    }
    break;
  }
  return false;
}

function isAssignmentTarget(node: ts.Node): boolean {
  let current: ts.Node = node;
  while (current.parent && (ts.isParenthesizedExpression(current.parent) || ts.isNonNullExpression(current.parent) ||
    ts.isAsExpression(current.parent) || ts.isTypeAssertionExpression(current.parent))) {
    current = current.parent;
  }
  return !!(current.parent && ts.isBinaryExpression(current.parent) && current.parent.left === current &&
    current.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
    current.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment);
}

function transformReference(node: ts.Node): ts.Node | undefined {
  if (node.parent && ts.isCallExpression(node.parent) && node.parent.expression === node) {
    return undefined;
  }

  if (isAssignmentTarget(node)) {
    return undefined;
  }

  if (isUnderTypeOfExpression(node)) {
    return undefined;
  }

  const chain = extractCallChain(node);
  if (!chain) {
    return undefined;
  }

  return ts.factory.createParenthesizedExpression(createClosureArrowFunction(chain));
}

function isApiAvailableExpression(expr: ts.Expression): boolean {
  if (ts.isNonNullExpression(expr) || ts.isParenthesizedExpression(expr) ||
    ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr)) {
    return isApiAvailableExpression(expr.expression);
  }
  if (ts.isPropertyAccessExpression(expr)) {
    return expr.name.text === TARGET_FUNCTION_NAME;
  }
  return false;
}

function extractCallChain(node: ts.Node): ts.Expression | null {
  if (ts.isPropertyAccessExpression(node)) {
    if (node.name.text === TARGET_FUNCTION_NAME) {
      return cloneExpression(node.expression);
    }
    if (node.questionDotToken) {
      return ts.factory.createPropertyAccessChain(
        extractCallChain(node.expression),
        node.questionDotToken,
        node.name
      );
    }
    return ts.factory.createPropertyAccessExpression(
      extractCallChain(node.expression),
      node.name
    );
  }
  if (ts.isElementAccessExpression(node)) {
    if (ts.isStringLiteral(node.argumentExpression) && node.argumentExpression.text === TARGET_FUNCTION_NAME) {
      return cloneExpression(node.expression);
    }
    if (node.questionDotToken) {
      return ts.factory.createElementAccessChain(
        extractCallChain(node.expression),
        node.questionDotToken,
        node.argumentExpression
      );
    }
    return ts.factory.createElementAccessExpression(
      extractCallChain(node.expression),
      node.argumentExpression
    );
  }
  if (ts.isNonNullExpression(node)) {
    return extractCallChain(node.expression);
  }
  if (ts.isParenthesizedExpression(node)) {
    return ts.factory.createParenthesizedExpression(
      extractCallChain(node.expression)
    );
  }
  if (ts.isAsExpression(node)) {
    return ts.factory.createAsExpression(
      extractCallChain(node.expression),
      node.type
    );
  }
  if (ts.isTypeAssertionExpression(node)) {
    return ts.factory.createTypeAssertion(
      node.type,
      extractCallChain(node.expression)
    );
  }
  if (ts.isIdentifier(node)) {
    return ts.factory.createIdentifier(node.text);
  }
  return node as ts.Expression;
}

function cloneExpression(node: ts.Expression): ts.Expression {
  if (ts.isPropertyAccessExpression(node)) {
    if (node.questionDotToken) {
      return ts.factory.createPropertyAccessChain(
        cloneExpression(node.expression),
        node.questionDotToken,
        node.name
      );
    }
    return ts.factory.createPropertyAccessExpression(
      cloneExpression(node.expression),
      node.name
    );
  }
  if (ts.isElementAccessExpression(node)) {
    if (node.questionDotToken) {
      return ts.factory.createElementAccessChain(
        cloneExpression(node.expression),
        node.questionDotToken,
        node.argumentExpression
      );
    }
    return ts.factory.createElementAccessExpression(
      cloneExpression(node.expression),
      node.argumentExpression
    );
  }
  if (ts.isNonNullExpression(node)) {
    return ts.factory.createNonNullExpression(
      cloneExpression(node.expression)
    );
  }
  if (ts.isParenthesizedExpression(node)) {
    return ts.factory.createParenthesizedExpression(
      cloneExpression(node.expression)
    );
  }
  if (ts.isAsExpression(node)) {
    return ts.factory.createAsExpression(
      cloneExpression(node.expression),
      node.type
    );
  }
  if (ts.isTypeAssertionExpression(node)) {
    return ts.factory.createTypeAssertion(
      node.type,
      cloneExpression(node.expression)
    );
  }
  if (ts.isIdentifier(node)) {
    return ts.factory.createIdentifier(node.text);
  }
  return node;
}

function createClosureArrowFunction(chain: ts.Expression): ts.ArrowFunction {
  const paramName = generateUniqueParamName(chain);
  const versionParam = ts.factory.createIdentifier(paramName);
  const mockCall = ts.factory.createCallExpression(
    ts.factory.createIdentifier(MOCK_FUNCTION_NAME),
    undefined,
    [cloneExpression(chain), versionParam]
  );

  return ts.factory.createArrowFunction(
    undefined,
    undefined,
    [ts.factory.createParameterDeclaration(
      undefined,
      undefined,
      versionParam
    )],
    undefined,
    ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
    mockCall
  );
}

function generateUniqueParamName(chain: ts.Expression): string {
  const usedNames = new Set<string>();
  collectIdentifiers(chain, usedNames);
  if (!usedNames.has('v')) {
    return 'v';
  }
  let i = 0;
  while (usedNames.has('v' + i)) {
    i++;
  }
  return 'v' + i;
}

function collectIdentifiers(node: ts.Node, names: Set<string>): void {
  if (ts.isIdentifier(node)) {
    names.add(node.text);
  }
  ts.forEachChild(node, child => collectIdentifiers(child, names));
}

/**
 * Create the __mockApiAvailable helper function declaration.
 *
 * param e - sdkModule, must be truthy otherwise return false
 * param t - version, supports number or string format
 *
 * Number version:
 *   - Range: [1, 25], must be integer
 *   - Compare: e.sdkApiVersion >= t
 *
 * String version (semantic versioning "major.minor.patch" or "major.minor.patch(OHVersion)"):
 *   - When isOH = true (OpenHarmony):
 *     * Major version must be >= 26
 *     * OHVersion number is not allowed
 *     * Regex: /^(2[6-9]|[3-9]\d)\.(0|[1-9]\d{0,1})\.(0|[1-9]\d{0,1})$/
 *   - When isOH = false (non-OpenHarmony):
 *     * Major version: [1, 99]
 *     * OHVersion number is optional, but rejected when major >= 26
 *     * Regex: /^([1-9]\d{0,1})\.(0|[1-9]\d{0,1})\.(0|[1-9]\d{0,1})(\(([1-9]\d{0,1})\))?$/
 *   - Parsed as numeric version: 10000 * major + 100 * minor + patch
 *   - Compare: e.distributionOSApiVersion >= parsed version
 * Generates:
 * (isOH = true )
 *  function __mockApiAvailable(sdkModule, version) {
 *    if (!sdkModule) {
 *        return false;
 *    }
 *    if (typeof version === 'number') {
 *        if (version < 1 || version > 25 || !Number.isInteger(version)) {
 *            return false;
 *        }
 *        return sdkModule.sdkApiVersion >= version;
 *    }
 *    if (typeof version === 'string') {
 *        var match = version.match(/^(2[6-9]|[3-9]\d)\.(0|[1-9]\d{0,1})\.(0|[1-9]\d{0,1})$/);
 *        if (!match) {
 *            return false;
 *        }
 *        var major = Number(match[1]);
 *        var minor = Number(match[2]);
 *        var patch = Number(match[3]);
 *        var build = 10000 * major + 100 * minor + patch;
 *        return sdkModule.distributionOSApiVersion >= build;
 *    }
 *    return false;
 *  }
 *
 * (isOH = false )
 *  function __mockApiAvailable(sdkModule, version) {
 *    if (!sdkModule) {
 *        return false;
 *    }
 *    if (typeof version === 'number') {
 *        if (version < 1 || version > 25 || !Number.isInteger(version)) {
 *            return false;
 *        }
 *        return sdkModule.sdkApiVersion >= version;
 *    }
 *    if (typeof version === 'string') {
 *        var match = version.match(/^([1-9]\d{0,1})\.(0|[1-9]\d{0,1})\.(0|[1-9]\d{0,1})(\(([1-9]\d{0,1})\))?$/);
 *        if (!match) {
 *            return false;
 *        }
 *        var major = Number(match[1]);
 *        var minor = Number(match[2]);
 *        var patch = Number(match[3]);
 *        // 主版本号 >= 26 时不允许带构建号
 *        if (major >= 26 && match[4]) {
 *            return false;
 *        }
 *        var build = 10000 * major + 100 * minor + patch;
 *        return sdkModule.distributionOSApiVersion >= build;
 *    }
 *    return false;
 *  }
 */
export function createHelperFunctionDeclaration(): ts.FunctionDeclaration {
  const isOH = projectConfig.runtimeOS === RUNTIME_OS_OH;
  const varDecl = isOH ? 'r, s' : 'r, s, i';
  const regexOH = isOH ?
    '^(2[6-9]|[3-9]\\d)\\.(0|[1-9]\\d{0,1})\\.(0|[1-9]\\d{0,1})$' :
    '^([1-9]\\d{0,1})\\.(0|[1-9]\\d{0,1})\\.(0|[1-9]\\d{0,1})(\\(([1-9]\\d{0,1})\\))?$';
  const postMatch = isOH ?
    '!!(t = t.match(/' + regexOH + '/)) && ((r = Number(t[1])), (s = Number(t[2])), (t = Number(t[3])),' +
    ' e.distributionOSApiVersion >= 1e4 * r + 100 * s + t)' :
    '!!(t = t.match(/' + regexOH + '/)) && ((r = Number(t[1])), (s = Number(t[2])), (i = Number(t[3])),' +
    ' (r < 26 || !t[4])) && e.distributionOSApiVersion >= 1e4 * r + 100 * s + i';
  const content = `function ${MOCK_FUNCTION_NAME}(e, t) {
    var ${varDecl};
    return (!!e && ('number' == typeof t ? !(t < 1 || 25 < t || !Number.isInteger(t)) && e.sdkApiVersion >= t
        : 'string' == typeof t && ${postMatch}));
  }`;
  const sourceFile = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true);
  stripRanges(sourceFile);
  return sourceFile.statements[0] as ts.FunctionDeclaration;
}

function stripRanges(node: ts.Node): void {
  node.pos = -1;
  node.end = -1;
  ts.forEachChild(node, stripRanges);
}

/**
 * Check if the version string is a point-based version (e.g., '26.0.0')
 * The first version number is greater than 26
 * @param version Version string to check
 * @return true if it's a point-based version, false otherwise
 */
export function isPointVersion(version: string): boolean {
  const REG_MSF = /^\'?(?:2[6-9]|[3-9][0-9])\.(?:[0-9]|[1-9][0-9]?)\.(?:[0-9]|[1-9][0-9]?)(\(\d+\))?\'?$/;
  return REG_MSF.test(version);
}