1/*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16interface IFetchingRangeEvaluator {
17  updateRangeToFetch(whatHappened: RangeUpdateEvent): void;
18}
19
20type RangeUpdateEvent =
21  | {
22      kind: 'visible-area-changed';
23      minVisible: number;
24      maxVisible: number;
25    }
26  | {
27      kind: 'item-fetched';
28      itemIndex: number;
29      fetchDuration: number;
30    }
31  | {
32      kind: 'collection-changed';
33      totalCount: number;
34    }
35  | {
36      kind: 'item-added';
37      itemIndex: number;
38    }
39  | {
40      kind: 'item-removed';
41      itemIndex: number;
42    };
43
44// eslint-disable-next-line @typescript-eslint/no-unused-vars
45class FetchingRangeEvaluator implements IFetchingRangeEvaluator {
46  protected totalItems = 0;
47
48  constructor(
49    private readonly itemsOnScreen: ItemsOnScreenProvider,
50    private readonly prefetchCount: PrefetchCount,
51    private readonly prefetchRangeRatio: PrefetchRangeRatio,
52    protected readonly fetchedRegistry: FetchedRegistry,
53    private readonly logger: ILogger = dummyLogger,
54  ) {}
55
56  updateRangeToFetch(whatHappened: RangeUpdateEvent): void {
57    switch (whatHappened.kind) {
58      case 'visible-area-changed':
59        this.onVisibleAreaChange(whatHappened.minVisible, whatHappened.maxVisible);
60        break;
61      case 'item-fetched':
62        this.onItemFetched(whatHappened.itemIndex, whatHappened.fetchDuration);
63        break;
64      case 'collection-changed':
65        this.onCollectionChanged(whatHappened.totalCount);
66        break;
67      case 'item-added':
68        this.onItemAdded(whatHappened.itemIndex);
69        break;
70      case 'item-removed':
71        this.onItemDeleted(whatHappened.itemIndex);
72        break;
73      default:
74        assertNever(whatHappened);
75    }
76  }
77
78  protected onVisibleAreaChange(minVisible: number, maxVisible: number): void {
79    const oldVisibleRange = this.itemsOnScreen.visibleRange;
80    this.itemsOnScreen.update(minVisible, maxVisible);
81
82    this.logger.debug(
83      `visibleAreaChanged itemsOnScreen=${this.itemsOnScreen.visibleRange.length}, meanImagesOnScreen=${this.itemsOnScreen.meanValue}, prefetchCountCurrentLimit=${this.prefetchCount.currentMaxItems}, prefetchCountMaxRatio=${this.prefetchRangeRatio.maxRatio}`,
84    );
85
86    if (!oldVisibleRange.equals(this.itemsOnScreen.visibleRange)) {
87      this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('visible-area-changed');
88      const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems);
89      this.fetchedRegistry.updateRangeToFetch(rangeToFetch);
90    }
91  }
92
93  protected onItemFetched(index: number, fetchDuration: number): void {
94    if (!this.fetchedRegistry.rangeToFetch.contains(index)) {
95      return;
96    }
97
98    this.logger.debug(`onItemFetched`);
99    let maxRatioChanged = false;
100    if (this.prefetchRangeRatio.update(index, fetchDuration) === 'ratio-changed') {
101      maxRatioChanged = true;
102      this.logger.debug(
103        `choosePrefetchCountLimit prefetchCountMaxRatio=${this.prefetchRangeRatio.maxRatio}, prefetchCountMinRatio=${this.prefetchRangeRatio.minRatio}, prefetchCountCurrentLimit=${this.prefetchCount.currentMaxItems}`,
104      );
105    }
106
107    this.fetchedRegistry.addFetched(index);
108
109    this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('resolved', maxRatioChanged);
110    const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems);
111    this.fetchedRegistry.updateRangeToFetch(rangeToFetch);
112  }
113
114  private evaluatePrefetchCount(event: 'resolved' | 'visible-area-changed', maxRatioChanged?: boolean): number {
115    let ratio = this.prefetchRangeRatio.calculateRatio(this.prefetchCount.prefetchCountValue, this.totalItems);
116    let evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio);
117
118    if (maxRatioChanged) {
119      ratio = this.prefetchRangeRatio.calculateRatio(evaluatedPrefetchCount, this.totalItems);
120      evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio);
121    }
122
123    if (!this.prefetchRangeRatio.hysteresisEnabled) {
124      if (event === 'resolved') {
125        this.prefetchRangeRatio.updateRatioRange(ratio);
126        this.prefetchRangeRatio.hysteresisEnabled = true;
127      } else if (event === 'visible-area-changed') {
128        this.prefetchRangeRatio.oldRatio = ratio;
129      }
130    } else if (this.prefetchRangeRatio.range.contains(ratio)) {
131      return this.prefetchCount.prefetchCountValue;
132    } else {
133      if (event === 'resolved') {
134        this.prefetchRangeRatio.updateRatioRange(ratio);
135      } else if (event === 'visible-area-changed') {
136        this.prefetchRangeRatio.setEmptyRange();
137        this.prefetchRangeRatio.oldRatio = ratio;
138        this.prefetchRangeRatio.hysteresisEnabled = false;
139      }
140    }
141
142    this.logger.debug(
143      `evaluatePrefetchCount event=${event}, ${this.prefetchRangeRatio.hysteresisEnabled ? 'inHysteresis' : 'setHysteresis'} prefetchCount=${evaluatedPrefetchCount}, ratio=${ratio}, hysteresisRange=${this.prefetchRangeRatio.range}`,
144    );
145
146    return evaluatedPrefetchCount;
147  }
148
149  protected onCollectionChanged(totalCount: number): void {
150    this.totalItems = Math.max(0, totalCount);
151    let newRangeToFetch: IndexRange;
152    if (this.fetchedRegistry.rangeToFetch.length > 0) {
153      newRangeToFetch = this.itemsOnScreen.visibleRange;
154    } else {
155      newRangeToFetch = this.fetchedRegistry.rangeToFetch;
156    }
157    if (newRangeToFetch.end > this.totalItems) {
158      const end = this.totalItems;
159      const start = newRangeToFetch.start < end ? newRangeToFetch.start : end;
160      newRangeToFetch = new IndexRange(start, end);
161    }
162
163    this.fetchedRegistry.clearFetched(newRangeToFetch);
164  }
165
166  private onItemDeleted(itemIndex: number): void {
167    if (this.totalItems === 0) {
168      return;
169    }
170    this.totalItems--;
171    this.fetchedRegistry.removeFetched(itemIndex);
172
173    const end =
174      this.fetchedRegistry.rangeToFetch.end < this.totalItems ? this.fetchedRegistry.rangeToFetch.end : this.totalItems;
175    const rangeToFetch = new IndexRange(this.fetchedRegistry.rangeToFetch.start, end);
176
177    this.fetchedRegistry.decrementFetchedGreaterThen(itemIndex, rangeToFetch);
178  }
179
180  private onItemAdded(itemIndex: number): void {
181    this.totalItems++;
182    if (itemIndex > this.fetchedRegistry.rangeToFetch.end) {
183      return;
184    }
185
186    const end = this.fetchedRegistry.rangeToFetch.end + 1;
187    const rangeToFetch = new IndexRange(this.fetchedRegistry.rangeToFetch.start, end);
188    this.fetchedRegistry.incrementFetchedGreaterThen(itemIndex - 1, rangeToFetch);
189  }
190}
191