//
// Copyright 2021 Vulcanize, Inc.
//

import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions{{#if (subgraphPath)}}, ObjectLiteral{{/if}} } from 'typeorm';
import debug from 'debug';
{{#if queries}}
import JSONbig from 'json-bigint';
{{/if}}
import { ethers, constants } from 'ethers';
{{#if (subgraphPath)}}
import { SelectionNode } from 'graphql';
{{/if}}

import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import {
  Indexer as BaseIndexer,
  IndexerInterface,
  ValueResult,
  ServerConfig,
  JobQueue,
  Where,
  QueryOptions,
  {{#if hasStateVariableElementaryType}}
  updateStateForElementaryType,
  {{/if}}
  {{#if hasStateVariableMappingType}}
  updateStateForMappingType,
  {{/if}}
  {{#if (subgraphPath)}}
  BlockHeight,
  ResultMeta,
  updateSubgraphState,
  dumpSubgraphState,
  GraphWatcherInterface,
  {{/if}}
  StateKind,
  StateStatus,
  ResultEvent,
  getResultEvent,
  DatabaseInterface,
  Clients,
  EthClient,
  UpstreamConfig,
  EthFullBlock,
  EthFullTransaction,
  ExtraEventData
} from '@cerc-io/util';
{{#if (subgraphPath)}}
import { GraphWatcher } from '@cerc-io/graph-node';
{{/if}}

{{#each contracts as | contract |}}
import {{contract.contractName}}Artifacts from './artifacts/{{contract.contractName}}.json';
{{/each}}
import { Database, ENTITIES{{#if (subgraphPath)}}, SUBGRAPH_ENTITIES{{/if}} } from './database';
import { createInitialState, handleEvent, createStateDiff, createStateCheckpoint } from './hooks';
import { Contract } from './entity/Contract';
import { Event } from './entity/Event';
import { SyncStatus } from './entity/SyncStatus';
import { StateSyncStatus } from './entity/StateSyncStatus';
import { BlockProgress } from './entity/BlockProgress';
import { State } from './entity/State';
{{#if (subgraphPath)}}
/* eslint-disable @typescript-eslint/no-unused-vars */
{{#each subgraphEntities as | subgraphEntity |}}
import { {{subgraphEntity.className}} } from './entity/{{subgraphEntity.className}}';
{{/each}}
/* eslint-enable @typescript-eslint/no-unused-vars */

import { FrothyEntity } from './entity/FrothyEntity';
{{/if}}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const log = debug('vulcanize:indexer');
{{#if queries}}
const JSONbigNative = JSONbig({ useNativeBigInt: true });
{{/if}}

{{#each contracts as | contract |}}
const KIND_{{capitalize contract.contractName}} = '{{contract.contractKind}}';

{{/each}}
{{#each uniqueEvents as | event |}}
const {{capitalize event}}_EVENT = '{{event}}';

{{/each}}
export class Indexer implements IndexerInterface {
  _db: Database;
  _ethClient: EthClient;
  _ethProvider: BaseProvider;
  _baseIndexer: BaseIndexer;
  _serverConfig: ServerConfig;
  _upstreamConfig: UpstreamConfig;
  {{#if (subgraphPath)}}
  _graphWatcher: GraphWatcher;
  {{/if}}

  _abiMap: Map<string, JsonFragment[]>;
  _storageLayoutMap: Map<string, StorageLayout>;
  _contractMap: Map<string, ethers.utils.Interface>;
  eventSignaturesMap: Map<string, string[]>;

  {{#if (subgraphPath)}}
  _entityTypesMap: Map<string, { [key: string]: string }>;
  _relationsMap: Map<any, { [key: string]: any }>;

  _subgraphStateMap: Map<string, any>;

  {{/if}}
  constructor (
    config: {
      server: ServerConfig;
      upstream: UpstreamConfig;
    },
    db: DatabaseInterface,
    clients: Clients,
    ethProvider: BaseProvider,
    jobQueue: JobQueue{{#if (subgraphPath)}},{{/if}}
    {{#if (subgraphPath)}}
    graphWatcher?: GraphWatcherInterface
    {{/if}}
  ) {
    assert(db);
    assert(clients.ethClient);

    this._db = db as Database;
    this._ethClient = clients.ethClient;
    this._ethProvider = ethProvider;
    this._serverConfig = config.server;
    this._upstreamConfig = config.upstream;
    this._baseIndexer = new BaseIndexer(config, this._db, this._ethClient, this._ethProvider, jobQueue);
    {{#if (subgraphPath)}}
    assert(graphWatcher);
    this._graphWatcher = graphWatcher as GraphWatcher;
    {{/if}}

    this._abiMap = new Map();
    this._storageLayoutMap = new Map();
    this._contractMap = new Map();
    this.eventSignaturesMap = new Map();
    {{#each contracts as | contract |}}

    const { abi: {{contract.contractName}}ABI{{#if contract.contractStorageLayout}}, storageLayout: {{contract.contractName}}StorageLayout{{/if}} } = {{contract.contractName}}Artifacts;
    {{/each}}
    {{#each contracts as | contract |}}

    assert({{contract.contractName}}ABI);
    this._abiMap.set(KIND_{{capitalize contract.contractName}}, {{contract.contractName}}ABI);

    const {{contract.contractName}}ContractInterface = new ethers.utils.Interface({{contract.contractName}}ABI);
    this._contractMap.set(KIND_{{capitalize contract.contractName}}, {{contract.contractName}}ContractInterface);

    const {{contract.contractName}}EventSignatures = Object.values({{contract.contractName}}ContractInterface.events).map(value => {
      return {{contract.contractName}}ContractInterface.getEventTopic(value);
    });
    this.eventSignaturesMap.set(KIND_{{capitalize contract.contractName}}, {{contract.contractName}}EventSignatures);
    {{#if contract.contractStorageLayout}}

    assert({{contract.contractName}}StorageLayout);
    this._storageLayoutMap.set(KIND_{{capitalize contract.contractName}}, {{contract.contractName}}StorageLayout);
    {{/if}}
    {{/each}}
    {{#if (subgraphPath)}}

    this._entityTypesMap = new Map();
    this._populateEntityTypesMap();

    this._relationsMap = new Map();
    this._populateRelationsMap();

    this._subgraphStateMap = new Map();
    {{/if}}
  }

  get serverConfig (): ServerConfig {
    return this._serverConfig;
  }

  get upstreamConfig (): UpstreamConfig {
    return this._upstreamConfig;
  }

  get storageLayoutMap (): Map<string, StorageLayout> {
    return this._storageLayoutMap;
  }

  {{#if (subgraphPath)}}
  get graphWatcher (): GraphWatcher {
    return this._graphWatcher;
  }

  {{/if}}
  async init (): Promise<void> {
    await this._baseIndexer.fetchContracts();
    await this._baseIndexer.fetchStateStatus();
  }

  {{#if (subgraphPath)}}
  async getMetaData (block: BlockHeight): Promise<ResultMeta | null> {
    return this._baseIndexer.getMetaData(block);
  }

  {{/if}}
  getResultEvent (event: Event): ResultEvent {
    return getResultEvent(event);
  }

  {{#each queries as | query |}}
  async {{query.name}} (blockHash: string, contractAddress: string
    {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}}
    {{~#if query.stateVariableType~}}
    , diff = false): Promise<ValueResult> {
    {{else~}}
    ): Promise<ValueResult> {
    {{/if}}
    const entity = await this._db.{{query.getQueryName}}({ blockHash, contractAddress
    {{~#each query.params}}, {{this.name~}} {{~/each}} });
    if (entity) {
      log('{{query.name}}: db hit.');

      return {
        {{#if (compare query.returnTypes.length 1 operator=">")}}
        value: {
          {{#each query.returnTypes}}
          value{{@index}}: entity.value{{@index}}{{#unless @last}},{{/unless}}
          {{/each}}
        },
        {{else}}
        value: entity.value,
        {{/if}}
        proof: JSON.parse(entity.proof)
      };
    }

    const { block: { number } } = await this._ethClient.getBlockByHash(blockHash);
    const blockNumber = ethers.BigNumber.from(number).toNumber();

    log('{{query.name}}: db miss, fetching from upstream server');

    {{#if (compare query.mode @root.constants.MODE_ETH_CALL)}}
    const abi = this._abiMap.get(KIND_{{capitalize query.contract}});
    assert(abi);

    const contract = new ethers.Contract(contractAddress, abi, this._ethProvider);
    const contractResult = await contract.{{query.name}}(
    {{~#each query.params}}{{this.name}}, {{/each}}{ blockTag: blockHash });

    {{#if (compare query.returnTypes.length 1 operator=">")}}
    const value = {
    {{#each query.returnTypes as |returnType index|}}
      {{#if (compare returnType 'bigint')}}
      value{{index}}: ethers.BigNumber.from(contractResult[{{index}}]).toBigInt()
      {{~else}}
      {{!-- https://github.com/handlebars-lang/handlebars.js/issues/1716 --}}
      {{#if (compare returnType 'bigint[]')}}
      value{{index}}: contractResult[{{index}}].map((val: ethers.BigNumber | number) => ethers.BigNumber.from(val).toBigInt())
      {{~else}}
      value{{index}}: contractResult[{{index}}]
      {{~/if}}
      {{/if}}
      {{~#unless @last}},{{/unless}}
    {{/each}}
    };
    {{else}}
    {{#if (compare query.returnTypes.[0] 'bigint')}}
    const value = ethers.BigNumber.from(contractResult).toBigInt();
    {{else if (compare query.returnTypes.[0] 'bigint[]')}}
    const value = contractResult.map((val: ethers.BigNumber | number) => ethers.BigNumber.from(val).toBigInt());
    {{else}}
    const value = contractResult;
    {{~/if}}
    {{~/if}}

    const result: ValueResult = { value };
    {{/if}}

    {{~#if (compare query.mode @root.constants.MODE_STORAGE)}}
    const storageLayout = this._storageLayoutMap.get(KIND_{{capitalize query.contract}});
    assert(storageLayout);

    const result = await this._baseIndexer.getStorageValue(
      storageLayout,
      blockHash,
      contractAddress,
      '{{query.name}}'{{#if query.params.length}},{{/if}}
      {{#each query.params}}
      {{this.name}}{{#unless @last}},{{/unless}}
      {{/each}}
    );
    {{/if}}

    await this._db.{{query.saveQueryName}}({ blockHash, blockNumber, contractAddress,{{~#each query.params}} {{this.name~}},{{/each}}{{#each query.returnTypes}}{{~#if (compare query.returnTypes.length 1 operator=">")}} value{{@index}}: value.value{{@index}},{{else}} value: result.value,{{/if}}{{/each}} proof: JSONbigNative.stringify(result.proof) });

    {{#if query.stateVariableType}}
    {{#if (compare query.stateVariableType 'Mapping')}}
    if (diff) {
      const stateUpdate = updateStateForMappingType({}, '{{query.name}}', [
      {{~#each query.params}}
      {{~this.name}}.toString() {{~#unless @last}}, {{/unless~}}
      {{/each~}}
      ], result.value.toString());
      await this.createDiffStaged(contractAddress, blockHash, stateUpdate);
    }

    {{else if (compare query.stateVariableType 'ElementaryTypeName')}}
    if (diff) {
      const stateUpdate = updateStateForElementaryType({}, '{{query.name}}', result.value.toString());
      await this.createDiffStaged(contractAddress, blockHash, stateUpdate);
    }

    {{else}}
    assert(state === 'none', 'Type not supported for default state.');

    {{/if}}
    {{/if}}
    return result;
  }

  {{/each}}
  async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
    return this._baseIndexer.getStorageValue(
      storageLayout,
      blockHash,
      contractAddress,
      variable,
      ...mappingKeys
    );
  }

  async getEntitiesForBlock (blockHash: string, tableName: string): Promise<any[]> {
    return this._db.getEntitiesForBlock(blockHash, tableName);
  }

  async processInitialState (contractAddress: string, blockHash: string): Promise<any> {
    // Call initial state hook.
    return createInitialState(this, contractAddress, blockHash);
  }

  async processStateCheckpoint (contractAddress: string, blockHash: string): Promise<boolean> {
    // Call checkpoint hook.
    return createStateCheckpoint(this, contractAddress, blockHash);
  }

  async processCanonicalBlock (blockHash: string{{~#if (subgraphPath)}}, blockNumber: number{{/if}}): Promise<void> {
    console.time('time:indexer#processCanonicalBlock-finalize_auto_diffs');
    // Finalize staged diff blocks if any.
    await this._baseIndexer.finalizeDiffStaged(blockHash);
    console.timeEnd('time:indexer#processCanonicalBlock-finalize_auto_diffs');

    // Call custom stateDiff hook.
    await createStateDiff(this, blockHash);
    {{#if (subgraphPath)}}

    this._graphWatcher.pruneEntityCacheFrothyBlocks(blockHash, blockNumber);
    {{/if}}
  }

  async processCheckpoint (blockHash: string): Promise<void> {
    // Return if checkpointInterval is <= 0.
    const checkpointInterval = this._serverConfig.checkpointInterval;
    if (checkpointInterval <= 0) return;

    console.time('time:indexer#processCheckpoint-checkpoint');
    await this._baseIndexer.processCheckpoint(this, blockHash, checkpointInterval);
    console.timeEnd('time:indexer#processCheckpoint-checkpoint');
  }

  async processCLICheckpoint (contractAddress: string, blockHash?: string): Promise<string | undefined> {
    return this._baseIndexer.processCLICheckpoint(this, contractAddress, blockHash);
  }

  async getPrevState (blockHash: string, contractAddress: string, kind?: string): Promise<State | undefined> {
    return this._db.getPrevState(blockHash, contractAddress, kind);
  }

  async getLatestState (contractAddress: string, kind: StateKind | null, blockNumber?: number): Promise<State | undefined> {
    return this._db.getLatestState(contractAddress, kind, blockNumber);
  }

  async getStatesByHash (blockHash: string): Promise<State[]> {
    return this._baseIndexer.getStatesByHash(blockHash);
  }

  async getStateByCID (cid: string): Promise<State | undefined> {
    return this._baseIndexer.getStateByCID(cid);
  }

  async getStates (where: FindConditions<State>): Promise<State[]> {
    return this._db.getStates(where);
  }

  getStateData (state: State): any {
    return this._baseIndexer.getStateData(state);
  }

  // Method used to create auto diffs (diff_staged).
  async createDiffStaged (contractAddress: string, blockHash: string, data: any): Promise<void> {
    console.time('time:indexer#createDiffStaged-auto_diff');
    await this._baseIndexer.createDiffStaged(contractAddress, blockHash, data);
    console.timeEnd('time:indexer#createDiffStaged-auto_diff');
  }

  // Method to be used by createStateDiff hook.
  async createDiff (contractAddress: string, blockHash: string, data: any): Promise<void> {
    const block = await this.getBlockProgress(blockHash);
    assert(block);

    await this._baseIndexer.createDiff(contractAddress, block, data);
  }

  // Method to be used by createStateCheckpoint hook.
  async createStateCheckpoint (contractAddress: string, blockHash: string, data: any): Promise<void> {
    const block = await this.getBlockProgress(blockHash);
    assert(block);

    return this._baseIndexer.createStateCheckpoint(contractAddress, block, data);
  }

  // Method to be used by export-state CLI.
  async createCheckpoint (contractAddress: string, blockHash: string): Promise<string | undefined> {
    const block = await this.getBlockProgress(blockHash);
    assert(block);

    return this._baseIndexer.createCheckpoint(this, contractAddress, block);
  }

  {{#if (subgraphPath)}}
  // Method to be used by fill-state CLI.
  async createInit (blockHash: string, blockNumber: number): Promise<void> {
    // Create initial state for contracts.
    await this._baseIndexer.createInit(this, blockHash, blockNumber);
  }

  {{/if}}
  async saveOrUpdateState (state: State): Promise<State> {
    return this._baseIndexer.saveOrUpdateState(state);
  }

  async removeStates (blockNumber: number, kind: StateKind): Promise<void> {
    await this._baseIndexer.removeStates(blockNumber, kind);
  }

  {{#if (subgraphPath)}}
  async getSubgraphEntity<Entity extends ObjectLiteral> (
    entity: new () => Entity,
    id: string,
    block: BlockHeight,
    selections: ReadonlyArray<SelectionNode> = []
  ): Promise<any> {
    const data = await this._graphWatcher.getEntity(entity, id, this._relationsMap, block, selections);

    return data;
  }

  async getSubgraphEntities<Entity extends ObjectLiteral> (
    entity: new () => Entity,
    block: BlockHeight,
    where: { [key: string]: any } = {},
    queryOptions: QueryOptions = {},
    selections: ReadonlyArray<SelectionNode> = []
  ): Promise<any[]> {
    return this._graphWatcher.getEntities(entity, this._relationsMap, block, where, queryOptions, selections);
  }

  {{/if}}
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  async triggerIndexingOnEvent (event: Event, extraData: ExtraEventData): Promise<void> {
    const resultEvent = this.getResultEvent(event);

    {{#if (subgraphPath)}}
    console.time('time:indexer#processEvent-mapping_code');
    // Call subgraph handler for event.
    await this._graphWatcher.handleEvent(resultEvent, extraData);
    console.timeEnd('time:indexer#processEvent-mapping_code');

    {{/if}}
    // Call custom hook function for indexing on event.
    await handleEvent(this, resultEvent);
  }

  async processEvent (event: Event, extraData: ExtraEventData): Promise<void> {
    // Trigger indexing of data based on the event.
    await this.triggerIndexingOnEvent(event, extraData);
  }

  async processBlock (blockProgress: BlockProgress): Promise<void> {
    console.time('time:indexer#processBlock-init_state');
    // Call a function to create initial state for contracts.
    await this._baseIndexer.createInit(this, blockProgress.blockHash, blockProgress.blockNumber);
    console.timeEnd('time:indexer#processBlock-init_state');
    {{#if (subgraphPath)}}

    this._graphWatcher.updateEntityCacheFrothyBlocks(blockProgress);
    {{/if}}
  }

  {{#if (subgraphPath)}}
  async processBlockAfterEvents (blockHash: string, blockNumber: number, extraData: ExtraEventData): Promise<void> {
    console.time('time:indexer#processBlockAfterEvents-mapping_code');
    // Call subgraph handler for block.
    await this._graphWatcher.handleBlock(blockHash, blockNumber, extraData);
    console.timeEnd('time:indexer#processBlockAfterEvents-mapping_code');

    console.time('time:indexer#processBlockAfterEvents-dump_subgraph_state');
    // Persist subgraph state to the DB.
    await this.dumpSubgraphState(blockHash);
    console.timeEnd('time:indexer#processBlockAfterEvents-dump_subgraph_state');
  }

  {{/if}}
  parseEventNameAndArgs (kind: string, logObj: any): any {
    const { topics, data } = logObj;

    const contract = this._contractMap.get(kind);
    assert(contract);

    const logDescription = contract.parseLog({ data, topics });

    const { eventName, eventInfo, eventSignature } = this._baseIndexer.parseEvent(logDescription);

    return {
      eventName,
      eventInfo,
      eventSignature
    };
  }

  async getStateSyncStatus (): Promise<StateSyncStatus | undefined> {
    return this._db.getStateSyncStatus();
  }

  async updateStateSyncStatusIndexedBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus | undefined> {
    if (!this._serverConfig.enableState) {
      return;
    }

    const dbTx = await this._db.createTransactionRunner();
    let res;

    try {
      res = await this._db.updateStateSyncStatusIndexedBlock(dbTx, blockNumber, force);
      await dbTx.commitTransaction();
    } catch (error) {
      await dbTx.rollbackTransaction();
      throw error;
    } finally {
      await dbTx.release();
    }

    return res;
  }

  async updateStateSyncStatusCheckpointBlock (blockNumber: number, force?: boolean): Promise<StateSyncStatus> {
    const dbTx = await this._db.createTransactionRunner();
    let res;

    try {
      res = await this._db.updateStateSyncStatusCheckpointBlock(dbTx, blockNumber, force);
      await dbTx.commitTransaction();
    } catch (error) {
      await dbTx.rollbackTransaction();
      throw error;
    } finally {
      await dbTx.release();
    }

    return res;
  }

  async getLatestCanonicalBlock (): Promise<BlockProgress | undefined> {
    const syncStatus = await this.getSyncStatus();
    assert(syncStatus);

    if (syncStatus.latestCanonicalBlockHash === constants.HashZero) {
      return;
    }

    const latestCanonicalBlock = await this.getBlockProgress(syncStatus.latestCanonicalBlockHash);
    assert(latestCanonicalBlock);

    return latestCanonicalBlock;
  }

  async getLatestStateIndexedBlock (): Promise<BlockProgress> {
    return this._baseIndexer.getLatestStateIndexedBlock();
  }

  {{#if (subgraphPath)}}
  async addContracts (): Promise<void> {
    // Watching all the contracts in the subgraph.
    await this._graphWatcher.addContracts();
  }

  {{/if}}
  async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number, context?: any): Promise<void> {
    return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock, context);
  }

  updateStateStatusMap (address: string, stateStatus: StateStatus): void {
    this._baseIndexer.updateStateStatusMap(address, stateStatus);
  }

  cacheContract (contract: Contract): void {
    return this._baseIndexer.cacheContract(contract);
  }

  async saveEventEntity (dbEvent: Event): Promise<Event> {
    return this._baseIndexer.saveEventEntity(dbEvent);
  }

  async saveEvents (dbEvents: Event[]): Promise<void> {
    return this._baseIndexer.saveEvents(dbEvents);
  }

  async getEventsByFilter (blockHash: string, contract?: string, name?: string): Promise<Array<Event>> {
    return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
  }

  isWatchedContract (address : string): Contract | undefined {
    return this._baseIndexer.isWatchedContract(address);
  }

  getWatchedContracts (): Contract[] {
    return this._baseIndexer.getWatchedContracts();
  }

  getContractsByKind (kind: string): Contract[] {
    return this._baseIndexer.getContractsByKind(kind);
  }

  async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> {
    return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
  }

  async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
    return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber, this._serverConfig.maxEventsBlockRange);
  }

  async getSyncStatus (): Promise<SyncStatus | undefined> {
    return this._baseIndexer.getSyncStatus();
  }

  async getBlocks (blockFilter: { blockHash?: string, blockNumber?: number }): Promise<any> {
    return this._baseIndexer.getBlocks(blockFilter);
  }

  async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    return this._baseIndexer.updateSyncStatusIndexedBlock(blockHash, blockNumber, force);
  }

  async updateSyncStatusChainHead (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    return this._baseIndexer.updateSyncStatusChainHead(blockHash, blockNumber, force);
  }

  async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    const syncStatus = this._baseIndexer.updateSyncStatusCanonicalBlock(blockHash, blockNumber, force);
    {{#if (subgraphPath)}}
    await this.pruneFrothyEntities(blockNumber);
    {{/if}}

    return syncStatus;
  }

  async updateSyncStatusProcessedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
    return this._baseIndexer.updateSyncStatusProcessedBlock(blockHash, blockNumber, force);
  }

  async updateSyncStatusIndexingError (hasIndexingError: boolean): Promise<SyncStatus | undefined> {
    return this._baseIndexer.updateSyncStatusIndexingError(hasIndexingError);
  }

  async updateSyncStatus (syncStatus: DeepPartial<SyncStatus>): Promise<SyncStatus> {
    return this._baseIndexer.updateSyncStatus(syncStatus);
  }

  async getEvent (id: string): Promise<Event | undefined> {
    return this._baseIndexer.getEvent(id);
  }

  async getBlockProgress (blockHash: string): Promise<BlockProgress | undefined> {
    return this._baseIndexer.getBlockProgress(blockHash);
  }

  async getBlockProgressEntities (where: FindConditions<BlockProgress>, options: FindManyOptions<BlockProgress>): Promise<BlockProgress[]> {
    return this._baseIndexer.getBlockProgressEntities(where, options);
  }

  async getBlocksAtHeight (height: number, isPruned: boolean): Promise<BlockProgress[]> {
    return this._baseIndexer.getBlocksAtHeight(height, isPruned);
  }

  async fetchAndSaveFilteredEventsAndBlocks (startBlock: number, endBlock: number): Promise<{
    blockProgress: BlockProgress,
    events: DeepPartial<Event>[],
    ethFullBlock: EthFullBlock;
    ethFullTransactions: EthFullTransaction[];
  }[]> {
    return this._baseIndexer.fetchAndSaveFilteredEventsAndBlocks(startBlock, endBlock, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
  }

  async fetchEventsForContracts (blockHash: string, blockNumber: number, addresses: string[]): Promise<DeepPartial<Event>[]> {
    return this._baseIndexer.fetchEventsForContracts(blockHash, blockNumber, addresses, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));
  }

  async saveBlockAndFetchEvents (block: DeepPartial<BlockProgress>): Promise<[
    BlockProgress,
    DeepPartial<Event>[],
    EthFullTransaction[]
  ]> {
    return this._saveBlockAndFetchEvents(block);
  }

  async getBlockEvents (blockHash: string, where: Where, queryOptions: QueryOptions): Promise<Array<Event>> {
    return this._baseIndexer.getBlockEvents(blockHash, where, queryOptions);
  }

  async removeUnknownEvents (block: BlockProgress): Promise<void> {
    return this._baseIndexer.removeUnknownEvents(Event, block);
  }

  async markBlocksAsPruned (blocks: BlockProgress[]): Promise<void> {
    await this._baseIndexer.markBlocksAsPruned(blocks);
    {{#if (subgraphPath)}}

    await this._graphWatcher.pruneEntities(FrothyEntity, blocks, SUBGRAPH_ENTITIES);
    {{/if}}
  }

  {{#if (subgraphPath)}}
  async pruneFrothyEntities (blockNumber: number): Promise<void> {
    await this._graphWatcher.pruneFrothyEntities(FrothyEntity, blockNumber);
  }

  async resetLatestEntities (blockNumber: number): Promise<void> {
    await this._graphWatcher.resetLatestEntities(blockNumber);
  }

  {{/if}}
  async updateBlockProgress (block: BlockProgress, lastProcessedEventIndex: number): Promise<BlockProgress> {
    return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
  }

  async getAncestorAtHeight (blockHash: string, height: number): Promise<string> {
    return this._baseIndexer.getAncestorAtHeight(blockHash, height);
  }

  async resetWatcherToBlock (blockNumber: number): Promise<void> {
    {{#if (subgraphPath)}}
    const entities = [...ENTITIES, FrothyEntity];
    {{else}}
    const entities = [...ENTITIES];
    {{/if}}
    await this._baseIndexer.resetWatcherToBlock(blockNumber, entities);
    {{#if (subgraphPath)}}

    await this.resetLatestEntities(blockNumber);
    {{/if}}
  }

  async clearProcessedBlockData (block: BlockProgress): Promise<void> {
    {{#if (subgraphPath)}}
    const entities = [...ENTITIES, FrothyEntity];
    {{else}}
    const entities = [...ENTITIES];
    {{/if}}
    await this._baseIndexer.clearProcessedBlockData(block, entities);
    {{#if (subgraphPath)}}

    await this.resetLatestEntities(block.blockNumber);
    {{/if}}
  }
  {{#if (subgraphPath)}}

  getEntityTypesMap (): Map<string, { [key: string]: string }> {
    return this._entityTypesMap;
  }

  getRelationsMap (): Map<any, { [key: string]: any }> {
    return this._relationsMap;
  }

  updateSubgraphState (contractAddress: string, data: any): void {
    return updateSubgraphState(this._subgraphStateMap, contractAddress, data);
  }

  async dumpSubgraphState (blockHash: string, isStateFinalized = false): Promise<void> {
    return dumpSubgraphState(this, this._subgraphStateMap, blockHash, isStateFinalized);
  }

  _populateEntityTypesMap (): void {
  {{#each subgraphEntities as | subgraphEntity |}}
    this._entityTypesMap.set('{{subgraphEntity.className}}', {
      {{#each subgraphEntity.columns as | column |}}
      {{#unless column.isDerived}}
      {{~#unless @first}},
      {{/unless}}
      {{column.name}}: '{{column.type}}'
      {{~/unless}}
      {{/each}}

    });
  {{/each}}
  }

  _populateRelationsMap (): void {
  {{#each subgraphEntities as | subgraphEntity |}}
    {{#if subgraphEntity.relations}}
    this._relationsMap.set({{subgraphEntity.className}}, {
      {{#each subgraphEntity.relations as | relation |}}
      {{~#unless @first}},
      {{/unless}}
      {{relation.name}}: {
        entity: {{relation.type}},
        isArray: {{relation.isArray}},
        isDerived: {{relation.isDerived}}
        {{~#if relation.isDerived}},
        field: '{{relation.derivedFromField}}'
        {{~/if}}

      }
      {{~/each}}

    });
    {{/if}}
  {{/each}}
  }
  {{/if}}

  async _saveBlockAndFetchEvents ({
    cid: blockCid,
    blockHash,
    blockNumber,
    blockTimestamp,
    parentHash
  }: DeepPartial<BlockProgress>): Promise<[
    BlockProgress,
    DeepPartial<Event>[],
    EthFullTransaction[]
  ]> {
    assert(blockHash);
    assert(blockNumber);

    const { events: dbEvents, transactions } = await this._baseIndexer.fetchEvents(blockHash, blockNumber, this.eventSignaturesMap, this.parseEventNameAndArgs.bind(this));

    const dbTx = await this._db.createTransactionRunner();
    try {
      const block = {
        cid: blockCid,
        blockHash,
        blockNumber,
        blockTimestamp,
        parentHash
      };

      console.time(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);
      const blockProgress = await this._db.saveBlockWithEvents(dbTx, block, dbEvents);
      await dbTx.commitTransaction();
      console.timeEnd(`time:indexer#_saveBlockAndFetchEvents-db-save-${blockNumber}`);

      return [blockProgress, [], transactions];
    } catch (error) {
      await dbTx.rollbackTransaction();
      throw error;
    } finally {
      await dbTx.release();
    }
  }

  async getFullTransactions (txHashList: string[]): Promise<EthFullTransaction[]> {
    return this._baseIndexer.getFullTransactions(txHashList);
  }
}
