// @flow
import { t } from '@lingui/macro';
import * as React from 'react';
import { type StorageProvider, type FileMetadata } from '../index';
import { serializeToJSON } from '../../Utils/Serializer';
import GoogleDrive from '../../UI/CustomSvgIcons/GoogleDrive';
import GoogleDriveSaveAsDialog from './GoogleDriveSaveAsDialog';
import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
import { type AppArguments } from '../../Utils/Window';
import { loadScript } from '../../Utils/LoadScript';
const isDev = process.env.NODE_ENV === 'development';

const DEVELOPER_KEY = isDev
  ? 'AIzaSyDH3UNpxzIpcTyd6aMCWI5oNFSptG_BhOc'
  : 'AIzaSyDJYQmzLCfjXnNImDa1X_cDTWjl2BOrcM4';
const CLIENT_ID = isDev
  ? '28563107180-bd29h9f3og4h1632m94nv6hat2igrej6.apps.googleusercontent.com'
  : '44882707384-3t4tubr3fbht87sbtdp7u5mlo45k5uku.apps.googleusercontent.com';
const APP_ID = isDev ? '28563107180' : '44882707384'; // This is the first part of CLIENT_ID.
const DISCOVERY_DOCS = [
  'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest',
];
const SCOPE =
  'https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.install';

let apisLoaded = false;
let apisLoadingPromise = null;

/**
 * Load Google Drive APIs. Return a fulfilled promise when done.
 */
const initializeApis = (): Promise<void> => {
  if (apisLoaded) {
    return Promise.resolve();
  }

  if (apisLoadingPromise) {
    // Only do a single initialization attempt at a given time.
    return apisLoadingPromise;
  }

  apisLoadingPromise = loadScript('https://apis.google.com/js/api:client.js')
    .then(() => {
      const gapi = global.gapi;
      if (!gapi) {
        throw new Error(
          'No gapi global object found after loading Google Drive API script'
        );
      }

      return new Promise((resolve, reject) => {
        gapi.load('client:auth2:picker', {
          callback: () => {
            const auth2LoadPromise = gapi.auth2.init({
              apiKey: DEVELOPER_KEY,
              clientId: CLIENT_ID,
              discoveryDocs: DISCOVERY_DOCS,
              scope: SCOPE,
            });

            gapi.client.setApiKey(DEVELOPER_KEY);
            const driveLoadPromise = gapi.client.load('drive', 'v3');

            resolve(Promise.all([auth2LoadPromise, driveLoadPromise]));
          },
          onerror: () => {
            reject(
              new Error(
                'Unable to load auth2 and picker APIs for Google Drive.'
              )
            );
          },
        });
      });
    })
    .then(() => {
      apisLoaded = true;
      apisLoadingPromise = null;
    })
    .catch(error => {
      console.error('Error while loading Google Drive APIs:', error);
      apisLoadingPromise = null;

      throw error;
    });

  return apisLoadingPromise;
};

type GoogleUser = {
  getAuthResponse: () => { access_token: string, error?: ?Error },
};

let isAuthenticated = false;

/**
 * Sign in the user to Google Drive, returning the user object after a successful sign up
 * (or if the user is already signed in).
 */
export const authenticate = (): Promise<GoogleUser> => {
  return initializeApis().then(() => {
    const gapi = global.gapi;
    const googleAuth = gapi.auth2.getAuthInstance();
    if (isAuthenticated && googleAuth.isSignedIn.get()) {
      return googleAuth.currentUser.get();
    }

    isAuthenticated = false;
    googleAuth.isSignedIn.listen(authenticated => {
      console.info('Authenticated with Google APIs: ', authenticated);
      isAuthenticated = authenticated;
    });
    return googleAuth
      .signIn({ scope: SCOPE })
      .then((googleUser: GoogleUser) => {
        if (
          !googleUser.getAuthResponse() ||
          googleUser.getAuthResponse().error
        ) {
          console.error(
            'OAuth2 error while sign in:',
            googleUser.getAuthResponse()
              ? googleUser.getAuthResponse().error
              : 'No AuthResponse'
          );
          throw new Error('Authentication error');
        }

        isAuthenticated = true;
        return googleUser;
      });
  });
};

/**
 * Update a JSON file, given its file id.
 */
