import type {
  FunctionCallData,
  FunctionCallsData,
  FunctionCallParametersData,
} from '@/application/collections/';
import { isArray, isPlainObject } from '@/TypeHelpers';
import { createTypeValidator, type TypeValidator } from '@/application/Common/TypeValidator';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { ParsedFunctionCall } from './ParsedFunctionCall';
import { createFunctionCallArgument, type FunctionCallArgumentFactory } from './Argument/FunctionCallArgument';
import type { FunctionCall } from './FunctionCall';

export interface FunctionCallsParser {
  (
    calls: FunctionCallsData,
    utilities?: FunctionCallParsingUtilities,
  ): FunctionCall[];
}

interface FunctionCallParsingUtilities {
  readonly typeValidator: TypeValidator;
  readonly createCallArgument: FunctionCallArgumentFactory;
}

const DefaultUtilities: FunctionCallParsingUtilities = {
  typeValidator: createTypeValidator(),
  createCallArgument: createFunctionCallArgument,
};

export const parseFunctionCalls: FunctionCallsParser = (
  calls,
  utilities = DefaultUtilities,
) => {
  const sequence = getCallSequence(calls, utilities.typeValidator);
  return sequence.map((call) => parseFunctionCall(call, utilities));
};

function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): FunctionCallData[] {
  if (!isPlainObject(calls) && !isArray(calls)) {
    throw new Error('called function(s) must be an object or array');
  }
  if (isArray(calls)) {
    validator.assertNonEmptyCollection({
      value: calls,
      valueName: 'Function call sequence',
    });
    return calls as FunctionCallData[];
  }
  const singleCall = calls as FunctionCallData;
  return [singleCall];
}

function parseFunctionCall(
  call: FunctionCallData,
  utilities: FunctionCallParsingUtilities,
): FunctionCall {
  utilities.typeValidator.assertObject({
    value: call,
    valueName: 'Function call',
    allowedProperties: ['function', 'parameters'],
  });
  const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
  return new ParsedFunctionCall(call.function, callArgs);
}

function parseArgs(
  parameters: FunctionCallParametersData | undefined,
  createArgument: FunctionCallArgumentFactory,
): FunctionCallArgumentCollection {
  const parametersMap = parameters ?? {};
  return Object.keys(parametersMap)
    .map((parameterName) => {
      const argumentValue = parametersMap[parameterName];
      return createArgument(parameterName, argumentValue);
    })
    .reduce((args, arg) => {
      args.addArgument(arg);
      return args;
    }, new FunctionCallArgumentCollection());
}
