export class ListMock implements IListService {

    public getAll(): Promise<Array<ListItem>> {
      return new Promise<Array<ListItem>>((resolve:any) => {

        const fakeData: Array<ListItem> = [

            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/JAVASCRIPT.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/JAVASCRIPT.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/JAVASCRIPT.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/JAVASCRIPT.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            }
        ];

        resolve(fakeData);
      });
    }
}


public getAll(): Promise<Array<ListItem>> {
      return new Promise<Array<ListItem>>((resolve:any) => {

        const fakeData: Array<ListItem> = [

            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/JAVASCRIPT.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/JAVASCRIPT.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/JAVASCRIPT.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/JAVASCRIPT.png'
            },
            {
                Title: 'A convergent value empowers the standard-setters',
                Description: 'The General Head of IT Strategy benchmarks business-for-business agilities',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/OFFICE365.png'
            },
            {
                Title: 'The Digital Marketers empower a digitized correlation',
                Description: 'Whereas synchronized brand values promote strategy formulations',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/POWERSHELL.png'
            },
            {
                Title: 'The market thinker strategically standardizes a competitive success',
                Description: 'The thinkers/planners benchmark a disciplined growth momentum',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/PYTHON.png'
            },
            {
                Title: 'We are going to secure our cross-pollinations',
                Description: 'We are working hard to reintermediate a competitive advantage, while the gatekeeper straightforwardly identifies barriers to success',
                ImageUrl: 'https://blog.velingeorgiev.com/static/images/SP.png'
            }
        ];

        resolve(fakeData);
      });
    }


() => {
    it('should display the github user', () => {
        render(<Profile user={mockUser} />)
        screen.getByRole('heading', { name: mockUser.name })
        expect(screen.getByAltText('Github Profile')).toHaveAttribute('src', mockUser.picture)
        expect(screen.getByRole('link', { name: mockUser.username })).toHaveAttribute('href', mockUser.url)
        screen.getByText(mockUser.description!)
        expect(screen.getByText('Repos').textContent).toEqual('Repos 1')
        expect(screen.getByText('Following').textContent).toEqual(`Following ${mockUser.following}`)
        expect(screen.getByText('Followers').textContent).toEqual(`Followers ${mockUser.followers}`)
    })

    it('should not display description if the user has no description', () => {
        let user: GithubUser = { ...mockUser }
        delete user.description
        render(<Profile user={user} />)
        screen.getByRole('heading', { name: mockUser.name })
        expect(screen.queryByText(mockUser.description!)).not.toBeInTheDocument()
    })
}


() => {
        render(<Profile user={mockUser} />)
        screen.getByRole('heading', { name: mockUser.name })
        expect(screen.getByAltText('Github Profile')).toHaveAttribute('src', mockUser.picture)
        expect(screen.getByRole('link', { name: mockUser.username })).toHaveAttribute('href', mockUser.url)
        screen.getByText(mockUser.description!)
        expect(screen.getByText('Repos').textContent).toEqual('Repos 1')
        expect(screen.getByText('Following').textContent).toEqual(`Following ${mockUser.following}`)
        expect(screen.getByText('Followers').textContent).toEqual(`Followers ${mockUser.followers}`)
    }


() => {
        let user: GithubUser = { ...mockUser }
        delete user.description
        render(<Profile user={user} />)
        screen.getByRole('heading', { name: mockUser.name })
        expect(screen.queryByText(mockUser.description!)).not.toBeInTheDocument()
    }


export class SearchAvailablePhoneNumbersCommand extends $Command<
  SearchAvailablePhoneNumbersCommandInput,
  SearchAvailablePhoneNumbersCommandOutput,
  ChimeClientResolvedConfig
> {
  // Start section: command_properties
  // End section: command_properties

  constructor(readonly input: SearchAvailablePhoneNumbersCommandInput) {
    // Start section: command_constructor
    super();
    // End section: command_constructor
  }

  resolveMiddleware(
    clientStack: MiddlewareStack<ServiceInputTypes, ServiceOutputTypes>,
    configuration: ChimeClientResolvedConfig,
    options?: __HttpHandlerOptions
  ): Handler<SearchAvailablePhoneNumbersCommandInput, SearchAvailablePhoneNumbersCommandOutput> {
    this.middlewareStack.use(getSerdePlugin(configuration, this.serialize, this.deserialize));

    const stack = clientStack.concat(this.middlewareStack);

    const { logger } = configuration;
    const clientName = "ChimeClient";
    const commandName = "SearchAvailablePhoneNumbersCommand";
    const handlerExecutionContext: HandlerExecutionContext = {
      logger,
      clientName,
      commandName,
      inputFilterSensitiveLog: SearchAvailablePhoneNumbersRequest.filterSensitiveLog,
      outputFilterSensitiveLog: SearchAvailablePhoneNumbersResponse.filterSensitiveLog,
    };

    if (typeof logger.info === "function") {
      logger.info({
        clientName,
        commandName,
      });
    }

    const { requestHandler } = configuration;
    return stack.resolve(
      (request: FinalizeHandlerArguments<any>) =>
        requestHandler.handle(request.request as __HttpRequest, options || {}),
      handlerExecutionContext
    );
  }

  private serialize(input: SearchAvailablePhoneNumbersCommandInput, context: __SerdeContext): Promise<__HttpRequest> {
    return serializeAws_restJson1SearchAvailablePhoneNumbersCommand(input, context);
  }

  private deserialize(
    output: __HttpResponse,
    context: __SerdeContext
  ): Promise<SearchAvailablePhoneNumbersCommandOutput> {
    return deserializeAws_restJson1SearchAvailablePhoneNumbersCommand(output, context);
  }

  // Start section: command_body_extra
  // End section: command_body_extra
}


export type SearchAvailablePhoneNumbersCommandInput = SearchAvailablePhoneNumbersRequest;


export type SearchAvailablePhoneNumbersCommandOutput = SearchAvailablePhoneNumbersResponse & __MetadataBearer;


resolveMiddleware(
    clientStack: MiddlewareStack<ServiceInputTypes, ServiceOutputTypes>,
    configuration: ChimeClientResolvedConfig,
    options?: __HttpHandlerOptions
  ): Handler<SearchAvailablePhoneNumbersCommandInput, SearchAvailablePhoneNumbersCommandOutput> {
    this.middlewareStack.use(getSerdePlugin(configuration, this.serialize, this.deserialize));

    const stack = clientStack.concat(this.middlewareStack);

    const { logger } = configuration;
    const clientName = "ChimeClient";
    const commandName = "SearchAvailablePhoneNumbersCommand";
    const handlerExecutionContext: HandlerExecutionContext = {
      logger,
      clientName,
      commandName,
      inputFilterSensitiveLog: SearchAvailablePhoneNumbersRequest.filterSensitiveLog,
      outputFilterSensitiveLog: SearchAvailablePhoneNumbersResponse.filterSensitiveLog,
    };

    if (typeof logger.info === "function") {
      logger.info({
        clientName,
        commandName,
      });
    }

    const { requestHandler } = configuration;
    return stack.resolve(
      (request: FinalizeHandlerArguments<any>) =>
        requestHandler.handle(request.request as __HttpRequest, options || {}),
      handlerExecutionContext
    );
  }


private serialize(input: SearchAvailablePhoneNumbersCommandInput, context: __SerdeContext): Promise<__HttpRequest> {
    return serializeAws_restJson1SearchAvailablePhoneNumbersCommand(input, context);
  }


private deserialize(
    output: __HttpResponse,
    context: __SerdeContext
  ): Promise<SearchAvailablePhoneNumbersCommandOutput> {
    return deserializeAws_restJson1SearchAvailablePhoneNumbersCommand(output, context);
  }


/*
 * Simple Action Creators (Internal)
 * We still need to export them for tests
 */
export function entryLoading(collection: Collection, slug: string) {
  return {
    type: ENTRY_REQUEST,
    payload: {
      collection: collection.get('name'),
      slug,
    },
  };
}


export function entryLoaded(collection: Collection, entry: EntryValue) {
  return {
    type: ENTRY_SUCCESS,
    payload: {
      collection: collection.get('name'),
      entry,
    },
  };
}


export function entryLoadError(error: Error, collection: Collection, slug: string) {
  return {
    type: ENTRY_FAILURE,
    payload: {
      error,
      collection: collection.get('name'),
      slug,
    },
  };
}


export function entriesLoading(collection: Collection) {
  return {
    type: ENTRIES_REQUEST,
    payload: {
      collection: collection.get('name'),
    },
  };
}


export function entriesLoaded(
  collection: Collection,
  entries: EntryValue[],
  pagination: number | null,
  cursor: Cursor,
  append = true,
) {
  return {
    type: ENTRIES_SUCCESS,
    payload: {
      collection: collection.get('name'),
      entries,
      page: pagination,
      cursor: Cursor.create(cursor),
      append,
    },
  };
}


export function entriesFailed(collection: Collection, error: Error) {
  return {
    type: ENTRIES_FAILURE,
    error: 'Failed to load entries',
    payload: error.toString(),
    meta: { collection: collection.get('name') },
  };
}


export function entryPersisting(collection: Collection, entry: EntryMap) {
  return {
    type: ENTRY_PERSIST_REQUEST,
    payload: {
      collectionName: collection.get('name'),
      entrySlug: entry.get('slug'),
    },
  };
}


export function entryPersisted(collection: Collection, entry: EntryMap, slug: string) {
  return {
    type: ENTRY_PERSIST_SUCCESS,
    payload: {
      collectionName: collection.get('name'),
      entrySlug: entry.get('slug'),

      /**
       * Pass slug from backend for newly created entries.
       */
      slug,
    },
  };
}


export function entryPersistFail(collection: Collection, entry: EntryMap, error: Error) {
  return {
    type: ENTRY_PERSIST_FAILURE,
    error: 'Failed to persist entry',
    payload: {
      collectionName: collection.get('name'),
      entrySlug: entry.get('slug'),
      error: error.toString(),
    },
  };
}


