import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

import {EventEmitter, Handler, Subscription} from './emitter';

export { Handler as EventHandler } from './emitter';

export type ModelOrModelArray = Model | ModelArray<Model>;
export type ObjectId = string;

export interface Deferred<T> {
  promise: Promise<T>;
  resolve: (val?: T) => void;
  reject: (err: Error) => void;
}

export interface ArrayEmitter<T> extends Array<T> {
  $emitter: EventEmitter;
  $subject: BehaviorSubject<ArrayEmitter<T>>;
  on: {(name: string, handler: Handler): void};
  off: {(name: string, handler: Handler): void};
  listen: {(name: string, handler: Handler): Subscription};
  once: {(name: string, handler: Handler): void};
  addListener: {(name: string, handler: Handler): void};
  removeListener: {(name: string, handler: Handler): void};
}

export interface Model {
  _id?: ObjectId;

  $created: boolean;
  $deleted: boolean;
  $promise: Promise<Model>;
  $subject: BehaviorSubject<Model>;
  $resolvedSubject: Subject<Model>;
  $deferred: Deferred<Model>;
  $resolved: boolean;
  $loc: LocalModel;
  $id: string;

  $save: {(): Promise<Model>};
  $remove: {(noPrompt?: boolean, skipLocal?: boolean): Promise<boolean>};
  $delete: {(): Promise<boolean>};
  $type: {(): string};
  $reset: {(): void};
  $refresh: {(): void};
  $toObject: {(): any};
  $updateServer: {(val: any): void};
}

export interface Resource<T> {
  get?: {
    (id: string, force?: boolean): T,
    (ids: string[], force?: boolean): ModelArray<T>,
    (ids: string | string[], force?: boolean): (ModelArray<T> | T),
  };
  query?: {(qry: any, limit?: number): Query<T>};
  chain?: ChainFunction;
  type?: {(): string};
  collect?: {(seeds: Model | ModelArray<T>, relation: CollectionSearcher): Collection<T>};
  updateOrCreate?: {(val: any): T};

  //  Event emitter methods
  dispose: {(): void};
  addListener: {(name: string, handler: Handler): void};
  emit: {(fnName: string, ...data: any[]): void};
  listen: {(name: string, handler: Handler): Subscription};
  off: {(name: string, handler: Handler): void};
  on: {(name: string, handler: Handler): void};
  once: {(name: string, handler: Handler): void};
  removeListener: {(name: string, handler: Handler): void};

  new (val?: any, fromLoc?: boolean, lastRequested?: (number | Date)): T;
}

export interface ResourceFactory<T> {
  (url: string, rootKey: string, rootKeyPlural?: string): Resource<T>;
  Chain?: Chain;
}

export interface LocalModel {
  _id?: ObjectId;

  $emitter: EventEmitter;
  $created: boolean;
  $createdAt: number;
  $deleted: boolean;
  $promise: Promise<LocalModel>;
  $deferred: Deferred<LocalModel>;
  $resolved: boolean;
  $resync: any[];
  $dbsync: Promise<void>;
  $saveprom: Promise<any>;
  $mod: Model;
  $id: string;
  $dbresync: boolean;
  $serv: ServerModel;

  $save: {(vals: any): Promise<LocalModel>};
  $remove: {(skipServer?: boolean): Promise<boolean>};
  $delete: {(): Promise<boolean>};
  $refresh: {(): void};
  $toObject: {(): any};
  $updateServer: {(val: any): void};
}

export interface LocalResource {
  (val: any, fromServ?: boolean, mod?: any, lastRequested?: (Date | number)): void;
  get?: {(ids: (string[] | string), force?: boolean, transform?: Function): (ModelArray<Model> | Model)};
  query?: {(qry: any, limit: number, Resource: any): Query<Model>};
  collect?: {(Resource: Resource<Model>,
              seeds: Model | ModelArray<Model>,
              relation: CollectionSearcher): Collection<Model>};
}

export interface LocalResourceFactory {
  (url: string, rootKey: string, rootKeyPlural: string): LocalResource;
}

export interface ServerModel {
  _id?: ObjectId;

  $id: string;
  $emitter: EventEmitter;
  $deleted: boolean;
  $promise: Promise<ServerModel>;
  $resolved: boolean;
  $fetching: boolean;
  $deferred: Deferred<ServerModel>;

  $save: {(patch: JSONPatch): Promise<ServerModel>};
  $remove: {(): Promise<boolean>};
  $delete: {(): Promise<boolean>};
  $refresh: {(): void};
  $toObject: {(): any};
  $updateServer: {(val: any): void};
  $updateVal: {(val: any): void};
}

export interface ServerResource {
  (val?: any, id?: string): void;
  get?: {(ids: (string[] | string), force?: boolean, transform?: Function): (any[] | any)};
}

export interface ServerResourceFactory {
  (url: string, rootKey: string, rootKeyPlural: string): ServerResource;
}

export interface ModelArray<T> extends Array<T> {
  $promise?: Promise<ModelArray<T>>;
}

