/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 *
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

/*
 * Licensed to Elasticsearch B.V. under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch B.V. licenses this file to you 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 { cloneElement, ReactChild } from 'react';
import {
  isBoolean,
  isString,
  isNumber,
  isUndefined,
} from '../../services/predicate';
import { isElement } from 'react-is';
import { RenderableValues } from '../context/context';

function isPrimitive(value: ReactChild | undefined) {
  return (
    isBoolean(value) || isString(value) || isNumber(value) || isUndefined(value)
  );
}

type Child = string | { propName: string } | ReactChild | undefined;

function hasPropName(child: Child): child is { propName: string } {
  return child
    ? typeof child === 'object' && child.hasOwnProperty('propName')
    : false;
}

/**
 * Replaces placeholder values in `input` with their matching value in `values`
 * e.g. input:'Hello, {name}' will replace `{name}` with `values[name]`
 * @param {string} input
 * @param {RenderableValues} values
 * @param {Function} i18nMappingFunc
 * @returns {string | React.ReactChild[]}
 */
export function processStringToChildren(
  input: string,
  values: RenderableValues,
  i18nMappingFunc?: (token: string) => string
): string | ReactChild[] {
  const children: ReactChild[] = [];

  let child: Child;

  function appendCharToChild(char: string) {
    if (child === undefined) {
      // starting a new string literal
      child = char;
    } else if (typeof child === 'string') {
      // existing string literal
      child = child + char;
    } else if (hasPropName(child)) {
      // adding to the propName of a values lookup
      child.propName = child.propName + char;
    }
  }

  function appendValueToChildren(value: Child) {
    if (value === undefined) {
      return;
    } else if (isElement(value)) {
      // an array with any ReactElements will be kept as an array
      // so they need to be assigned a key
      children.push(cloneElement(value, { key: children.length }));
    } else if (hasPropName(value)) {
      // this won't be called, propName children are converted to a ReactChild before calling this
    } else {
      // everything else can go straight in
      if (i18nMappingFunc !== undefined && typeof value === 'string') {
        value = i18nMappingFunc(value);
      }
      children.push(value);
    }
  }

  // if we don't encounter a non-primitive
  // then `children` can be concatenated together at the end
  let encounteredNonPrimitive = false;
  for (let i = 0; i < input.length; i++) {
    const char = input[i];

    if (char === '\\') {
      // peek at the next character to know if this is an escape
      const nextChar = input[i + 1];
      let charToAdd = char; // if this isn't an escape sequence then we will add the backslash

      if (nextChar === '{' || nextChar === '}') {
        // escaping a brace
        i += 1; // advance passed the brace
        charToAdd = input[i];
      }
      appendCharToChild(charToAdd);
    } else if (char === '{') {
      appendValueToChildren(child);
      child = { propName: '' };
    } else if (char === '}') {
      const propName = (child as { propName: string }).propName;
      if (!values.hasOwnProperty(propName)) {
        throw new Error(
          `Key "${propName}" not found in ${JSON.stringify(values, null, 2)}`
        );
      }
      const propValue = values[propName];
      encounteredNonPrimitive =
        encounteredNonPrimitive || !isPrimitive(propValue);
      appendValueToChildren(propValue);
      child = undefined;
    } else {
      appendCharToChild(char);
    }
  }

  // include any remaining child value
  appendValueToChildren(child);

  return encounteredNonPrimitive ? children : children.join('');
}