export const patchJsonFile = (
  fileId: string,
  googleUser: GoogleUser,
  content: string
): Promise<void> => {
  return fetch(`https://www.googleapis.com/upload/drive/v3/files/${fileId}`, {
    method: 'PATCH',
    headers: new Headers({
      Authorization: `Bearer ${googleUser.getAuthResponse().access_token}`,
      'Content-Type': 'application/json',
    }),
    body: content,
  }).then(res => {
    if (res.status !== 200) {
      if (res.status === 401) {
        isAuthenticated = false;
      }

      throw res;
    }
  });
};

/**
 * Create a new empty JSON file, returning its fileid.
 */
export const createNewJsonFile = (
  parentId: string,
  name: string
): Promise<string> => {
  const gapi = global.gapi;

  return gapi.client.drive.files
    .create({
      'content-type': 'application/json',
      uploadType: 'multipart',
      name: name,
      parents: [parentId],
      mimeType: 'application/json',
      fields: 'id, name, kind, size',
    })
    .then(apiResponse => {
      return apiResponse.result.id;
    });
};

/**
 * Information about a file or folder picked by the user.
 */
export type GoogleDriveFileOrFolder =
  | {|
      type: 'FOLDER',
      id: string,
      name: string,
    |}
  | {|
      type: 'FILE',
      id: string,
      name: string,
      parentId: string,
    |};

export type GoogleDriveFilePickerOptions = {|
  selectFolderEnabled: boolean,
  showUploadView: boolean,
|};

/**
 * Display a file picker as a modal, resolving with the selected file or folder,
 * if any.
 *
 * The picker dialog is not playing nice with material-ui dialogs or overlays. They should
 * not be displayed when the picker is on screen.
 */
const showFilePicker = ({
  selectFolderEnabled,
  showUploadView,
}: GoogleDriveFilePickerOptions): Promise<?GoogleDriveFileOrFolder> => {
  return authenticate().then(googleUser => {
    const google = global.google;

    return new Promise(resolve => {
      let picker = null;
      const pickerBuilder = new google.picker.PickerBuilder()
        .addView(
          new google.picker.DocsView()
            .setIncludeFolders(true)
            .setSelectFolderEnabled(selectFolderEnabled)
        )
        .setOAuthToken(googleUser.getAuthResponse().access_token)
        .setDeveloperKey(DEVELOPER_KEY)
        .setAppId(APP_ID) // App ID is required to correctly identify files created with the app.
        .setCallback(data => {
          if (
            data[google.picker.Response.ACTION] === google.picker.Action.PICKED
          ) {
            const doc = data[google.picker.Response.DOCUMENTS][0];
            const id: string = doc[google.picker.Document.ID];
            const name: string = doc[google.picker.Document.NAME];
            const parentId: string = doc[google.picker.Document.PARENT_ID];
            if (
              doc[google.picker.Document.TYPE] ===
                google.picker.Type.LOCATION ||
              doc[google.picker.Document.MIME_TYPE] ===
                'application/vnd.google-apps.folder'
            ) {
              resolve({ id, name, type: 'FOLDER' });
            } else {
              resolve({ id, name, type: 'FILE', parentId });
            }
            if (picker) picker.dispose();
          } else if (
            data[google.picker.Response.ACTION] === google.picker.Action.CANCEL
          ) {
            resolve(null);
            if (picker) picker.dispose();
          }
        });
      if (showUploadView) {
        pickerBuilder.addView(
          new google.picker.DocsUploadView().setIncludeFolders(true)
        );
      }

      picker = pickerBuilder.build();

      picker.setVisible(true);
      const pickerElements = document.getElementsByClassName('picker-dialog');
      for (var i = 0; i < pickerElements.length; ++i) {
        pickerElements[i].style.zIndex = '5000'; // Higher than Material UI modals
      }
    });
  });
};

/**
 * A storage that is using Google Drive to open and store files.
 */