export function entryDeleting(collection: Collection, slug: string) {
  return {
    type: ENTRY_DELETE_REQUEST,
    payload: {
      collectionName: collection.get('name'),
      entrySlug: slug,
    },
  };
}


export function entryDeleted(collection: Collection, slug: string) {
  return {
    type: ENTRY_DELETE_SUCCESS,
    payload: {
      collectionName: collection.get('name'),
      entrySlug: slug,
    },
  };
}


export function entryDeleteFail(collection: Collection, slug: string, error: Error) {
  return {
    type: ENTRY_DELETE_FAILURE,
    payload: {
      collectionName: collection.get('name'),
      entrySlug: slug,
      error: error.toString(),
    },
  };
}


export function emptyDraftCreated(entry: EntryValue) {
  return {
    type: DRAFT_CREATE_EMPTY,
    payload: entry,
  };
}


/*
 * Exported simple Action Creators
 */
export function createDraftFromEntry(entry: EntryMap, metadata?: Map<string, unknown>) {
  return {
    type: DRAFT_CREATE_FROM_ENTRY,
    payload: { entry, metadata },
  };
}


export function draftDuplicateEntry(entry: EntryMap) {
  return {
    type: DRAFT_CREATE_DUPLICATE_FROM_ENTRY,
    payload: createEntry(entry.get('collection'), '', '', { data: entry.get('data') }),
  };
}


export function discardDraft() {
  return { type: DRAFT_DISCARD };
}


export function changeDraftField(field: string, value: string, metadata: Record<string, unknown>) {
  return {
    type: DRAFT_CHANGE_FIELD,
    payload: { field, value, metadata },
  };
}


export function changeDraftFieldValidation(
  uniquefieldId: string,
  errors: { type: string; message: string }[],
) {
  return {
    type: DRAFT_VALIDATION_ERRORS,
    payload: { uniquefieldId, errors },
  };
}


export function clearFieldErrors() {
  return { type: DRAFT_CLEAR_ERRORS };
}


export function localBackupRetrieved(entry: EntryValue) {
  return {
    type: DRAFT_LOCAL_BACKUP_RETRIEVED,
    payload: { entry },
  };
}


export function loadLocalBackup() {
  return {
    type: DRAFT_CREATE_FROM_LOCAL_BACKUP,
  };
}


export function addDraftEntryMediaFile(file: ImplementationMediaFile) {
  return { type: ADD_DRAFT_ENTRY_MEDIA_FILE, payload: file };
}


export function removeDraftEntryMediaFile({ id }: { id: string }) {
  return { type: REMOVE_DRAFT_ENTRY_MEDIA_FILE, payload: { id } };
}


export function persistLocalBackup(entry: EntryMap, collection: Collection) {
  return (_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);

    return backend.persistLocalDraftBackup(entry, collection);
  };
}


export function createDraftDuplicateFromEntry(entry: EntryMap) {
  return (dispatch: ThunkDispatch<State, {}, AnyAction>) => {
    dispatch(
      waitUntil({
        predicate: ({ type }) => type === DRAFT_CREATE_EMPTY,
        run: () => dispatch(draftDuplicateEntry(entry)),
      }),
    );
  };
}


export function retrieveLocalBackup(collection: Collection, slug: string) {
  return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);
    const { entry } = await backend.getLocalDraftBackup(collection, slug);

    if (entry) {
      // load assets from backup
      const mediaFiles = entry.mediaFiles || [];
      const assetProxies: AssetProxy[] = mediaFiles.map(file =>
        createAssetProxy({ path: file.path, file: file.file, url: file.url }),
      );
      dispatch(addAssets(assetProxies));

      return dispatch(localBackupRetrieved(entry));
    }
  };
}


export function deleteLocalBackup(collection: Collection, slug: string) {
  return (_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);
    return backend.deleteLocalDraftBackup(collection, slug);
  };
}


/*
 * Exported Thunk Action Creators
 */
