/*
 * Copyright 2018 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.android.tv.twopanelsettings.slices.compat;

import static android.app.slice.Slice.EXTRA_RANGE_VALUE;
import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
import static android.app.slice.Slice.HINT_ACTIONS;
import static android.app.slice.Slice.HINT_ERROR;
import static android.app.slice.Slice.HINT_KEYWORDS;
import static android.app.slice.Slice.HINT_LAST_UPDATED;
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_PERMISSION_REQUEST;
import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.HINT_TTL;
import static android.app.slice.Slice.SUBTYPE_MAX;
import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_BUNDLE;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.HINT_CACHED;
import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_HOST_EXTRAS;
import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_MIN;
import static com.android.tv.twopanelsettings.slices.compat.widget.EventInfo.ROW_TYPE_PROGRESS;
import static com.android.tv.twopanelsettings.slices.compat.widget.EventInfo.ROW_TYPE_SLIDER;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.math.MathUtils;
import androidx.core.util.Pair;
import com.android.tv.twopanelsettings.slices.compat.core.SliceAction;
import com.android.tv.twopanelsettings.slices.compat.core.SliceActionImpl;
import com.android.tv.twopanelsettings.slices.compat.core.SliceHints;
import com.android.tv.twopanelsettings.slices.compat.core.SliceQuery;
import com.android.tv.twopanelsettings.slices.compat.widget.EventInfo;
import com.android.tv.twopanelsettings.slices.compat.widget.ListContent;
import com.android.tv.twopanelsettings.slices.compat.widget.RowContent;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

/**
 * Utility class to parse a {@link Slice} and provide access to information around its contents.
 *
 * <p>Slice framework has been deprecated, it will not receive any updates moving forward. If you
 * are looking for a framework that handles communication across apps, consider using {@link
 * android.app.appsearch.AppSearchManager}.
 */
// @Deprecated // Supported for TV
public class SliceMetadata {

  /** */
  // @RestrictTo(RestrictTo.Scope.LIBRARY)
  @IntDef({LOADED_NONE, LOADED_PARTIAL, LOADED_ALL})
  @Retention(RetentionPolicy.SOURCE)
  public @interface SliceLoadingState {}

  /** Indicates this slice is empty and waiting for content to be loaded. */
  public static final int LOADED_NONE = 0;

  /** Indicates this slice has some content but is waiting for other content to be loaded. */
  public static final int LOADED_PARTIAL = 1;

  /** Indicates this slice has fully loaded and is not waiting for other content. */
  public static final int LOADED_ALL = 2;

  @NonNull private Slice mSlice;
  @Nullable private Context mContext;
  private long mExpiry;
  private long mLastUpdated;
  private ListContent mListContent;
  private RowContent mHeaderContent;
  private SliceAction mPrimaryAction;
  private List<SliceAction> mSliceActions;
  private @EventInfo.SliceRowType int mTemplateType;
  private final Bundle mHostExtras;

  /**
   * Create a SliceMetadata object to provide access to some information around the slice and its
   * contents.
   *
   * @param context the context to use for the slice.
   * @param slice the slice to extract metadata from.
   * @return the metadata associated with the provided slice.
   */
  @NonNull
  public static SliceMetadata from(@Nullable Context context, @NonNull Slice slice) {
    return new SliceMetadata(context, slice);
  }

