text: const lazy = require('./lazy')
  const typef = require('typeforce')
  const OPS = require('bitcoin-ops')
  
  const bcrypto = require('../crypto')
  const bscript = require('../script')
  const BITCOIN_NETWORK = require('../networks').bitcoin
  const bs58check = require('bs58check')
  
  function stacksEqual (a, b) {
    if (a.length !== b.length) return false
  
    return a.every(function (x, i) {
      return x.equals(b[i])
    })
  }
  
  // input: [redeemScriptSig ...] {redeemScript}
  // witness: <?>
  // output: OP_HASH160 {hash160(redeemScript)} OP_EQUAL
  function p2sh (a, opts) {
    if (
      !a.address &&
      !a.hash &&
      !a.output &&
      !a.redeem &&
      !a.input
    ) throw new TypeError('Not enough data')
    opts = Object.assign({ validate: true }, opts || {})
  
    typef({
      network: typef.maybe(typef.Object),
  
      address: typef.maybe(typef.String),
      hash: typef.maybe(typef.BufferN(20)),
      output: typef.maybe(typef.BufferN(23)),
  
      redeem: typef.maybe({
        network: typef.maybe(typef.Object),
        output: typef.maybe(typef.Buffer),
        input: typef.maybe(typef.Buffer),
        witness: typef.maybe(typef.arrayOf(typef.Buffer))
      }),
      input: typef.maybe(typef.Buffer),
      witness: typef.maybe(typef.arrayOf(typef.Buffer))
    }, a)
  
    let network = a.network
    if (!network) {
      network = (a.redeem && a.redeem.network) || BITCOIN_NETWORK
    }
  
    const o = { network }
  
    const _address = lazy.value(function () {
      const payload = bs58check.decode(a.address)
      const version = (network.versionBytes === 2) ? payload.readUInt16BE(0) : payload.readUInt8(0)
      const hash = payload.slice(network.versionBytes)
      return { version, hash }
    })
    const _chunks = lazy.value(function () { return bscript.decompile(a.input) })
    const _redeem = lazy.value(function () {
      const chunks = _chunks()
      return {
        network,
        output: chunks[chunks.length - 1],
        input: bscript.compile(chunks.slice(0, -1)),
        witness: a.witness || []
      }
    })
  
    // output dependents
    lazy.prop(o, 'address', function () {
      if (!o.hash) return
  
      const payload = Buffer.allocUnsafe(network.bytes)
      if (network.versionBytes === 1) {
        payload.writeUInt8(network.scriptHash, 0)
      } else if (network.versionBytes === 2) {
        payload.writeUInt16BE(network.scriptHash, 0)
      }
      o.hash.copy(payload, network.versionBytes)
      return bs58check.encode(payload)
    })
    lazy.prop(o, 'hash', function () {
      // in order of least effort
      if (a.output) return a.output.slice(2, 22)
      if (a.address) return _address().hash
      if (o.redeem && o.redeem.output) return bcrypto.hash160(o.redeem.output)
    })
    lazy.prop(o, 'output', function () {
      if (!o.hash) return
      return bscript.compile([
        OPS.OP_HASH160,
        o.hash,
        OPS.OP_EQUAL
      ])
    })
  
    // input dependents
    lazy.prop(o, 'redeem', function () {
      if (!a.input) return
      return _redeem()
    })
    lazy.prop(o, 'input', function () {
      if (!a.redeem || !a.redeem.input || !a.redeem.output) return
      return bscript.compile([].concat(
        bscript.decompile(a.redeem.input),
        a.redeem.output
      ))
    })
    lazy.prop(o, 'witness', function () {
      if (o.redeem && o.redeem.witness) return o.redeem.witness
      if (o.input) return []
    })
  
    if (opts.validate) {
      let hash
      if (a.address) {
        if (_address().version !== network.scriptHash) throw new TypeError('Invalid version or Network mismatch')
        if (_address().hash.length !== 20) throw new TypeError('Invalid address')
        hash = _address().hash
      }
  
      if (a.hash) {
        if (hash && !hash.equals(a.hash)) throw new TypeError('Hash mismatch')
        else hash = a.hash
      }
  
      if (a.output) {
        if (
          a.output.length !== 23 ||
          a.output[0] !== OPS.OP_HASH160 ||
          a.output[1] !== 0x14 ||
          a.output[22] !== OPS.OP_EQUAL) throw new TypeError('Output is invalid')
  
        const hash2 = a.output.slice(2, 22)
        if (hash && !hash.equals(hash2)) throw new TypeError('Hash mismatch')
        else hash = hash2
      }
  
      // inlined to prevent 'no-inner-declarations' failing
      const checkRedeem = function (redeem) {
        // is the redeem output empty/invalid?
        if (redeem.output) {
          const decompile = bscript.decompile(redeem.output)
          if (!decompile || decompile.length < 1) throw new TypeError('Redeem.output too short')
  
          // match hash against other sources
          const hash2 = bcrypto.hash160(redeem.output)
          if (hash && !hash.equals(hash2)) throw new TypeError('Hash mismatch')
          else hash = hash2
        }
  
        if (redeem.input) {
          const hasInput = redeem.input.length > 0
          const hasWitness = redeem.witness && redeem.witness.length > 0
          if (!hasInput && !hasWitness) throw new TypeError('Empty input')
          if (hasInput && hasWitness) throw new TypeError('Input and witness provided')
          if (hasInput) {
            const richunks = bscript.decompile(redeem.input)
            if (!bscript.isPushOnly(richunks)) throw new TypeError('Non push-only scriptSig')
          }
        }
      }
  
      if (a.input) {
        const chunks = _chunks()
        if (!chunks || chunks.length < 1) throw new TypeError('Input too short')
        if (!Buffer.isBuffer(_redeem().output)) throw new TypeError('Input is invalid')
  
        checkRedeem(_redeem())
      }
  
      if (a.redeem) {
        if (a.redeem.network && a.redeem.network !== network) throw new TypeError('Network mismatch')
        if (a.input) {
          const redeem = _redeem()
          if (a.redeem.output && !a.redeem.output.equals(redeem.output)) throw new TypeError('Redeem.output mismatch')
          if (a.redeem.input && !a.redeem.input.equals(redeem.input)) throw new TypeError('Redeem.input mismatch')
        }
  
        checkRedeem(a.redeem)
      }
  
      if (a.witness) {
        if (
          a.redeem &&
          a.redeem.witness &&
          !stacksEqual(a.redeem.witness, a.witness)) throw new TypeError('Witness and redeem.witness mismatch')
      }
    }
  
    return Object.assign(o, a)
  }
  
  module.exports = p2sh
  