const assert = require("assert");
const GenUtils = require("../../common/GenUtils");
const MoneroBlockHeader = require("./MoneroBlockHeader");
const MoneroTx = require("./MoneroTx");
const MoneroTxQuery = require("../../wallet/model/MoneroTxQuery");
const MoneroTxWallet = require("../../wallet/model/MoneroTxWallet");

/**
 * Models a Monero block in the blockchain.
 * 
 * @extends {MoneroBlockHeader}
 */
class MoneroBlock extends MoneroBlockHeader {
  
  /**
   * Construct the model.
   * 
   * @param {MoneroBlock|MoneroBlockHeader|object} state is existing state to initialize from (optional)
   * @param {MoneroBlock.DeserializationType} txType informs the tx deserialization type (MoneroTx, MoneroTxWallet, MoneroTxQuery)
   */
  constructor(state, txType) {
    super(state);
    state = this.state;
    
    // deserialize miner tx
    if (state.minerTx && !(state.minerTx instanceof MoneroTx)) state.minerTx = new MoneroTx(state.minerTx).setBlock(this);
    
    // deserialize non-miner txs
    if (state.txs) {
      for (let i = 0; i < state.txs.length; i++) {
        if (txType === MoneroBlock.DeserializationType.TX || txType === undefined) {
          if (!(state.txs[i] instanceof MoneroTx)) state.txs[i] = new MoneroTx(state.txs[i]).setBlock(this);
        } else if (txType === MoneroBlock.DeserializationType.TX_WALLET) {
          if (!(state.txs[i] instanceof MoneroTxWallet)) state.txs[i] = new MoneroTxWallet(state.txs[i]).setBlock(this);
        } else if (txType === MoneroBlock.DeserializationType.TX_QUERY) {
          if (!(state.txs[i] instanceof MoneroTxQuery)) state.txs[i] = new MoneroTxQuery(state.txs[i]).setBlock(this);
        } else {
          throw new Error("Unrecognized tx deserialization type: " + txType);
        }
      }
    }
  }
  
  getHex() {
    return this.state.hex;
  }
  
  setHex(hex) {
    this.state.hex = hex;
    return this;
  }
  
  getMinerTx() {
    return this.state.minerTx;
  }
  
  setMinerTx(minerTx) {
    this.state.minerTx = minerTx;
    return this;
  }
  
  getTxs() {
    return this.state.txs;
  }
  
  setTxs(txs) {
    this.state.txs = txs;
    return this;
  }
  
  getTxHashes() {
    return this.state.txHashes;
  }
  
  setTxHashes(txHashes) {
    this.state.txHashes = txHashes;
    return this;
  }
  
  copy() {
    return new MoneroBlock(this);
  }
  
  toJson() {
    let json = super.toJson();
    if (this.getMinerTx()) json.minerTx = this.getMinerTx().toJson();
    if (this.getTxs()) {
      json.txs = [];
      for (let tx of this.getTxs()) json.txs.push(tx.toJson());
    }
    return json;
  }
  
  merge(block) {
    assert(block instanceof MoneroBlock);
    if (this === block) return this;
    
    // merge header fields
    super.merge(block);
    
    // merge reconcilable block extensions
    this.setHex(GenUtils.reconcile(this.getHex(), block.getHex()));
    this.setTxHashes(GenUtils.reconcile(this.getTxHashes(), block.getTxHashes()));
    
    // merge miner tx
    if (this.getMinerTx() === undefined) this.setMinerTx(block.getMinerTx());
    if (block.getMinerTx() !== undefined) {
      block.getMinerTx().setBlock(this);
      this.getMinerTx().merge(block.getMinerTx());
    }
    
    // merge non-miner txs
    if (block.getTxs() !== undefined) {
      for (let tx of block.getTxs()) {
        tx.setBlock(this);
        MoneroBlock._mergeTx(this.getTxs(), tx);
      }
    }

    return this;
  }
  
  toString(indent = 0) {
    let str = super.toString(indent) + "\n";
    str += GenUtils.kvLine("Hex", this.getHex(), indent);
    if (this.getTxs()) {
      str += GenUtils.kvLine("Txs", "", indent);
      for (let tx of this.getTxs()) {
        str += tx.toString(indent + 1) + "\n";
      }
    }
    if (this.getMinerTx()) {
      str += GenUtils.kvLine("Miner tx", "", indent);
      str += this.getMinerTx().toString(indent + 1) + "\n";
    }
    str += GenUtils.kvLine("Txs hashes", this.getTxHashes(), indent);
    return str[str.length - 1] === "\n" ? str.slice(0, str.length - 1) : str  // strip last newline
  }
  
  // private helper to merge txs
  static _mergeTx(txs, tx) {
    for (let aTx of txs) {
      if (aTx.getHash() === tx.getHash()) {
        aTx.merge(tx);
        return;
      }
    }
    txs.push(tx);
  }
}

MoneroBlock.DeserializationType = {
    TX: 0,
    TX_WALLET: 1,
    TX_QUERY: 2
}

module.exports = MoneroBlock;