import { IContact, ContactType, ContactSubscription } from './Contact.interface';
import Storage from './Storage';
import JID from './JID';
import Account from './Account';
import PersistentMap from './util/PersistentMap';
import IIdentifiable from './Identifiable.interface';
import Log from './util/Log';
import { Presence } from './connection/AbstractConnection';
import { EncryptionState } from './plugin/AbstractPlugin';
import Transcript from './Transcript';
import ChatWindowController from './ChatWindowController';
import Avatar from './Avatar';
import Message from './Message';
import ChatWindow from './ui/ChatWindow';
import ContactProvider from './ContactProvider';
import DiscoInfo from './DiscoInfo';
import { IJID } from './JID.interface';
import { IMessage } from './Message.interface';

export default class Contact implements IIdentifiable, IContact {
   protected storage: Storage;

   protected readonly account: Account;

   protected data: PersistentMap;

   protected id: string;

   protected jid: JID;

   protected chatWindow;

   protected chatWindowController;

   protected transcript: Transcript;

   public static getProviderId(account: Account, id: string) {
      let data = new PersistentMap(account.getStorage(), 'contact', id);

      return data.get('provider');
   }

   constructor(account: Account, jid: IJID, name?: string);
   constructor(account: Account, id: string);
   constructor() {
      this.account = arguments[0];
      this.storage = this.account.getStorage();

      if (typeof arguments[1] === 'string') {
         this.initExistingContact(arguments[1]);
      } else {
         this.initNewContact(arguments[1], arguments[2]);
      }

      this.id = this.data.get('id');
   }

   private initExistingContact(id: string) {
      this.data = new PersistentMap(this.storage, 'contact', id);

      if (!this.data.get('id')) {
         throw new Error(`Could not find existing contact with id "${id}".`);
      }

      this.jid = new JID(this.data.get('jid'));
   }

   private initNewContact(jid: JID, name?: string) {
      let id = jid.bare;
      this.jid = jid;

      let defaultData = {
         id,
         jid: this.jid.full,
         name: name || this.jid.bare,
         presence: Presence.offline,
         status: '',
         subscription: ContactSubscription.NONE,
         resources: {},
         groups: [],
         type: ContactType.CHAT,
         provider: 'fallback',
         rnd: Math.random(), // force storage event
      };

      this.data = new PersistentMap(this.storage, 'contact', id);

      this.data.set(defaultData);
   }

   public async delete() {
      this.data.delete();
   }

   public getChatWindow(): ChatWindow {
      if (!this.chatWindow) {
         this.chatWindow = new ChatWindow(this);
      }

      return this.chatWindow;
   }

   public getChatWindowController(): ChatWindowController {
      if (!this.chatWindowController) {
         this.chatWindowController = new ChatWindowController(this, this.data);
      }

      return this.chatWindowController;
   }

   public getAccount(): Account {
      return this.account;
   }

   public addSystemMessage(messageString: string): IMessage {
      let message = new Message({
         peer: this.getJid(),
         direction: Message.DIRECTION.SYS,
         plaintextMessage: messageString,
         unread: false,
      });

      this.getTranscript().pushMessage(message);

      return message;
   }

   public setResource = (resource: string) => {
      this.jid = new JID(this.jid.bare + '/' + resource);

      this.data.set('jid', this.jid.full);
   };

   public clearResources() {
      this.data.set('resources', {});
   }

   public setPresence(resource: string, presence: Presence) {
      Log.debug('set presence for ' + this.jid.bare + ' / ' + resource, presence);

      if (resource) {
         let resources = this.data.get('resources') || {};

         if (presence === Presence.offline) {
            delete resources[resource];
         } else {
            resources[resource] = presence;
         }

         this.data.set('resources', resources);
      }

      presence = this.getHighestPresence();

      this.data.set('presence', presence);
   }

   public getCapableResources(features: string[]): Promise<string[]>;
   public getCapableResources(features: string): Promise<string[]>;
   public getCapableResources(features): Promise<string[]> {
      return this.account.getDiscoInfoRepository().getCapableResources(this, features);
   }

   public hasFeatureByResource(resource: string, features: string[]): Promise<{}>;
   public hasFeatureByResource(resource: string, feature: string): Promise<{}>;
   public hasFeatureByResource(resource, feature) {
      if (!resource) {
         throw new Error('I can not lookup a feature without resource');
      }

      let jid = new JID(this.jid.bare + '/' + resource);

      return this.account.getDiscoInfoRepository().hasFeature(jid, feature);
   }

