import Utils, { IUtils, IUtilsDiff } from '../Utils';
import * as _ from 'underscore';
import * as io from 'socket.io-client';

import EventEmitter, { IEmitter } from '../Emitter';
import { IPageContentProvider, IPageDelta, IPageCellStyle, IPageQuery } from './Content';
import { IPageContent } from './Content';
import { PageContent } from './Content';
import { IPageProvider } from './Provider';
import { Providers } from './Provider';
import { ProviderREST } from './Provider';
import { ProviderSocket } from './Provider';
import { IPageRangesCollection } from './Range';
import { RangesWrap } from './Range';
import { Ranges } from './Range';
import { IEncryptionKey, ICryptoService } from '../Crypto';
import { IApiService } from '../Api';
import { IAuthService } from '../Auth';
import { IStorageService } from '../Storage';
import { IIPPConfig } from '../Config';
import Promise from 'bluebird';
import { IActions, Actions } from '../Actions/Actions';
import { IFunctions } from '../Functions';
import { Functions } from '..';
import { IKeysService } from '../Keys';
import Helpers, { IHelpers } from '../Helpers';

const helpers: IHelpers = new Helpers();

export interface IPageColumns {
  index: number;
  column: string;
  value: string;
}

export interface IPageColumnDefs {
  name: string;
  display_name?: string;
  style?: IPageCellStyle;
  width?: number;
}

export interface IPageTypes {
  regular: number;
  pageAccessReport: number;
  domainUsageReport: number;
  globalUsageReport: number;
  pageUpdateReport: number;
  alert: number;
  pdf: number;
  liveUsage: number;
}

export interface IPageServiceContent {
  id: number;
  seq_no: number;
  content_modified_timestamp: Date;
  content: IPageContent;
  content_modified_by: any;
  push_interval: number;
  pull_interval: number;
  is_public: boolean;
  description: string;
  encrypted_content: string;
  encryption_key_used: string;
  encryption_type_used: number;
  special_page_type: number;
}

export interface IPageServiceMeta {
  by_name_url: string;
  id: number;
  name: string;
  description: string;
  url: string;
  uuid: string;
  access_rights: string;
  range_access: string;
  action_definitions: string;
  background_color: string;
  content: IPageContent;
  content_modified_by: any;
  content_modified_timestamp: Date;
  created_by: any;
  created_timestamp: Date;
  domain_id: number;
  domain_name: string;
  domain_url: string;
  encrypted_content: string;
  encryption_key_to_use: string;
  encryption_key_used: string;
  encryption_type_to_use: number;
  encryption_type_used: number;
  is_obscured_public: boolean;
  is_public: boolean;
  is_template: boolean;
  modified_by: any;
  modified_timestamp: Date;
  pull_interval: number;
  push_interval: number;
  record_history: boolean;
  seq_no: number;
  show_gridlines: boolean;
  special_page_type: number;
  ws_enabled: boolean;
  symphony_sid: string;
  page_schema: any;
}

export interface IPage extends IPageServiceMeta {
  [key: string]: any;
}

// @todo This should be in some domain/folder service
export interface IUserPageDomainCurrentUserAccess {
  default_page_id: number;
  default_page_url: string;
  domain_id: number;
  domain_url: string;
  is_active: boolean;
  is_administrator: boolean;
  is_default_domain: boolean;
  is_pending: boolean;
  page_count: number;
  user_id: number;
  user_url: string;
  access_level: string;
}

// @todo This should be probably in some domain/folder service
export interface IUserPageDomainAccess {
  alerts_enabled: boolean;
  by_name_url: string;
  current_user_domain_access: IUserPageDomainCurrentUserAccess;
  description: string;
  display_name: string;
  domain_type: number;
  encryption_enabled: boolean;
  id: number;
  is_page_access_mode_selectable: boolean;
  is_paying_customer: boolean;
  login_screen_background_color: '';
  logo_url: string;
  name: string;
  page_access_mode: number;
  page_access_url: string;
  url: string;
  default_symphony_sid: string;
}

export interface IUserPageAccess {
  by_name_url: string;
  content_by_name_url: string;
  content_url: string;
  domain: IUserPageDomainAccess;
  domain_id: number;
  domain_name: string;
  domain_url: string;
  encryption_to_use: number;
  encryption_key_to_use: string;
  id: number;
  is_administrator: boolean;
  is_public: boolean;
  is_users_default_page: boolean;
  name: string;
  pull_interval: number;
  push_interval: number;
  special_page_type: number;
  url: string;
  write_access: boolean;
  ws_enabled: boolean;
}

export interface IPageTemplate {
  by_name_url: string;
  category: string;
  description: string;
  domain_id: number;
  domain_name: string;
  id: number;
  name: string;
  special_page_type: number;
  url: string;
  uuid: string;
}

export interface IPageCloneOptions {
  clone_ranges?: boolean;
}

export interface IPageCopyOptions {
  content?: boolean;
  access_rights?: boolean;
  settings?: boolean;
}

export interface IPageMain extends IEmitter {
  TYPE_REGULAR: number;
  TYPE_ALERT: number;
  TYPE_PDF: number;
  TYPE_STRUCTURED: number;
  TYPE_PAGE_ACCESS_REPORT: number;
  TYPE_DOMAIN_USAGE_REPORT: number;
  TYPE_GLOBAL_USAGE_REPORT: number;
  TYPE_PAGE_UPDATE_REPORT: number;
  TYPE_LIVE_USAGE_REPORT: number;

