import Storage from './Storage';
import { IConnection } from './connection/Connection.interface';
import Connector from './connection/xmpp/Connector';
import StorageConnection from './connection/storage/Connection';
import JID from './JID';
import Contact from './Contact';
import { Presence } from './connection/AbstractConnection';
import Client from './Client';
import { NoticeManager } from './NoticeManager';
import PluginRepository from './plugin/PluginRepository';
import DiscoInfoRepository from './DiscoInfoRepository';
import DiscoInfoChangeable from './DiscoInfoChangeable';
import HookRepository from './util/HookRepository';
import Options from './Options';
import UUID from './util/UUID';
import Pipe from './util/Pipe';
import ChatWindow from '@ui/ChatWindow';
import { IJID } from './JID.interface';
import { IContact } from './Contact.interface';
import RosterContactProvider from './RosterContactProvider';
import ContactManager from './ContactManager';
import FallbackContactProvider from './FallbackContactProvider';
import Log from '@util/Log';
import CommandRepository from './CommandRepository';
import AvatarSet from '@ui/AvatarSet';
import { IAvatar } from './Avatar.interface';
import CallManager from './CallManager';
import MenuChatMessage from './MenuChatMessage';

type ConnectionCallback = (status: number, condition?: string) => void;

export default class Account {
   private storage: Storage;

   private sessionStorage: Storage;

   private sessionId: string;

   private uid: string;

   private connection: IConnection;

   private connector: Connector;

   private contact: Contact;

   private noticeManager: NoticeManager;

   private pluginRepository: PluginRepository;

   private discoInfoRepository: DiscoInfoRepository;

   private ownDiscoInfo: DiscoInfoChangeable;

   private hookRepository = new HookRepository();

   private contactManager: ContactManager;

   private commandRepository: CommandRepository;

   private callManager: CallManager;

   private chatMessageMenu: MenuChatMessage;

   private options: Options;

   private pipes = {};

   constructor(url: string, jid: string, sid: string, rid: string);
   constructor(url: string, jid: string, password: string);
   constructor(uid: string);
   constructor() {
      if (arguments.length === 1) {
         this.uid = arguments[0];
         this.sessionId = this.getStorage().getItem('sessionId');
      } else if (arguments.length === 3 || arguments.length === 4) {
         let jid = new JID(arguments[1]);

         // anonymous accounts start without node
         this.uid = jid.node ? jid.bare : UUID.v4().slice(0, 8) + '=' + jid.domain;
         this.sessionId = UUID.v4();

         let oldSessionId = this.getStorage().getItem('sessionId');
         this.getStorage().setItem('sessionId', this.sessionId);

         if (oldSessionId) {
            Storage.clear(this.uid + '@' + oldSessionId);
         }
      } else {
         throw new Error('Unsupported number of arguments');
      }

      this.options = new Options(this.getStorage());

      this.connector = new Connector(this, arguments[0], arguments[1], arguments[2], arguments[3]);
      this.connection = new StorageConnection(this);

      if (arguments.length === 1) {
         this.pluginRepository = new PluginRepository(this);
      }

      this.contact = new Contact(this, new JID(this.uid), this.uid);

      let rosterContactProvider = new RosterContactProvider(this.getContactManager(), this);
      this.getContactManager().registerContactProvider(rosterContactProvider);

      let fallbackContactProvider = new FallbackContactProvider(this.getContactManager(), this);
      this.getContactManager().registerContactProvider(fallbackContactProvider);

      let connectionCallback = this.getOption('connectionCallback');

      if (typeof connectionCallback === 'function') {
         this.registerConnectionHook((status, condition) => {
            connectionCallback(this.uid, status, condition);
         });
      }

      this.getContactManager().restoreCache();
      this.getNoticeManager();
   }

   public getOptions(): Options {
      return this.options;
   }

   public getOption(key: string) {
      return this.options.get(key);
   }

   public setOption(key: string, value: any) {
      this.options.set(key, value);
   }

   public connect = (pause: boolean = false): Promise<void> => {
      let targetPresence = Client.getPresenceController().getTargetPresence();

      if (targetPresence === Presence.offline) {
         Client.getPresenceController().setTargetPresence(Presence.online);
      }

      return this.connector
         .connect()
         .then(async ([status, connection]) => {
            this.connection = connection;

            if (pause) {
               connection.pause();
            }

            let storage = this.getSessionStorage();

            if (!storage.getItem('connection', 'created')) {
               storage.setItem('connection', 'created', new Date());
            }

            if (!storage.getItem('options', 'loaded')) {
               let jid = this.connector.getJID();
               await Options.load(jid.bare, this.connector.getPassword(), jid);
               this.connector.clearPassword();

               storage.setItem('options', 'loaded', true);
            }

            if (!pause) {
               this.initConnection(status);
            }
         })
         .catch(err => {
            if (Client.getAccountManager().getAccounts().length <= 1) {
               Client.getPresenceController().setTargetPresence(Presence.offline);
            }

            throw err;
         });
   };

   private async initConnection(status): Promise<void> {
      let storage = this.getSessionStorage();

      if (storage.getItem('connection', 'inited')) {
         return;
      }

      await this.getContactManager().loadContacts();

      let targetPresence = Client.getPresenceController().getTargetPresence();
      this.getConnection().sendPresence(targetPresence);

      storage.setItem('connection', 'inited', true);
   }

