use anyhow::{Context, Result};
use bitcoin_hashes::hex::ToHex;
use bitcoincash::blockdata::opcodes;
use bitcoincash::blockdata::script::read_uint;
use core::slice::Iter;

/**
 * Reads a item pushed in a script. Advances the iterator to after item and returns it.
 * Fails if the first byte is not a push operation.
 * Fails if there are fewer bytes left in iterator than indicated by push operation.
 */
pub fn read_push_from_script(mut iter: Iter<u8>) -> Result<(Iter<u8>, Option<Vec<u8>>)> {
    macro_rules! take_stack_item {
        ($iter:expr, $len:expr) => {{
            let p = $iter.by_ref().take($len).cloned().collect::<Vec<u8>>();
            if p.len() < $len {
                return Err(anyhow!(
                    "Item on stack is smaller than expected ({} > {})",
                    p.len(),
                    $len
                ));
            }
            p
        }};
    }

    let opcode = if let Some(o) = iter.next() {
        opcodes::All::from(*o)
    } else {
        return Ok((iter, None));
    };

    if let opcodes::Class::PushBytes(n) = opcode.classify(opcodes::ClassifyContext::Legacy) {
        let item = Some(take_stack_item!(iter, n as usize));
        return Ok((iter, item));
    }
    if let opcodes::Class::PushNum(n) = opcode.classify(opcodes::ClassifyContext::Legacy) {
        return Ok((iter, Some((n as u8).to_be_bytes().to_vec())));
    }

    let n = match opcode {
        opcodes::all::OP_PUSHDATA1 => {
            let n = read_uint(iter.as_slice(), 1).context("Invalid PUSHDATA1")?;
            iter.next().context("Truncated PUSHDATA1 length")?;
            n
        }
        opcodes::all::OP_PUSHDATA2 => {
            let n = read_uint(iter.as_slice(), 2).context("Invalid PUSHDATA2")?;
            iter.next().context("Truncated PUSHDATA2 length")?;
            iter.next().context("Truncated PUSHDATA2 length")?;
            n
        }
        opcodes::all::OP_PUSHDATA4 => {
            let n = read_uint(iter.as_slice(), 4).context("Invalid PUSHDATA4")?;
            iter.next().context("Truncated PUSHDATA4 length")?;
            iter.next().context("Truncated PUSHDATA4 length")?;
            iter.next().context("Truncated PUSHDATA4 length")?;
            iter.next().context("Truncated PUSHDATA4 length")?;
            n
        }
        _ => bail!("0x{} is not a push operation", opcode.to_u8().to_hex()),
    };
    let item = take_stack_item!(iter, n);
    Ok((iter, Some(item)))
}
