import './styles/global.css';

import { BoostProvider } from '@8base/boost';
import get from 'lodash/get';
import React from 'react';
import { ApolloProvider } from 'react-apollo';
import invariant, { InvariantError } from 'ts-invariant';

import { createApolloClient } from './apollo';
import css from './chat.module.css';
import {
  AddToChannelDialog,
  AlertDialog,
  ChatWindow,
  EditChannelDialog,
  LeaveChannelDialog,
  Sidebar,
} from './components';
import DeleteChannelDialog from './components/dialogs/delete-channel';
import { ChatContext, IChatContext, SetChannel, SetTabs } from './context';
import { DocumentPreview } from './shared/components';
import {
  CHAT_MESSAGES_LAST,
  CHAT_TAB_MARGIN_R,
  CHAT_TAB_WIDTH,
} from './shared/constants';
import {
  ChannelIdentityCommonFragment,
  ChannelIdentityCommonFragmentDoc,
  ChannelMessagesDocument,
  ChannelMessagesQuery,
  ChannelMessagesQueryVariables,
  ChannelPreviewCommonFragment,
  ChannelPreviewCommonFragmentDoc,
  MutationType,
  UserChannelsMessagesSubDocument,
  UserChannelsMessagesSubSubscription,
  UserChannelsPreviewDocument,
  UserChannelsPreviewQuery,
  UserChannelsPreviewQueryVariables,
  UserDmsPreviewDocument,
  UserDmsPreviewQuery,
  UserDmsPreviewQueryVariables,
} from './shared/graphql/__generated__';
import * as icons from './shared/icons';
import theme from './shared/theme';
import { isDevEnv } from './shared/utils';
import { IUser, SectionsConfig } from './types';

// -- TYPES

export interface IChatProps {
  currentUser: IUser;
  uri: string;
  authToken: string;
  workspaceId: string;
  maxTabsCount: number;
  top: string;
  isSidebarVisible: boolean;
  sections: SectionsConfig;
  onChangeSidebar?: IChatContext['setSidebarVisibility'];
  usersFilter?: {};
  channelMembersFilter?: {};
}

export interface IChatState extends Pick<IChatContext, 'openedChannel' | 'tabs'> {}

type SubscriptionMessageNode = NonNullable<
  NonNullable<UserChannelsMessagesSubSubscription['Message']>['node']
>;

// -- MAIN

class Chat extends React.Component<IChatProps, IChatState> {
  static defaultProps = {
    top: '0px',
    maxTabsCount: 2,
    isSidebarVisible: true,
    sections: {
      channels: true,
      dm: true,
      contacts: true,
    },
  };

  private client: ReturnType<typeof createApolloClient>;

  constructor(props: IChatProps) {
    super(props);

    const { currentUser, uri, authToken, workspaceId, usersFilter } = this.props;

    if (!currentUser || !uri || !authToken) {
      throw new InvariantError(
        "You need to specify 'currentUser', 'authToken', 'workspaceId' and 'uri' to <Chat />.",
      );
    }

    invariant(typeof uri === 'string', "'uri' must be string.");
    invariant(typeof authToken === 'string', "'authToken' must be string.");
    invariant(typeof workspaceId === 'string', "'workspaceId' must be string.");

    this.state = {
      openedChannel: null,
      tabs: [],
    };

    this.client = createApolloClient({ uri, token: authToken, workspaceId });

    if (isDevEnv()) {
      // @ts-ignore
      window.__CHAT_APOLLO_CLIENT__ = this.client;
    }
  }

  componentDidMount() {
    this.subscribeToMessages();
  }

  render() {
    const { tabs, openedChannel } = this.state;
    const inlineStyles = this.getInlineStyles();
    const contextValue = this.getContextValue();
    const { onChangeSidebar } = this.props;
    const closeSidebar =
      onChangeSidebar &&
      (() => {
        onChangeSidebar(false);
      });
    return (
      <BoostProvider icons={icons} theme={theme}>
        <ApolloProvider client={this.client}>
          <div className={css.base} style={inlineStyles}>
            <ChatContext.Provider value={contextValue}>
              <EditChannelDialog />
              <AddToChannelDialog />
              <LeaveChannelDialog />
              <DeleteChannelDialog />
              <AlertDialog />
              <DocumentPreview />

              {tabs.map((tab, inx) => (
                <ChatWindow
                  style={this.getTabInlineStyles(inx)}
                  key={tab.channelIdentityId}
                  channelIdentityId={tab.channelIdentityId}
                  isTabCollapsed={tab.isCollapsed}
                  isTab={true}
                />
              ))}

              {this.props.isSidebarVisible && (
                <>
                  {openedChannel && (
                    <ChatWindow channelIdentityId={openedChannel.channelIdentityId} />
                  )}
                  <Sidebar sections={this.props.sections} onClose={closeSidebar} />
                </>
              )}
            </ChatContext.Provider>
          </div>
        </ApolloProvider>
      </BoostProvider>
    );
  }

