/*
 * Copyright (C) 2017 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.dialer.calllog.ui;

import android.app.Activity;
import java.util.Objects;
import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
import com.android.dialer.compat.telephony.TelephonyManagerCompat;
import com.android.dialer.CoalescedIds;
import android.provider.CallLog.Calls;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.Toast;
import com.android.dialer.calllog.model.CoalescedRow;
import com.android.dialer.calllogutils.CallLogDates;
import com.android.dialer.common.Assert;
import com.android.dialer.logging.Logger;
import com.android.dialer.promotion.Promotion;
import com.android.dialer.time.Clock;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** {@link RecyclerView.Adapter} for the new call log fragment. */
final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> {

  /** IntDef for the different types of rows that can be shown in the call log. */
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({
    RowType.PROMOTION_CARD,
    RowType.HEADER_TODAY,
    RowType.HEADER_YESTERDAY,
    RowType.HEADER_OLDER,
    RowType.CALL_LOG_ENTRY
  })
  @interface RowType {
    /** The promotion card. */
    int PROMOTION_CARD = 1;

    /** Header that displays "Today". */
    int HEADER_TODAY = 2;

    /** Header that displays "Yesterday". */
    int HEADER_YESTERDAY = 3;

    /** Header that displays "Older". */
    int HEADER_OLDER = 4;

    /** A row representing a call log entry (which could represent one or more calls). */
    int CALL_LOG_ENTRY = 5;
  }

  private final Clock clock;
  private final Activity activity;
  private final RealtimeRowProcessor realtimeRowProcessor;
  private final PopCounts popCounts = new PopCounts();
  @Nullable private final Promotion promotion;
  private final Runnable onCallDeleted;
  @Nullable private NewCallLogViewHolder openSwipeHolder;

  private ImmutableList<CoalescedRow> coalescedRows;

  /** Position of the promotion card. Null when it should not be displayed. */
  @Nullable private Integer promotionCardPosition;

  /** Position of the "Today" header. Null when it should not be displayed. */
  @Nullable private Integer todayHeaderPosition;

  /** Position of the "Yesterday" header. Null when it should not be displayed. */
  @Nullable private Integer yesterdayHeaderPosition;

  /** Position of the "Older" header. Null when it should not be displayed. */
  @Nullable private Integer olderHeaderPosition;

  NewCallLogAdapter(
      Activity activity,
      ImmutableList<CoalescedRow> coalescedRows,
      Clock clock,
      @Nullable Promotion promotion,
      Runnable onCallDeleted) {
    this.activity = activity;
    this.coalescedRows = coalescedRows;
    this.clock = clock;
    this.realtimeRowProcessor = CallLogUiComponent.get(activity).realtimeRowProcessor();
    this.promotion = promotion;
    this.onCallDeleted = onCallDeleted;

    setCardAndHeaderPositions();
  }

  void updateRows(ImmutableList<CoalescedRow> coalescedRows) {
    this.coalescedRows = coalescedRows;
    this.realtimeRowProcessor.clearCache();
    this.popCounts.reset();

    setCardAndHeaderPositions();
    notifyDataSetChanged();
  }

  void appendRows(ImmutableList<CoalescedRow> olderRows) {
    if (olderRows.isEmpty()) {
      return;
    }

    ImmutableList.Builder<CoalescedRow> builder = ImmutableList.builder();
    int olderStart = 0;
    if (!coalescedRows.isEmpty()
        && canMergeBoundary(coalescedRows.get(coalescedRows.size() - 1), olderRows.get(0))) {
      for (int i = 0; i < coalescedRows.size() - 1; i++) {
        builder.add(coalescedRows.get(i));
      }
      builder.add(
          mergeBoundary(coalescedRows.get(coalescedRows.size() - 1), olderRows.get(0)));
      olderStart = 1;
    } else {
      builder.addAll(coalescedRows);
    }

    for (int i = olderStart; i < olderRows.size(); i++) {
      builder.add(olderRows.get(i));
    }
    coalescedRows = builder.build();
    setCardAndHeaderPositions();
    notifyDataSetChanged();
  }

  boolean isEmpty() {
    return coalescedRows.isEmpty();
  }

  void closeOpenSwipe() {
    if (openSwipeHolder != null) {
      NewCallLogViewHolder holder = openSwipeHolder;
      openSwipeHolder = null;
      holder.closeSwipe(true);
    }
  }

  private static boolean canMergeBoundary(CoalescedRow newer, CoalescedRow older) {
    if (newer.getCoalescedIds().getCoalescedIdCount()
            + older.getCoalescedIds().getCoalescedIdCount()
        > 500) {
      return false;
    }
    if (newer.getCallType() != older.getCallType()) {
      return false;
    }
    if (newer.getNumberPresentation() != older.getNumberPresentation()) {
      return false;
    }
    if (!Objects.equals(
        newer.getPhoneAccountComponentName(), older.getPhoneAccountComponentName())) {
      return false;
    }
    if (!Objects.equals(newer.getPhoneAccountId(), older.getPhoneAccountId())) {
      return false;
    }

    int featureMask =
        TelephonyManagerCompat.FEATURES_ASSISTED_DIALING
            | Calls.FEATURES_VIDEO
            | Calls.FEATURES_RTT;
    if ((newer.getFeatures() & featureMask) != (older.getFeatures() & featureMask)) {
      return false;
    }
    return new DialerPhoneNumberUtil().isMatch(newer.getNumber(), older.getNumber());
  }

  private static CoalescedRow mergeBoundary(CoalescedRow newer, CoalescedRow older) {
    CoalescedIds ids =
        CoalescedIds.newBuilder()
            .addAllCoalescedId(newer.getCoalescedIds().getCoalescedIdList())
            .addAllCoalescedId(older.getCoalescedIds().getCoalescedIdList())
            .build();
    return newer.toBuilder()
        .setFeatures(newer.getFeatures() | older.getFeatures())
        .setCoalescedIds(ids)
        .build();
  }

  void clearCache() {
    this.realtimeRowProcessor.clearCache();
  }

  void logMetrics(Context context) {
    Logger.get(context).logAnnotatedCallLogMetrics(popCounts.popped, popCounts.didNotPop);
  }

  private void setCardAndHeaderPositions() {
    // Set the position for the promotion card if it should be shown.
    promotionCardPosition = null;
    int numCards = 0;
    if (promotion != null && promotion.isEligibleToBeShown()) {
      promotionCardPosition = 0;
      numCards++;
    }

    // If there are no rows to display, set all header positions to null.
    if (coalescedRows.isEmpty()) {
      todayHeaderPosition = null;
      yesterdayHeaderPosition = null;
      olderHeaderPosition = null;
      return;
    }

    // Calculate positions for headers.
    long currentTimeMillis = clock.currentTimeMillis();

    int numItemsInToday = 0;
    int numItemsInYesterday = 0;
    int numItemsInOlder = 0;
    for (CoalescedRow coalescedRow : coalescedRows) {
      long timestamp = coalescedRow.getTimestamp();
      long dayDifference = CallLogDates.getDayDifference(currentTimeMillis, timestamp);
      if (dayDifference == 0) {
        numItemsInToday++;
      } else if (dayDifference == 1) {
        numItemsInYesterday++;
      } else {
        numItemsInOlder = coalescedRows.size() - numItemsInToday - numItemsInYesterday;
        break;
      }
    }

    if (numItemsInToday > 0) {
      numItemsInToday++; // including the "Today" header;
    }
    if (numItemsInYesterday > 0) {
      numItemsInYesterday++; // including the "Yesterday" header;
    }
    if (numItemsInOlder > 0) {
      numItemsInOlder++; // include the "Older" header;
    }

    // Set all header positions.
    // A header position will be null if there is no item to be displayed under that header.
    todayHeaderPosition = numItemsInToday > 0 ? numCards : null;
    yesterdayHeaderPosition = numItemsInYesterday > 0 ? numItemsInToday + numCards : null;
    olderHeaderPosition =
        numItemsInOlder > 0 ? numItemsInToday + numItemsInYesterday + numCards : null;
  }

  @Override
  public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    super.onAttachedToRecyclerView(recyclerView);

    // Register a OnScrollListener that records when the promotion is viewed.
    if (promotion != null && promotion.isEligibleToBeShown()) {
      recyclerView.addOnScrollListener(
          new OnScrollListenerForRecordingPromotionCardFirstViewTime(promotion));
    }
  }

  @Override
  public ViewHolder onCreateViewHolder(ViewGroup viewGroup, @RowType int viewType) {
    switch (viewType) {
      case RowType.PROMOTION_CARD:
        return new PromotionCardViewHolder(
            LayoutInflater.from(activity)
                .inflate(
                    R.layout.new_call_log_promotion_card, viewGroup, /* attachToRoot = */ false),
            promotion);
      case RowType.HEADER_TODAY:
      case RowType.HEADER_YESTERDAY:
      case RowType.HEADER_OLDER:
        return new HeaderViewHolder(
            LayoutInflater.from(activity)
                .inflate(R.layout.new_call_log_header, viewGroup, /* attachToRoot = */ false));
      case RowType.CALL_LOG_ENTRY:
        return new NewCallLogViewHolder(
            activity,
            LayoutInflater.from(activity)
                .inflate(R.layout.new_call_log_entry, viewGroup, /* attachToRoot = */ false),
            clock,
            realtimeRowProcessor,
            popCounts,
            new NewCallLogViewHolder.SwipeActionListener() {
              @Override
              public void onSwipeOpened(NewCallLogViewHolder holder) {
                if (openSwipeHolder != null && openSwipeHolder != holder) {
                  NewCallLogViewHolder previous = openSwipeHolder;
                  openSwipeHolder = holder;
                  previous.closeSwipe(true);
                } else {
                  openSwipeHolder = holder;
                }
              }

              @Override
              public void onSwipeClosed(NewCallLogViewHolder holder) {
                if (openSwipeHolder == holder) {
                  openSwipeHolder = null;
                }
              }

              @Override
              public void onDeleteRequested(NewCallLogViewHolder holder, CoalescedRow row) {
                SingleCallLogDeletion.deleteNewest(
                    activity,
                    row.getCoalescedIds(),
                    () -> {
                      if (openSwipeHolder == holder) {
                        openSwipeHolder = null;
                      }
                      holder.closeSwipe(false);
                      onCallDeleted.run();
                    },
                    () -> {
                      holder.restoreAfterDeleteFailure();
                      Toast.makeText(
                              activity,
                              R.string.lemenzo_call_delete_failed,
                              Toast.LENGTH_SHORT)
                          .show();
                    });
              }
            });
      default:
        throw Assert.createUnsupportedOperationFailException("Unsupported view type: " + viewType);
    }
  }

  @Override
  public void onBindViewHolder(ViewHolder viewHolder, int position) {
    @RowType int viewType = getItemViewType(position);
    switch (viewType) {
      case RowType.PROMOTION_CARD:
        ((PromotionCardViewHolder) viewHolder)
            .setDismissListener(
                () -> {
                  notifyItemRemoved(promotionCardPosition);
                  setCardAndHeaderPositions();
                });
        break;
      case RowType.HEADER_TODAY:
        ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_today);
        break;
      case RowType.HEADER_YESTERDAY:
        ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_yesterday);
        break;
      case RowType.HEADER_OLDER:
        ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_older);
        break;
      case RowType.CALL_LOG_ENTRY:
        NewCallLogViewHolder newCallLogViewHolder = (NewCallLogViewHolder) viewHolder;
        int previousCardAndHeaders = 0;
        if (promotionCardPosition != null && position > promotionCardPosition) {
          previousCardAndHeaders++;
        }
        if (todayHeaderPosition != null && position > todayHeaderPosition) {
          previousCardAndHeaders++;
        }
        if (yesterdayHeaderPosition != null && position > yesterdayHeaderPosition) {
          previousCardAndHeaders++;
        }
        if (olderHeaderPosition != null && position > olderHeaderPosition) {
          previousCardAndHeaders++;
        }
        newCallLogViewHolder.bind(coalescedRows.get(position - previousCardAndHeaders));
        break;
      default:
        throw Assert.createIllegalStateFailException(
            "Unexpected view type " + viewType + " at position: " + position);
    }
  }

  @Override
  @RowType
  public int getItemViewType(int position) {
    if (promotionCardPosition != null && position == promotionCardPosition) {
      return RowType.PROMOTION_CARD;
    }
    if (todayHeaderPosition != null && position == todayHeaderPosition) {
      return RowType.HEADER_TODAY;
    }
    if (yesterdayHeaderPosition != null && position == yesterdayHeaderPosition) {
      return RowType.HEADER_YESTERDAY;
    }
    if (olderHeaderPosition != null && position == olderHeaderPosition) {
      return RowType.HEADER_OLDER;
    }
    return RowType.CALL_LOG_ENTRY;
  }

  @Override
  public int getItemCount() {
    int numberOfCards = 0;
    int numberOfHeaders = 0;

    if (promotionCardPosition != null) {
      numberOfCards++;
    }
    if (todayHeaderPosition != null) {
      numberOfHeaders++;
    }
    if (yesterdayHeaderPosition != null) {
      numberOfHeaders++;
    }
    if (olderHeaderPosition != null) {
      numberOfHeaders++;
    }
    return coalescedRows.size() + numberOfHeaders + numberOfCards;
  }

  /**
   * A {@link RecyclerView.OnScrollListener} that records the timestamp at which the promotion card
   * is first viewed.
   *
   * <p>We consider the card as viewed if the user scrolls the containing RecyclerView since such
   * action is a strong proof.
   */
  private static final class OnScrollListenerForRecordingPromotionCardFirstViewTime
      extends RecyclerView.OnScrollListener {

    private final Promotion promotion;

    OnScrollListenerForRecordingPromotionCardFirstViewTime(Promotion promotion) {
      this.promotion = promotion;
    }

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
      if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
        promotion.onViewed();

        // Recording promotion is viewed is this listener's sole responsibility.
        // We can remove it from the containing RecyclerView after the job is done.
        recyclerView.removeOnScrollListener(this);
      }

      super.onScrollStateChanged(recyclerView, newState);
    }
  }

  static class PopCounts {
    int popped;
    int didNotPop;

    private void reset() {
      popped = 0;
      didNotPop = 0;
    }
  }
}
