// SPDX-License-Identifier: Apache-2.0

use crate::codegen::Expression;
use crate::sema::ast;
use num_bigint::BigInt;
use num_traits::FromPrimitive;
use num_traits::One;
use num_traits::Zero;

use super::expression::{expression, load_storage};
use super::Options;
use super::{
    cfg::{ControlFlowGraph, Instr},
    vartable::Vartable,
};
use crate::codegen::expression::assert_failure;
use crate::sema::ast::{Function, Namespace, RetrieveType, Type};
use solang_parser::pt;

/// Given a storage slot which is the start of the array, calculate the
/// offset of the array element. This function exists to avoid doing
/// 256 bit multiply if possible.
pub fn array_offset(
    loc: &pt::Loc,
    start: Expression,
    index: Expression,
    elem_ty: Type,
    ns: &Namespace,
) -> Expression {
    let elem_size = elem_ty.storage_slots(ns);
    let slot_ty = ns.storage_type();

    // the index needs to be cast to i256 and multiplied by the number
    // of slots for each element
    if elem_size == BigInt::one() {
        Expression::Add(*loc, slot_ty, true, Box::new(start), Box::new(index))
    } else if (elem_size.clone() & (elem_size.clone() - BigInt::one())) == BigInt::zero() {
        // elem_size is power of 2
        Expression::Add(
            *loc,
            slot_ty.clone(),
            true,
            Box::new(start),
            Box::new(Expression::ShiftLeft(
                *loc,
                slot_ty.clone(),
                Box::new(index),
                Box::new(Expression::NumberLiteral(
                    *loc,
                    slot_ty,
                    BigInt::from_u64(elem_size.bits() - 1).unwrap(),
                )),
            )),
        )
    } else {
        Expression::Add(
            *loc,
            slot_ty.clone(),
            true,
            Box::new(start),
            Box::new(Expression::Multiply(
                *loc,
                slot_ty.clone(),
                true,
                Box::new(index),
                Box::new(Expression::NumberLiteral(*loc, slot_ty, elem_size)),
            )),
        )
    }
}

/// Push() method on dynamic array in storage
pub fn storage_slots_array_push(
    loc: &pt::Loc,
    args: &[ast::Expression],
    cfg: &mut ControlFlowGraph,
    contract_no: usize,
    func: Option<&Function>,
    ns: &Namespace,
    vartab: &mut Vartable,
    opt: &Options,
) -> Expression {
    // set array+length to val_expr
    let slot_ty = ns.storage_type();
    let length_pos = vartab.temp_anonymous(&slot_ty);

    let var_expr = expression(&args[0], cfg, contract_no, func, ns, vartab, opt);

    let expr = load_storage(loc, &slot_ty, var_expr.clone(), cfg, vartab);

    cfg.add(
        vartab,
        Instr::Set {
            loc: pt::Loc::Codegen,
            res: length_pos,
            expr,
        },
    );

    let elem_ty = args[0].ty().storage_array_elem();

    let entry_pos = vartab.temp_anonymous(&slot_ty);

    cfg.add(
        vartab,
        Instr::Set {
            loc: pt::Loc::Codegen,
            res: entry_pos,
            expr: array_offset(
                loc,
                Expression::Keccak256(*loc, slot_ty.clone(), vec![var_expr.clone()]),
                Expression::Variable(*loc, slot_ty.clone(), length_pos),
                elem_ty.clone(),
                ns,
            ),
        },
    );

    if args.len() == 2 {
        let value = expression(&args[1], cfg, contract_no, func, ns, vartab, opt);

        cfg.add(
            vartab,
            Instr::SetStorage {
                ty: elem_ty.clone(),
                value,
                storage: Expression::Variable(*loc, slot_ty.clone(), entry_pos),
            },
        );
    }

    // increase length
    let new_length = Expression::Add(
        *loc,
        slot_ty.clone(),
        true,
        Box::new(Expression::Variable(*loc, slot_ty.clone(), length_pos)),
        Box::new(Expression::NumberLiteral(
            *loc,
            slot_ty.clone(),
            BigInt::one(),
        )),
    );

    cfg.add(
        vartab,
        Instr::SetStorage {
            ty: slot_ty,
            value: new_length,
            storage: var_expr,
        },
    );

    if args.len() == 1 {
        Expression::Variable(*loc, elem_ty, entry_pos)
    } else {
        Expression::Poison
    }
}