export interface Query<T> extends ArrayEmitter<T> {
  $refresh: {(force?: boolean): Promise<Query<T>>};
  $promise: Promise<Query<T>>;
  $subject: BehaviorSubject<Query<T>>;
  $resolvedSubject: Subject<Query<T>>;
  $resolved: boolean;
  $Model: any;
  $serverResults?: Query<T>;
  $skip?: number;
  $hasNext: boolean;
  $hasPrev: boolean;

  extend: {(obj: any, noSanitize?: boolean, retServer?: boolean): Promise<Query<T>>};
  replace: {(qry: any, retServer?: boolean): Promise<Query<T>>};
  get: {(...args: any[]): any};
  next: {(num?: number): Promise<Query<T>>};
  prev: {(num?: number): Promise<Query<T>>};
  chain: {(Model: any, qryFn: QueryFunction): Query<Model>};
  transform: Function;
  currentLimit: number;
  currentSkip: number;
  loading: boolean;
  hasNext: boolean;
  hasPrev: boolean;
}

export interface QueryList {
  (qry: any, limit: number, Resource: any, toRes: {(id: string): Model}): Query<Model>;
  refresh?: {(): any};
}

export interface QueryFactory {
  (url: string, rootKey: string, rootKeyPlural: string, db: Database): QueryList;
}

export type QueryFunction = (this: Query<Model>, results: Model[]) => any;
export type MultiQueryFunction = (this: Query<Model>, results: Model[][]) => any;

export interface ChainFunction {
  (origQry: ChainableQuery<Model>,
   modelOrQryFn: (Resource<Model> | QueryFunction),
   qryFn?: QueryFunction): Query<Model>;
}

export interface Chain extends ChainFunction {
  all?: {(origQueries: Array<ChainableQuery<Model>>, Model: Resource<Model>, qryFn: MultiQueryFunction): Query<Model>};
}

export interface ChainableQuery<T> extends Array<T> {
  $emitter: EventEmitter;
  $promise: Promise<T[]>;
}

export interface Seeds extends Array<Model> {
  on?: {(name: string, handler: Handler): void};
  removeListener?: {(name: string, handler: Handler): void};
}

export interface CollectionFactory<T> {
  (): CollectionStatic<T>;
}

export interface CollectionStatic<T> {
  new (Resource: Resource<T>, seeds: Model | Seeds, searcherOrProp: CollectionSearcher): Collection<T>;
}

export interface Collection<T> extends ArrayEmitter<T> {
  $promise: Promise<Collection<T>>;
  $resolved: boolean;

  chain: {(Model: any, qryFn: QueryFunction): Query<T>};
  check: {(): void};
}

export interface CollectionSearcher {
  (objs: Model[]): Observable<(Model[] | string[])>;
}

export interface SocketFactory {
  (url: string, rootKey: string, rootKeyPlural?: string): Socket;
}

export interface Socket {
  get: {(ids: string[]): Promise<any>};
  query: {(query: any): Promise<any>};
  patch: {(id: string, patch: JSONPatch): Promise<any>};
  create: {(data: any): Promise<any>};
  remove: {(id: string): Promise<any>};
}

export interface DatabaseFactory {
  (): Database;
}

export abstract class Database {
  public abstract update(res: LocalModel): Promise<any>;
  public abstract query(qry: any): Promise<string[]>;
  public abstract qryIsSimple(qry: any): boolean;
}

export type JSONPatch = (OpPatch | OpPatch[]);
export type OpPatch = AddPath | RemovePath | ReplacePath | MovePath | CopyPath | TestPath;
export interface Patch {
    op: string;
}
export interface AddPath extends Patch {
    path: string;
    value: any;
}
export interface RemovePath extends Patch {
    path: string;
}
export interface ReplacePath extends Patch {
    path: string;
    value: any;
}
export interface MovePath extends Patch {
    from: string;
    path: string;
}
export interface CopyPath extends Patch {
    from: string;
    path: string;
}
export interface TestPath extends Patch {
    path: string;
    value: any;
}

export interface LocalForage {
  getItem<T>(key: string): Promise<T>;
  getItem<T>(key: string, callback: (err: any, value: T) => void): void;

  setItem<T>(key: string, value: T): Promise<T>;
  setItem<T>(key: string, value: T, callback: (err: any, value: T) => void): void;

  removeItem(key: string): Promise<void>;
  removeItem(key: string, callback: (err: any) => void): void;

  clear(): Promise<void>;
  clear(callback: (err: any) => void): void;

  length(): Promise<number>;
  length(callback: (err: any, numberOfKeys: number) => void): void;

  key(keyIndex: number): Promise<string>;
  key(keyIndex: number, callback: (err: any, key: string) => void): void;

  keys(): Promise<string[]>;
  keys(callback: (err: any, keys: string[]) => void): void;

  iterate(iteratee: (value: any, key: string, iterationNumber: number) => any): Promise<any>;
  iterate(iteratee: (value: any, key: string, iterationNumber: number) => any,
      callback: (err: any, result: any) => void): void;

  driver(): string;
}

export interface Plugin<T> {
  (Resource: Resource<T>, modelName: string): void;
}
