namespace gdjs {
  export namespace evtTools {
    export namespace firebaseTools {
      /**
       * Firebase Cloud Firestore Event Tools.
       * @namespace
       */
      export namespace firestore {
        const queries = new Map<string, firebase.firestore.Query>();

        /**
         * Converts a firebase document snapshot to a plain dictionary,
         * so that it may be serialized or converted to a {@link gdjs.Variable}.
         *
         * @param doc - The document snapshot.
         * @returns - The converted object.
         */
        const documentSnapshotToSerializable = (
          doc: firebase.firestore.DocumentSnapshot
        ) => ({
          data: doc.data(),
          exists: doc.exists,
          id: doc.id,
        });

        /**
         * Converts a firebase query snapshot to a plain dictionary,
         * so that it may be serialized or converted to a {@link gdjs.Variable}.
         *
         * @param query - The query snapshot.
         * @returns - The converted object.
         */
        const querySnapshotToSerializable = (
          query: firebase.firestore.QuerySnapshot
        ) => ({
          size: query.size,
          empty: query.empty,
          docs: query.docs.map(documentSnapshotToSerializable),
        });

        /**
         * Initiate a query over a collection.
         * @param queryID - The name of the new query.
         * @param collectionName - The name of the collection to query.
         */
        export const startQuery = (queryID: string, collectionName: string) => {
          queries.set(queryID, firebase.firestore().collection(collectionName));
        };

        /**
         * Create a new query from a base query.
         * @param queryID - The name of the new query.
         * @param sourceQueryID - The name of the source query.
         */
        export const startQueryFrom = (
          queryID: string,
          sourceQueryID: string
        ) => {
          if (queries.has(sourceQueryID))
            queries.set(queryID, queries.get(sourceQueryID)!);
        };

        /**
         * Filters out documents whose fields do not match a condition
         * from a query.
         * @param queryID - The query to add the filter to.
         * @param field - The field to run the condition on.
         * @param op - The condtion operator.
         * @param value - The value to check against.
         */
        export const queryWhere = (
          queryID: string,
          field: string,
          op: Exclude<
            firebase.firestore.WhereFilterOp,
            // Exclude unsupported "batch" operations (as they require an array as value to check)
            'in' | 'array-contains-any' | 'not-in'
          >,
          value: string | number
        ) => {
          if (queries.has(queryID))
            queries.set(queryID, queries.get(queryID)!.where(field, op, value));
        };

        /**
         * Orders the documents in a query.
         *
         * @param queryID - The query to add the filter to.
         * @param field - The field to order by.
         * @param direction - The direction of ordering (ascending or descending).
         */
        export const queryOrderBy = (
          queryID: string,
          field: string,
          direction: firebase.firestore.OrderByDirection
        ) => {
          if (queries.has(queryID))
            queries.set(
              queryID,
              queries.get(queryID)!.orderBy(field, direction)
            );
        };

        /**
         * Limits the amount of documents returned by the query.
         *
         * @param queryID - The query to add the filter to.
         * @param amount - The amount of documents to limit to
         * @param last - If true, limits to the last documents instead of the first documents.
         */
        export const queryLimit = (
          queryID: string,
          amount: integer,
          last: boolean
        ) => {
          if (queries.has(queryID))
            queries.set(
              queryID,
              queries.get(queryID)![last ? 'limitToLast' : 'limit'](amount)
            );
        };

        /**
         * Makes a query skip documents after or before a certain
         * value of a field the query was ordered with.
         *
         * @param queryID - The query to add the filter to.
         * @param value - The value of the field ordered by.
         * @param before - If set to true, all documents before the document are skipped, else all documents after it are skipped.
         * @param includeSelf - If set to true, doesn't skip the document.
         */
        export const querySkipSome = (
          queryID: string,
          value: number,
          before: boolean,
          includeSelf: boolean
        ) => {
          if (queries.has(queryID))
            queries.set(
              queryID,
              queries
                .get(queryID)!
                [
                  before
                    ? includeSelf
                      ? 'endAt'
                      : 'endBefore'
                    : includeSelf
                    ? 'startAt'
                    : 'startAfter'
                ](value)
            );
        };

