use rustpython_bytecode::bytecode::{self, Instruction};

use super::{InstructionMetadata, OptimizationBuffer};

macro_rules! metas {
    [$($metas:expr),*$(,)?] => {
        InstructionMetadata::from(vec![$($metas),*])
    };
}
macro_rules! lc {
    ($name:ident {$($field:tt)*}) => {
        Instruction::LoadConst {
            value: bytecode::Constant::$name {$($field)*},
        }
    };
    ($name:ident, $($value:tt)*) => {
        lc!($name { value: $($value)* })
    };
}
macro_rules! emitconst {
    ($buf:expr, [$($metas:expr),*$(,)?], $($arg:tt)*) => {
        $buf.emit(
            lc!($($arg)*),
            metas![$($metas),*],
        )
    };
}

pub fn operator(buf: &mut impl OptimizationBuffer) {
    let (instruction, meta) = buf.pop();
    if let Instruction::BinaryOperation { op, inplace } = instruction {
        let (rhs, rhs_meta) = buf.pop();
        let (lhs, lhs_meta) = buf.pop();
        macro_rules! op {
            ($op:ident) => {
                bytecode::BinaryOperator::$op
            };
        }
        match (op, lhs, rhs) {
            (op!(Add), lc!(Integer, lhs), lc!(Integer, rhs)) => {
                emitconst!(buf, [lhs_meta, rhs_meta], Integer, lhs + rhs)
            }
            (op!(Subtract), lc!(Integer, lhs), lc!(Integer, rhs)) => {
                emitconst!(buf, [lhs_meta, rhs_meta], Integer, lhs - rhs)
            }
            (op!(Add), lc!(Float, lhs), lc!(Float, rhs)) => {
                emitconst!(buf, [lhs_meta, rhs_meta], Float, lhs + rhs)
            }
            (op!(Subtract), lc!(Float, lhs), lc!(Float, rhs)) => {
                emitconst!(buf, [lhs_meta, rhs_meta], Float, lhs - rhs)
            }
            (op!(Multiply), lc!(Float, lhs), lc!(Float, rhs)) => {
                emitconst!(buf, [lhs_meta, rhs_meta], Float, lhs * rhs)
            }
            (op!(Divide), lc!(Float, lhs), lc!(Float, rhs)) => {
                emitconst!(buf, [lhs_meta, rhs_meta], Float, lhs / rhs)
            }
            (op!(Power), lc!(Float, lhs), lc!(Float, rhs)) => {
                emitconst!(buf, [lhs_meta, rhs_meta], Float, lhs.powf(rhs))
            }
            (op!(Add), lc!(String, mut lhs), lc!(String, rhs)) => {
                lhs.push_str(&rhs);
                emitconst!(buf, [lhs_meta, rhs_meta], String, lhs);
            }
            (op, lhs, rhs) => {
                buf.emit(lhs, lhs_meta);
                buf.emit(rhs, rhs_meta);
                buf.emit(Instruction::BinaryOperation { op, inplace }, meta);
            }
        }
    } else {
        buf.emit(instruction, meta)
    }
}

pub fn unpack(buf: &mut impl OptimizationBuffer) {
    let (instruction, meta) = buf.pop();
    if let Instruction::UnpackSequence { size } = instruction {
        let (arg, arg_meta) = buf.pop();
        match arg {
            Instruction::BuildTuple {
                size: tup_size,
                unpack,
            } if !unpack && tup_size == size => {
                buf.emit(
                    Instruction::Reverse { amount: size },
                    vec![arg_meta, meta].into(),
                );
            }
            arg => {
                buf.emit(arg, arg_meta);
                buf.emit(instruction, meta);
            }
        }
    } else {
        buf.emit(instruction, meta)
    }
}