  /**
   * Create a SliceMetadata object to provide access to some information around the slice and its
   * contents.
   *
   * @param context the context to use for the slice.
   * @param slice the slice to extract metadata from.
   */
  private SliceMetadata(@Nullable Context context, @NonNull Slice slice) {
    mSlice = slice;
    mContext = context;
    SliceItem ttlItem = SliceQuery.find(slice, FORMAT_LONG, HINT_TTL, null);
    if (ttlItem != null) {
      mExpiry = ttlItem.getLong();
    }
    SliceItem updatedItem = SliceQuery.find(slice, FORMAT_LONG, HINT_LAST_UPDATED, null);
    if (updatedItem != null) {
      mLastUpdated = updatedItem.getLong();
    }
    SliceItem hostExtrasItem = SliceQuery.findSubtype(slice, FORMAT_BUNDLE, SUBTYPE_HOST_EXTRAS);
    if (hostExtrasItem != null && hostExtrasItem.mObj instanceof Bundle) {
      mHostExtras = (Bundle) hostExtrasItem.mObj;
    } else {
      mHostExtras = Bundle.EMPTY;
    }
    mListContent = new ListContent(slice);
    mHeaderContent = mListContent.getHeader();
    mTemplateType = mListContent.getHeaderTemplateType();
    mPrimaryAction = mListContent.getShortcut(mContext);
    mSliceActions = mListContent.getSliceActions();
    if (mSliceActions == null
        && mHeaderContent != null
        && SliceQuery.hasHints(mHeaderContent.getSliceItem(), HINT_LIST_ITEM)) {
      // It's not a real header, check it for end items.
      List<SliceItem> items = mHeaderContent.getEndItems();
      List<SliceAction> actions = new ArrayList<>();
      for (int i = 0; i < items.size(); i++) {
        if (SliceQuery.find(items.get(i), FORMAT_ACTION) != null) {
          actions.add(new SliceActionImpl(items.get(i)));
        }
      }
      if (!actions.isEmpty()) {
        mSliceActions = actions;
      }
    }
  }

  /**
   * @return the title associated with this slice, if it exists.
   */
  @Nullable
  public CharSequence getTitle() {
    CharSequence title = null;
    if (mHeaderContent != null && mHeaderContent.getTitleItem() != null) {
      title = mHeaderContent.getTitleItem().getText();
    }
    if (TextUtils.isEmpty(title) && mPrimaryAction != null) {
      return mPrimaryAction.getTitle();
    }
    return title;
  }

  /**
   * @return the subtitle associated with this slice, if it exists.
   */
  @Nullable
  public CharSequence getSubtitle() {
    if (mHeaderContent != null && mHeaderContent.getSubtitleItem() != null) {
      return mHeaderContent.getSubtitleItem().getText();
    }
    return null;
  }

  /**
   * @return the summary associated with this slice, if it exists.
   */
  @Nullable
  public CharSequence getSummary() {
    if (mHeaderContent != null && mHeaderContent.getSummaryItem() != null) {
      return mHeaderContent.getSummaryItem().getText();
    }
    return null;
  }

  /**
   * @return the group of actions associated with this slice, if they exist.
   */
  @Nullable
  public List<SliceAction> getSliceActions() {
    return mSliceActions;
  }

  /**
   * @return the primary action for this slice, null if none specified.
   */
  @Nullable
  public SliceAction getPrimaryAction() {
    return mPrimaryAction;
  }

  /**
   * @return the type of row that is used for the header of this slice, -1 if unknown.
   */
  public @EventInfo.SliceRowType int getHeaderType() {
    return mTemplateType;
  }

  /**
   * @return whether this slice has content to show when presented in {@link
   *     com.android.tv.twopanelsettings.slices.compat.widget.SliceViewPolicy#MODE_LARGE}.
   */
  public boolean hasLargeMode() {
    return mListContent.getRowItems().size() > 1;
  }

  /**
   * @return the toggles associated with the header of this slice.
   */
  public List<SliceAction> getToggles() {
    List<SliceAction> toggles = new ArrayList<>();
    // Is it the primary action?
    if (mPrimaryAction != null && mPrimaryAction.isToggle()) {
      toggles.add(mPrimaryAction);
    } else if (mSliceActions != null && !mSliceActions.isEmpty()) {
      for (int i = 0; i < mSliceActions.size(); i++) {
        SliceAction action = mSliceActions.get(i);
        if (action.isToggle()) {
          toggles.add(action);
        }
      }
    } else if (mHeaderContent != null) {
      toggles.addAll(mHeaderContent.getToggleItems());
    }
    return toggles;
  }

  @NonNull
  public Bundle getHostExtras() {
    return mHostExtras;
  }