   public triggerPresenceHook = (contact: IContact, presence, oldPresence) => {
      this.hookRepository.trigger('presence', contact, presence, oldPresence);
   };

   public registerPresenceHook = func => {
      this.hookRepository.registerHook('presence', func);
   };

   public triggerConnectionHook = (status: number, condition?: string) => {
      this.hookRepository.trigger('connection', status, condition);
   };

   public registerConnectionHook = (func: ConnectionCallback) => {
      this.hookRepository.registerHook('connection', func);
   };

   public triggerChatWindowInitializedHook = (chatWindow: ChatWindow, contact: Contact) => {
      this.hookRepository.trigger('chatWindowInitialized', chatWindow, contact);
   };

   public registerChatWindowInitializedHook = (func: (chatWindow?: ChatWindow, contact?: Contact) => void) => {
      this.hookRepository.registerHook('chatWindowInitialized', func);
   };

   public triggerChatWindowClearedHook = (chatWindow: ChatWindow, contact: Contact) => {
      this.hookRepository.trigger('chatWindowCleared', chatWindow, contact);
   };

   public registerChatWindowClearedHook = (func: (chatWindow?: ChatWindow, contact?: Contact) => void) => {
      this.hookRepository.registerHook('chatWindowCleared', func);
   };

   public getContactManager(): ContactManager {
      if (!this.contactManager) {
         this.contactManager = new ContactManager(this);
      }

      return this.contactManager;
   }

   public getPluginRepository(): PluginRepository {
      return this.pluginRepository;
   }

   public getDiscoInfoRepository(): DiscoInfoRepository {
      if (!this.discoInfoRepository) {
         this.discoInfoRepository = new DiscoInfoRepository(this);
      }

      return this.discoInfoRepository;
   }

   public getDiscoInfo(): DiscoInfoChangeable {
      if (!this.ownDiscoInfo) {
         this.ownDiscoInfo = new DiscoInfoChangeable(this.uid);
      }

      return this.ownDiscoInfo;
   }

   public getCommandRepository(): CommandRepository {
      if (!this.commandRepository) {
         this.commandRepository = new CommandRepository();
      }

      return this.commandRepository;
   }

   public getCallManager() {
      if (!this.callManager) {
         this.callManager = new CallManager(this);
      }

      return this.callManager;
   }

   public getChatMessageMenu(): MenuChatMessage {
      if (!this.chatMessageMenu) {
         this.chatMessageMenu = new MenuChatMessage();
      }

      return this.chatMessageMenu;
   }

   public getContact(jid?: IJID): IContact {
      return jid && jid.bare !== this.getJID().bare ? this.getContactManager().getContact(jid) : this.contact;
   }

   public getNoticeManager(): NoticeManager {
      if (!this.noticeManager) {
         this.noticeManager = new NoticeManager(this.getStorage());
      }

      return this.noticeManager;
   }

   public getStorage() {
      if (!this.storage) {
         this.storage = new Storage(this.uid);
      }

      return this.storage;
   }

   public getSessionStorage() {
      if (!this.sessionStorage) {
         let name = this.uid + '@' + this.sessionId;

         this.sessionStorage = new Storage(name);
      }

      return this.sessionStorage;
   }

   public getPresence(): Presence {
      let sessionStorage = this.getSessionStorage();
      let presence = sessionStorage.getItem('presence');

      return typeof presence === 'number' ? presence : Presence.offline;
   }

   public setPresence(presence: Presence) {
      this.getSessionStorage().setItem('presence', presence);
   }

   public getConnection(): IConnection {
      return this.connection;
   }

   public getUid(): string {
      return this.uid;
   }

   public getSessionId(): string {
      return this.sessionId;
   }

   public getJID(): JID {
      let jid = this.connector.getJID();

      if (!jid) {
         Log.warn('Empty JID for account', this.getUid());
      }

      return jid;
   }

   public getConnectionUrl(): string {
      return this.connector.getUrl();
   }

   public async updateAvatar(avatar: IAvatar | null) {
      const pipe = this.getPipe<[IAvatar]>('publishAvatar');

      const [processedAvatar] = await pipe.run(avatar);

      if (processedAvatar && avatar !== null) {
         Log.warn('Avatar could not be updated');

         return;
      }

      let avatarUI = AvatarSet.get(this.getContact());
      avatarUI.reload();

      this.getConnection().sendPresence();
   }

   public getPipe<params extends any[] = any[]>(name: string): Pipe<params> {
      if (!this.pipes[name]) {
         this.pipes[name] = new Pipe<params>();
      }

      return this.pipes[name];
   }

   public remove() {
      this.destroy();

      Client.getAccountManager().removeAccount(this);
   }

   public destroy() {
      this.getContactManager().removeAllContactsFromCache();

      this.getConnection().close();
      this.getStorage().destroy();
      this.getSessionStorage().destroy();
      this.getNoticeManager().removeAll();

      for (const name in this.pipes) {
         this.pipes[name].destroy();
      }

      if (this.getPluginRepository()) {
         this.getPluginRepository().destroyAllPlugins();
      }
   }

   public connectionDisconnected() {
      this.setPresence(Presence.offline);

      this.remove();
   }
}
