/* global global:writable */

import * as semver from 'semver';

export const LOCATION = {
  LOCAL: 'local',
  SESSION: 'session',
  WINDOW: 'window',
};

const LOCATION_SHADOWING = [ LOCATION.WINDOW, LOCATION.SESSION, LOCATION.LOCAL ];

export class StorageManager {

  namespace;
  version;
  _options;
  _global;

  constructor(namespace, version, options) {
    this.namespace = namespace;
    this.version = semver.valid(version);

    if (!namespace) {
      throw new Error('Cannot setup StorageManager without namespace');
    }
    if (!this.version) {
      throw new Error('Cannot setup StorageManager without valid semver version');
    }

    if (options !== undefined) {
      this._options = options;
    }

    if (global !== undefined)
      this._global = global;
    else if (window !== undefined)
      this._global = window;
    else
      throw new Error('Cannot setup StorageManager without window or global');
  }

  save(key, value, options=this._options) {
    if (!key) {
      throw new Error('StorageManager.save called with invalid key');
    }

    if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
      throw new Error('StorageManager.save called with unsupported location option');
    }

    const location = options && options.location !== undefined ? options.location : LOCATION_SHADOWING[0];

    switch (location) {
      case LOCATION.LOCAL: {
        this._saveToLocalStorage({ ...this._getFromLocalStorage(), [key]: value });
        break;
      }
      case LOCATION.WINDOW: {
        this._saveToWindow({ ...this._getFromLocalStorage(), [key]: value });
        break;
      }
      default:
        console.error('StorageManager.save cannot save item with unsupported location');
    }
  }

  load(key, options=this._options) {
    if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
      throw new Error('StorageManager.load called with unsupported location option');
    }

    let locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;

    while (locations.length > 0) {
      const location = locations.shift();
      let val;
      
      switch (location) {
        case LOCATION.LOCAL: {
          val = this._getFromLocalStorage()[key];
          break;
        }
        case LOCATION.SESSION: {
          val = this._getFromSessionStorage()[key];
          break;
        }
        case LOCATION.WINDOW: {
          val = this._getFromWindow()[key];
          break;
        }
        default:
          console.error('StorageManager.load cannot load item with unsupported location');
      }

      if (val !== undefined || locations.length === 0) {
        return val;
      }
    }
  }

  remove(key, options=this._options) {
    if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
      throw new Error('StorageManager.load called with unsupported location option');
    }

    const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;

    for (const location of locations) {
      switch (location) {
        case LOCATION.LOCAL: {
          let val = this._getFromLocalStorage();

          delete val[key];

          return this._saveToLocalStorage(val);
        }
        case LOCATION.WINDOW: {
          let val = this._getFromWindow();

          delete val[key];

          return this._saveToWindow(val);
        }
        default:
          console.error('StorageManager.load cannot load item with unsupported location');
      }
    }
  }

  clear(options) {
     if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
      throw new Error('StorageManager.clear called with unsupported location option');
    }

    const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;

    for (const location of locations) {
      switch (location) {
        case LOCATION.LOCAL:
          return this._clearLocalStorage();
        case LOCATION.WINDOW:
          return this._clearWindow();
        default:
          console.error('StorageManager.clear cannot clear with unsupported location');
      }
    }
   
  }

  
  _getFromLocalStorage() {
    let state;

    try {
      state = JSON.parse(window.localStorage.getItem(this.namespace));
    } catch {
      state = null;
    }

    if (state === null || !state.version || !semver.satisfies(this.version, `^${state.version}`)) {
      // remove item from localStorage if it stores an invalid state
      this._clearLocalStorage();
      return {};
    }

    if ('state' in state)
      return state.state;
    else {
      delete state.version;
      return state;
    }
  }

  _saveToLocalStorage(state) {
    if (!state)
      return this._clearLocalStorage();

    let versionedState;
    
    if ('version' in state || 'state' in state) {
      versionedState = { version: this.version, state: state };
    } else {
      versionedState = { version: this.version, ...state };
    }
    
    window.localStorage.setItem(this.namespace, JSON.stringify(versionedState));
  }

  _clearLocalStorage() {
    window.localStorage.removeItem(this.namespace);
  }

  
  _getFromWindow() {
    if (!this._global || !this._global.App)
      return {};

    if (!this._global.App.Storage)
      this._global.App.Storage = {};

    return this._global.App.Storage[this.namespace] || {};
  }

  _saveToWindow(value) {
    if (!this._global || !this._global.App) {
      throw new Error('StorageManager._saveToWindow called when window.App is not available');
    }

    if (!value)
      return this._clearWindow();

    if (!this._global.App.Storage)
      this._global.App.Storage = {};
 
    this._global.App.Storage[this.namespace] = value;
  }

  _clearWindow() {
    if (!this._global || !this._global.App) {
      throw new Error('StorageManager._saveToWindow called when window.App is not available');
    }
 
    if (this._global.App.Storage) {
      delete this._global.App.Storage[this.namespace];
    }
  }


  _getFromSessionStorage() {
    let state;

    try {
      state = JSON.parse(window.sessionStorage.getItem(this.namespace));
    } catch {
      state = null;
    }

    if (state === null || !state.version || !semver.satisfies(this.version, `^${state.version}`)) {
      // remove item from session if it stores an invalid state
      this._clearSessionStorage();
      return {};
    }

    if ('state' in state)
      return state.state;
    else {
      delete state.version;
      return state;
    }
  }

  _saveToSessionStorage(state) {
    if (!state)
      return this._clearSessionStorage();

    let versionedState;
    
    if ('version' in state || 'state' in state) {
      versionedState = { version: this.version, state: state };
    } else {
      versionedState = { version: this.version, ...state };
    }
    
    window.sessionStorage.setItem(this.namespace, JSON.stringify(versionedState));
  }

  _clearSessionStorage() {
    window.sessionStorage.removeItem(this.namespace);
  }
}