  /**
   * Sends the intent to adjust the state of the provided toggle action.
   *
   * @param toggleAction the toggle action.
   * @param toggleValue the new value to set the toggle to.
   * @return whether there was an action to send.
   */
  public boolean sendToggleAction(SliceAction toggleAction, boolean toggleValue)
      throws PendingIntent.CanceledException {
    if (toggleAction != null) {
      Intent intent =
          new Intent()
              .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
              .putExtra(EXTRA_TOGGLE_STATE, toggleValue);
      if (mContext != null) {
        SliceUtils.fireAction(mContext, toggleAction.getActionParcelable(), intent);
      }
      return true;
    }
    return false;
  }

  /**
   * Gets the input range action associated with the header of this slice, if it exists.
   *
   * @return the {@link android.app.PendingIntent} for the input range.
   */
  @Nullable
  public Parcelable getInputRangeAction() {
    if (mTemplateType == ROW_TYPE_SLIDER) {
      SliceItem range = mHeaderContent.getRange();
      if (range != null) {
        return range.getActionParcelable();
      }
    }
    return null;
  }

  /**
   * Sends the intent to adjust the input range value for the header of this slice, if it exists.
   *
   * @param newValue the value to set the input range to.
   * @return whether there was an action to send.
   */
  public boolean sendInputRangeAction(int newValue) throws PendingIntent.CanceledException {
    if (mTemplateType == ROW_TYPE_SLIDER) {
      SliceItem range = mHeaderContent.getRange();
      if (range != null) {
        // Ensure new value is valid
        Pair<Integer, Integer> validRange = getRange();
        int adjustedValue = MathUtils.clamp(newValue, validRange.first, validRange.second);
        Intent intent =
            new Intent()
                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
                .putExtra(EXTRA_RANGE_VALUE, adjustedValue);
        range.fireAction(mContext, intent);
        return true;
      }
    }
    return false;
  }