/// Pop() method on dynamic array in storage
pub fn storage_slots_array_pop(
    loc: &pt::Loc,
    args: &[ast::Expression],
    return_ty: &Type,
    cfg: &mut ControlFlowGraph,
    contract_no: usize,
    func: Option<&Function>,
    ns: &Namespace,
    vartab: &mut Vartable,
    opt: &Options,
) -> Expression {
    // set array+length to val_expr
    let slot_ty = ns.storage_type();
    let length_ty = ns.storage_type();
    let length_pos = vartab.temp_anonymous(&slot_ty);

    let ty = args[0].ty();
    let var_expr = expression(&args[0], cfg, contract_no, func, ns, vartab, opt);

    let expr = load_storage(loc, &length_ty, var_expr.clone(), cfg, vartab);

    cfg.add(
        vartab,
        Instr::Set {
            loc: pt::Loc::Codegen,
            res: length_pos,
            expr,
        },
    );

    let empty_array = cfg.new_basic_block("empty_array".to_string());
    let has_elements = cfg.new_basic_block("has_elements".to_string());

    cfg.add(
        vartab,
        Instr::BranchCond {
            cond: Expression::Equal(
                *loc,
                Box::new(Expression::Variable(*loc, length_ty.clone(), length_pos)),
                Box::new(Expression::NumberLiteral(
                    *loc,
                    length_ty.clone(),
                    BigInt::zero(),
                )),
            ),
            true_block: empty_array,
            false_block: has_elements,
        },
    );

    cfg.set_basic_block(empty_array);
    assert_failure(loc, None, ns, cfg, vartab);

    cfg.set_basic_block(has_elements);
    let new_length = vartab.temp_anonymous(&slot_ty);

    cfg.add(
        vartab,
        Instr::Set {
            loc: pt::Loc::Codegen,
            res: new_length,
            expr: Expression::Subtract(
                *loc,
                length_ty.clone(),
                true,
                Box::new(Expression::Variable(*loc, length_ty.clone(), length_pos)),
                Box::new(Expression::NumberLiteral(*loc, length_ty, BigInt::one())),
            ),
        },
    );

    // The array element will be loaded before clearing. So, the return
    // type of pop() is the derefenced array dereference
    let elem_ty = ty.storage_array_elem().deref_any().clone();
    let entry_pos = vartab.temp_anonymous(&slot_ty);

    cfg.add(
        vartab,
        Instr::Set {
            loc: pt::Loc::Codegen,
            res: entry_pos,
            expr: array_offset(
                loc,
                Expression::Keccak256(*loc, slot_ty.clone(), vec![var_expr.clone()]),
                Expression::Variable(*loc, slot_ty.clone(), new_length),
                elem_ty.clone(),
                ns,
            ),
        },
    );

    let val = if *return_ty != Type::Void {
        let res_pos = vartab.temp_anonymous(&elem_ty);

        let expr = load_storage(
            loc,
            &elem_ty,
            Expression::Variable(*loc, elem_ty.clone(), entry_pos),
            cfg,
            vartab,
        );

        cfg.add(
            vartab,
            Instr::Set {
                loc: *loc,
                res: res_pos,
                expr,
            },
        );
        Expression::Variable(*loc, elem_ty.clone(), res_pos)
    } else {
        Expression::Undefined(elem_ty.clone())
    };

    cfg.add(
        vartab,
        Instr::ClearStorage {
            ty: elem_ty,
            storage: Expression::Variable(*loc, slot_ty.clone(), entry_pos),
        },
    );

    // set decrease length
    cfg.add(
        vartab,
        Instr::SetStorage {
            ty: slot_ty.clone(),
            value: Expression::Variable(*loc, slot_ty, new_length),
            storage: var_expr,
        },
    );

    val
}

/// Push() method on array or bytes in storage
pub fn array_push(
    loc: &pt::Loc,
    args: &[ast::Expression],
    cfg: &mut ControlFlowGraph,
    contract_no: usize,
    func: Option<&Function>,
    ns: &Namespace,
    vartab: &mut Vartable,
    opt: &Options,
) -> Expression {
    let storage = expression(&args[0], cfg, contract_no, func, ns, vartab, opt);

    let mut ty = args[0].ty().storage_array_elem();

    let value = if args.len() > 1 {
        Some(expression(
            &args[1],
            cfg,
            contract_no,
            func,
            ns,
            vartab,
            opt,
        ))
    } else {
        ty.deref_any().default(ns)
    };

    if !ty.is_reference_type(ns) {
        ty = ty.deref_into();
    }

    let res = vartab.temp_anonymous(&ty);

    cfg.add(
        vartab,
        Instr::PushStorage {
            res,
            ty: ty.deref_any().clone(),
            storage,
            value,
        },
    );

    Expression::Variable(*loc, ty, res)
}

/// Pop() method on array or bytes in storage
pub fn array_pop(
    loc: &pt::Loc,
    args: &[ast::Expression],
    return_ty: &Type,
    cfg: &mut ControlFlowGraph,
    contract_no: usize,
    func: Option<&Function>,
    ns: &Namespace,
    vartab: &mut Vartable,
    opt: &Options,
) -> Expression {
    let storage = expression(&args[0], cfg, contract_no, func, ns, vartab, opt);

    let ty = args[0].ty().storage_array_elem().deref_into();

    let res = if *return_ty != Type::Void {
        Some(vartab.temp_anonymous(&ty))
    } else {
        None
    };

    cfg.add(
        vartab,
        Instr::PopStorage {
            res,
            ty: ty.clone(),
            storage,
        },
    );

    if let Some(res) = res {
        Expression::Variable(*loc, ty, res)
    } else {
        Expression::Undefined(ty)
    }
}
