import { Injectable } from '@angular/core';
import { firstValueFrom, Subject, Subscription} from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { BlockExtended } from '../interfaces/node-api.interface';
import { StateService } from './state.service';
import { ApiService } from './api.service';

const BLOCK_CACHE_SIZE = 500;
const KEEP_RECENT_BLOCKS = 50;

@Injectable({
  providedIn: 'root'
})
export class CacheService {
  loadedBlocks$ = new Subject<BlockExtended>();
  tip: number = 0;

  txCache: { [txid: string]: Transaction } = {};

  network: string;
  blockCache: { [height: number]: BlockExtended } = {};
  blockLoading: { [height: number]: boolean } = {};
  copiesInBlockQueue: { [height: number]: number } = {};
  blockPriorities: number[] = [];

  constructor(
    private stateService: StateService,
    private apiService: ApiService,
  ) {
    this.stateService.blocks$.subscribe(([block]) => {
      this.addBlockToCache(block);
      this.clearBlocks();
    });
    this.stateService.chainTip$.subscribe((height) => {
      this.tip = height;
    });
    this.stateService.networkChanged$.subscribe((network) => {
      this.network = network;
      this.resetBlockCache();
    });
  }

  setTxCache(transactions) {
    this.txCache = {};
    transactions.forEach(tx => {
      this.txCache[tx.txid] = tx;
    });
  }
 
  getTxFromCache(txid) {
    if (this.txCache && this.txCache[txid]) {
      return this.txCache[txid];
    } else {
      return null;
    }
  }

  addBlockToCache(block: BlockExtended) {
    this.blockCache[block.height] = block;
    this.bumpBlockPriority(block.height);
  }

  async loadBlock(height) {
    if (!this.blockCache[height] && !this.blockLoading[height]) {
      const chunkSize = 10;
      const maxHeight = Math.ceil(height / chunkSize) * chunkSize;
      for (let i = 0; i < chunkSize; i++) {
        this.blockLoading[maxHeight - i] = true;
      }
      let result;
      try {
        result = await firstValueFrom(this.apiService.getBlocks$(maxHeight));
      } catch (e) {
        console.log("failed to load blocks: ", e.message);
      }
      if (result && result.length) {
        result.forEach(block => {
          if (this.blockLoading[block.height]) {
            this.addBlockToCache(block);
            this.loadedBlocks$.next(block);
          }
        });
      }
      for (let i = 0; i < chunkSize; i++) {
        delete this.blockLoading[maxHeight - i];
      }
      this.clearBlocks();
    } else {
      this.bumpBlockPriority(height);
    }
  }

  // increase the priority of a block, to delay removal
  bumpBlockPriority(height) {
    this.blockPriorities.push(height);
    this.copiesInBlockQueue[height] = (this.copiesInBlockQueue[height] || 0) + 1;
  }

  // remove lowest priority blocks from the cache
  clearBlocks() {
    while (Object.keys(this.blockCache).length > (BLOCK_CACHE_SIZE + KEEP_RECENT_BLOCKS) && this.blockPriorities.length > KEEP_RECENT_BLOCKS) {
      const height = this.blockPriorities.shift();
      if (this.copiesInBlockQueue[height] > 1) {
        this.copiesInBlockQueue[height]--;
      } else if ((this.tip - height) < KEEP_RECENT_BLOCKS) {
        this.bumpBlockPriority(height);
      } else {
        delete this.blockCache[height];
        delete this.copiesInBlockQueue[height];
      }
    }
  }

  // remove all blocks from the cache
  resetBlockCache() {
    this.blockCache = {};
    this.blockLoading = {};
    this.copiesInBlockQueue = {};
    this.blockPriorities = [];
  }

  getCachedBlock(height) {
    return this.blockCache[height];
  }
}