  /**
   * Gets the range information associated with a progress bar or input range associated with this
   * slice, if it exists.
   *
   * @return a pair where the first item is the minimum value of the range and the second item is
   *     the maximum value of the range.
   */
  @Nullable
  public Pair<Integer, Integer> getRange() {
    if (mTemplateType == ROW_TYPE_SLIDER || mTemplateType == ROW_TYPE_PROGRESS) {
      SliceItem range = mHeaderContent.getRange();
      SliceItem maxItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
      SliceItem minItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MIN);
      int max = maxItem != null ? maxItem.getInt() : 100; // default max of range
      int min = minItem != null ? minItem.getInt() : 0; // default min of range
      return new Pair<>(min, max);
    }
    return null;
  }

  /**
   * Gets the current value for a progress bar or input range associated with this slice, if it
   * exists, -1 if unknown.
   *
   * @return the current value of a progress bar or input range associated with this slice.
   */
  public int getRangeValue() {
    if (mTemplateType == ROW_TYPE_SLIDER || mTemplateType == ROW_TYPE_PROGRESS) {
      SliceItem range = mHeaderContent.getRange();
      SliceItem currentItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
      return currentItem != null ? currentItem.getInt() : -1;
    }
    return -1;
  }

  /**
   * @return whether this slice is a selection (a drop-down list) slice.
   */
  public boolean isSelection() {
    return (mTemplateType == EventInfo.ROW_TYPE_SELECTION);
  }

  /**
   * @return the list of keywords associated with the provided slice, null if no keywords were
   *     specified or an empty list if the slice was specified to have no keywords.
   */
  @Nullable
  public List<String> getSliceKeywords() {
    SliceItem keywordGroup = SliceQuery.find(mSlice, FORMAT_SLICE, HINT_KEYWORDS, null);
    if (keywordGroup != null) {
      List<SliceItem> itemList = SliceQuery.findAll(keywordGroup, FORMAT_TEXT);
      if (itemList != null) {
        ArrayList<String> stringList = new ArrayList<>();
        for (int i = 0; i < itemList.size(); i++) {
          String keyword = (String) itemList.get(i).getText();
          if (!TextUtils.isEmpty(keyword)) {
            stringList.add(keyword);
          }
        }
        return stringList;
      }
    }
    return null;
  }

  /**
   * @return the current loading state for this slice.
   * @see #LOADED_NONE
   * @see #LOADED_PARTIAL
   * @see #LOADED_ALL
   */
  public int getLoadingState() {
    // Check loading state
    boolean hasHintPartial = SliceQuery.find(mSlice, null, HINT_PARTIAL, null) != null;
    if (!mListContent.isValid()) {
      // Empty slice
      return LOADED_NONE;
    } else if (hasHintPartial) {
      // Slice with specific content to load
      return LOADED_PARTIAL;
    } else {
      // Full slice
      return LOADED_ALL;
    }
  }

  /**
   * A slice contains an expiry to indicate when the content in the slice might no longer be valid.
   *
   * @return the time, measured in milliseconds, between the expiry time of this slice and midnight,
   *     January 1, 1970 UTC, or {@link
   *     com.android.tv.twopanelsettings.slices.compat.builders.ListBuilder#INFINITY} if the slice
   *     is not time-sensitive.
   */
  public long getExpiry() {
    return mExpiry;
  }

  /**
   * @return the time, measured in milliseconds, between when the slice was created or last updated,
   *     and midnight, January 1, 1970 UTC.
   */
  public long getLastUpdatedTime() {
    return mLastUpdated;
  }

  /**
   * To present a slice from another app, the app must grant uri permissions for the slice. If these
   * permissions have not been granted and the app slice is requested then a permission request
   * slice will be returned instead, allowing the user to grant permission. This method can be used
   * to identify if a slice is a permission request.
   *
   * @return whether this slice represents a permission request.
   */
  public boolean isPermissionSlice() {
    return mSlice.hasHint(HINT_PERMISSION_REQUEST);
  }

  /**
   * Indicates whether this slice indicates an error, i.e. the normal contents of this slice are
   * unavailable and instead the slice contains a message indicating an error.
   *
   * @return whether this slice represents an error.
   */
  public boolean isErrorSlice() {
    return mSlice.hasHint(HINT_ERROR);
  }

  /**
   * Indicates whether this slice was created using {@link SliceUtils#parseSlice} or through normal
   * binding.
   */
  public boolean isCachedSlice() {
    return mSlice.hasHint(HINT_CACHED);
  }

  /**
   * @return the group of slice actions associated with the provided slice, if they exist.
   */
  @Nullable
  // @RestrictTo(RestrictTo.Scope.LIBRARY)
  public static List<SliceAction> getSliceActions(@NonNull Slice slice) {
    SliceItem actionGroup = SliceQuery.find(slice, FORMAT_SLICE, HINT_ACTIONS, null);
    String[] hints = new String[] {HINT_ACTIONS, HINT_SHORTCUT};
    List<SliceItem> items =
        (actionGroup != null) ? SliceQuery.findAll(actionGroup, FORMAT_SLICE, hints, null) : null;
    if (items != null) {
      List<SliceAction> actions = new ArrayList<>(items.size());
      for (int i = 0; i < items.size(); i++) {
        SliceItem item = items.get(i);
        actions.add(new SliceActionImpl(item));
      }
      return actions;
    }
    return null;
  }

  /** */
  // @RestrictTo(RestrictTo.Scope.LIBRARY)
  public boolean isExpired() {
    long now = System.currentTimeMillis();
    return mExpiry != 0 && mExpiry != SliceHints.INFINITY && now > mExpiry;
  }

  /** */
  // @RestrictTo(RestrictTo.Scope.LIBRARY)
  public boolean neverExpires() {
    return mExpiry == SliceHints.INFINITY;
  }

  /** */
  // @RestrictTo(RestrictTo.Scope.LIBRARY)
  public long getTimeToExpiry() {
    long now = System.currentTimeMillis();
    return (mExpiry == 0 || mExpiry == SliceHints.INFINITY || now > mExpiry) ? 0 : mExpiry - now;
  }

  /** */
  // @RestrictTo(RestrictTo.Scope.LIBRARY)
  public ListContent getListContent() {
    return mListContent;
  }
}
