/*
 * 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 React, {
  Fragment,
  ReactChild,
  FunctionComponent,
  useContext,
  ReactElement,
} from 'react';
import { OuiI18nConsumer } from '../context';
import { ExclusiveUnion } from '../common';
import {
  I18nContext,
  I18nShape,
  Renderable,
  RenderableValues,
} from '../context/context';
import { processStringToChildren } from './i18n_util';

function errorOnMissingValues(token: string): never {
  throw new Error(
    `I18n mapping for token "${token}" is a formatting function but no values were provided.`
  );
}

function lookupToken<
  T extends RenderableValues,
  DEFAULT extends Renderable<T>,
  RESOLVED extends ResolvedType<DEFAULT>
>(
  token: string,
  i18nMapping: I18nShape['mapping'],
  valueDefault: DEFAULT,
  i18nMappingFunc?: (token: string) => string,
  values?: I18nTokenShape<T, DEFAULT>['values']
): RESOLVED {
  let renderable = (i18nMapping && i18nMapping[token]) || valueDefault;

  if (typeof renderable === 'function') {
    if (values === undefined) {
      return errorOnMissingValues(token);
    }
    // @ts-ignore TypeScript complains that `DEFAULT` doesn't have a call signature but we verified `renderable` is a function
    return renderable(values);
  } else if (values === undefined || typeof renderable !== 'string') {
    if (i18nMappingFunc && typeof valueDefault === 'string') {
      renderable = i18nMappingFunc(valueDefault);
    }
    // there's a hole in the typings here as there is no guarantee that i18nMappingFunc
    // returned the same type of the default value, but we need to keep that assumption
    return renderable as RESOLVED;
  }

  const children = processStringToChildren(renderable, values, i18nMappingFunc);
  if (typeof children === 'string') {
    // likewise, `processStringToChildren` returns a string or ReactChild[] depending on
    // the type of `values`, so we will make the assumption that the default value is correct.
    return children as RESOLVED;
  }

  const Component: FunctionComponent<any> = () => {
    return <Fragment>{children}</Fragment>;
  };

  // same reasons as above, we can't promise the transforms match the default's type
  return React.createElement(Component, values) as RESOLVED;
}

type ResolvedType<T> = T extends (...args: any[]) => any ? ReturnType<T> : T;

interface I18nTokenShape<T, DEFAULT extends Renderable<T>> {
  token: string;
  default: DEFAULT;
  children?: (x: ResolvedType<DEFAULT>) => ReactChild;
  values?: T;
}

interface I18nTokensShape<T extends any[]> {
  tokens: string[];
  defaults: T;
  children: (x: Array<T[number]>) => ReactChild;
}

export type OuiI18nProps<
  T,
  DEFAULT extends Renderable<T>,
  DEFAULTS extends any[]
> = ExclusiveUnion<I18nTokenShape<T, DEFAULT>, I18nTokensShape<DEFAULTS>>;

function isI18nTokensShape<T extends any[]>(
  x: OuiI18nProps<any, any, T>
): x is I18nTokensShape<T> {
  return x.tokens != null;
}

// Must use the generics <T extends {}>
// If instead typed with React.FunctionComponent there isn't feedback given back to the dev
// when using a `values` object with a renderer callback.
const OuiI18n = <
  T extends {},
  DEFAULT extends Renderable<T>,
  DEFAULTS extends any[]
>(
  props: OuiI18nProps<T, DEFAULT, DEFAULTS>
) => (
  <OuiI18nConsumer>
    {(i18nConfig) => {
      const { mapping, mappingFunc } = i18nConfig;
      if (isI18nTokensShape(props)) {
        return props.children(
          props.tokens.map((token, idx) =>
            lookupToken(token, mapping, props.defaults[idx], mappingFunc)
          )
        );
      }

      const tokenValue = lookupToken(
        props.token,
        mapping,
        props.default,
        mappingFunc,
        props.values
      );
      if (props.children) {
        return props.children(tokenValue);
      } else {
        return tokenValue;
      }
    }}
  </OuiI18nConsumer>
);

// A single default could be a string, react child, or render function
type DefaultRenderType<T, K extends Renderable<T>> = K extends ReactChild
  ? K
  : K extends () => infer RetValue
  ? RetValue
  : never;

// An array with multiple defaults can only be an array of strings or elements
type DefaultsRenderType<
  K extends Array<string | ReactElement>
> = K extends Array<infer Item> ? Item : never;

function useOuiI18n<T extends {}, DEFAULT extends Renderable<T>>(
  token: string,
  defaultValue: DEFAULT,
  values?: T
): DefaultRenderType<T, DEFAULT>;
function useOuiI18n<DEFAULTS extends Array<string | ReactElement>>(
  tokens: string[],
  defaultValues: DEFAULTS
): Array<DefaultsRenderType<DEFAULTS>>;
function useOuiI18n(...props: any[]) {
  const i18nConfig = useContext(I18nContext);
  const { mapping, mappingFunc } = i18nConfig;

  if (typeof props[0] === 'string') {
    const [token, defaultValue, values] = props;
    return lookupToken(token, mapping, defaultValue, mappingFunc, values);
  } else {
    const [tokens, defaultValues] = props as [string[], string[]];
    return tokens.map((token, idx) =>
      lookupToken(token, mapping, defaultValues[idx], mappingFunc)
    );
  }
}

export { OuiI18n, useOuiI18n };
