/**
 * @license
 * 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.
 */
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {customElement, property} from '@polymer/decorators';
import {CommentRange, PatchSetNum} from '../../../types/common';

export interface StorageLocation {
  changeNum: number;
  patchNum: PatchSetNum | '@change';
  path?: string;
  line?: number;
  range?: CommentRange;
}

export interface StorageObject {
  message?: string;
  updated: number;
}

const DURATION_DAY = 24 * 60 * 60 * 1000;

// Clean up old entries no more frequently than one day.
const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY;

const CLEANUP_PREFIXES_MAX_AGE_MAP = new Map<string, number>();
CLEANUP_PREFIXES_MAX_AGE_MAP.set('respectfultip', 14 * DURATION_DAY);
CLEANUP_PREFIXES_MAX_AGE_MAP.set('draft', DURATION_DAY);
CLEANUP_PREFIXES_MAX_AGE_MAP.set('editablecontent', DURATION_DAY);

declare global {
  interface HTMLElementTagNameMap {
    'gr-storage': GrStorage;
  }
}

export interface GrStorage {
  $: {};
}

@customElement('gr-storage')
export class GrStorage extends GestureEventListeners(
  LegacyElementMixin(PolymerElement)
) {
  @property({type: Number})
  _lastCleanup = 0;

  @property({type: Object})
  _storage = window.localStorage;

  @property({type: Boolean})
  _exceededQuota = false;

  getDraftComment(location: StorageLocation): StorageObject | null {
    this._cleanupItems();
    return this._getObject(this._getDraftKey(location));
  }

  setDraftComment(location: StorageLocation, message: string) {
    const key = this._getDraftKey(location);
    this._setObject(key, {message, updated: Date.now()});
  }

  eraseDraftComment(location: StorageLocation) {
    const key = this._getDraftKey(location);
    this._storage.removeItem(key);
  }

  getEditableContentItem(key: string): StorageObject | null {
    this._cleanupItems();
    return this._getObject(this._getEditableContentKey(key));
  }

  setEditableContentItem(key: string, message: string) {
    this._setObject(this._getEditableContentKey(key), {
      message,
      updated: Date.now(),
    });
  }

  getRespectfulTipVisibility(): StorageObject | null {
    this._cleanupItems();
    return this._getObject('respectfultip:visibility');
  }

  setRespectfulTipVisibility(delayDays = 0) {
    this._cleanupItems();
    this._setObject('respectfultip:visibility', {
      updated: Date.now() + delayDays * DURATION_DAY,
    });
  }

  eraseEditableContentItem(key: string) {
    this._storage.removeItem(this._getEditableContentKey(key));
  }

  _getDraftKey(location: StorageLocation): string {
    const range = location.range
      ? `${location.range.start_line}-${location.range.start_character}` +
        `-${location.range.end_character}-${location.range.end_line}`
      : null;
    let key = [
      'draft',
      location.changeNum,
      location.patchNum,
      location.path,
      location.line || '',
    ].join(':');
    if (range) {
      key = key + ':' + range;
    }
    return key;
  }

  _getEditableContentKey(key: string): string {
    return `editablecontent:${key}`;
  }

  _cleanupItems() {
    // Throttle cleanup to the throttle interval.
    if (
      this._lastCleanup &&
      Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL
    ) {
      return;
    }
    this._lastCleanup = Date.now();

    Object.keys(this._storage).forEach(key => {
      const entries = CLEANUP_PREFIXES_MAX_AGE_MAP.entries();
      for (const [prefix, expiration] of entries) {
        if (key.startsWith(prefix)) {
          const item = this._getObject(key);
          if (!item || Date.now() - item.updated > expiration) {
            this._storage.removeItem(key);
          }
        }
      }
    });
  }

  _getObject(key: string): StorageObject | null {
    const serial = this._storage.getItem(key);
    if (!serial) {
      return null;
    }
    return JSON.parse(serial) as StorageObject;
  }

  _setObject(key: string, obj: StorageObject) {
    if (this._exceededQuota) {
      return;
    }
    try {
      this._storage.setItem(key, JSON.stringify(obj));
    } catch (exc) {
      // Catch for QuotaExceededError and disable writes on local storage the
      // first time that it occurs.
      if (exc.code === 22) {
        this._exceededQuota = true;
        console.warn('Local storage quota exceeded: disabling');
        return;
      } else {
        throw exc;
      }
    }
  }
}