  EVENT_READY: string;
  EVENT_NEW_CONTENT: string;
  EVENT_NEW_CONTENT_DELTA: string;
  EVENT_NEW_META: string;
  EVENT_RANGES_UPDATED: string;
  EVENT_COLUMN_DEFS_UPDATED: string;
  EVENT_ACCESS_UPDATED: string;
  EVENT_DECRYPTED: string;
  EVENT_ERROR: string;
  EVENT_RESET: string;

  ready: boolean;
  decrypted: boolean;
  updatesOn: boolean;
  types: IPageTypes;

  encryptionKeyPull: IEncryptionKey;
  encryptionKeyPush: IEncryptionKey;

  data: IPage;
  access: IUserPageAccess;
  Content: IPageContentProvider;
  Ranges: IPageRangesCollection;

  start: () => void;
  stop: () => void;
  push: (forceFull?: boolean) => Promise<any>;
  saveMeta: (data: any) => Promise<any>;
  destroy: () => void;
  reset: () => void;
  decrypt: (key: IEncryptionKey) => void;
  clone: (
    folderId: number,
    name: string,
    options?: IPageCloneOptions
  ) => Promise<IPageMain>;
  copy: (
    folderId: number,
    name: string,
    options?: IPageCopyOptions
  ) => Promise<IPageMain>;
  canEdit: () => boolean;
  canEditSettings(): boolean;
  getColumnNames(): IPageColumns[];
  getColumnDefs(): IPageColumnDefs[];
  getUniqueValuesForColumn(
    index: number,
    type: string,
    headersRows?: number
  ): any[];
  reloadActions(): void;
}

export interface IPageService {
  new (
    pageId: number | string,
    folderId: number | string,
    uuid?: string
  ): IPageMain;
  create(
    folderId: number,
    name: string,
    type?: number,
    template?: IPageTemplate,
    organization_public?: boolean
  ): Promise<IPageMain>;
}
function staticImplements<T>() {
  return <U extends T>(constructor: U) => {
    constructor;
  };
}

// Main/public page service
let api: IApiService,
  auth: IAuthService,
  storage: IStorageService,
  crypto: ICryptoService,
  config: IIPPConfig,
  utils: IUtils,
  keys: IKeysService,
  providers: any,
  ranges: any;

interface IPageWrap {
  Page: Page;
}

export class PageWrap {
  constructor(
    // q: IQService,
    // timeout: ITimeoutService,
    // interval: IIntervalService,
    ippApi: IApiService,
    ippAuth: IAuthService,
    ippStorage: IStorageService,
    ippKeys: IKeysService,
    ippCrypto: ICryptoService,
    ippConf: IIPPConfig
  ) {
    api = ippApi;
    auth = ippAuth;
    storage = ippStorage;
    keys = ippKeys;
    crypto = ippCrypto;
    config = ippConf;
    utils = new Utils();
    providers = new Providers(api, auth, storage, config);
    ranges = new RangesWrap(api);

    // PageWrap.Page = Page;
  }
}

// export default PageWrap;
@staticImplements<IPageService>()
export class Page extends EventEmitter {
  public get TYPE_REGULAR(): number {
    return 0;
  }
  public get TYPE_ALERT(): number {
    return 5;
  }
  public get TYPE_PDF(): number {
    return 6;
  }
  public get TYPE_STRUCTURED(): number {
    return 8;
  }
  public get TYPE_PAGE_ACCESS_REPORT(): number {
    return 1001;
  }
  public get TYPE_DOMAIN_USAGE_REPORT(): number {
    return 1002;
  }
  public get TYPE_GLOBAL_USAGE_REPORT(): number {
    return 1003;
  }
  public get TYPE_PAGE_UPDATE_REPORT(): number {
    return 1004;
  }
  public get TYPE_LIVE_USAGE_REPORT(): number {
    return 1007;
  }

  public get EVENT_READY(): string {
    return 'ready';
  }
  public get EVENT_DECRYPTED(): string {
    return 'decrypted';
  }
  public get EVENT_NEW_CONTENT(): string {
    return 'new_content';
  }
  public get EVENT_NEW_CONTENT_DELTA(): string {
    return 'new_content_delta';
  }
  public get EVENT_EMPTY_UPDATE(): string {
    return 'empty_update';
  }
  public get EVENT_NEW_META(): string {
    return 'new_meta';
  }
  public get EVENT_RANGES_UPDATED(): string {
    return 'ranges_updated';
  }
  public get EVENT_COLUMN_DEFS_UPDATED(): string {
    return 'column_defs_updated';
  }
  public get EVENT_ACCESS_UPDATED(): string {
    return 'access_updated';
  }
  public get EVENT_ERROR(): string {
    return 'error';
  }
  public get EVENT_RESET(): string {
    return 'reset';
  }

  /**
   * Indicates when page is ready (both content and settings/meta are loaded)
   * @type {boolean}
   */
  public ready: boolean = false;

  /**
   * Indicates if page is decrypted.
   * @type {boolean}
   */
  public decrypted: boolean = true;

  /**
   * Indicates if page updates are on - page is requesting/receiving new updates
   * @type {boolean}
   */
  public updatesOn: boolean = true; // @todo I dont like this...

  /**
   * Mapping list of page types label - id
   * @type {IPageTypes}
   */
  public types!: IPageTypes;

  /**
   * Class for page range manipulation
   * @type {IPageRangesCollection}
   */
  public Ranges!: IPageRangesCollection;

  /**
   * Class for page actions manipulation
   * @type {IActions}
   */
  public Actions!: IActions;
  public Functions!: IFunctions;