        /**
         * Execute a query and store results in a callback variable.
         *
         * @param queryID - The query to execute.
         * @param [callbackValueVariable] - The variable where to store the result.
         * @param [callbackStatusVariable] - The variable where to store if the operation was successful.
         */
        export const executeQuery = (
          queryID: string,
          callbackValueVariable?: gdjs.Variable,
          callbackStatusVariable?: gdjs.Variable
        ) => {
          if (!queries.has(queryID)) return;
          queries
            .get(queryID)!
            .get()
            .then((snapshot) => {
              if (typeof callbackStatusVariable !== 'undefined')
                callbackStatusVariable.setString('ok');

              if (typeof callbackValueVariable !== 'undefined')
                callbackValueVariable.fromJSObject(
                  querySnapshotToSerializable(snapshot)
                );
            })
            .catch((error) => {
              if (typeof callbackStatusVariable !== 'undefined')
                callbackStatusVariable.setString(error.message);
            });
        };

        /**
         * Watch a query and store results in a callback
         * variable whenever a documents starts/stops
         * matching the query or a document matching
         * the query is modified.
         *
         * @param queryID - The query to execute.
         * @param [callbackValueVariable] - The variable where to store the result.
         * @param [callbackStatusVariable] - The variable where to store if the operation was successful.
         */
        export const watchQuery = (
          queryID: string,
          callbackValueVariable?: gdjs.Variable,
          callbackStatusVariable?: gdjs.Variable
        ) => {
          if (!queries.has(queryID)) return;
          queries.get(queryID)!.onSnapshot(
            (snapshot) => {
              if (typeof callbackStatusVariable !== 'undefined')
                callbackStatusVariable.setString('ok');

              if (typeof callbackValueVariable !== 'undefined')
                callbackValueVariable.fromJSObject(
                  querySnapshotToSerializable(snapshot)
                );
            },
            (error) => {
              if (typeof callbackStatusVariable !== 'undefined')
                callbackStatusVariable.setString(error.message);
            }
          );
        };