export default ({
  internalName: 'GoogleDrive',
  name: t`Google Drive`,
  renderIcon: () => <GoogleDrive />,
  getFileMetadataFromAppArguments: (appArguments: AppArguments) => {
    if (appArguments.state) {
      try {
        // See "state" argument passed by Google Drive API:
        // https://developers.google.com/drive/api/v3/enable-sdk#construct
        const googleDriveState = JSON.parse(appArguments.state);
        if (googleDriveState.ids && googleDriveState.ids[0]) {
          return {
            fileIdentifier: googleDriveState.ids[0],
          };
        }
      } catch (e) {
        console.warn(
          "Error while trying to parse the Google Drive 'ids' in 'state' from the app arguments."
        );
      }
    }

    return null;
  },
  createOperations: ({ setDialog, closeDialog }) => {
    initializeApis().catch(() => {
      // Ignore error as we'll retry later.
    });

    return {
      doesInitialOpenRequireUserInteraction: true, // Authentication will open a popup, requiring user interaction
      onOpen: (
        fileMetadata: FileMetadata
      ): Promise<{|
        content: Object,
      |}> => {
        const fileId = fileMetadata.fileIdentifier;

        return authenticate()
          .then(googleUser =>
            fetch(
              `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`,
              {
                method: 'GET',
                headers: new Headers({
                  Authorization: `Bearer ${
                    googleUser.getAuthResponse().access_token
                  }`,
                }),
              }
            )
          )
          .then(
            response => {
              return response.text().then(
                fileContent => {
                  return new Promise((resolve, reject) => {
                    try {
                      const dataObject = JSON.parse(fileContent);
                      return resolve({
                        content: dataObject,
                      });
                    } catch (ex) {
                      return reject(fileId + ' is a corrupted/malformed file.');
                    }
                  });
                },
                error => {
                  console.error(
                    'Error while reading the file from Google Drive API: ',
                    error
                  );
                  throw error;
                }
              );
            },
            error => {
              console.error(
                'Error while fetching the file from Google Drive API: ',
                error
              );
              throw error;
            }
          );
      },
      onOpenWithPicker: (): Promise<?FileMetadata> => {
        return authenticate()
          .then(googleUser =>
            showFilePicker({ selectFolderEnabled: false, showUploadView: true })
          )
          .then(googleDriveFileOrFolder => {
            if (!googleDriveFileOrFolder) {
              return null;
            }

            return {
              fileIdentifier: googleDriveFileOrFolder.id,
            };
          });
      },
      onSaveProject: (project: gdProject, fileMetadata: FileMetadata) => {
        const fileId = fileMetadata.fileIdentifier;

        const content = serializeToJSON(project);
        return authenticate()
          .then(googleUser => patchJsonFile(fileId, googleUser, content))
          .then(() => ({
            wasSaved: true,
            fileMetadata,
          }));
      },
      onSaveProjectAs: (project: gdProject, fileMetadata: ?FileMetadata) => {
        return new Promise(resolve => {
          setDialog(() => (
            <GoogleDriveSaveAsDialog
              onShowFilePicker={showFilePicker}
              onCancel={() => {
                closeDialog();
                resolve({ wasSaved: false, fileMetadata });
              }}
              onSave={({ selectedFileOrFolder, newFileName }) => {
                const content = serializeToJSON(project);

                if (selectedFileOrFolder.type === 'FOLDER') {
                  return authenticate().then(googleUser =>
                    createNewJsonFile(
                      selectedFileOrFolder.id,
                      newFileName
                    ).then(newFileId =>
                      patchJsonFile(newFileId, googleUser, content).then(() => {
                        closeDialog();
                        resolve({
                          wasSaved: true,
                          fileMetadata: {
                            fileIdentifier: newFileId,
                          },
                        });
                      })
                    )
                  );
                } else {
                  return authenticate()
                    .then(googleUser =>
                      patchJsonFile(
                        selectedFileOrFolder.id,
                        googleUser,
                        content
                      )
                    )
                    .then(() => {
                      closeDialog();
                      resolve({
                        wasSaved: true,
                        fileMetadata: {
                          fileIdentifier: selectedFileOrFolder.id,
                        },
                      });
                    });
                }
              }}
            />
          ));
        });
      },
      getOpenErrorMessage: (error: Error): MessageDescriptor => {
        if (!apisLoaded) {
          return t`Google Drive could not be loaded. Check that you are not offline and have a proper internet connection, then try again.`;
        }

        return t`Check that you don't have any blocked popup (if so, allow them and retry) and that you have the authorizations for reading the file you're trying to access.`;
      },
    };
  },
}: StorageProvider);