  /**
   * Page content provider class
   * @type {IPageContentProvider}
   */
  public Content!: IPageContentProvider;

  // public freeze: IPageFreezeRange = {
  //     valid: false,
  //     row: -1,
  //     col: -1
  // };

  /**
   * Indicates if client supports websockets
   * @type {boolean}
   * @private
   */
  private _supportsWS: boolean = true; // let's be optimistic by default
  private _wsDisabled: boolean = false;
  private _checkAccess: boolean = true;

  /**
   * Object that holds page data provider
   * @type {IPageProvider}
   */
  private _provider!: IPageProvider;

  /**
   * Object that holds the setInterval object for requesting page access data
   * @type {number}
   */
  private _accessInterval!: Promise<any>;

  /**
   * Holds page content and page meta together
   * @type {IPage}
   */
  private _data!: IPage;

  /**
   * Holds page access values
   * @type {IUserPageAccess}
   */
  private _access!: IUserPageAccess;

  private _pageId!: number;
  private _folderId!: number;
  private _pageName!: string;
  private _folderName!: string;
  private _uuid!: string;

  // Ouch... but what else can I do....
  private _contentLoaded: boolean = false;
  private _metaLoaded: boolean = false;
  private _accessLoaded: boolean = false;
  private _hasAccess: boolean = false;
  private _error: boolean = false;
  private _destroyed: boolean = false;

  private _encryptionKeyPull: IEncryptionKey = {
    name: '',
    passphrase: ''
  };
  private _encryptionKeyPush: IEncryptionKey = {
    name: '',
    passphrase: ''
  };

  // private _providers: any;

  /**
   * Creates new page in the system
   *
   * @param folderId
   * @param name
   * @param type
   * @param template
   * @returns {Promise<IPageMain>}
   */
  public static create(
    folderId: number,
    name: string,
    type: number = 0,
    template?: IPageTemplate,
    organization_public: boolean = false
  ): Promise<IPageMain> {
    let p: Promise<IPageMain> = new Promise((resolve, reject) => {
      if (template) {
        let page: IPageMain = new Page(template.id, template.domain_id);
        page.on(page.EVENT_READY, () => {
          page
            .clone(folderId, name)
            .then(resolve, reject)
            .finally(() => {
              page.destroy();
            });
        });
      } else {
        api
          .createPage({
            domainId: folderId,
            data: {
              name: name,
              special_page_type: type,
              organization_public: organization_public
            }
          })
          .then(
            res => {
              // Start new page
              // @todo Why ?
              let page: IPageMain = new Page(res.data.id, folderId);
              page.on(page.EVENT_READY, () => {
                page.stop();
                resolve(page);
              });
              page.on(page.EVENT_ERROR, (err: any) => {
                page.stop();
                reject(err);
              });
            },
            err => {
              reject(err);
            }
          );
      }
    });
    return p;
  }

  /**
   * Starts new page object
   *
   * @param pageId
   * @param folderId
   */
  constructor(
    pageId: number | string,
    folderId: number | string,
    uuid?: string
  ) {
    super();

    // this._providers = providers;

    // Types
    this.types = {
      regular: this.TYPE_REGULAR,
      pageAccessReport: this.TYPE_PAGE_ACCESS_REPORT,
      domainUsageReport: this.TYPE_DOMAIN_USAGE_REPORT,
      globalUsageReport: this.TYPE_GLOBAL_USAGE_REPORT,
      pageUpdateReport: this.TYPE_PAGE_UPDATE_REPORT,
      alert: this.TYPE_ALERT,
      pdf: this.TYPE_PDF,
      liveUsage: this.TYPE_LIVE_USAGE_REPORT
    };

    // Decide if client can use websockets
    this._supportsWS = 'WebSocket' in window || 'MozWebSocket' in window;

    // Process page and folder id/name
    this._folderId = !isNaN(+folderId) ? <number>folderId : 0;
    this._pageId = !isNaN(+pageId) ? <number>pageId : 0;
    this._folderName = isNaN(+folderId) ? <string>folderId : '';
    this._pageName = isNaN(+pageId) ? <string>pageId : '';
    this._uuid = uuid || '';

    if (!this._pageId && !this._uuid) {
      // If we get folder name and page name, first get page id from REST and then continue with sockets - fiddly, but only way around it at the moment
      this.getPageId(this._folderName, this._pageName).then(
        (res: any) => {
          if (!res.pageId) {
            this.onPageError({
              code: 404,
              message: 'Page not found'
            });
            return;
          }
          this._pageId = res.pageId;
          this._folderId = res.folderId;
          this.init();
        },
        err => {
          this.onPageError(
            err || {
              code: 404,
              message: 'Page not found'
            }
          );
        }
      );
    } else {
      this.init(this._uuid ? true : false);
    }
  }

  /**
   * Setter for pull encryption key
   * @param key
   */
  public set encryptionKeyPull(key: IEncryptionKey) {
    this._encryptionKeyPull = key;
  }

  /**
   * Setter for push encryption key
   * @param key
   */
  public set encryptionKeyPush(key: IEncryptionKey) {
    this._encryptionKeyPush = key;
  }

  /**
   * Getter for page data
   * @returns {IPage}
   */
  public get data(): IPage {
    return this._data;
  }

  /**
   * Getter for page access
   * @returns {IUserPageAccess}
   */
  public get access(): IUserPageAccess {
    return this._access;
  }

  public get error(): boolean {
    return this._error;
  }

  /**
   * Start page updates
   */
  public start(): void {
    if (!this.updatesOn) {
      this._provider.start();
      this.updatesOn = true;
    }
  }

