/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.
 */

package com.google.android.accessibility.utils.parsetree;

import android.content.res.Resources;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import com.google.android.libraries.accessibility.utils.log.LogUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class ParseTreeResourceNode extends ParseTreeNode {

  private static final String TAG = "ParseTreeResourceNode";

  private static final Pattern RESOURCE_PATTERN =
      Pattern.compile("@(string|plurals|raw|array)/(\\w+)");

  @IntDef({TYPE_STRING, TYPE_PLURALS, TYPE_RESOURCE_ID})
  @Retention(RetentionPolicy.SOURCE)
  @interface Type {}

  static final int TYPE_STRING = 0;
  static final int TYPE_PLURALS = 1;
  static final int TYPE_RESOURCE_ID = 2;

  private final Resources mResources;
  private final int mResourceId;
  private final @Type int mType;
  private final List<ParseTreeNode> mParams = new ArrayList<>();

  ParseTreeResourceNode(Resources resources, String resourceName, String packageName) {
    mResources = resources;

    Matcher matcher = RESOURCE_PATTERN.matcher(resourceName);
    if (!matcher.matches()) {
      throw new IllegalArgumentException("Resource parameter is malformed: " + resourceName);
    }

    String type = matcher.group(1);
    String name = matcher.group(2);
    if (type == null || name == null) {
      throw new IllegalArgumentException("Resource parameter is malformed: " + resourceName);
    }

    switch (type) {
      case "string":
        mType = TYPE_STRING;
        break;
      case "plurals":
        mType = TYPE_PLURALS;
        break;
      case "raw":
      case "array":
        mType = TYPE_RESOURCE_ID;
        break;
      default:
        throw new IllegalArgumentException("Unknown resource type: " + type);
    }

    mResourceId = mResources.getIdentifier(name, type, packageName);

    if (mResourceId == 0) {
      throw new IllegalStateException("Missing resource: " + resourceName);
    }
  }

  void addParams(List<ParseTreeNode> params) {
    mParams.addAll(params);
  }

  @Override
  public int getType() {
    switch (mType) {
      case TYPE_STRING:
      case TYPE_PLURALS:
        return ParseTree.VARIABLE_STRING;

      case TYPE_RESOURCE_ID:
      default:
        return ParseTree.VARIABLE_INTEGER;
    }
  }

  @Override
  public int resolveToInteger(ParseTree.VariableDelegate delegate, String logIndent) {
    return mResourceId;
  }

  @Override
  public CharSequence resolveToString(ParseTree.VariableDelegate delegate, String logIndent) {
    switch (mType) {
      case TYPE_STRING:
        Object[] stringParamList = getParamList(mParams, 0, delegate, logIndent);
        String templateString = mResources.getString(mResourceId);
        return SpannedStringUtils.getSpannedFormattedString(templateString, stringParamList);

      case TYPE_PLURALS:
        if (mParams.isEmpty() || mParams.get(0).getType() != ParseTree.VARIABLE_INTEGER) {
          LogUtils.e(TAG, "First parameter for plurals must be the count");
          return "";
        }

        Object[] pluralParamList = getParamList(mParams, 1, delegate, logIndent);
        String templatePlural =
            mResources.getQuantityString(
                mResourceId, mParams.get(0).resolveToInteger(delegate, logIndent));
        return SpannedStringUtils.getSpannedFormattedString(templatePlural, pluralParamList);

      case TYPE_RESOURCE_ID:
        LogUtils.e(TAG, "Cannot resolve resource ID to string");
        return "";

      default:
        LogUtils.e(TAG, "Unknown resource type: " + mType);
        return "";
    }
  }

  private static Object[] getParamList(
      List<ParseTreeNode> params,
      int start,
      ParseTree.VariableDelegate delegate,
      String logIndent) {
    List<Object> result = new ArrayList<>();
    for (ParseTreeNode node : params.subList(start, params.size())) {
      switch (node.getType()) {
        case ParseTree.VARIABLE_BOOL:
          result.add(node.resolveToBoolean(delegate, logIndent));
          break;

        case ParseTree.VARIABLE_STRING:
          result.add(node.resolveToString(delegate, logIndent));
          break;

        case ParseTree.VARIABLE_INTEGER:
          result.add(node.resolveToInteger(delegate, logIndent));
          break;

        case ParseTree.VARIABLE_NUMBER:
          result.add(node.resolveToNumber(delegate, logIndent));
          break;

        case ParseTree.VARIABLE_ENUM:
        case ParseTree.VARIABLE_ARRAY:
        case ParseTree.VARIABLE_CHILD_ARRAY:
          LogUtils.e(TAG, "Cannot format string with type: " + node.getType());
          break;
        default: // fall out
      }
    }
    return result.toArray();
  }

  /** The utility class provide ways to keep spans in template string. */
  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  protected static class SpannedStringUtils {
    /**
     * Creates CharSequence from {@code templateString} and its {@code parameters}. And spans in
     * parameters are keep in result.
     *
     * @param templateString template string that may contains parameters with spans.
     * @param parameters object arrays that are supposed but not necessary to be Spanned. If it is
     *     Spanned, the spans are keep in result Spannable.
     * @return CharSequence that composed by formatted template string and parameters.
     */
    private static CharSequence getSpannedFormattedString(
        String templateString, Object[] parameters) {
      List<CharSequence> stringTypeList = new ArrayList<>();
      for (Object param : parameters) {
        if (param instanceof CharSequence) {
          stringTypeList.add((CharSequence) param);
        }
      }

      String formattedString = String.format(templateString, parameters);
      if (stringTypeList.isEmpty()) {
        return formattedString;
      }

      CharSequence expandableTemplate = toExpandableTemplate(templateString, parameters);
      try {
        // It will throw IllegalArgumentException if the template requests a value that was not
        // provided, or if more than 9 values are provided.
        return TextUtils.expandTemplate(
            expandableTemplate, stringTypeList.toArray(new CharSequence[stringTypeList.size()]));
      } catch (IllegalArgumentException exception) {
        LogUtils.e(
            TAG,
            "TextUtils.expandTemplate fail then try copySpansFromTemplateParameters."
                + " Exception=%s ",
            exception);
        // This is a fall-back method that may copy spans inaccurately
        return copySpansFromTemplateParameters(
            formattedString, stringTypeList.toArray(new CharSequence[stringTypeList.size()]));
      }
    }

    /**
     * Creates CharSequence from template string by its parameters. The template string will be
     * transformed to contain "^1"-style placeholder values dynamically to match the format of
     * {@link TextUtils#expandTemplate(CharSequence, CharSequence...)} and formatted by other
     * none-string type parameters.
     *
     * @param templateString template string that may contains parameters with strings.
     * @param parameters object arrays that are supposed but not necessary to be string. If it is
     *     string, the corresponding placeholder value will be changed to "^1"-style. If not string
     *     type, the placeholder is kept and adjust the index.
     * @return CharSequence that composed by template string with "^1"-style placeholder values.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    protected static CharSequence toExpandableTemplate(String templateString, Object[] parameters) {
      String expandTemplateString = templateString;
      List<Object> otherTypeList = new ArrayList<>();

      int spanTypeIndex = 1;
      int otherTypeIndex = 1;
      for (int i = 1; i <= parameters.length; i++) {
        Object param = parameters[i - 1];
        if (param instanceof CharSequence) {
          // replaces string type "%1$s" or "%s" to "^1" and so on.
          if (expandTemplateString.contains("%" + i + "$s")) {
            expandTemplateString =
                expandTemplateString.replace(("%" + i + "$s"), ("^" + spanTypeIndex));
          } else if (expandTemplateString.contains("%s")) {
            expandTemplateString = expandTemplateString.replaceFirst("%s", ("^" + spanTypeIndex));
          }
          spanTypeIndex++;
        } else {
          // keeps and assigns correct index to other type parameters
          expandTemplateString = expandTemplateString.replace(("%" + i), ("%" + otherTypeIndex));
          otherTypeList.add(param);
          otherTypeIndex++;
        }
      }
      return String.format(expandTemplateString, otherTypeList.toArray());
    }

    /**
     * Creates spannable from text that includes some Spanned. If a template parameter occurs
     * multiple times in the final text, this function copies the parameter's spans to the first
     * instance.
     *
     * @param text some text that potentially contains CharSequence parameters.
     * @param templateParameters CharSequence arrays that contains spans and need to be copied to
     *     result Spannable.
     * @return Spannable object that contains incoming text and spans from templateParameters.
     */
    private static Spannable copySpansFromTemplateParameters(
        String text, CharSequence[] templateParameters) {
      SpannableString result = new SpannableString(text);
      for (CharSequence params : templateParameters) {
        if (params instanceof Spanned) {
          int index = text.indexOf(params.toString());
          if (index >= 0) {
            copySpans(result, (Spanned) params, index);
          }
        }
      }
      return result;
    }

    /**
     * Utility that copies spans from {@code fromSpan} to {@code toSpan}.
     *
     * @param toSpan Spannable that is supposed to contain fromSpan.
     * @param fromSpan Spannable that could contain spans that would be copied to toSpan.
     * @param toSpanStartIndex Starting index of occurrence fromSpan in toSpan.
     */
    private static void copySpans(Spannable toSpan, Spanned fromSpan, int toSpanStartIndex) {
      if (toSpanStartIndex < 0 || toSpanStartIndex >= toSpan.length()) {
        LogUtils.e(
            TAG,
            "startIndex parameter (%d) is out of toSpan length %d",
            toSpanStartIndex,
            toSpan.length());
        return;
      }

      Object[] spans = fromSpan.getSpans(0, fromSpan.length(), Object.class);
      if (spans != null && spans.length > 0) {
        for (Object span : spans) {
          int spanStartIndex = fromSpan.getSpanStart(span);
          int spanEndIndex = fromSpan.getSpanEnd(span);
          if (spanStartIndex >= spanEndIndex) {
            continue;
          }
          int spanFlags = fromSpan.getSpanFlags(span);
          toSpan.setSpan(
              span,
              (toSpanStartIndex + spanStartIndex),
              (toSpanStartIndex + spanEndIndex),
              spanFlags);
        }
      }
    }
  }
}