   public getCapabilitiesByResource(resource: string): Promise<DiscoInfo | void> {
      let jid = new JID(this.jid.bare + '/' + resource);

      return this.account.getDiscoInfoRepository().getCapabilities(jid);
   }

   public registerCapableResourcesHook(features: string[], cb: (resources: string[]) => void);
   public registerCapableResourcesHook(features: string, cb: (resources: string[]) => void);
   public registerCapableResourcesHook(features, cb: (resources: string[]) => void) {
      if (typeof features === 'string') {
         features = [features];
      }

      this.getCapableResources(features).then(cb);

      this.registerHook('resources', () => {
         //@REVIEW trigger only on changes
         this.getCapableResources(features).then(cb);
      });
   }

   public getId(): string {
      return this.id;
   }

   public getUid(): string {
      return this.account.getUid() + '@' + this.getId();
   }

   public getJid(): JID {
      return this.jid;
   }

   public getResources(): string[] {
      return Object.keys(this.data.get('resources') || {});
   }

   public getPresence(resource?: string): Presence {
      if (resource) {
         let resources = this.data.get('resources') || {};

         return resources[resource];
      }

      return this.data.get('presence');
   }

   public getType(): ContactType {
      return this.data.get('type');
   }

   public isGroupChat() {
      return this.getType() === ContactType.GROUPCHAT;
   }

   public isChat() {
      return this.getType() === ContactType.CHAT;
   }

   public getNumberOfUnreadMessages(): number {
      return this.transcript.getNumberOfUnreadMessages();
   }

   public hasName(): boolean {
      return !!this.data.get('name');
   }

   public getName(): string {
      return this.data.get('name') || this.jid.bare;
   }

   public getProviderId(): string {
      return this.data.get('provider');
   }

   public getAvatar(): Promise<Avatar> {
      return this.account
         .getPipe('avatar')
         .run(this, undefined)
         .then(([, avatar]) => {
            if (!avatar) {
               throw new Error('No avatar available for ' + this.getId());
            }

            return avatar;
         });
   }

   public getSubscription(): ContactSubscription {
      return this.data.get('subscription');
   }

   public getVcard(): Promise<any> {
      return this.account.getConnection().getVcardService().loadVcard(this.getJid());
   }

   public setEncryptionState(state: EncryptionState, sourceId: string) {
      if (state !== EncryptionState.Plaintext && !sourceId) {
         throw new Error('No encryption source provided');
      }
      this.data.set('encryptionPlugin', state === EncryptionState.Plaintext ? null : sourceId);
      this.data.set('encryptionState', state);
   }

   public getEncryptionState(): EncryptionState {
      return this.data.get('encryptionState') || EncryptionState.Plaintext;
   }

   public getEncryptionPluginId(): string | null {
      return this.data.get('encryptionPlugin') || null;
   }

   public isEncrypted(): boolean {
      return this.getEncryptionState() !== EncryptionState.Plaintext && !!this.getEncryptionPluginId();
   }

   public getTranscript(): Transcript {
      if (!this.transcript) {
         this.transcript = new Transcript(this.storage, this);
      }

      return this.transcript;
   }

   public getStatus(): string {
      return this.data.get('status');
   }

   public setStatus(status: string) {
      return this.data.set('status', status);
   }

   public setName(name: string) {
      this.data.set('name', name);
   }

   public setProvider(provider: ContactProvider) {
      this.data.set('provider', provider.getUid());
   }

   public setSubscription(subscription: ContactSubscription) {
      this.data.set('subscription', subscription);
   }

   public setGroups(groups: string[]) {
      this.data.set('groups', groups);
   }

   public getGroups(): string[] {
      return this.data.get('groups') || [];
   }

   public getLastMessageDate(): Date {
      let lastMessage = this.data.get('lastMessage');

      return lastMessage ? new Date(lastMessage) : undefined;
   }

   public setLastMessageDate(lastMessage: Date) {
      this.data.set('lastMessage', lastMessage.toISOString());
   }

   public registerHook(property: string, func: (newValue: any, oldValue: any) => void) {
      this.data.registerHook(property, func);
   }

   private getHighestPresence(): Presence {
      let maxPresence = Presence.offline;
      let resources = this.data.get('resources');

      for (let resource in resources) {
         if (resources[resource] < maxPresence) {
            maxPresence = resources[resource];
         }
      }

      return maxPresence;
   }
}