  /**
   * Stop page updates
   */
  public stop(): void {
    if (this.updatesOn) {
      this._provider.stop();
      this.updatesOn = false;
    }
  }

  /**
   * Push new data to a page. This method accepts either full page content or delta content update
   * @param forceFull
   * @returns {Promise<any>}
   */
  public push(
    forceFull: boolean = false,
    otherRequestData: any = {}
  ): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      // let currentData: any = _.clone(this.Content ? this.Content.original : undefined);

      // check if we have content
      if (!this.data.content.length || !this.data.content[0].length) {
        forceFull = true;
      }
      // this.Content.dirty = false;
      let deltas: IPageContent = this.Content.deltas.concat();
      // let deltaUpdate: IPageDelta = <IPageDelta>(
      //   this.Content.getCellDeltas(
      //     this.data.special_page_type === this.TYPE_STRUCTURED
      //   )
      // );

      let onSuccess: any = (data: any) => {
        this.Content.dirty = false;
        // this.Content.cleanDirty();
        // this.Content.update(this.Content.current, !this.Content.dirty); // @todo Ouch!

        this._data.content_modified_timestamp = new Date();
        this._data.content_modified_by = auth.user;

        // @todo this._data.content has old value
        data = { ...this._data, ...data.data };

        if (this._provider instanceof ProviderREST) {
          this._provider.seqNo = data.seq_no;
        }

        // let diff: IUtilsDiff | false = currentData
        //   ? utils.comparePageContent(currentData, this.Content.current, true)
        //   : false;
        // if (diff) {
        //   data.diff = diff;
        // } else {
        //   data.diff = undefined;
        // }
        data._provider = false;
        if (deltas.length) {
          data.diff = {
            content_diff: deltas,
            content: this.Content.current,
            colDiff: 0,
            rowDiff: 0,
            sizeDiff: 0
          };
        } else {
          data.diff = undefined;
        }

        // if (delta) {
        if (deltas.length && !forceFull) {
          this.Content.updateDelta(deltas);
          this.Functions.updateContentDelta(deltas);
          this.emit(this.EVENT_NEW_CONTENT_DELTA, data);
        }
        if (this._provider instanceof ProviderREST) {
          this._provider.requestOngoing = false;
        }
        // } else {
        // this.emit(this.EVENT_NEW_CONTENT, data);
        // }

        // this.start();

        resolve(data);
      };

      // structure pages
      if (this.data.special_page_type === this.TYPE_STRUCTURED) {
        let query: IPageQuery = this.Content.getQuery();
        if (query.error) {
          reject({
            code: 400,
            message: query.error
          });
          return;
        }
        api.queryPageContent({pageId: this.data.id, data: query}).then(onSuccess).catch(reject);
        return;
      }
      
      let deltaUpdate: IPageDelta = <IPageDelta>(
        this.Content.getCellDeltas()
      );