export function loadEntry(collection: Collection, slug: string) {
  return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);
    dispatch(entryLoading(collection, slug));
    return backend
      .getEntry(state, collection, slug)
      .then((loadedEntry: EntryValue) => {
        return dispatch(entryLoaded(collection, loadedEntry));
      })
      .catch((error: Error) => {
        console.error(error);
        dispatch(
          notifSend({
            message: {
              details: error.message,
              key: 'ui.toast.onFailToLoadEntries',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        dispatch(entryLoadError(error, collection, slug));
      });
  };
}


export function loadEntries(collection: Collection, page = 0) {
  return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    if (collection.get('isFetching')) {
      return;
    }
    const state = getState();
    const backend = currentBackend(state.config);
    const integration = selectIntegration(state, collection.get('name'), 'listEntries');
    const provider = integration
      ? getIntegrationProvider(state.integrations, backend.getToken, integration)
      : backend;
    const append = !!(page && !isNaN(page) && page > 0);
    dispatch(entriesLoading(collection));
    provider
      .listEntries(collection, page)
      .then((response: { cursor: typeof Cursor }) => ({
        ...response,

        // The only existing backend using the pagination system is the
        // Algolia integration, which is also the only integration used
        // to list entries. Thus, this checking for an integration can
        // determine whether or not this is using the old integer-based
        // pagination API. Other backends will simply store an empty
        // cursor, which behaves identically to no cursor at all.
        cursor: integration
          ? Cursor.create({
              actions: ['next'],
              meta: { usingOldPaginationAPI: true },
              data: { nextPage: page + 1 },
            })
          : Cursor.create(response.cursor),
      }))
      .then((response: { cursor: Cursor; pagination: number; entries: EntryValue[] }) =>
        dispatch(
          entriesLoaded(
            collection,
            response.cursor.meta!.get('usingOldPaginationAPI')
              ? response.entries.reverse()
              : response.entries,
            response.pagination,
            addAppendActionsToCursor(response.cursor),
            append,
          ),
        ),
      )
      .catch((err: Error) => {
        dispatch(
          notifSend({
            message: {
              details: err,
              key: 'ui.toast.onFailToLoadEntries',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        return Promise.reject(dispatch(entriesFailed(collection, err)));
      });
  };
}


function traverseCursor(backend: Backend, cursor: Cursor, action: string) {
  if (!cursor.actions!.has(action)) {
    throw new Error(`The current cursor does not support the pagination action "${action}".`);
  }
  return backend.traverseCursor(cursor, action);
}


export function traverseCollectionCursor(collection: Collection, action: string) {
  return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const collectionName = collection.get('name');
    if (state.entries.getIn(['pages', `${collectionName}`, 'isFetching'])) {
      return;
    }
    const backend = currentBackend(state.config);

    const { action: realAction, append } = appendActions.has(action)
      ? appendActions.get(action).toJS()
      : { action, append: false };
    const cursor = selectCollectionEntriesCursor(state.cursors, collection.get('name'));

    // Handle cursors representing pages in the old, integer-based
    // pagination API
    if (cursor.meta!.get('usingOldPaginationAPI', false)) {
      return dispatch(loadEntries(collection, cursor.data!.get('nextPage') as number));
    }

    try {
      dispatch(entriesLoading(collection));
      const { entries, cursor: newCursor } = await traverseCursor(backend, cursor, realAction);
      // Pass null for the old pagination argument - this will
      // eventually be removed.
      return dispatch(
        entriesLoaded(collection, entries, null, addAppendActionsToCursor(newCursor), append),
      );
    } catch (err) {
      console.error(err);
      dispatch(
        notifSend({
          message: {
            details: err,
            key: 'ui.toast.onFailToPersist',
          },
          kind: 'danger',
          dismissAfter: 8000,
        }),
      );
      return Promise.reject(dispatch(entriesFailed(collection, err)));
    }
  };
}


export function createEmptyDraft(collection: Collection) {
  return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const dataFields = createEmptyDraftData(collection.get('fields', List()));

    let mediaFiles = [] as MediaFile[];
    if (!collection.has('media_folder')) {
      await waitForMediaLibraryToLoad(dispatch, getState());
      mediaFiles = getState().mediaLibrary.get('files');
    }

    const newEntry = createEntry(collection.get('name'), '', '', { data: dataFields, mediaFiles });
    dispatch(emptyDraftCreated(newEntry));
  };
}


export function createEmptyDraftData(fields: EntryFields, withNameKey = true) {
  return fields.reduce(
    (reduction: DraftEntryData | string | undefined, value: EntryField | undefined) => {
      const acc = reduction as DraftEntryData;
      const item = value as EntryField;
      const subfields = item.get('field') || item.get('fields');
      const list = item.get('widget') == 'list';
      const name = item.get('name');
      const defaultValue = item.get('default', null);
      const isEmptyDefaultValue = (val: unknown) => [[{}], {}].some(e => isEqual(val, e));

      if (List.isList(subfields)) {
        const subDefaultValue = list
          ? [createEmptyDraftData(subfields as EntryFields)]
          : createEmptyDraftData(subfields as EntryFields);
        if (!isEmptyDefaultValue(subDefaultValue)) {
          acc[name] = subDefaultValue;
        }
        return acc;
      }

      if (Map.isMap(subfields)) {
        const subDefaultValue = list
          ? [createEmptyDraftData(List([subfields as EntryField]), false)]
          : createEmptyDraftData(List([subfields as EntryField]));
        if (!isEmptyDefaultValue(subDefaultValue)) {
          acc[name] = subDefaultValue;
        }
        return acc;
      }

      if (defaultValue !== null) {
        if (!withNameKey) {
          return defaultValue;
        }
        acc[name] = defaultValue;
      }

      return acc;
    },
    {} as DraftEntryData,
  );
}


export async function getMediaAssets({
  getState,
  dispatch,
  collection,
  entry,
}: {
  getState: () => State;
  collection: Collection;
  entry: EntryMap;
  dispatch: Dispatch;
}) {
  const filesArray = entry.get('mediaFiles').toJS() as MediaFile[];
  const assets = await Promise.all(
    filesArray
      .filter(file => file.draft)
      .map(file => getAsset({ collection, entry, path: file.path })(dispatch, getState)),
  );

  return assets;
}


export function persistEntry(collection: Collection) {
  return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const entryDraft = state.entryDraft;
    const fieldsErrors = entryDraft.get('fieldsErrors');
    const usedSlugs = selectPublishedSlugs(state, collection.get('name'));

    // Early return if draft contains validation errors
    if (!fieldsErrors.isEmpty()) {
      const hasPresenceErrors = fieldsErrors.some(errors =>
        errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE),
      );

      if (hasPresenceErrors) {
        dispatch(
          notifSend({
            message: {
              key: 'ui.toast.missingRequiredField',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
      }

      return Promise.reject();
    }

    const backend = currentBackend(state.config);
    const entry = entryDraft.get('entry');
    const assetProxies = await getMediaAssets({
      getState,
      dispatch,
      collection,
      entry,
    });

    /**
     * Serialize the values of any fields with registered serializers, and
     * update the entry and entryDraft with the serialized values.
     */
    const fields = selectFields(collection, entry.get('slug'));
    const serializedData = serializeValues(entryDraft.getIn(['entry', 'data']), fields);
    const serializedEntry = entry.set('data', serializedData);
    const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
    dispatch(entryPersisting(collection, serializedEntry));
    return backend
      .persistEntry({
        config: state.config,
        collection,
        entryDraft: serializedEntryDraft,
        assetProxies,
        usedSlugs,
      })
      .then((slug: string) => {
        dispatch(
          notifSend({
            message: {
              key: 'ui.toast.entrySaved',
            },
            kind: 'success',
            dismissAfter: 4000,
          }),
        );
        // re-load media library if entry had media files
        if (assetProxies.length > 0) {
          dispatch(loadMedia());
        }
        dispatch(entryPersisted(collection, serializedEntry, slug));
      })
      .catch((error: Error) => {
        console.error(error);
        dispatch(
          notifSend({
            message: {
              details: error,
              key: 'ui.toast.onFailToPersist',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        return Promise.reject(dispatch(entryPersistFail(collection, serializedEntry, error)));
      });
  };
}


export function deleteEntry(collection: Collection, slug: string) {
  return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);

    dispatch(entryDeleting(collection, slug));
    return backend
      .deleteEntry(state.config, collection, slug)
      .then(() => {
        return dispatch(entryDeleted(collection, slug));
      })
      .catch((error: Error) => {
        dispatch(
          notifSend({
            message: {
              details: error,
              key: 'ui.toast.onFailToDelete',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        console.error(error);
        return Promise.reject(dispatch(entryDeleteFail(collection, slug, error)));
      });
  };
}


(_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);

    return backend.persistLocalDraftBackup(entry, collection);
  }


(dispatch: ThunkDispatch<State, {}, AnyAction>) => {
    dispatch(
      waitUntil({
        predicate: ({ type }) => type === DRAFT_CREATE_EMPTY,
        run: () => dispatch(draftDuplicateEntry(entry)),
      }),
    );
  }


({ type }) => type === DRAFT_CREATE_EMPTY


() => dispatch(draftDuplicateEntry(entry))


async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);
    const { entry } = await backend.getLocalDraftBackup(collection, slug);

    if (entry) {
      // load assets from backup
      const mediaFiles = entry.mediaFiles || [];
      const assetProxies: AssetProxy[] = mediaFiles.map(file =>
        createAssetProxy({ path: file.path, file: file.file, url: file.url }),
      );
      dispatch(addAssets(assetProxies));

      return dispatch(localBackupRetrieved(entry));
    }
  }


file =>
        createAssetProxy({ path: file.path, file: file.file, url: file.url })


(_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);
    return backend.deleteLocalDraftBackup(collection, slug);
  }


(dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);
    dispatch(entryLoading(collection, slug));
    return backend
      .getEntry(state, collection, slug)
      .then((loadedEntry: EntryValue) => {
        return dispatch(entryLoaded(collection, loadedEntry));
      })
      .catch((error: Error) => {
        console.error(error);
        dispatch(
          notifSend({
            message: {
              details: error.message,
              key: 'ui.toast.onFailToLoadEntries',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        dispatch(entryLoadError(error, collection, slug));
      });
  }


(loadedEntry: EntryValue) => {
        return dispatch(entryLoaded(collection, loadedEntry));
      }


(error: Error) => {
        console.error(error);
        dispatch(
          notifSend({
            message: {
              details: error.message,
              key: 'ui.toast.onFailToLoadEntries',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        dispatch(entryLoadError(error, collection, slug));
      }


(cursor: Cursor) => {
  return Cursor.create(cursor).updateStore('actions', (actions: Set<string>) => {
    return actions.union(
      appendActions
        .filter((v: Map<string, string | boolean>) => actions.has(v.get('action') as string))
        .keySeq(),
    );
  });
}


(actions: Set<string>) => {
    return actions.union(
      appendActions
        .filter((v: Map<string, string | boolean>) => actions.has(v.get('action') as string))
        .keySeq(),
    );
  }


(v: Map<string, string | boolean>) => actions.has(v.get('action') as string)


(dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    if (collection.get('isFetching')) {
      return;
    }
    const state = getState();
    const backend = currentBackend(state.config);
    const integration = selectIntegration(state, collection.get('name'), 'listEntries');
    const provider = integration
      ? getIntegrationProvider(state.integrations, backend.getToken, integration)
      : backend;
    const append = !!(page && !isNaN(page) && page > 0);
    dispatch(entriesLoading(collection));
    provider
      .listEntries(collection, page)
      .then((response: { cursor: typeof Cursor }) => ({
        ...response,

        // The only existing backend using the pagination system is the
        // Algolia integration, which is also the only integration used
        // to list entries. Thus, this checking for an integration can
        // determine whether or not this is using the old integer-based
        // pagination API. Other backends will simply store an empty
        // cursor, which behaves identically to no cursor at all.
        cursor: integration
          ? Cursor.create({
              actions: ['next'],
              meta: { usingOldPaginationAPI: true },
              data: { nextPage: page + 1 },
            })
          : Cursor.create(response.cursor),
      }))
      .then((response: { cursor: Cursor; pagination: number; entries: EntryValue[] }) =>
        dispatch(
          entriesLoaded(
            collection,
            response.cursor.meta!.get('usingOldPaginationAPI')
              ? response.entries.reverse()
              : response.entries,
            response.pagination,
            addAppendActionsToCursor(response.cursor),
            append,
          ),
        ),
      )
      .catch((err: Error) => {
        dispatch(
          notifSend({
            message: {
              details: err,
              key: 'ui.toast.onFailToLoadEntries',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        return Promise.reject(dispatch(entriesFailed(collection, err)));
      });
  }


(response: { cursor: typeof Cursor }) => ({
        ...response,

        // The only existing backend using the pagination system is the
        // Algolia integration, which is also the only integration used
        // to list entries. Thus, this checking for an integration can
        // determine whether or not this is using the old integer-based
        // pagination API. Other backends will simply store an empty
        // cursor, which behaves identically to no cursor at all.
        cursor: integration
          ? Cursor.create({
              actions: ['next'],
              meta: { usingOldPaginationAPI: true },
              data: { nextPage: page + 1 },
            })
          : Cursor.create(response.cursor),
      })


(response: { cursor: Cursor; pagination: number; entries: EntryValue[] }) =>
        dispatch(
          entriesLoaded(
            collection,
            response.cursor.meta!.get('usingOldPaginationAPI')
              ? response.entries.reverse()
              : response.entries,
            response.pagination,
            addAppendActionsToCursor(response.cursor),
            append,
          ),
        )


(err: Error) => {
        dispatch(
          notifSend({
            message: {
              details: err,
              key: 'ui.toast.onFailToLoadEntries',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        return Promise.reject(dispatch(entriesFailed(collection, err)));
      }


async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const collectionName = collection.get('name');
    if (state.entries.getIn(['pages', `${collectionName}`, 'isFetching'])) {
      return;
    }
    const backend = currentBackend(state.config);

    const { action: realAction, append } = appendActions.has(action)
      ? appendActions.get(action).toJS()
      : { action, append: false };
    const cursor = selectCollectionEntriesCursor(state.cursors, collection.get('name'));

    // Handle cursors representing pages in the old, integer-based
    // pagination API
    if (cursor.meta!.get('usingOldPaginationAPI', false)) {
      return dispatch(loadEntries(collection, cursor.data!.get('nextPage') as number));
    }

    try {
      dispatch(entriesLoading(collection));
      const { entries, cursor: newCursor } = await traverseCursor(backend, cursor, realAction);
      // Pass null for the old pagination argument - this will
      // eventually be removed.
      return dispatch(
        entriesLoaded(collection, entries, null, addAppendActionsToCursor(newCursor), append),
      );
    } catch (err) {
      console.error(err);
      dispatch(
        notifSend({
          message: {
            details: err,
            key: 'ui.toast.onFailToPersist',
          },
          kind: 'danger',
          dismissAfter: 8000,
        }),
      );
      return Promise.reject(dispatch(entriesFailed(collection, err)));
    }
  }


async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const dataFields = createEmptyDraftData(collection.get('fields', List()));

    let mediaFiles = [] as MediaFile[];
    if (!collection.has('media_folder')) {
      await waitForMediaLibraryToLoad(dispatch, getState());
      mediaFiles = getState().mediaLibrary.get('files');
    }

    const newEntry = createEntry(collection.get('name'), '', '', { data: dataFields, mediaFiles });
    dispatch(emptyDraftCreated(newEntry));
  }


(reduction: DraftEntryData | string | undefined, value: EntryField | undefined) => {
      const acc = reduction as DraftEntryData;
      const item = value as EntryField;
      const subfields = item.get('field') || item.get('fields');
      const list = item.get('widget') == 'list';
      const name = item.get('name');
      const defaultValue = item.get('default', null);
      const isEmptyDefaultValue = (val: unknown) => [[{}], {}].some(e => isEqual(val, e));

      if (List.isList(subfields)) {
        const subDefaultValue = list
          ? [createEmptyDraftData(subfields as EntryFields)]
          : createEmptyDraftData(subfields as EntryFields);
        if (!isEmptyDefaultValue(subDefaultValue)) {
          acc[name] = subDefaultValue;
        }
        return acc;
      }

      if (Map.isMap(subfields)) {
        const subDefaultValue = list
          ? [createEmptyDraftData(List([subfields as EntryField]), false)]
          : createEmptyDraftData(List([subfields as EntryField]));
        if (!isEmptyDefaultValue(subDefaultValue)) {
          acc[name] = subDefaultValue;
        }
        return acc;
      }

      if (defaultValue !== null) {
        if (!withNameKey) {
          return defaultValue;
        }
        acc[name] = defaultValue;
      }

      return acc;
    }


(val: unknown) => [[{}], {}].some(e => isEqual(val, e))


e => isEqual(val, e)


file => file.draft


file => getAsset({ collection, entry, path: file.path })(dispatch, getState)


async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const entryDraft = state.entryDraft;
    const fieldsErrors = entryDraft.get('fieldsErrors');
    const usedSlugs = selectPublishedSlugs(state, collection.get('name'));

    // Early return if draft contains validation errors
    if (!fieldsErrors.isEmpty()) {
      const hasPresenceErrors = fieldsErrors.some(errors =>
        errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE),
      );

      if (hasPresenceErrors) {
        dispatch(
          notifSend({
            message: {
              key: 'ui.toast.missingRequiredField',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
      }

      return Promise.reject();
    }

    const backend = currentBackend(state.config);
    const entry = entryDraft.get('entry');
    const assetProxies = await getMediaAssets({
      getState,
      dispatch,
      collection,
      entry,
    });

    /**
     * Serialize the values of any fields with registered serializers, and
     * update the entry and entryDraft with the serialized values.
     */
    const fields = selectFields(collection, entry.get('slug'));
    const serializedData = serializeValues(entryDraft.getIn(['entry', 'data']), fields);
    const serializedEntry = entry.set('data', serializedData);
    const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
    dispatch(entryPersisting(collection, serializedEntry));
    return backend
      .persistEntry({
        config: state.config,
        collection,
        entryDraft: serializedEntryDraft,
        assetProxies,
        usedSlugs,
      })
      .then((slug: string) => {
        dispatch(
          notifSend({
            message: {
              key: 'ui.toast.entrySaved',
            },
            kind: 'success',
            dismissAfter: 4000,
          }),
        );
        // re-load media library if entry had media files
        if (assetProxies.length > 0) {
          dispatch(loadMedia());
        }
        dispatch(entryPersisted(collection, serializedEntry, slug));
      })
      .catch((error: Error) => {
        console.error(error);
        dispatch(
          notifSend({
            message: {
              details: error,
              key: 'ui.toast.onFailToPersist',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        return Promise.reject(dispatch(entryPersistFail(collection, serializedEntry, error)));
      });
  }


errors =>
        errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE)


error => error.type && error.type === ValidationErrorTypes.PRESENCE


(slug: string) => {
        dispatch(
          notifSend({
            message: {
              key: 'ui.toast.entrySaved',
            },
            kind: 'success',
            dismissAfter: 4000,
          }),
        );
        // re-load media library if entry had media files
        if (assetProxies.length > 0) {
          dispatch(loadMedia());
        }
        dispatch(entryPersisted(collection, serializedEntry, slug));
      }


(error: Error) => {
        console.error(error);
        dispatch(
          notifSend({
            message: {
              details: error,
              key: 'ui.toast.onFailToPersist',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        return Promise.reject(dispatch(entryPersistFail(collection, serializedEntry, error)));
      }


(dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
    const state = getState();
    const backend = currentBackend(state.config);

    dispatch(entryDeleting(collection, slug));
    return backend
      .deleteEntry(state.config, collection, slug)
      .then(() => {
        return dispatch(entryDeleted(collection, slug));
      })
      .catch((error: Error) => {
        dispatch(
          notifSend({
            message: {
              details: error,
              key: 'ui.toast.onFailToDelete',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        console.error(error);
        return Promise.reject(dispatch(entryDeleteFail(collection, slug, error)));
      });
  }


() => {
        return dispatch(entryDeleted(collection, slug));
      }


(error: Error) => {
        dispatch(
          notifSend({
            message: {
              details: error,
              key: 'ui.toast.onFailToDelete',
            },
            kind: 'danger',
            dismissAfter: 8000,
          }),
        );
        console.error(error);
        return Promise.reject(dispatch(entryDeleteFail(collection, slug, error)));
      }


interface DraftEntryData {
  [name: string]: string | null | DraftEntryData | DraftEntryData[] | (string | DraftEntryData)[];
}


export default class Web3Client {
  public parentWeb3: Web3
  public web3: Web3
  public parentDefaultOptions: SendOptions
  public maticDefaultOptions: SendOptions
  public events: any

  constructor(parentProvider, maticProvider, parentDefaultOptions, maticDefaultOptions) {
    this.parentWeb3 = new Web3(parentProvider)
    this.web3 = new Web3(maticProvider)
    this.parentDefaultOptions = parentDefaultOptions
    this.maticDefaultOptions = maticDefaultOptions
    this.web3.extend({
      property: 'bor',
      methods: [
        {
          name: 'getRootHash',
          call: 'eth_getRootHash',
          params: 2,
          inputFormatter: [Number, Number],
          outputFormatter: String,
        },
      ],
    })
  }

  set wallet(_wallet) {
    this.parentWeb3.eth.accounts.wallet.add(_wallet)
    this.web3.eth.accounts.wallet.add(_wallet)
  }

  async call(method, options?: SendOptions) {
    return method.call(options || this.parentDefaultOptions)
  }

  async fillOptions(txObject: any, onRootChain: boolean, options?: SendOptions) {
    if (onRootChain) {
      return this._fillOptions(txObject, this.parentWeb3, options || this.parentDefaultOptions)
    }
    return this._fillOptions(txObject, this.web3, options || this.maticDefaultOptions)
  }

  private async _fillOptions(txObject, web3, _options) {
    if (!_options.from) throw new Error('from is not specified')
    const from = _options.from
    delete txObject.chainId

    const [gasLimit, gasPrice, nonce, chainId] = await Promise.all([
      !(_options.gasLimit || _options.gas)
        ? txObject.estimateGas({ from, value: _options.value })
        : _options.gasLimit || _options.gas,
      !_options.gasPrice ? web3.eth.getGasPrice() : _options.gasPrice,
      !_options.nonce ? web3.eth.getTransactionCount(from, 'pending') : _options.nonce,
      !_options.chainId ? web3.eth.net.getId() : _options.chainId,
    ])

    return {
      from,
      gas: gasLimit,
      gasLimit: gasLimit,
      gasPrice,
      nonce,
      chainId,
      value: _options.value || 0,
      to: _options.to || null,
      data: _options.data,
      encodeAbi: _options.encodeAbi || false,
    }
  }

  wrapWeb3Promise(promise, callbacks) {
    if (callbacks) {
      if (callbacks.onTransactionHash) {
        promise.on('transactionHash', callbacks.onTransactionHash)
      }
      if (callbacks.onReceipt) {
        promise.on('receipt', callbacks.onReceipt)
      }
      if (callbacks.onConfirmation) {
        promise.on('confirmation', callbacks.onConfirmation)
      }
      if (callbacks.onError) {
        promise.on('error', callbacks.onError)
      }
    }
    return promise
  }

  send(txObject, web3Options?, callbacks?) {
    const _web3Options = web3Options || {}

    // since we use the delegated proxy patterns, the following should be a good way to provide enough gas
    // apparently even when provided with a buffer of 20k, the call reverts. This shouldn't be happening because the actual gas used is less than what the estimation returns
    // providing higher buffer for now
    // @todo handle hex values of gas

    if (web3Options.parent) {
      _web3Options.gas = (_web3Options.gas || this.parentDefaultOptions.gas) + EXTRAGASFORPROXYCALL
      _web3Options.gasPrice = _web3Options.gasPrice || this.parentDefaultOptions.gasPrice
    } else {
      _web3Options.gas = _web3Options.gas || this.maticDefaultOptions.gas
      _web3Options.gasPrice = _web3Options.gasPrice || this.maticDefaultOptions.gasPrice
    }
    logger.debug('sending tx with', { _web3Options })
    return this.wrapWeb3Promise(txObject.send(_web3Options), callbacks)
  }

  getParentWeb3() {
    return this.parentWeb3
  }

  getMaticWeb3() {
    return this.web3
  }

  getWallet() {
    return this.web3.eth.accounts.wallet
  }

  setParentDefaultOptions(options: any) {
    this.parentDefaultOptions = options
  }

  setMaticDefaultOptions(options: any) {
    this.maticDefaultOptions = options
  }

  setParentProvider(provider) {
    this.parentWeb3 = new Web3(provider)
  }
}


async call(method, options?: SendOptions) {
    return method.call(options || this.parentDefaultOptions)
  }


async fillOptions(txObject: any, onRootChain: boolean, options?: SendOptions) {
    if (onRootChain) {
      return this._fillOptions(txObject, this.parentWeb3, options || this.parentDefaultOptions)
    }
    return this._fillOptions(txObject, this.web3, options || this.maticDefaultOptions)
  }


private async _fillOptions(txObject, web3, _options) {
    if (!_options.from) throw new Error('from is not specified')
    const from = _options.from
    delete txObject.chainId

    const [gasLimit, gasPrice, nonce, chainId] = await Promise.all([
      !(_options.gasLimit || _options.gas)
        ? txObject.estimateGas({ from, value: _options.value })
        : _options.gasLimit || _options.gas,
      !_options.gasPrice ? web3.eth.getGasPrice() : _options.gasPrice,
      !_options.nonce ? web3.eth.getTransactionCount(from, 'pending') : _options.nonce,
      !_options.chainId ? web3.eth.net.getId() : _options.chainId,
    ])

    return {
      from,
      gas: gasLimit,
      gasLimit: gasLimit,
      gasPrice,
      nonce,
      chainId,
      value: _options.value || 0,
      to: _options.to || null,
      data: _options.data,
      encodeAbi: _options.encodeAbi || false,
    }
  }


wrapWeb3Promise(promise, callbacks) {
    if (callbacks) {
      if (callbacks.onTransactionHash) {
        promise.on('transactionHash', callbacks.onTransactionHash)
      }
      if (callbacks.onReceipt) {
        promise.on('receipt', callbacks.onReceipt)
      }
      if (callbacks.onConfirmation) {
        promise.on('confirmation', callbacks.onConfirmation)
      }
      if (callbacks.onError) {
        promise.on('error', callbacks.onError)
      }
    }
    return promise
  }


send(txObject, web3Options?, callbacks?) {
    const _web3Options = web3Options || {}

    // since we use the delegated proxy patterns, the following should be a good way to provide enough gas
    // apparently even when provided with a buffer of 20k, the call reverts. This shouldn't be happening because the actual gas used is less than what the estimation returns
    // providing higher buffer for now
    // @todo handle hex values of gas

    if (web3Options.parent) {
      _web3Options.gas = (_web3Options.gas || this.parentDefaultOptions.gas) + EXTRAGASFORPROXYCALL
      _web3Options.gasPrice = _web3Options.gasPrice || this.parentDefaultOptions.gasPrice
    } else {
      _web3Options.gas = _web3Options.gas || this.maticDefaultOptions.gas
      _web3Options.gasPrice = _web3Options.gasPrice || this.maticDefaultOptions.gasPrice
    }
    logger.debug('sending tx with', { _web3Options })
    return this.wrapWeb3Promise(txObject.send(_web3Options), callbacks)
  }


getParentWeb3() {
    return this.parentWeb3
  }


getMaticWeb3() {
    return this.web3
  }


getWallet() {
    return this.web3.eth.accounts.wallet
  }


setParentDefaultOptions(options: any) {
    this.parentDefaultOptions = options
  }


setMaticDefaultOptions(options: any) {
    this.maticDefaultOptions = options
  }


setParentProvider(provider) {
    this.parentWeb3 = new Web3(provider)
  }


export function isDate(val: any): val is Date {
  return toString.call(val) === '[object Date]'
}


// export function isObject(val: any): val is Object {
//   return val !== null && typeof val === 'object'
// }
export function isPlainObject(val: any): val is Object {
  return toString.call(val) === '[object Object]'
}


export function extend<T, U>(to: T, from: U): T & U {
  for (const key in from) {
    ;(to as T & U)[key] = from[key] as any
  }
  return to as T & U
}


// 深拷贝
export function deepMerge(...objs: any[]): any {
  const result = Object.create(null)
  objs.forEach(obj => {
    if (obj) {
      Object.keys(obj).forEach(key => {
        const val = obj[key]
        if (isPlainObject(val)) {
          // 递归
          if (isPlainObject(result[key])) {
            result[key] = deepMerge(result[key], val)
          } else {
            result[key] = deepMerge(val)
          }
        } else {
          result[key] = val
        }
      })
    }
  })
  return result
}


export function isFormData(val: any): val is FormData {
  return typeof val !== 'undefined' && val instanceof FormData
}


export function isURLSearchParams(val: any): val is URLSearchParams {
  return typeof val !== 'undefined' && val instanceof URLSearchParams
}


obj => {
    if (obj) {
      Object.keys(obj).forEach(key => {
        const val = obj[key]
        if (isPlainObject(val)) {
          // 递归
          if (isPlainObject(result[key])) {
            result[key] = deepMerge(result[key], val)
          } else {
            result[key] = deepMerge(val)
          }
        } else {
          result[key] = val
        }
      })
    }
  }


key => {
        const val = obj[key]
        if (isPlainObject(val)) {
          // 递归
          if (isPlainObject(result[key])) {
            result[key] = deepMerge(result[key], val)
          } else {
            result[key] = deepMerge(val)
          }
        } else {
          result[key] = val
        }
      }


export class ServerInfo {
  constructor(private client: Client) {
  }

  /**
   * Returns information about the Jira instance.
   *
   * This operation can be accessed anonymously.
   *
   * **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:** None. */
  async getServerInfo<T = Models.ServerInformation>(callback: Callback<T>): Promise<void>;
  /**
   * Returns information about the Jira instance.
   *
   * This operation can be accessed anonymously.
   *
   * **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:** None. */
  async getServerInfo<T = Models.ServerInformation>(callback?: never): Promise<T>;
  async getServerInfo<T = Models.ServerInformation>(callback?: Callback<T>): Promise<void | T> {
    const config: RequestConfig = {
      url: '/rest/api/2/serverInfo',
      method: 'GET',
    };

    return this.client.sendRequest(config, callback, { methodName: 'version2.serverInfo.getServerInfo' });
  }
}


/**
   * Returns information about the Jira instance.
   *
   * This operation can be accessed anonymously.
   *
   * **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:** None. */
async getServerInfo<T = Models.ServerInformation>(callback: Callback<T>): Promise<void>;


/**
   * Returns information about the Jira instance.
   *
   * This operation can be accessed anonymously.
   *
   * **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:** None. */
async getServerInfo<T = Models.ServerInformation>(callback?: never): Promise<T>;


async getServerInfo<T = Models.ServerInformation>(callback?: Callback<T>): Promise<void | T> {
    const config: RequestConfig = {
      url: '/rest/api/2/serverInfo',
      method: 'GET',
    };

    return this.client.sendRequest(config, callback, { methodName: 'version2.serverInfo.getServerInfo' });
  }


export class LineDashedMaterial extends LineBasicMaterial {

	constructor( parameters?: LineDashedMaterialParameters );

	scale: number;
	dashSize: number;
	gapSize: number;
	readonly isLineDashedMaterial: true;

	setValues( parameters: LineDashedMaterialParameters ): void;

}


export interface LineDashedMaterialParameters extends LineBasicMaterialParameters {
	scale?: number;
	dashSize?: number;
	gapSize?: number;
}


setValues( parameters: LineDashedMaterialParameters ): void;


({ color = '#A5A5A5' }) => (
    <svg width="26px"


export class RespostaPesquisaLoja {
    idPesquisa: string;
    texto_chave: string;
    cpf: string;
    nome: string;
    data_nasc: string;
    sexo: string;
    tamanho_roupa: string;
    data_pesquisa: string;
    timer:number;
    cronometro:string;
    ver_botao:boolean;
    resultado_vr: Broadcast;
    url: string[];
    idLoja:string;
}


@Component({
  selector: 'app-cadastro-tarefa',
  templateUrl: './cadastro-tarefa.component.html',
  styleUrls: ['./cadastro-tarefa.component.css']
})
export class CadastroTarefaComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}


(attrs: any) => {
    this.setState({
      isOpen: attrs.isOpen,
    });
  }


() => {
    this.onOpenChange({ isOpen: !this.state.isOpen });
  }


(disabled: boolean) => (
      <ToolbarButton
        spacing={isReducedSpacing ? 'none' : 'default'}


() => {
    const {
      textFormattingState,
      clearFormattingState,
      editorView,
      intl: { formatMessage },
    } = this.props;
    const { code, underline, subsup, strike } = editorView.state.schema.marks;
    let items: any[] = [];

    if (textFormattingState) {
      const {
        underlineHidden,
        codeHidden,
        strikeHidden,
        subscriptHidden,
        superscriptHidden,
      } = textFormattingState;
      if (!underlineHidden && underline) {
        this.addRecordToItems(
          items,
          formatMessage(messages.underline),
          'underline',
          tooltip(toggleUnderline),
        );
      }
      if (!strikeHidden && strike) {
        this.addRecordToItems(
          items,
          formatMessage(messages.strike),
          'strike',
          tooltip(toggleStrikethrough),
        );
      }
      if (!codeHidden && code) {
        this.addRecordToItems(
          items,
          formatMessage(messages.code),
          'code',
          tooltip(toggleCode),
        );
      }
      if (!subscriptHidden && subsup) {
        this.addRecordToItems(
          items,
          formatMessage(messages.subscript),
          'subscript',
        );
      }
      if (!superscriptHidden && subsup) {
        this.addRecordToItems(
          items,
          formatMessage(messages.superscript),
          'superscript',
        );
      }
    }
    if (clearFormattingState) {
      this.addRecordToItems(
        items,
        formatMessage(messages.clearFormatting),
        'clearFormatting',
        tooltip(clearFormattingKeymap),
        !clearFormattingState.formattingIsPresent,
      );
    }
    return [{ items }];
  }


(
    items: Array<any>,
    content: string,
    value: string,
    tooltip?: string,
    isDisabled?: boolean,
  ) => {
    let active = false;
    let disabled = false;
    if (this.props.textFormattingState) {
      active =
        this.props.textFormattingState![
          `${value}Active` as keyof TextFormattingState
        ] || false;
      disabled =
        isDisabled ||
        this.props.textFormattingState![
          `${value}Disabled` as keyof TextFormattingState
        ] ||
        false;
    }
    items.push({
      key: value,
      content,
      elemAfter: <Shortcut>{tooltip}</Shortcut>,
      value,
      isActive: active,
      isDisabled: disabled,
    });
  }


({ item }: { item: any }) => {
    analyticsService.trackEvent(`atlassian.editor.format.${item.value}.button`);

    const { state, dispatch } = this.props.editorView;
    switch (item.value) {
      case 'underline':
        commands.toggleUnderlineWithAnalytics({
          inputMethod: INPUT_METHOD.TOOLBAR,
        })(state, dispatch);
        break;
      case 'code':
        commands.toggleCodeWithAnalytics({ inputMethod: INPUT_METHOD.TOOLBAR })(
          state,
          dispatch,
        );
        break;
      case 'strike':
        commands.toggleStrikeWithAnalytics({
          inputMethod: INPUT_METHOD.TOOLBAR,
        })(state, dispatch);
        break;
      case 'subscript':
        commands.toggleSubscriptWithAnalytics()(state, dispatch);
        break;
      case 'superscript':
        commands.toggleSuperscriptWithAnalytics()(state, dispatch);
        break;
      case 'clearFormatting':
        clearFormattingWithAnalytics(INPUT_METHOD.TOOLBAR)(state, dispatch);
        break;
    }
    this.setState({ isOpen: false });
  }


class ToolbarAdvancedTextFormatting extends PureComponent<
  Props & InjectedIntlProps,
  State
> {
  state: State = {
    isOpen: false,
  };

  private onOpenChange = (attrs: any) => {
    this.setState({
      isOpen: attrs.isOpen,
    });
  };

  private handleTriggerClick = () => {
    this.onOpenChange({ isOpen: !this.state.isOpen });
  };

  render() {
    const { isOpen } = this.state;
    const {
      popupsMountPoint,
      popupsBoundariesElement,
      popupsScrollableElement,
      isReducedSpacing,
      textFormattingState = {},
      clearFormattingState = {},
      intl: { formatMessage },
    } = this.props;
    const {
      codeActive,
      underlineActive,
      strikeActive,
      subscriptActive,
      superscriptActive,
      codeDisabled,
      underlineDisabled,
      strikeDisabled,
      subscriptDisabled,
      superscriptDisabled,
    } = textFormattingState;
    const { formattingIsPresent } = clearFormattingState;
    const items = this.createItems();
    const labelMoreFormatting = formatMessage(messages.moreFormatting);

    const toolbarButtonFactory = (disabled: boolean) => (
      <ToolbarButton
        spacing={isReducedSpacing ? 'none' : 'default'}
        selected={
          isOpen ||
          underlineActive ||
          codeActive ||
          strikeActive ||
          subscriptActive ||
          superscriptActive
        }
        disabled={disabled}
        onClick={this.handleTriggerClick}
        title={labelMoreFormatting}
        iconBefore={
          <TriggerWrapper>
            <MoreIcon label=


export interface Props {
  isDisabled?: boolean;
  editorView: EditorView;
  textFormattingState?: TextFormattingState;
  clearFormattingState?: ClearFormattingState;
  popupsMountPoint?: HTMLElement;
  popupsBoundariesElement?: HTMLElement;
  popupsScrollableElement?: HTMLElement;
  isReducedSpacing?: boolean;
}


export interface State {
  isOpen?: boolean;
}


render() {
    const { isOpen } = this.state;
    const {
      popupsMountPoint,
      popupsBoundariesElement,
      popupsScrollableElement,
      isReducedSpacing,
      textFormattingState = {},
      clearFormattingState = {},
      intl: { formatMessage },
    } = this.props;
    const {
      codeActive,
      underlineActive,
      strikeActive,
      subscriptActive,
      superscriptActive,
      codeDisabled,
      underlineDisabled,
      strikeDisabled,
      subscriptDisabled,
      superscriptDisabled,
    } = textFormattingState;
    const { formattingIsPresent } = clearFormattingState;
    const items = this.createItems();
    const labelMoreFormatting = formatMessage(messages.moreFormatting);

    const toolbarButtonFactory = (disabled: boolean) => (
      <ToolbarButton
        spacing={isReducedSpacing ? 'none' : 'default'}
        selected={
          isOpen ||
          underlineActive ||
          codeActive ||
          strikeActive ||
          subscriptActive ||
          superscriptActive
        }


toolbarButtonFactory(false)


toolbarButtonFactory(true)


/**
 * Message used to communicate the encryption public key linked to a given Ethereum account
 */
export class PublicKeyMessage {
  private static Type = new Type('PublicKeyMessage')
    .add(new Field('encryptionPublicKey', 1, 'bytes'))
    .add(new Field('ethAddress', 2, 'bytes'))
    .add(new Field('signature', 3, 'bytes'));
  private static Root = new Root()
    .define('messages')
    .add(PublicKeyMessage.Type);

  constructor(public payload: PublicKeyMessagePayload) {}

  public encode(): Uint8Array {
    const message = PublicKeyMessage.Type.create(this.payload);
    return PublicKeyMessage.Type.encode(message).finish();
  }

  public static decode(
    bytes: Uint8Array | Buffer
  ): PublicKeyMessage | undefined {
    const payload = PublicKeyMessage.Type.decode(
      bytes
    ) as unknown as PublicKeyMessagePayload;
    if (
      !payload.signature ||
      !payload.encryptionPublicKey ||
      !payload.ethAddress
    ) {
      console.log('Field missing on decoded Public Key Message', payload);
      return;
    }
    return new PublicKeyMessage(payload);
  }

  get encryptionPublicKey(): Uint8Array {
    return this.payload.encryptionPublicKey;
  }

  get ethAddress(): Uint8Array {
    return this.payload.ethAddress;
  }

  get signature(): Uint8Array {
    return this.payload.signature;
  }
}


/**
 * Encrypted Message used for private communication over the Waku network.
 */
export class PrivateMessage {
  private static Type = new Type('PrivateMessage')
    .add(new Field('toAddress', 1, 'bytes'))
    .add(new Field('message', 2, 'string'));
  private static Root = new Root().define('messages').add(PrivateMessage.Type);

  constructor(public payload: PrivateMessagePayload) {}

  public encode(): Uint8Array {
    const message = PrivateMessage.Type.create(this.payload);
    return PrivateMessage.Type.encode(message).finish();
  }

  public static decode(bytes: Uint8Array | Buffer): PrivateMessage | undefined {
    const payload = PrivateMessage.Type.decode(
      bytes
    ) as unknown as PrivateMessagePayload;
    if (!payload.toAddress || !payload.message) {
      console.log('Field missing on decoded PrivateMessage', payload);
      return;
    }
    return new PrivateMessage(payload);
  }

  get toAddress(): Uint8Array {
    return this.payload.toAddress;
  }

  get message(): string {
    return this.payload.message;
  }
}


export interface PublicKeyMessagePayload {
  encryptionPublicKey: Uint8Array;
  ethAddress: Uint8Array;
  signature: Uint8Array;
}


export interface PrivateMessagePayload {
  toAddress: Uint8Array;
  message: string;
}


public encode(): Uint8Array {
    const message = PublicKeyMessage.Type.create(this.payload);
    return PublicKeyMessage.Type.encode(message).finish();
  }


public static decode(
    bytes: Uint8Array | Buffer
  ): PublicKeyMessage | undefined {
    const payload = PublicKeyMessage.Type.decode(
      bytes
    ) as unknown as PublicKeyMessagePayload;
    if (
      !payload.signature ||
      !payload.encryptionPublicKey ||
      !payload.ethAddress
    ) {
      console.log('Field missing on decoded Public Key Message', payload);
      return;
    }
    return new PublicKeyMessage(payload);
  }


public encode(): Uint8Array {
    const message = PrivateMessage.Type.create(this.payload);
    return PrivateMessage.Type.encode(message).finish();
  }


public static decode(bytes: Uint8Array | Buffer): PrivateMessage | undefined {
    const payload = PrivateMessage.Type.decode(
      bytes
    ) as unknown as PrivateMessagePayload;
    if (!payload.toAddress || !payload.message) {
      console.log('Field missing on decoded PrivateMessage', payload);
      return;
    }
    return new PrivateMessage(payload);
  }


(err, key, loader) => {
  console.error(`Error while loading "${key}" with ${loader.name}: ${err.message}`)
}


(err, key, cache) => {
  console.error(`Error while caching "${key}" with ${cache.name}: ${err.message}`)
}


(result, value, index) => {
      if (value.isCache) {
        result.push(index)
      }
      return result
    }


(cacheIndex) => {
      promises.push(
        Promise.resolve()
          .then(() => {
            return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).clear()
          })
          .catch((err) => {
            this.params.cacheUpdateErrorHandler(err, undefined, this.loaders[cacheIndex])
          })
      )
    }


() => {
            return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).clear()
          }


(err) => {
            this.params.cacheUpdateErrorHandler(err, undefined, this.loaders[cacheIndex])
          }


(cacheIndex) => {
      promises.push(
        Promise.resolve()
          .then(() => {
            return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).delete(key)
          })
          .catch((err) => {
            this.params.cacheUpdateErrorHandler(err, key, this.loaders[cacheIndex])
          })
      )
    }


() => {
            return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).delete(key)
          }


(err) => {
            this.params.cacheUpdateErrorHandler(err, key, this.loaders[cacheIndex])
          }


() => {
          return this.loaders[index].get(key)
        }


(err) => {
          this.params.loadErrorHandler(err, key, this.loaders[index])

          // if last loader, fail
          if (index === this.loaders.length - 1) {
            throw new Error(`Failed to resolve value for key "${key}": ${err.message}`)
          }
        }


(cacheIndex) => {
            return cacheIndex < index
          }


(cacheIndex) => {
            Promise.resolve()
              .then(() => {
                return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).set(key, resolvedValue)
              })
              .catch((err) => {
                this.params.cacheUpdateErrorHandler(err, key, this.loaders[cacheIndex])
              })
          }


() => {
                return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).set(key, resolvedValue)
              }


(err) => {
                this.params.cacheUpdateErrorHandler(err, key, this.loaders[cacheIndex])
              }


(resolve, reject) => {
      this.resolveValue(key)
        .then((resolvedValue) => {
          this.runningLoads.set(key, undefined)

          if (resolvedValue === undefined && this.params.throwIfUnresolved) {
            return reject(new Error(`Failed to resolve value for key "${key}"`))
          }
          return resolve(resolvedValue)
        })
        .catch(reject)
    }


(resolvedValue) => {
          this.runningLoads.set(key, undefined)

          if (resolvedValue === undefined && this.params.throwIfUnresolved) {
            return reject(new Error(`Failed to resolve value for key "${key}"`))
          }
          return resolve(resolvedValue)
        }


export class LoadingOperation<LoadedValue> {
  private readonly params: LoadingOperationConfig
  private readonly loaders: readonly Loader<LoadedValue>[]
  private readonly cacheIndexes: readonly number[]
  private readonly runningLoads: Map<string, Promise<LoadedValue | undefined | null> | undefined>

  constructor(loaders: readonly Loader<LoadedValue>[], params: Partial<LoadingOperationConfig> = DEFAULT_CONFIG) {
    this.params = {
      ...DEFAULT_CONFIG,
      ...params,
    }
    this.loaders = loaders
    this.runningLoads = new Map()

    this.cacheIndexes = loaders.reduce((result, value, index) => {
      if (value.isCache) {
        result.push(index)
      }
      return result
    }, [] as number[])
  }

  public invalidateCache() {
    const promises: Promise<any>[] = []

    this.cacheIndexes.forEach((cacheIndex) => {
      promises.push(
        Promise.resolve()
          .then(() => {
            return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).clear()
          })
          .catch((err) => {
            this.params.cacheUpdateErrorHandler(err, undefined, this.loaders[cacheIndex])
          })
      )
    })

    return Promise.all(promises)
  }

  public invalidateCacheFor(key: string) {
    const promises: Promise<any>[] = []

    this.cacheIndexes.forEach((cacheIndex) => {
      promises.push(
        Promise.resolve()
          .then(() => {
            return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).delete(key)
          })
          .catch((err) => {
            this.params.cacheUpdateErrorHandler(err, key, this.loaders[cacheIndex])
          })
      )
    })
    return Promise.all(promises)
  }

  private async resolveValue(key: string): Promise<LoadedValue | undefined | null> {
    for (let index = 0; index < this.loaders.length; index++) {
      const resolvedValue = await Promise.resolve()
        .then(() => {
          return this.loaders[index].get(key)
        })
        .catch((err) => {
          this.params.loadErrorHandler(err, key, this.loaders[index])

          // if last loader, fail
          if (index === this.loaders.length - 1) {
            throw new Error(`Failed to resolve value for key "${key}": ${err.message}`)
          }
        })

      if (resolvedValue) {
        // update caches
        this.cacheIndexes
          .filter((cacheIndex) => {
            return cacheIndex < index
          })
          .forEach((cacheIndex) => {
            Promise.resolve()
              .then(() => {
                return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).set(key, resolvedValue)
              })
              .catch((err) => {
                this.params.cacheUpdateErrorHandler(err, key, this.loaders[cacheIndex])
              })
          })

        return resolvedValue
      }
    }
    return undefined
  }

  public async get(key: string): Promise<LoadedValue | undefined | null> {
    const existingLoad = this.runningLoads.get(key)
    if (existingLoad) {
      return existingLoad
    }

    const loadingPromise = new Promise<LoadedValue | undefined | null>((resolve, reject) => {
      this.resolveValue(key)
        .then((resolvedValue) => {
          this.runningLoads.set(key, undefined)

          if (resolvedValue === undefined && this.params.throwIfUnresolved) {
            return reject(new Error(`Failed to resolve value for key "${key}"`))
          }
          return resolve(resolvedValue)
        })
        .catch(reject)
    })

    this.runningLoads.set(key, loadingPromise)
    return loadingPromise
  }
}


export type LoadingOperationConfig = {
  throwIfUnresolved: boolean
  cacheUpdateErrorHandler: LoaderErrorHandler
  loadErrorHandler: LoaderErrorHandler
}


export type LoaderErrorHandler = (err: Error, key: string | undefined, loader: Loader<any>) => void


public invalidateCache() {
    const promises: Promise<any>[] = []

    this.cacheIndexes.forEach((cacheIndex) => {
      promises.push(
        Promise.resolve()
          .then(() => {
            return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).clear()
          })
          .catch((err) => {
            this.params.cacheUpdateErrorHandler(err, undefined, this.loaders[cacheIndex])
          })
      )
    })

    return Promise.all(promises)
  }


public invalidateCacheFor(key: string) {
    const promises: Promise<any>[] = []

    this.cacheIndexes.forEach((cacheIndex) => {
      promises.push(
        Promise.resolve()
          .then(() => {
            return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).delete(key)
          })
          .catch((err) => {
            this.params.cacheUpdateErrorHandler(err, key, this.loaders[cacheIndex])
          })
      )
    })
    return Promise.all(promises)
  }


private async resolveValue(key: string): Promise<LoadedValue | undefined | null> {
    for (let index = 0; index < this.loaders.length; index++) {
      const resolvedValue = await Promise.resolve()
        .then(() => {
          return this.loaders[index].get(key)
        })
        .catch((err) => {
          this.params.loadErrorHandler(err, key, this.loaders[index])

          // if last loader, fail
          if (index === this.loaders.length - 1) {
            throw new Error(`Failed to resolve value for key "${key}": ${err.message}`)
          }
        })

      if (resolvedValue) {
        // update caches
        this.cacheIndexes
          .filter((cacheIndex) => {
            return cacheIndex < index
          })
          .forEach((cacheIndex) => {
            Promise.resolve()
              .then(() => {
                return (this.loaders[cacheIndex] as unknown as Cache<LoadedValue>).set(key, resolvedValue)
              })
              .catch((err) => {
                this.params.cacheUpdateErrorHandler(err, key, this.loaders[cacheIndex])
              })
          })

        return resolvedValue
      }
    }
    return undefined
  }


public async get(key: string): Promise<LoadedValue | undefined | null> {
    const existingLoad = this.runningLoads.get(key)
    if (existingLoad) {
      return existingLoad
    }

    const loadingPromise = new Promise<LoadedValue | undefined | null>((resolve, reject) => {
      this.resolveValue(key)
        .then((resolvedValue) => {
          this.runningLoads.set(key, undefined)

          if (resolvedValue === undefined && this.params.throwIfUnresolved) {
            return reject(new Error(`Failed to resolve value for key "${key}"`))
          }
          return resolve(resolvedValue)
        })
        .catch(reject)
    })

    this.runningLoads.set(key, loadingPromise)
    return loadingPromise
  }


(state, { payload }) => payload


(props: IconProps) => (
  <Icon
    viewBox="0 0 512 512"


export class AesCcmParams implements IParams {

  /**
   * length of the data where 0 <= dataLength < 2^8L
   */
  public dataLength: number;
  /**
   * the nonce
   */
  public nonce: Buffer;
  /**
   * the additional authentication data
   * - This data is authenticated but not encrypted
   */
  public aad: Buffer;
  /**
   * length of authentication tag (output following cipher text) in bits.
   * - Can be any value between 0 and 128
   */
  public macLength: number;

  public type = MechParams.AesCCM;

  constructor(dataLength: number, nonce: Buffer, aad: Buffer = Buffer.alloc(0), macLength: number = 0) {
    this.dataLength = dataLength;
    this.nonce = nonce;
    this.aad = aad;
    this.macLength = macLength;
  }

  public toCKI(): pkcs11.AesCCM {
    return {
      aad: this.aad,
      dataLen: this.dataLength,
      macLen: this.macLength,
      nonce: this.nonce,
      type: this.type,
    };
  }
}


public toCKI(): pkcs11.AesCCM {
    return {
      aad: this.aad,
      dataLen: this.dataLength,
      macLen: this.macLength,
      nonce: this.nonce,
      type: this.type,
    };
  }


(wordID) => {
              this.addToLesson(lessonID,wordID,(wordLesson)=>{
                return res.json(200);
              })
          }


(wordLesson)=>{
                return res.json(200);
              }


function orientedTypesIndex(orientedTypes: readonly OrientedType[]) {
  const calculatedNumOrientedTypes = numOrientedTypes(orientedTypes);
  return sum(orientedTypes.map((orientedType, index) => orientedType.index * calculatedNumOrientedTypes ** index));
}


function combineOrientedTypess(left: readonly OrientedType[], right: readonly OrientedType[]): OrientedType[] {
  assertEqual(left.length, right.length);
  return zip(left, right).map(([left, right]) => left.plus(right));
}


function combine(algTraceWithCost: AlgTraceWithCost, twistWithCost: TwistWithCost): AlgTraceWithCost {
  const targetOrientedTypes = combineOrientedTypess(algTraceWithCost.targetOrientedTypes, twistWithCost.twist.orientedTypes);
  const index = orientedTypesIndex(targetOrientedTypes);
  return {
    targetOrientedTypes,
    index,
    algTrace: algTraceWithCost.algTrace.withSuffix(twistWithCost.twist),
    cost: algTraceWithCost.cost + twistWithCost.cost,
  };
}


export function createTwistSolver(decider: Decider, pieceDescription: PieceDescription): TwistSolver {
  return createTwistSolverInternal(decider.twistsWithCosts, pieceDescription);
}


// Exported for testing
export function createTwistSolverInternal(twistsWithCosts: readonly TwistWithCost[], pieceDescription: PieceDescription): TwistSolver {
  assert(pieceDescription.numOrientedTypes <= 3);
  const numItems = pieceDescription.numOrientedTypes ** pieceDescription.pieces.length;
  const algsByIndex: Optional<AlgTrace>[] = Array(numItems).fill(none);
  const costByIndex: number[] = Array(numItems).fill(Infinity);
  const targetOrientedTypes: readonly OrientedType[] = pieceDescription.pieces.map(() => solvedOrientedType);
  const index = orientedTypesIndex(targetOrientedTypes);
  const solvedAlgTraceWithCost: AlgTraceWithCost = {targetOrientedTypes, index, algTrace: emptyAlgTrace(), cost: 0};
  const unprocessed = new AlgTracesPriorityQueue();
  unprocessed.pushOrDecreaseCost(solvedAlgTraceWithCost);
  while (!unprocessed.empty) {
    const current = unprocessed.extractMin();
    for (let twistWithCost of twistsWithCosts) {
      const neighbor = combine(current, twistWithCost);
      const oldNeighborCost = costByIndex[neighbor.index];
      if (neighbor.cost < oldNeighborCost) {
        algsByIndex[neighbor.index] = some(neighbor.algTrace);
        costByIndex[neighbor.index] = neighbor.cost;
        unprocessed.pushOrDecreaseCost(neighbor);
      }
    }
  }
  const actualSolved = count(algsByIndex, hasValue);
  const expectedSolved = pieceDescription.numOrientedTypes ** (pieceDescription.pieces.length - 1);
  assert(actualSolved === expectedSolved, `The set of given twists is not sufficient to solve all twists (${actualSolved}/${expectedSolved})`);
  return new TwistSolverImpl(algsByIndex);
}


(orientedType, index) => orientedType.index * calculatedNumOrientedTypes ** index


([solvable, orientedTypes]) => {
      return this.algsForOrientedTypes(orientedTypes).map(algs => [solvable, algs]);
    }


algs => [solvable, algs]


e => e.index === algTraceWithCost.index


index => { this.elements[index] = algTraceWithCost; }


() => { this.elements.push(algTraceWithCost); }


(e, i) => [e, i]


e => e[0].cost


([left, right]) => left.plus(right)


() => solvedOrientedType


class TwistSolverImpl implements TwistSolver {
  constructor(private readonly algsByIndex: Optional<AlgTrace>[]) {}

  algs<T extends Solvable<T>>(solvable: T): Probabilistic<[T, AlgTrace]> {
    return solvable.decideOrientedTypes().flatMap(([solvable, orientedTypes]) => {
      return this.algsForOrientedTypes(orientedTypes).map(algs => [solvable, algs]);
    });
  }

  algsForOrientedTypes(orientedTypes: readonly OrientedType[]): Probabilistic<AlgTrace> {
    const index = orientedTypesIndex(orientedTypes);
    if (!hasValue(this.algsByIndex[index])) {
      console.log(this);
      console.log(orientedTypes);
    }
    const algTrace = forceValue(this.algsByIndex[index]);
    return deterministic(algTrace);
  }
}


// Note that this is not efficient, but it doesn't have to be because we have only 8 corners and not that many twists.
class AlgTracesPriorityQueue {
  readonly elements: AlgTraceWithCost[] = [];

  get empty() {
    return this.elements.length === 0;
  }

  pushOrDecreaseCost(algTraceWithCost: AlgTraceWithCost) {
    const maybeReplacedIndex = findIndex(this.elements, e => e.index === algTraceWithCost.index);
    ifPresentOrElse(maybeReplacedIndex,
                    index => { this.elements[index] = algTraceWithCost; },
                    () => { this.elements.push(algTraceWithCost); });
  }

  extractMin(): AlgTraceWithCost {
    const unprocessedWithIndex: [AlgTraceWithCost, number][] = this.elements.map((e, i) => [e, i]);
    const min = forceValue(minBy(unprocessedWithIndex, e => e[0].cost));
    this.elements.splice(min[1], 1);
    return min[0];
  }
}


export interface TwistSolver {
  algs<T extends Solvable<T>>(solvable: T): Probabilistic<[T, AlgTrace]>;
  algsForOrientedTypes(orientedTypes: readonly OrientedType[]): Probabilistic<AlgTrace>;
}


interface AlgTraceWithCost {
  readonly index: number;
  readonly targetOrientedTypes: readonly OrientedType[];
  readonly algTrace: AlgTrace;
  readonly cost: number;
}


algs<T extends Solvable<T>>(solvable: T): Probabilistic<[T, AlgTrace]> {
    return solvable.decideOrientedTypes().flatMap(([solvable, orientedTypes]) => {
      return this.algsForOrientedTypes(orientedTypes).map(algs => [solvable, algs]);
    });
  }


algsForOrientedTypes(orientedTypes: readonly OrientedType[]): Probabilistic<AlgTrace> {
    const index = orientedTypesIndex(orientedTypes);
    if (!hasValue(this.algsByIndex[index])) {
      console.log(this);
      console.log(orientedTypes);
    }
    const algTrace = forceValue(this.algsByIndex[index]);
    return deterministic(algTrace);
  }


pushOrDecreaseCost(algTraceWithCost: AlgTraceWithCost) {
    const maybeReplacedIndex = findIndex(this.elements, e => e.index === algTraceWithCost.index);
    ifPresentOrElse(maybeReplacedIndex,
                    index => { this.elements[index] = algTraceWithCost; },
                    () => { this.elements.push(algTraceWithCost); });
  }


extractMin(): AlgTraceWithCost {
    const unprocessedWithIndex: [AlgTraceWithCost, number][] = this.elements.map((e, i) => [e, i]);
    const min = forceValue(minBy(unprocessedWithIndex, e => e[0].cost));
    this.elements.splice(min[1], 1);
    return min[0];
  }


_ => timer(OK_TO_INITIAL_DELAY)


_ => {
      this.state$.next(RequestState.Off);
    }


st => {
      switch (st) {
        case RequestState.Pending:
          this.activity = true;
          this.result = false;
          this.resultStatus = undefined;
          break;
        case RequestState.Success:
          this.activity = false;
          this.result = true;
          this.resultStatus = true;
          this.startResetTimer();
          break;
        case RequestState.Error:
          this.activity = false;
          this.result = true;
          this.resultStatus = false;
          // this.startResetTimer();
          break;
        case RequestState.Off:
        default:
          this.activity = false;
          this.result = false;
          this.resultStatus = undefined;
      }
    }


_ => state$.next(RequestState.Success)


_ => state$.next(RequestState.Error)


export class WatchedRequest {
  constructor(public request: Observable<any>, public state: Observable<RequestState>) {}
}


export class BackendActivityState {
  state$ = new BehaviorSubject<RequestState>(RequestState.Off);
  activity = false;
  result = false;
  resultStatus;
  stateResetter = new Subject<any>();

  constructor() {
    this.stateResetter.pipe(switchMap(_ => timer(OK_TO_INITIAL_DELAY))).subscribe(_ => {
      this.state$.next(RequestState.Off);
    });

    this.state$.subscribe(st => {
      switch (st) {
        case RequestState.Pending:
          this.activity = true;
          this.result = false;
          this.resultStatus = undefined;
          break;
        case RequestState.Success:
          this.activity = false;
          this.result = true;
          this.resultStatus = true;
          this.startResetTimer();
          break;
        case RequestState.Error:
          this.activity = false;
          this.result = true;
          this.resultStatus = false;
          // this.startResetTimer();
          break;
        case RequestState.Off:
        default:
          this.activity = false;
          this.result = false;
          this.resultStatus = undefined;
      }
    });
  }

  startResetTimer() {
    this.stateResetter.next('');
  }
}


/**
 * Watch a request and provide a status.
 */
@Injectable({
  providedIn: 'root'
})
export class RequestWatcherService {
  // state$: BehaviorSubject<RequestState> = new BehaviorSubject(RequestState.Off);
  // watching: Observable<any>;
  constructor() {}

  watchRequest(req: Observable<any>, state$: Subject<RequestState>) {
    const watched = req.pipe(share());
    state$.next(RequestState.Pending);
    watched.subscribe(_ => state$.next(RequestState.Success), _ => state$.next(RequestState.Error));
    return new WatchedRequest(watched, state$);
  }
}


export enum RequestState {
  Off = 1,
  Pending,
  Success,
  Error
}


startResetTimer() {
    this.stateResetter.next('');
  }


watchRequest(req: Observable<any>, state$: Subject<RequestState>) {
    const watched = req.pipe(share());
    state$.next(RequestState.Pending);
    watched.subscribe(_ => state$.next(RequestState.Success), _ => state$.next(RequestState.Error));
    return new WatchedRequest(watched, state$);
  }


/**
 * Contains default implementations for ASP.NET Core MVC validation attributes.
 */
export declare class MvcValidationProviders {
    /**
     * Validates whether the input has a value.
     */
    required: ValidationProvider;
    /**
     * Validates whether the input value satisfies the length contstraint.
     */
    stringLength: ValidationProvider;
    /**
     * Validates whether the input value is equal to another input value.
     */
    compare: ValidationProvider;
    /**
     * Validates whether the input value is a number within a given range.
     */
    range: ValidationProvider;
    /**
     * Validates whether the input value satisfies a regular expression pattern.
     */
    regex: ValidationProvider;
    /**
     * Validates whether the input value is an email in accordance to RFC822 specification, with a top level domain.
     */
    email: ValidationProvider;
    /**
     * Validates whether the input value is a credit card number, with Luhn's Algorithm.
     */
    creditcard: ValidationProvider;
    /**
     * Validates whether the input value is a URL.
     */
    url: ValidationProvider;
    /**
     * Validates whether the input value is a phone number.
     */
    phone: ValidationProvider;
    /**
     * Asynchronously validates the input value to a JSON GET API endpoint.
     */
    remote: ValidationProvider;
}