  private getInlineStyles() {
    return {
      top: this.props.top,
      height: `calc(100vh - ${this.props.top})`,
    };
  }

  private getTabInlineStyles(inx: number) {
    const nth = inx + 1;
    const offset = nth * CHAT_TAB_WIDTH + nth * CHAT_TAB_MARGIN_R;

    return {
      position: 'absolute',
      left: `-${offset}px`,
    } as const;
  }

  private getContextValue(): IChatContext {
    const user = this.props.currentUser;
    const usersFilter = this.props.usersFilter
      ? this.props.usersFilter
      : {
          id: {
            not_in: [user ? user.id : ''],
          },
        };

    const channelMembersFilter = this.props.channelMembersFilter;

    return {
      ...this.state,
      user: this.props.currentUser,
      setChannel: this.setChannel,
      setTabs: this.setTabs,
      setSidebarVisibility: this.props.onChangeSidebar,
      isSidebarVisible: this.props.isSidebarVisible,
      usersFilter,
      channelMembersFilter,
    };
  }

  private setChannel: SetChannel = (newChannel, callback) => {
    this.setState(state => {
      const newValue =
        typeof newChannel === 'function' ? newChannel(state.openedChannel) : newChannel;

      const newChannelIdentity = newValue && newValue.channelIdentityId;

      if (
        state.openedChannel &&
        state.openedChannel.channelIdentityId === newChannelIdentity
      ) {
        return null;
      }

      return {
        ...state,
        openedChannel: newValue,
        tabs: state.tabs.filter(tab => tab.channelIdentityId !== newChannelIdentity),
      };
    }, callback);
  };

  private setTabs: SetTabs = (newTabs, callback) => {
    this.setState(state => {
      const newValue = typeof newTabs === 'function' ? newTabs(state.tabs) : newTabs;

      const windowChannelIdentityId =
        state.openedChannel && state.openedChannel.channelIdentityId;

      const shouldCloseWindow = newValue.some(
        tab => tab.channelIdentityId === windowChannelIdentityId,
      );

      return {
        ...state,
        openedChannel: shouldCloseWindow ? null : state.openedChannel,
        tabs:
          newValue.length > this.props.maxTabsCount
            ? newValue.slice(0, this.props.maxTabsCount)
            : newValue,
      };
    }, callback);
  };

  private subscribeToMessages() {
    if (!this.props.currentUser) {
      return;
    }

    const userId = this.props.currentUser.id;

    this.client
      .subscribe({
        query: UserChannelsMessagesSubDocument,
        variables: {
          userId,
        },
        fetchPolicy: 'no-cache',
      })
      .subscribe({
        next: ({ data }: { data: UserChannelsMessagesSubSubscription }) => {
          const response = data && data.Message && data.Message;

          if (!response || !response.node) {
            return;
          }

          this.updateChannelMessages(response.node, response.mutation);
          this.checkAndMarkUnread(response.node);
        },
      });
  }