      if (
        !this._data.encryption_type_to_use &&
        !this._data.encryption_type_used &&
        this.Content.canDoDelta &&
        !forceFull
      ) {
        if (this._provider instanceof ProviderREST) {
          this._provider.requestOngoing = true;
        }
        this.pushDelta(deltaUpdate, otherRequestData)
          .then(onSuccess)
          .catch(reject);
      } else {
        this.pushFull(
          <IPageContent>(
            this.Content.getFull()
          ),
          otherRequestData
        )
          .then(onSuccess)
          .catch(reject);
      }
    });
    return p;
  }

  /**
   * Save page settings/meta
   * @param data
   * @returns {Promise<any>}
   */
  public saveMeta(data: any): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      // Remove access rights (if any)
      // delete data.access_rights;

      // Just a small condition - this seems to be left behind quite often
      if (data.encryption_type_to_use === 0) {
        data.encryption_key_to_use = '';
      }

      // @todo Validation
      api
        .savePageSettings({
          domainId: this._folderId,
          pageId: this._pageId,
          data: data
        })
        .then(
          res => {
            // Apply data to current object
            this._data = { ...this._data, ...res.data };
            this.emit(this.EVENT_NEW_META, res.data); // @todo I am not sure about this...
            resolve(res);
          },
          err => {
            reject(utils.parseApiError(err, 'Could not save page settings'));
          }
        );
    });
    return p;
  }

  /**
   * Sets current page as folders default for current user
   * @returns {Promise<IRequestResult>}
   */
  public setAsFoldersDefault(): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      let requestData: any = {
        domainId: this._folderId,
        data: {
          default_page_id: this._pageId
        }
      };

      api.setDomainDefault(requestData).then(res => {
        // @todo probably not a best way to do that, but is there any other option?
        this._access.is_users_default_page = true;
        resolve(res);
      }, reject);
    });
    return p;
  }

  /**
   * Deletes current page
   * @returns {Promise<IRequestResult>}
   */
  public del(): Promise<any> {
    let requestData: any = {
      domainId: this._folderId,
      pageId: this._pageId
    };

    return api.deletePage(requestData);
  }

  /**
   * Check if page is encrypted and decrypt it if it is
   * @param key
   */
  // @todo This is NOT good
  public decrypt(key?: IEncryptionKey): void {
    // @todo Oh lord...
    if (!key) {
      key = this._encryptionKeyPull.name
        ? this._encryptionKeyPull
        : keys.getKey(
            this.access ? this.access.domain.name : '',
            this.data.encryption_key_used
          );
    }

    // Fail silently if we dont have passphrase
    // @todo oh sweet jesus...
    if (this._data.encryption_type_used && !key.passphrase) {
      this.decrypted = false;
      return;
    }

    // Check for encryption and decrypt
    if (this._data.encryption_type_used) {
      if (!crypto) {
        this.emit(this.EVENT_ERROR, new Error(`Encrypted pages not supported`));
        this.decrypted = false;
        return;
      }
      let decrypted: any = crypto.decryptContent(
        {
          name: key.name,
          passphrase: key.passphrase
        },
        this._data.encrypted_content
      );

      if (decrypted) {
        this.decrypted = true;
        this._data.content = decrypted;
        this._encryptionKeyPull = key;
        this._encryptionKeyPush = key;
        keys.saveKey(this.access.domain.name, key);
      } else {
        this.decrypted = false;
        // @todo I am pretty sure we will want something more specific for decryption than just message
        this.emit(
          this.EVENT_ERROR,
          new Error(
            `Could not decrypt page with key "${key.name}" and passphrase "${key.passphrase}"`
          )
        );
      }
    } else {
      this.decrypted = true;
    }

    // @todo ouch... should not be here
    if (this.decrypted) {
      this._error = false;
      if (this.Content) {
        this.Content.update(this._data.content, false, false);
        this.Functions.content = this.Content.current;
        // this.Actions.parse(this._data.action_definitions);
      } else {
        this.Content = new PageContent(this._data.content);
        this.Functions = new Functions(this.Content.current, {
          id: auth.user.id,
          username: auth.user.screen_name,
          lastname: auth.user.last_name,
          firstname: auth.user.first_name,
          email: auth.user.email,
          pageid: `${this.data.id}`,
          pagename: this.data.name,
          pagedescription: this.data.description,
          folderid: `${this.data.domain_id}`,
          foldername: this.data.domain_name,
          folderdescription: this.access.domain.description
        });
        if (this.Actions) {
          this.Actions.Functions = this.Functions;
          this.Actions.Content = this.Content;
        }
        // this.Actions = new Actions(api, this.Content, this.Functions, this._folderId, this._pageId);
        // this.Actions.on(this.Actions.EVENT_UPDATED, () => {
        //   console.log('this.Actions.EVENT_UPDATED', 'decrypt');
        //   this.emit(this.EVENT_RANGES_UPDATED);
        // });
        // this.Actions.parse(this._data.action_definitions);
      }
      this.Content.structured = this.data.special_page_type === this.TYPE_STRUCTURED;

      // @todo Emitting Decrypted and New content events will lead to confusion. Eventually you will want to subscribe to both for rendering, so you will have double rendering
      this.emit(this.EVENT_DECRYPTED);
    }
  }

  /**
   * Destroy page object
   */
  public destroy(): void {
    if (this._provider) {
      this._provider.destroy();
    }

    // $interval.cancel(this._accessInterval);
    this.removeEvent();
    this._checkAccess = false;
    this.ready = false;
    this._destroyed = true;
  }

  public reset(): void {
    this.Content.reset();
    this.Functions.content = this.Content.current;
    this.emit(this.EVENT_RESET);
  }

  /**
   * Clone current page. Clones page content and some settings, can specify more options via options param.
   * @param folderId
   * @param name
   * @param options
   * @deprecated
   * @returns {Promise<IPageMain>}
   */
  public clone(
    folderId: number,
    name: string,
    options: IPageCloneOptions = {}
  ): Promise<IPageMain> {
    let p: Promise<IPageMain> = new Promise((resolve, reject) => {
      if (!this.ready) {
        reject('Page is not ready');
        return;
      }

      // Prevent cloning ranges between folders
      // @todo This is done silently at the moment, should it reject the transaction?
      if (options.clone_ranges && this._folderId * 1 !== folderId * 1) {
        options.clone_ranges = false;
      }

      // Create new page
      Page.create(folderId, name, this._data.special_page_type).then(
        (newPage: IPageMain) => {
          newPage.Content = this.Content;
          Promise.all([newPage.push(true)]).then(res => {
            if (options.clone_ranges) {
              api
                .savePageSettings({
                  domainId: folderId,
                  pageId: newPage.data.id,
                  data: {
                    access_rights: this._data.access_rights,
                    action_definitions: this._data.action_definitions
                  }
                })
                .finally(() => {
                  resolve(newPage);
                });
            } else {
              resolve(newPage);
            }
          }, reject); // @todo Handle properly
        },
        err => {
          reject(err);
        }
      );
    });
    return p;
  }

  /**
   * Clone current page. Clones page content and some settings, can specify more options via options param.
   * @param folderId
   * @param name
   * @param copyContent
   * @param data
   * @returns {Promise<IPageMain>}
   */
  public copy(
    folderId: number,
    name: string,
    options: IPageCopyOptions = {}
  ): Promise<IPageMain> {
    let p: Promise<IPageMain> = new Promise((resolve, reject) => {
      if (!this.ready) {
        reject('Page is not ready');
        return;
      }

      let data: any = {
        range_access: true,
        action_definitions: true,
        description: true,
        push_interval: true,
        pull_interval: true,
        is_public: true,
        encryption_type_to_use: true,
        encryption_key_to_use: true,
        background_color: true,
        is_obscured_public: true,
        record_history: true,
        symphony_sid: true
      };

      // Prevent cloning ranges between folders
      // @todo This is done silently at the moment, should it reject the transaction?
      if (
        !options.access_rights ||
        (options.access_rights && this._folderId * 1 !== folderId * 1)
      ) {
        data.range_access = false;
        data.action_definitions = false;
      }

      // Gather settings data
      let settingsData: any = {};
      for (let key in data) {
        if (!data[key] || !this._data[key]) {
          continue;
        }
        settingsData[key] = this._data[key];
      }

      // Create new page
      Page.create(folderId, name, this._data.special_page_type).then(
        (newPage: IPageMain) => {
          if (options.content && Object.keys(settingsData).length > 0) {
            newPage.Content = this.Content;
            newPage.push(true).then(() => {
              api
                .savePageSettings({
                  domainId: folderId,
                  pageId: newPage.data.id,
                  data: settingsData
                })
                .then(() => {
                  resolve(newPage);
                }, reject);
            }, reject);
          } else if (options.content) {
            newPage.Content = this.Content;
            newPage.push(true).then(() => {
              resolve(newPage);
            }, reject);
          } else if (Object.keys(settingsData).length > 0) {
            api
              .savePageSettings({
                domainId: folderId,
                pageId: newPage.data.id,
                data: settingsData
              })
              .then(() => {
                resolve(newPage);
              }, reject);
          } else {
            resolve(newPage);
          }
        },
        err => {
          reject(err);
        }
      );
    });
    return p;
  }

  public canEdit(): boolean {
    if (this._error) return false;
    if (this.data.organization_public === 'rw') return true;
    if (this.access && this.access.write_access) return true;
    // if (this.access && this.access.domain && !this.access.domain.current_user_domain_access) return false;
    // if (this.data.organization_public !== 'rw') return false;
    // if (this.access && this.access.domain && this.access.domain.current_user_domain_access.access_level !== 'RW') return false;
    return false;
  }

  public canEditSettings(): boolean {
    if (this._error) return false;
    return this._hasAccess;
  }

  public getColumnNames(): IPageColumns[] {
    let names: IPageColumns[] = [];
    if (!this.Content) return names;
    for (let i = 0; i < this.Content.current[0].length; i++) {
      names.push({
        index: i,
        column: helpers.toColumnName(i + 1),
        value: `${this.Content.current[0][i].formatted_value ||
          this.Content.current[0][i].value}`
      });
    }
    return names;
  }

  public getUniqueValuesForColumn(
    colIndex: number,
    type: string = 'string',
    headersRows: number = 0
  ): any[] {
    let values: any[] = [];
    if (!this.Content) return values;
    for (
      let rowIndex = headersRows;
      rowIndex < this.Content.current.length;
      rowIndex++
    ) {
      let value: any =
        type === 'string'
          ? `${this.Content.current[rowIndex][colIndex].formatted_value ||
              this.Content.current[rowIndex][colIndex].value}`
          : parseFloat(`${this.Content.current[rowIndex][colIndex].value}`);
      if (type === 'number' && isNaN(value)) {
        value = '';
      }
      if (values.indexOf(value) < 0) {
        values.push(value);
      }
    }
    return values.sort();
  }

  public getColumnDefs(): IPageColumnDefs[] {
    if (
      this.data &&
      this.data.page_schema &&
      this.data.page_schema.schema &&
      this.data.page_schema.schema.columns
    )
      return this.data.page_schema.schema.columns || [];
    return [];
  }

  public reloadActions(): void {
    this.Actions.parse(this.data.action_definitions);
  }

  private createProvider(ignoreWS?: boolean): void {
    if (this._provider) {
      this._provider.destroy();
    }
    this._wsDisabled =
      ignoreWS ||
      !this._supportsWS ||
      typeof io === 'undefined' ||
      config.transport === 'polling' ||
      !io.connect ||
      !this.access ||
      !this.access.ws_enabled;
    this._provider = this._wsDisabled
      ? new ProviderREST(this._pageId, this._folderId, this._uuid)
      : new ProviderSocket(this._pageId, this._folderId);
    this._checkAccess = true;
  }

  /**
   * Actual bootstrap of the page, starts page updates, registeres provider, starts page access updates
   */
  private init(ignoreWS?: boolean): void {
    if (!this._supportsWS || typeof io === 'undefined') {
      console.warn(
        '[iPushPull] Cannot use websocket technology as it is not supported or websocket library is not included'
      );
    }

    if (this._uuid) {
      this.createProvider(ignoreWS);
      this._accessLoaded = true;
      this.registerListeners();
      return;
    }

    // Start pulling page access
    this.getPageAccess().then(() => {
      if (this._destroyed) return;
      this.createProvider(ignoreWS);
      this.registerListeners();
      this.checkPageAccess();
    });

    // Create Ranges object
    // this.Ranges = new Ranges(this._folderId, this._pageId);
    // this.Ranges.on(this.Ranges.EVENT_UPDATED, () => {
    //   console.log('this.Ranges.EVENT_UPDATED', 'init');
    //   this.emit(this.EVENT_RANGES_UPDATED);
    // });

    // Create Actions object

    // this.setFreezeRange();
  }

  // private setFreezeRange(): void {
  //     this.Ranges.ranges.forEach(range => {
  //         if (range.name === "frozen_rows") {
  //             this.freeze.row = range.count;
  //         } else if (range.name === "frozen_cols") {
  //             this.freeze.col = range.count;
  //         }
  //     });
  //     this.freeze.valid = this.freeze.row > 0 || this.freeze.col > 0;
  // }

  private checkPageAccess(): void {
    if (!this._checkAccess) {
      return;
    }
    this.getPageAccess().finally(() => {
      setTimeout(() => {
        this.checkPageAccess();
      }, 30000);
    });
  }

  /**
   * In case page is requested with name, get page ID from service
   * @param folderName
   * @param pageName
   * @returns {Promise<any>}
   */
  private getPageId(folderName: string, pageName: string): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      // @todo Need specific/lightweight endpoint - before arguing my way through this, I can use page detail (or write my own)

      api.getPageByName({ domainId: folderName, pageId: pageName }).then(
        res => {
          resolve({
            pageId: res.data.id,
            folderId: res.data.domain_id,
            wsEnabled: res.data.ws_enabled
          });
        },
        err => {
          // Convert it into socket error
          reject(err);
        }
      );
    });
    return p;
  }

  /**
   * Load page access
   * @returns {Promise<any>}
   */
  private getPageAccess(): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      api
        .getPageAccess({
          domainId: this._folderId,
          pageId: this._pageId
        })
        .then((res: any) => {
          if (res.httpCode && res.httpCode >= 300) {
            reject({
              code: res.httpCode,
              message: res.data ? res.data.detail : 'Unauthorized access'
            });
            throw 'return';
          }

          // auth recovery
          if (this._error) {
            this._error = false;
            this.emit(this.EVENT_READY, true);
            resolve();
            throw 'return';
          }

          const prevAccess = JSON.stringify(this._access);
          const newAccess = JSON.stringify(res.data);
          this._access = res.data;
          this._accessLoaded = true;
          this.checkReady(); // @todo ouch...

          if (prevAccess !== undefined && prevAccess !== newAccess) {
            this.emit(this.EVENT_ACCESS_UPDATED, {
              before: prevAccess,
              after: newAccess
            });
            this.Functions.updateVars({
              id: auth.user.id,
              username: auth.user.screen_name,
              lastname: auth.user.last_name,
              firstname: auth.user.first_name,
              email: auth.user.email,
              pageid: `${this.data.id}`,
              pagename: this.data.name,
              pagedescription: this.data.description,
              folderid: `${this.data.domain_id}`,
              foldername: this.data.domain_name,
              folderdescription: this.access.domain.description
            });
          }

          // resolve();
          api
            .getPageById({
              domainId: this._folderId,
              pageId: this._pageId
            })
            .then((res: any) => {
              this._hasAccess = true;
              this.applyMetaUpdate(res.data);
              resolve();
            })
            .catch(() => {
              this._hasAccess = false;
              // this.applyMetaUpdate(res.data);
              resolve();
            });
        })
        .catch((err: any) => {
          if (err === 'return') return;
          this.onPageError(err);
          reject();
        });
    });
    return p;
  }

  /**
   * Register listeners. THese are listeners on events emitted from page providers, which are processed and then re-emitted to public
   */
  private registerListeners(): void {
    // Setup listeners
    this._provider.on('content_update', (data: IPageServiceContent) => {
      data.special_page_type = this.updatePageType(data.special_page_type);

      let currentData: any = _.clone(this.Content ? this.Content.current : 0);

      this._data = { ...this._data, ...data };
      this.decrypt();
      this._contentLoaded = true;

      // emit page if ready
      this.checkReady();

      // @todo This should be emitted before decryption probably
      let diff: any = currentData
        ? utils.comparePageContent(currentData, this.Content.current)
        : false;
      if (diff) {
        this._data.diff = diff;
      } else {
        this._data.diff = undefined;
      }
      this._data._provider = true;
      this.emit(this.EVENT_NEW_CONTENT, this._data);
    });

    this._provider.on('meta_update', (data: IPage) => {
      // if (this._metaLoaded) return;s
      this.applyMetaUpdate(data);
      // Check if this page should be handled by websockets and switch
      if (data.ws_enabled && !(this._provider instanceof ProviderSocket)) {
        if (this._supportsWS && !this._wsDisabled) {
          this._provider.destroy();
          this._provider = new ProviderSocket(this._pageId, this._folderId);
          this.registerListeners();
        }
      }
    });

    this._provider.on('error', this.onPageError);

    this._provider.on('empty_update', () => {
      this._error = false;
      this.emit(this.EVENT_EMPTY_UPDATE, this._data);
    });

    auth.on(auth.EVENT_LOGGED_IN, () => {
      console.warn('EVENT_LOGGED_IN');
      this.getPageAccess();
    });
    auth.on(auth.EVENT_LOGGED_OUT, () => {
      console.warn('EVENT_LOGGED_OUT');
      this.getPageAccess();
    });
  }

  private onPageError: any = (err: any) => {
    if (!err) {
      return;
    }

    err.code = err.httpCode || err.code;
    err.message =
      err.httpText ||
      err.message ||
      (typeof err.data === 'string' ? err.data : 'Page not found');

    this.emit(this.EVENT_ERROR, err);

    if (err.code === 404) {
      this.destroy();
    }

    // fallback to REST
    if (err.type === 'redirect') {
      this._wsDisabled = true;
      this.init(true);
    } else {
      this._error = true;
    }
  };

  /**
   * Push full content update
   * @param content
   * @returns {Promise<any>}
   */
  private pushFull(
    content: IPageContent,
    otherRequestData: any = {}
  ): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      // If encrypted
      if (this._data.encryption_type_to_use) {
        if (
          !this._encryptionKeyPush ||
          this._data.encryption_key_to_use !== this._encryptionKeyPush.name
        ) {
          // @todo Proper error
          reject('None or wrong encryption key');
          return;
        }

        let encrypted: string | null = this.encrypt(
          this._encryptionKeyPush,
          content
        );

        if (encrypted) {
          this._data.encrypted_content = encrypted;
          this._data.encryption_type_used = 1;
          this._data.encryption_key_used = this._encryptionKeyPush.name;

          // Ehm...
          this._encryptionKeyPull = _.clone(this._encryptionKeyPush);
        } else {
          // @todo proper error
          reject('Encryption failed');
          return;
        }
      } else {
        // @todo: webservice should do this automatically
        this._data.encryption_key_used = '';
        this._data.encryption_type_used = 0;
        this._data.content = content;
      }

      let data: any = {
        content: !this._data.encryption_type_used ? this._data.content : '',
        encrypted_content: this._data.encrypted_content,
        encryption_type_used: this._data.encryption_type_used,
        encryption_key_used: this._data.encryption_key_used
      };
      data = { ...data, ...otherRequestData };

      let requestData: any = {
        domainId: this._folderId,
        pageId: this._pageId,
        data: data
      };

      api.savePageContent(requestData).then((res: any) => {
        // @todo Do we need this? should be probably updated in rest - if not updated rest will load update even though it already has it
        this._data.seq_no = res.data.seq_no;
        resolve(res);
      }, reject);
    });
    return p;
  }

  /**
   * Push delta content update
   * @param data
   * @returns {Promise<any>}
   */
  private pushDelta(
    data: IPageDelta,
    otherRequestData: any = {}
  ): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      // @todo Handle empty data/delta
      data = { ...data, ...otherRequestData };
      let requestData: any = {
        domainId: this._folderId,
        pageId: this._pageId,
        data: data
      };

      api
        .savePageContentDelta(requestData)
        .then(resolve)
        .catch(reject);
    });
    return p;
  }

  /**
   * Check if page is considered to be ready
   */
  // @todo Not a great logic - When do we consider for a page to actually be ready?
  private checkReady(): void {
    if (
      this._contentLoaded &&
      this._metaLoaded &&
      this._accessLoaded &&
      !this.ready
    ) {
      this.ready = true;
      this.emit(this.EVENT_READY);
    }
  }

  /**
   * Temporary fix to update page types. This will take any page report types and adds 1000 to them. This way can do easier filtering.
   * @param pageType
   * @returns {number}
   */
  private updatePageType(pageType: number): number {
    if ((pageType > 0 && pageType < 5) || pageType === 7) {
      pageType += 1000;
    }

    return pageType;
  }

  /**
   * Encrypt page content with given key
   * @param key
   * @param content
   * @returns {string}
   */
  private encrypt(key: IEncryptionKey, content: IPageContent): string | null {
    // @todo: Handle encryption error
    return crypto.encryptContent(key, content);
  }

  private applyMetaUpdate(data: IPage): void {
    data.special_page_type = this.updatePageType(data.special_page_type);

    // Remove content fields (should not be here and in the future will not be here)
    delete data.content;
    delete data.encrypted_content;

    // Process ranges

    // Create Ranges object
    if (!this.Ranges) {
      this.Ranges = new Ranges(data.domain_id, data.id);
      this.Ranges.parse(data.range_access || '[]');
      this.Ranges.on(this.Ranges.EVENT_UPDATED, () => {
        console.log('this.Ranges.EVENT_UPDATED');
        this.emit(this.EVENT_RANGES_UPDATED);
      });
      // this.setFreezeRange();
    }
    if (!this.Actions) {
      this.Actions = new Actions(
        api,
        this.Content,
        this.Functions,
        this._folderId,
        this._pageId
      );
      this.Actions.parse(data.action_definitions);
      this.Actions.on(this.Actions.EVENT_UPDATED, () => {
        console.log('this.Actions.EVENT_UPDATED');
        this.emit(this.EVENT_RANGES_UPDATED);
      });
    }
    let rangesUpdates: boolean = false;
    if (
      data.range_access &&
      this._data &&
      this._data.range_access !== data.range_access
    ) {
      this.Ranges.parse(data.range_access || '[]');
      data.access_rights = JSON.stringify(this.Ranges.ranges);
      rangesUpdates = true;
    }
    if (
      data.action_definitions &&
      this._data &&
      this._data.action_definitions !== data.action_definitions
    ) {
      this.Actions.parse(data.action_definitions);
      rangesUpdates = true;
    }
    // has meta data changed

    if (!this._metaLoaded) {
      this._data = data;
      this._metaLoaded = true;
    } else {
      let metaChanged = false;
      let columnDefsChanged = false;
      Object.keys(data).forEach(key => {
        if (['range_access', 'action_definitions'].indexOf(key) > -1) return;
        let changed = false;
        if (this._data[key] === undefined) {
          metaChanged = true;
          return;
        }
        if (data[key] !== null && typeof data[key] === 'object') {
          if (JSON.stringify(data[key]) !== JSON.stringify(this._data[key])) {
            metaChanged = true;
            changed = true;
          }
        } else if (data[key] !== this._data[key]) {
          metaChanged = true;
        }
        if (key === 'page_schema' && changed) {
          columnDefsChanged = true;
        }
      });
      this._data = { ...this._data, ...data };
      this.checkReady();
      if (!this.ready) return;
      if (metaChanged) this.emit(this.EVENT_NEW_META, data);
      if (rangesUpdates) {
        this.emit(this.EVENT_RANGES_UPDATED);
      }
      if (columnDefsChanged) {
        this.emit(this.EVENT_COLUMN_DEFS_UPDATED);
      }
    }

    // Check if this page should be handled by websockets and switch
    // if (data.ws_enabled && !(this._provider instanceof ProviderSocket)) {
    //   if (this._supportsWS && !this._wsDisabled) {
    //     this._provider.destroy();
    //     this._provider = new ProviderSocket(this._pageId, this._folderId);
    //     this.registerListeners();
    //   }
    // }
  }
}