        /**
         * Adds a variable in a collection as document with a unique name.
         * @param collectionName - The collection where to store the variable.
         * @param variable - The variable to write.
         * @param [callbackStateVariable] - The variable where to store the result.
         */
        export const addDocument = (
          collectionName: string,
          variable: gdjs.Variable,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .add(replaceTimestampsInObject(variable.toJSObject()))
            .then(() => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Writes a variable in a collection as document.
         * @param collectionName - The collection where to store the variable.
         * @param variableName - The name under wich the variable will be saved (document name).
         * @param variable - The variable to write.
         * @param [callbackStateVariable] - The variable where to store the result.
         */
        export const writeDocument = (
          collectionName: string,
          variableName: string,
          variable: gdjs.Variable,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(variableName)
            .set(replaceTimestampsInObject(variable.toJSObject()))
            .then(() => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Writes a field of a document.
         * @param collectionName - The collection where to store the document.
         * @param documentName - The name of the document where to write a field.
         * @param field - The field where to write.
         * @param value - The value to write.
         * @param [callbackStateVariable] - The variable where to store the result.
         * @param [merge] - Should the new field replace the document or be merged with the document?
         */
        export const writeField = (
          collectionName: string,
          documentName: string,
          field: string,
          value: any,
          callbackStateVariable?: gdjs.Variable,
          merge: boolean = true
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(documentName)
            .set({ [field]: replaceTimestampInString(value) }, { merge: merge })
            .then(() => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Updates a variable/document.
         * @param collectionName - The collection where the document is stored.
         * @param variableName - The name under wich the variable will be saved (document name).
         * @param variable - The variable to update.
         * @param [callbackStateVariable] - The variable where to store the result.
         */
        export const updateDocument = (
          collectionName: string,
          variableName: string,
          variable: gdjs.Variable,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(variableName)
            .update(replaceTimestampsInObject(variable.toJSObject()))
            .then(() => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Updates a field of a document.
         * @param collectionName - The collection where the document is stored.
         * @param documentName - The name of the document where to update a field.
         * @param field - The field where to update.
         * @param value - The value to write.
         * @param [callbackStateVariable] - The variable where to store the result.
         */
        export const updateField = (
          collectionName: string,
          documentName: string,
          field: string,
          value: any,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(documentName)
            .update({ [field]: replaceTimestampInString(value) })
            .then(() => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Deletes a document.
         * @param collectionName - The collection where the document is stored.
         * @param documentName - The name of the document to delete.
         * @param [callbackStateVariable] - The variable where to store the result.
         */
        export const deleteDocument = (
          collectionName: string,
          documentName: string,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(documentName)
            .delete()
            .then(() => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Deletes a field of a document.
         * @param collectionName - The collection where the document is stored.
         * @param documentName - The name of the document where to delete a field.
         * @param field - The field to delete.
         * @param [callbackStateVariable] - The variable where to store the result.
         */
        export const deleteField = (
          collectionName: string,
          documentName: string,
          field: string,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(documentName)
            .update({ [field]: firebase.firestore.FieldValue.delete() })
            .then(() => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Gets a document and store it in a variable.
         * @param collectionName - The collection where the document is stored.
         * @param documentName - The name of the document to get.
         * @param [callbackValueVariable] - The variable where to store the result.
         * @param [callbackStateVariable] - The variable where to store if the operation was successful.
         */
        export const getDocument = (
          collectionName: string,
          documentName: string,
          callbackValueVariable?: gdjs.Variable,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(documentName)
            .get()
            .then((doc) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');

              if (callbackValueVariable)
                callbackValueVariable.fromJSObject(doc.data());
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Gets a field of a document and store it in a variable.
         * @param collectionName - The collection where the document is stored.
         * @param documentName - The name of the document.
         * @param field - The field to get.
         * @param [callbackValueVariable] - The variable where to store the result.
         * @param [callbackStateVariable] - The variable where to store if the operation was successful.
         */
        export const getField = (
          collectionName: string,
          documentName: string,
          field: string,
          callbackValueVariable?: gdjs.Variable,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(documentName)
            .get()
            .then((doc) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');

              if (callbackValueVariable)
                callbackValueVariable.fromJSObject(doc.get(field));
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Checks for existence of a document.
         * @param collectionName - The collection where the document is stored.
         * @param documentName - The name of the document to check.
         * @param [callbackValueVariable] - The variable where to store the result.
         * @param [callbackStateVariable] - The variable where to store if the operation was successful.
         */
        export const hasDocument = (
          collectionName: string,
          documentName: string,
          callbackValueVariable?: gdjs.Variable,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(documentName)
            .get()
            .then((doc) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');

              if (callbackValueVariable)
                callbackValueVariable.setBoolean(doc.exists);
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Checks for existence of a field.
         * @param collectionName - The collection where the document is stored.
         * @param documentName - The name of the document.
         * @param field - The field to check.
         * @param [callbackValueVariable] - The variable where to store the result.
         * @param [callbackStateVariable] - The variable where to store if the operation was successful.
         */
        export const hasField = (
          collectionName: string,
          documentName: string,
          field: string,
          callbackValueVariable?: gdjs.Variable,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .doc(documentName)
            .get()
            .then((doc) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString('ok');

              if (callbackValueVariable) {
                const value = doc.get(field, { serverTimestamps: 'estimate' });
                callbackValueVariable.setBoolean(
                  doc.exists && value !== undefined && value !== null
                );
              }
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Lists all the documents in a collection.
         * @param collectionName - The collection where to count documents.
         * @param [callbackValueVariable] - The variable where to store the result.
         * @param [callbackStateVariable] - The variable where to store if the operation was successful.
         *
         * @deprecated Use a query without filters instead.
         */
        export const listDocuments = (
          collectionName: string,
          callbackValueVariable?: gdjs.Variable,
          callbackStateVariable?: gdjs.Variable
        ) => {
          firebase
            .firestore()
            .collection(collectionName)
            .get()
            .then((snapshot) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(
                  snapshot.empty ? 'empty' : 'ok'
                );

              if (callbackValueVariable)
                callbackValueVariable.fromJSObject(
                  snapshot.docs.map((doc) => doc.id)
                );
            })
            .catch((error) => {
              if (typeof callbackStateVariable !== 'undefined')
                callbackStateVariable.setString(error.message);
            });
        };

        /**
         * Returns a special string replaced by a firebase serverTimestamp field value.
         */
        export const getServerTimestamp = () =>
          '[{__FIREBASE_SERVERSIDE_TIMESTAMP}]';

        const replaceTimestampInString = (str: any) => {
          if (str === '[{__FIREBASE_SERVERSIDE_TIMESTAMP}]')
            return firebase.firestore.FieldValue.serverTimestamp();
          else return str;
        };

        const replaceTimestampsInObject = (object: object): object => {
          for (const i in object) {
            const item = object[i];
            if (typeof item === 'object') replaceTimestampsInObject(item);
            else if (item === '[{__FIREBASE_SERVERSIDE_TIMESTAMP}]')
              object[i] = firebase.firestore.FieldValue.serverTimestamp();
          }
          return object;
        };
      }
    }
  }
}