  private updateChannelMessages(
    node: SubscriptionMessageNode,
    mutationType?: MutationType,
  ) {
    if (!node.channel) {
      return;
    }

    let cachedChannel: ChannelMessagesQuery | undefined | null;

    try {
      cachedChannel = this.client.cache.readQuery<
        ChannelMessagesQuery,
        ChannelMessagesQueryVariables
      >({
        query: ChannelMessagesDocument,
        variables: {
          id: node.channel.id as string,
          last: CHAT_MESSAGES_LAST,
        },
      });
      // tslint:disable-next-line: no-empty
    } catch (e) {}

    const { channel, ...message } = node;

    if (cachedChannel) {
      const oldItems = cachedChannel.channel!.messages!.items;

      let items = oldItems;
      let count = cachedChannel.channel!.messages!.count;

      switch (mutationType) {
        case 'create': {
          items = [...oldItems, message];
          count += 1;
          break;
        }
        case 'update': {
          if (message.isDeleted) {
            items = oldItems.filter(el => el.id !== message.id);
            count -= 1;
          } else {
            items = oldItems.map(el => (el.id === message.id ? message : el));
          }

          break;
        }
      }

      this.client.cache.writeQuery<ChannelMessagesQuery, ChannelMessagesQueryVariables>({
        query: ChannelMessagesDocument,
        variables: {
          id: channel.id as string,
          last: CHAT_MESSAGES_LAST,
        },
        data: {
          channel: {
            __typename: cachedChannel.channel!.__typename,
            id: channel.id,
            messages: {
              __typename: cachedChannel.channel!.messages!.__typename,
              count,
              items,
            },
          },
        },
      });

      this.client.query<ChannelMessagesQuery, ChannelMessagesQueryVariables>({
        query: ChannelMessagesDocument,
        variables: { id: channel.id as string, last: CHAT_MESSAGES_LAST },
      });

      return;
    }

    const { channelMembersFilter } = this.getContextValue();

    this.client.query({
      query:
        channel.type === 'channel' ? UserChannelsPreviewDocument : UserDmsPreviewDocument,
      variables: { channelMembersFilter },
      fetchPolicy: 'network-only',
    });
  }

  private checkAndMarkUnread(node: SubscriptionMessageNode) {
    if (!node.channel) {
      return;
    }

    const userId = this.props.currentUser && this.props.currentUser.id;
    const { channel, ...message } = node;

    if (!message.createdBy || userId === message.createdBy.id) {
      return;
    }

    let cachedChannelPreview: ChannelPreviewCommonFragment | undefined | null;

    try {
      cachedChannelPreview = this.client.cache.readFragment<ChannelPreviewCommonFragment>(
        {
          fragment: ChannelPreviewCommonFragmentDoc,
          fragmentName: 'ChannelPreviewCommon',
          id: `Channel:${channel.id}`,
        },
      );
      // tslint:disable-next-line: no-empty
    } catch (e) {}

    const lastChannelMsg = get(cachedChannelPreview, 'messages.items[0]');
    const isReceivedNewMessage =
      !lastChannelMsg ||
      new Date(lastChannelMsg.createdAt).getTime() <=
        new Date(message.createdAt).getTime();

    if (!message.createdAt || !isReceivedNewMessage || !cachedChannelPreview) {
      return;
    }

    this.client.cache.writeFragment<ChannelPreviewCommonFragment>({
      fragment: ChannelPreviewCommonFragmentDoc,
      fragmentName: 'ChannelPreviewCommon',
      id: `Channel:${channel.id}`,
      data: {
        __typename: 'Channel',
        ...cachedChannelPreview,
        messages: {
          __typename: 'MessageListResponse',
          items: [
            {
              __typename: 'Message',
              createdAt: message.createdAt,
              createdBy: message.createdBy,
            },
          ],
        },
      },
    });

    const { channelMembersFilter } = this.props;

    const cachedUserChannelsPreview = this.client.cache.readQuery<
      UserChannelsPreviewQuery,
      UserChannelsPreviewQueryVariables
    >({
      query: UserChannelsPreviewDocument,
      variables: { channelMembersFilter },
    });

    const cachedUserDmsPreview = this.client.cache.readQuery<
      UserDmsPreviewQuery,
      UserDmsPreviewQueryVariables
    >({
      query: UserDmsPreviewDocument,
      variables: { channelMembersFilter },
    });

    const identitiesPath = 'user.channelIdentities.items';

    const channelIdentities = get(cachedUserChannelsPreview, identitiesPath, []);
    const dmIdentities = get(cachedUserDmsPreview, identitiesPath, []);
    const identities = [...channelIdentities, ...dmIdentities];

    const channelIdentity = identities.find((el: any) => el.channel.id === channel.id);

    if (!channelIdentity) {
      return;
    }

    this.client.cache.writeFragment<ChannelIdentityCommonFragment>({
      fragment: ChannelIdentityCommonFragmentDoc,
      fragmentName: 'ChannelIdentityCommon',
      id: `ChannelMember:${channelIdentity.id}`,
      data: {
        ...channelIdentity,
        hasUnreads: true,
      },
    });
  }
}

export default Chat;
