use crate::{
    IdGenerator, aiken_fn,
    ast::{
        Annotation, ArgName, CallArg, DataType, DataTypeKey, Function, FunctionAccessKey,
        ModuleKind, OnTestFailure, RecordConstructor, RecordConstructorArg, Span, TypedArg,
        TypedDataType, TypedFunction, UnOp, well_known,
    },
    expr::TypedExpr,
    tipo::{
        Type, TypeConstructor, TypeInfo, ValueConstructor, ValueConstructorVariant,
        fields::FieldMap,
    },
};
use std::{collections::BTreeSet, sync::LazyLock};

use indexmap::IndexMap;
use std::{collections::HashMap, rc::Rc};
use strum::IntoEnumIterator;

use uplc::{
    builder::{CONSTR_FIELDS_EXPOSER, CONSTR_INDEX_EXPOSER},
    builtins::DefaultFunction,
};

pub const PRELUDE: &str = "aiken";
pub const BUILTIN: &str = "aiken/builtin";

pub static INTERNAL_FUNCTIONS: LazyLock<BTreeSet<&'static str>> = LazyLock::new(|| {
    let mut set = BTreeSet::new();
    set.insert("diagnostic");
    set.insert("do_from_int");
    set.insert("encode_base16");
    set.insert("enumerate");
    set.insert("from_int");
    set
});

/// Build a prelude that can be injected
/// into a compiler pipeline
pub fn prelude(id_gen: &IdGenerator) -> TypeInfo {
    let mut prelude = TypeInfo {
        name: PRELUDE.to_string(),
        package: "".to_string(),
        kind: ModuleKind::Lib,
        types: HashMap::new(),
        types_constructors: HashMap::new(),
        values: HashMap::new(),
        accessors: HashMap::new(),
        annotations: HashMap::new(),
    };

    // Data
    prelude.types.insert(
        well_known::DATA.to_string(),
        TypeConstructor::primitive(Type::data()),
    );

    // Int
    prelude.types.insert(
        well_known::INT.to_string(),
        TypeConstructor::primitive(Type::int()),
    );

    // ByteArray
    prelude.types.insert(
        well_known::BYTE_ARRAY.to_string(),
        TypeConstructor::primitive(Type::byte_array()),
    );

    // Bool
    prelude.types.insert(
        well_known::BOOL.to_string(),
        TypeConstructor::primitive(Type::bool()),
    );
    prelude.types_constructors.insert(
        well_known::BOOL.to_string(),
        ValueConstructor::known_enum(
            &mut prelude.values,
            Type::bool(),
            well_known::BOOL_CONSTRUCTORS,
        ),
    );

    // G1Element
    prelude.types.insert(
        well_known::G1_ELEMENT.to_string(),
        TypeConstructor::primitive(Type::g1_element()),
    );

    // G2Element
    prelude.types.insert(
        well_known::G2_ELEMENT.to_string(),
        TypeConstructor::primitive(Type::g2_element()),
    );

    // MillerLoopResult
    prelude.types.insert(
        well_known::MILLER_LOOP_RESULT.to_string(),
        TypeConstructor::primitive(Type::miller_loop_result()),
    );

    // Ordering
    prelude.types.insert(
        well_known::ORDERING.to_string(),
        TypeConstructor::primitive(Type::ordering()),
    );
    prelude.types_constructors.insert(
        well_known::ORDERING.to_string(),
        ValueConstructor::known_enum(
            &mut prelude.values,
            Type::ordering(),
            well_known::ORDERING_CONSTRUCTORS,
        ),
    );

    // String
    prelude.types.insert(
        well_known::STRING.to_string(),
        TypeConstructor::primitive(Type::string()),
    );

    // Void
    prelude.types.insert(
        well_known::VOID.to_string(),
        TypeConstructor::primitive(Type::void()),
    );
    prelude.types_constructors.insert(
        well_known::VOID.to_string(),
        ValueConstructor::known_enum(
            &mut prelude.values,
            Type::void(),
            well_known::VOID_CONSTRUCTORS,
        ),
    );

    // List(a)
    prelude.types.insert(
        well_known::LIST.to_string(),
        TypeConstructor::primitive(Type::list(Type::generic_var(id_gen.next()))),
    );

    // Pair(a, b)
    let pair_left = Type::generic_var(id_gen.next());
    let pair_right = Type::generic_var(id_gen.next());
    prelude.types.insert(
        well_known::PAIR.to_string(),
        TypeConstructor::primitive(Type::pair(pair_left.clone(), pair_right.clone())),
    );
    prelude.types_constructors.insert(
        well_known::PAIR.to_string(),
        ValueConstructor::known_adt(
            &mut prelude.values,
            &[(
                well_known::PAIR,
                Type::function(
                    vec![pair_left.clone(), pair_right.clone()],
                    Type::pair(pair_left, pair_right),
                ),
            )],
        ),
    );

    // Pairs<k, v> = List<Pair<k, v>>
    prelude.types.insert(
        well_known::PAIRS.to_string(),
        TypeConstructor::primitive(Type::map(
            Type::generic_var(id_gen.next()),
            Type::generic_var(id_gen.next()),
        )),
    );

    // Option(value)
    let option_value = Type::generic_var(id_gen.next());
    prelude.types.insert(
        well_known::OPTION.to_string(),
        TypeConstructor::primitive(Type::option(option_value.clone())),
    );
    let some_type = Type::function(
        vec![option_value.clone()],
        Type::option(option_value.clone()),
    );
    let none_type = Type::option(option_value);
    prelude.types_constructors.insert(
        well_known::OPTION.to_string(),
        ValueConstructor::known_adt(
            &mut prelude.values,
            &[
                (well_known::OPTION_CONSTRUCTORS[0], some_type),
                (well_known::OPTION_CONSTRUCTORS[1], none_type),
            ],
        ),
    );

    // Never
    prelude.types.insert(
        well_known::NEVER.to_string(),
        TypeConstructor::primitive(Type::never()),
    );
    prelude.types_constructors.insert(
        well_known::NEVER.to_string(),
        ValueConstructor::known_adt(
            &mut prelude.values,
            &[(well_known::NEVER_CONSTRUCTORS[1], Type::never())],
        ),
    );

    // Cardano ScriptContext
    prelude.types.insert(
        well_known::SCRIPT_CONTEXT.to_string(),
        TypeConstructor::primitive(Type::script_context()),
    );
    prelude.types_constructors.insert(
        well_known::SCRIPT_CONTEXT.to_string(),
        vec![
            well_known::SCRIPT_CONTEXT_TRANSACTION.to_string(),
            well_known::SCRIPT_CONTEXT_REDEEMER.to_string(),
            well_known::SCRIPT_CONTEXT_PURPOSE.to_string(),
        ],
    );

    // Cardano ScriptPurpose
    prelude.types.insert(
        well_known::SCRIPT_PURPOSE.to_string(),
        TypeConstructor::primitive(Type::script_purpose()),
    );

    prelude.types_constructors.insert(
        well_known::SCRIPT_PURPOSE.to_string(),
        ValueConstructor::known_adt(
            &mut prelude.values,
            &[
                (
                    well_known::SCRIPT_PURPOSE_MINT,
                    Type::function(vec![Type::data()], Type::script_purpose()),
                ),
                (
                    well_known::SCRIPT_PURPOSE_SPEND,
                    Type::function(
                        vec![Type::data(), Type::option(Type::data())],
                        Type::script_purpose(),
                    ),
                ),
                (
                    well_known::SCRIPT_PURPOSE_WITHDRAW,
                    Type::function(vec![Type::data()], Type::script_purpose()),
                ),
                (
                    well_known::SCRIPT_PURPOSE_PUBLISH,
                    Type::function(vec![Type::int(), Type::data()], Type::script_purpose()),
                ),
                (
                    well_known::SCRIPT_PURPOSE_VOTE,
                    Type::function(vec![Type::data()], Type::script_purpose()),
                ),
                (
                    well_known::SCRIPT_PURPOSE_PROPOSE,
                    Type::function(vec![Type::int(), Type::data()], Type::script_purpose()),
                ),
            ],
        ),
    );

    // not
    prelude.values.insert(
        "not".to_string(),
        ValueConstructor::public(
            Type::function(vec![Type::bool()], Type::bool()),
            ValueConstructorVariant::ModuleFn {
                name: "not".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 1,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // identity
    let identity_var = Type::generic_var(id_gen.next());
    prelude.values.insert(
        "identity".to_string(),
        ValueConstructor::public(
            Type::function(vec![identity_var.clone()], identity_var),
            ValueConstructorVariant::ModuleFn {
                name: "identity".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 1,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // as_data
    prelude.values.insert(
        "as_data".to_string(),
        ValueConstructor::public(
            Type::function(vec![Type::data()], Type::data()),
            ValueConstructorVariant::ModuleFn {
                name: "as_data".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 1,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // enumerate
    let enumerate_a = Type::generic_var(id_gen.next());
    let enumerate_b = Type::generic_var(id_gen.next());
    prelude.values.insert(
        "enumerate".to_string(),
        ValueConstructor::public(
            Type::function(
                vec![
                    Type::list(enumerate_a.clone()),
                    enumerate_b.clone(),
                    Type::function(
                        vec![enumerate_a.clone(), enumerate_b.clone()],
                        enumerate_b.clone(),
                    ),
                    Type::function(
                        vec![enumerate_a.clone(), enumerate_b.clone()],
                        enumerate_b.clone(),
                    ),
                ],
                enumerate_b,
            ),
            ValueConstructorVariant::ModuleFn {
                name: "enumerate".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 4,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // encode_base16
    prelude.values.insert(
        "encode_base16".to_string(),
        ValueConstructor::public(
            Type::function(
                vec![Type::byte_array(), Type::int(), Type::byte_array()],
                Type::byte_array(),
            ),
            ValueConstructorVariant::ModuleFn {
                name: "encode_base16".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 3,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // from_int
    prelude.values.insert(
        "from_int".to_string(),
        ValueConstructor::public(
            Type::function(vec![Type::int(), Type::byte_array()], Type::byte_array()),
            ValueConstructorVariant::ModuleFn {
                name: "from_int".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 2,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // do_from_int
    prelude.values.insert(
        "do_from_int".to_string(),
        ValueConstructor::public(
            Type::function(vec![Type::int(), Type::byte_array()], Type::byte_array()),
            ValueConstructorVariant::ModuleFn {
                name: "do_from_int".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 2,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // diagnostic
    prelude.values.insert(
        "diagnostic".to_string(),
        ValueConstructor::public(
            Type::function(vec![Type::data(), Type::byte_array()], Type::byte_array()),
            ValueConstructorVariant::ModuleFn {
                name: "diagnostic".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 2,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // always
    let always_a_var = Type::generic_var(id_gen.next());
    let always_b_var = Type::generic_var(id_gen.next());
    prelude.values.insert(
        "always".to_string(),
        ValueConstructor::public(
            Type::function(vec![always_a_var.clone(), always_b_var], always_a_var),
            ValueConstructorVariant::ModuleFn {
                name: "always".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 2,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // tautology
    let tautology_var = Type::generic_var(id_gen.next());
    prelude.values.insert(
        "tautology".to_string(),
        ValueConstructor::public(
            Type::function(vec![tautology_var], Type::void()),
            ValueConstructorVariant::ModuleFn {
                name: "tautology".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 1,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // flip
    let flip_a_var = Type::generic_var(id_gen.next());
    let flip_b_var = Type::generic_var(id_gen.next());
    let flip_c_var = Type::generic_var(id_gen.next());

    let input_type = Type::function(
        vec![flip_a_var.clone(), flip_b_var.clone()],
        flip_c_var.clone(),
    );

    let return_type = Type::function(vec![flip_b_var, flip_a_var], flip_c_var);

    prelude.values.insert(
        "flip".to_string(),
        ValueConstructor::public(
            Type::function(vec![input_type], return_type),
            ValueConstructorVariant::ModuleFn {
                name: "flip".to_string(),
                field_map: None,
                module: "".to_string(),
                arity: 1,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    // PRNG
    //
    // pub type PRNG {
    //   Seeded { seed: ByteArray, choices: ByteArray }
    //   Replayed { cursor: Int, choices: ByteArray }
    // }
    prelude.types.insert(
        well_known::PRNG.to_string(),
        TypeConstructor::primitive(Type::prng()),
    );

    prelude.types_constructors.insert(
        well_known::PRNG.to_string(),
        vec!["Seeded".to_string(), "Replayed".to_string()],
    );

    let mut seeded_fields = HashMap::new();
    seeded_fields.insert("seed".to_string(), (0, Span::empty()));
    seeded_fields.insert("choices".to_string(), (1, Span::empty()));
    prelude.values.insert(
        "Seeded".to_string(),
        ValueConstructor::public(
            Type::function(vec![Type::byte_array(), Type::byte_array()], Type::prng()),
            ValueConstructorVariant::Record {
                module: "".into(),
                name: "Seeded".to_string(),
                field_map: Some(FieldMap {
                    arity: 2,
                    fields: seeded_fields,
                    is_function: false,
                }),
                arity: 2,
                location: Span::empty(),
                constructors_count: 2,
            },
        ),
    );

    let mut replayed_fields = HashMap::new();
    replayed_fields.insert("cursor".to_string(), (0, Span::empty()));
    replayed_fields.insert("choices".to_string(), (1, Span::empty()));
    prelude.values.insert(
        "Replayed".to_string(),
        ValueConstructor::public(
            Type::function(vec![Type::int(), Type::byte_array()], Type::prng()),
            ValueConstructorVariant::Record {
                module: "".into(),
                name: "Replayed".to_string(),
                field_map: Some(FieldMap {
                    arity: 2,
                    fields: replayed_fields,
                    is_function: false,
                }),
                arity: 2,
                location: Span::empty(),
                constructors_count: 2,
            },
        ),
    );

    // Fuzzer
    //
    // pub type Fuzzer<a> =
    //   fn(PRNG) -> Option<(PRNG, a)>
    let fuzzer_generic = Type::generic_var(id_gen.next());
    prelude.types.insert(
        well_known::FUZZER.to_string(),
        TypeConstructor {
            location: Span::empty(),
            parameters: vec![fuzzer_generic.clone()],
            tipo: Type::fuzzer(fuzzer_generic),
            module: "".to_string(),
            public: true,
        },
    );

    // Sampler
    //
    // pub type Sampler<a> =
    //   fn(Int) -> Fuzzer<a>
    let sampler_generic = Type::generic_var(id_gen.next());
    prelude.types.insert(
        well_known::SAMPLER.to_string(),
        TypeConstructor {
            location: Span::empty(),
            parameters: vec![sampler_generic.clone()],
            tipo: Type::sampler(sampler_generic),
            module: "".to_string(),
            public: true,
        },
    );

    prelude
}

pub fn plutus(id_gen: &IdGenerator) -> TypeInfo {
    let mut plutus = TypeInfo {
        name: BUILTIN.to_string(),
        package: "".to_string(),
        kind: ModuleKind::Lib,
        types: HashMap::new(),
        types_constructors: HashMap::new(),
        values: HashMap::new(),
        accessors: HashMap::new(),
        annotations: HashMap::new(),
    };

    for builtin in DefaultFunction::iter() {
        let value = from_default_function(builtin, id_gen);
        plutus.values.insert(builtin.aiken_name(), value);
    }

    let index_tipo = Type::function(vec![Type::data()], Type::int());
    plutus.values.insert(
        "unconstr_index".to_string(),
        ValueConstructor::public(
            index_tipo,
            ValueConstructorVariant::ModuleFn {
                name: "unconstr_index".to_string(),
                field_map: None,
                module: BUILTIN.to_string(),
                arity: 1,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    let fields_tipo = Type::function(vec![Type::data()], Type::list(Type::data()));
    plutus.values.insert(
        "unconstr_fields".to_string(),
        ValueConstructor::public(
            fields_tipo,
            ValueConstructorVariant::ModuleFn {
                name: "unconstr_fields".to_string(),
                field_map: None,
                module: BUILTIN.to_string(),
                arity: 1,
                location: Span::empty(),
                builtin: None,
            },
        ),
    );

    plutus
}

pub fn from_default_function(builtin: DefaultFunction, id_gen: &IdGenerator) -> ValueConstructor {
    let (tipo, arity) = match builtin {
        DefaultFunction::AddInteger
        | DefaultFunction::SubtractInteger
        | DefaultFunction::MultiplyInteger
        | DefaultFunction::DivideInteger
        | DefaultFunction::QuotientInteger
        | DefaultFunction::RemainderInteger
        | DefaultFunction::ModInteger => {
            let tipo = Type::function(vec![Type::int(), Type::int()], Type::int());
            (tipo, 2)
        }

        DefaultFunction::EqualsInteger
        | DefaultFunction::LessThanInteger
        | DefaultFunction::LessThanEqualsInteger => {
            let tipo = Type::function(vec![Type::int(), Type::int()], Type::bool());

            (tipo, 2)
        }
        DefaultFunction::AppendByteString => {
            let tipo = Type::function(
                vec![Type::byte_array(), Type::byte_array()],
                Type::byte_array(),
            );

            (tipo, 2)
        }
        DefaultFunction::ConsByteString => {
            let tipo = Type::function(vec![Type::int(), Type::byte_array()], Type::byte_array());

            (tipo, 2)
        }
        DefaultFunction::SliceByteString => {
            let tipo = Type::function(
                vec![Type::int(), Type::int(), Type::byte_array()],
                Type::byte_array(),
            );

            (tipo, 3)
        }
        DefaultFunction::LengthOfByteString => {
            let tipo = Type::function(vec![Type::byte_array()], Type::int());

            (tipo, 1)
        }
        DefaultFunction::IndexByteString => {
            let tipo = Type::function(vec![Type::byte_array(), Type::int()], Type::int());

            (tipo, 2)
        }
        DefaultFunction::EqualsByteString
        | DefaultFunction::LessThanByteString
        | DefaultFunction::LessThanEqualsByteString => {
            let tipo = Type::function(vec![Type::byte_array(), Type::byte_array()], Type::bool());

            (tipo, 2)
        }
        DefaultFunction::Sha2_256
        | DefaultFunction::Sha3_256
        | DefaultFunction::Blake2b_224
        | DefaultFunction::Blake2b_256
        | DefaultFunction::Keccak_256 => {
            let tipo = Type::function(vec![Type::byte_array()], Type::byte_array());

            (tipo, 1)
        }

        DefaultFunction::VerifyEd25519Signature => {
            let tipo = Type::function(
                vec![Type::byte_array(), Type::byte_array(), Type::byte_array()],
                Type::bool(),
            );

            (tipo, 3)
        }

        DefaultFunction::VerifyEcdsaSecp256k1Signature => {
            let tipo = Type::function(
                vec![Type::byte_array(), Type::byte_array(), Type::byte_array()],
                Type::bool(),
            );

            (tipo, 3)
        }
        DefaultFunction::VerifySchnorrSecp256k1Signature => {
            let tipo = Type::function(
                vec![Type::byte_array(), Type::byte_array(), Type::byte_array()],
                Type::bool(),
            );

            (tipo, 3)
        }

        DefaultFunction::AppendString => {
            let tipo = Type::function(vec![Type::string(), Type::string()], Type::string());

            (tipo, 2)
        }
        DefaultFunction::EqualsString => {
            let tipo = Type::function(vec![Type::string(), Type::string()], Type::bool());

            (tipo, 2)
        }
        DefaultFunction::EncodeUtf8 => {
            let tipo = Type::function(vec![Type::string()], Type::byte_array());

            (tipo, 1)
        }
        DefaultFunction::DecodeUtf8 => {
            let tipo = Type::function(vec![Type::byte_array()], Type::string());

            (tipo, 1)
        }
        DefaultFunction::IfThenElse => {
            let ret = Type::generic_var(id_gen.next());

            let tipo = Type::function(vec![Type::bool(), ret.clone(), ret.clone()], ret);

            (tipo, 3)
        }
        DefaultFunction::HeadList => {
            let ret = Type::generic_var(id_gen.next());

            let tipo = Type::function(vec![Type::list(ret.clone())], ret);

            (tipo, 1)
        }
        DefaultFunction::TailList => {
            let ret = Type::list(Type::generic_var(id_gen.next()));

            let tipo = Type::function(vec![ret.clone()], ret);

            (tipo, 1)
        }
        DefaultFunction::NullList => {
            let ret = Type::list(Type::generic_var(id_gen.next()));

            let tipo = Type::function(vec![ret], Type::bool());

            (tipo, 1)
        }
        DefaultFunction::ConstrData => {
            let tipo = Type::function(vec![Type::int(), Type::list(Type::data())], Type::data());

            (tipo, 2)
        }
        DefaultFunction::MapData => {
            let tipo = Type::function(
                vec![Type::list(Type::pair(Type::data(), Type::data()))],
                Type::data(),
            );

            (tipo, 1)
        }
        DefaultFunction::ListData => {
            let tipo = Type::function(vec![Type::list(Type::data())], Type::data());

            (tipo, 1)
        }
        DefaultFunction::IData => {
            let tipo = Type::function(vec![Type::int()], Type::data());

            (tipo, 1)
        }
        DefaultFunction::BData => {
            let tipo = Type::function(vec![Type::byte_array()], Type::data());

            (tipo, 1)
        }
        DefaultFunction::UnConstrData => {
            let tipo = Type::function(
                vec![Type::data()],
                Type::pair(Type::int(), Type::list(Type::data())),
            );

            (tipo, 1)
        }
        DefaultFunction::UnMapData => {
            let tipo = Type::function(
                vec![Type::data()],
                Type::list(Type::pair(Type::data(), Type::data())),
            );

            (tipo, 1)
        }
        DefaultFunction::UnListData => {
            let tipo = Type::function(vec![Type::data()], Type::list(Type::data()));

            (tipo, 1)
        }
        DefaultFunction::UnIData => {
            let tipo = Type::function(vec![Type::data()], Type::int());

            (tipo, 1)
        }
        DefaultFunction::UnBData => {
            let tipo = Type::function(vec![Type::data()], Type::byte_array());

            (tipo, 1)
        }
        DefaultFunction::EqualsData => {
            let tipo = Type::function(vec![Type::data(), Type::data()], Type::bool());

            (tipo, 2)
        }
        DefaultFunction::SerialiseData => {
            let tipo = Type::function(vec![Type::data()], Type::byte_array());

            (tipo, 1)
        }
        DefaultFunction::ChooseData => {
            let a = Type::generic_var(id_gen.next());
            let tipo = Type::function(
                vec![
                    Type::data(),
                    a.clone(),
                    a.clone(),
                    a.clone(),
                    a.clone(),
                    a.clone(),
                ],
                a,
            );
            (tipo, 6)
        }
        DefaultFunction::MkPairData => {
            let tipo = Type::function(
                vec![Type::data(), Type::data()],
                Type::pair(Type::data(), Type::data()),
            );
            (tipo, 2)
        }
        DefaultFunction::MkNilData => {
            let tipo = Type::function(vec![], Type::list(Type::data()));
            (tipo, 0)
        }
        DefaultFunction::MkNilPairData => {
            let tipo = Type::function(vec![], Type::list(Type::pair(Type::data(), Type::data())));
            (tipo, 0)
        }
        DefaultFunction::ChooseUnit => {
            let a = Type::generic_var(id_gen.next());
            let tipo = Type::function(vec![Type::data(), a.clone()], a);
            (tipo, 2)
        }
        DefaultFunction::Trace => {
            let a = Type::generic_var(id_gen.next());
            let tipo = Type::function(vec![Type::string(), a.clone()], a);
            (tipo, 2)
        }
        DefaultFunction::FstPair => {
            let a = Type::generic_var(id_gen.next());
            let b = Type::generic_var(id_gen.next());
            let tipo = Type::function(vec![Type::pair(a.clone(), b)], a);
            (tipo, 1)
        }
        DefaultFunction::SndPair => {
            let a = Type::generic_var(id_gen.next());
            let b = Type::generic_var(id_gen.next());
            let tipo = Type::function(vec![Type::pair(a, b.clone())], b);
            (tipo, 1)
        }
        DefaultFunction::ChooseList => {
            let a = Type::generic_var(id_gen.next());
            let b = Type::generic_var(id_gen.next());
            let tipo = Type::function(vec![Type::list(a), b.clone(), b.clone()], b);
            (tipo, 3)
        }
        DefaultFunction::MkCons => {
            let a = Type::generic_var(id_gen.next());
            let tipo = Type::function(vec![a.clone(), Type::list(a.clone())], Type::list(a));
            (tipo, 2)
        }
        DefaultFunction::Bls12_381_G1_Add => {
            let tipo = Type::function(
                vec![Type::g1_element(), Type::g1_element()],
                Type::g1_element(),
            );

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_G1_Equal => {
            let tipo = Type::function(vec![Type::g1_element(), Type::g1_element()], Type::bool());

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_G1_Neg => {
            let tipo = Type::function(vec![Type::g1_element()], Type::g1_element());

            (tipo, 1)
        }
        DefaultFunction::Bls12_381_G1_ScalarMul => {
            let tipo = Type::function(vec![Type::int(), Type::g1_element()], Type::g1_element());

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_G1_Compress => {
            let tipo = Type::function(vec![Type::g1_element()], Type::byte_array());

            (tipo, 1)
        }
        DefaultFunction::Bls12_381_G1_Uncompress => {
            let tipo = Type::function(vec![Type::byte_array()], Type::g1_element());

            (tipo, 1)
        }
        DefaultFunction::Bls12_381_G1_HashToGroup => {
            let tipo = Type::function(
                vec![Type::byte_array(), Type::byte_array()],
                Type::g1_element(),
            );

            (tipo, 2)
        }

        DefaultFunction::Bls12_381_G2_Add => {
            let tipo = Type::function(
                vec![Type::g2_element(), Type::g2_element()],
                Type::g2_element(),
            );

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_G2_Equal => {
            let tipo = Type::function(vec![Type::g2_element(), Type::g2_element()], Type::bool());

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_G2_Neg => {
            let tipo = Type::function(vec![Type::g2_element()], Type::g2_element());

            (tipo, 1)
        }
        DefaultFunction::Bls12_381_G2_ScalarMul => {
            let tipo = Type::function(vec![Type::int(), Type::g2_element()], Type::g2_element());

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_G2_Compress => {
            let tipo = Type::function(vec![Type::g2_element()], Type::byte_array());

            (tipo, 1)
        }
        DefaultFunction::Bls12_381_G2_Uncompress => {
            let tipo = Type::function(vec![Type::byte_array()], Type::g2_element());

            (tipo, 1)
        }
        DefaultFunction::Bls12_381_G2_HashToGroup => {
            let tipo = Type::function(
                vec![Type::byte_array(), Type::byte_array()],
                Type::g2_element(),
            );

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_MillerLoop => {
            let tipo = Type::function(
                vec![Type::g1_element(), Type::g2_element()],
                Type::miller_loop_result(),
            );

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_MulMlResult => {
            let tipo = Type::function(
                vec![Type::miller_loop_result(), Type::miller_loop_result()],
                Type::miller_loop_result(),
            );

            (tipo, 2)
        }
        DefaultFunction::Bls12_381_FinalVerify => {
            let tipo = Type::function(
                vec![Type::miller_loop_result(), Type::miller_loop_result()],
                Type::bool(),
            );

            (tipo, 2)
        }
        DefaultFunction::IntegerToByteString => {
            let tipo = Type::function(
                vec![Type::bool(), Type::int(), Type::int()],
                Type::byte_array(),
            );

            (tipo, 3)
        }
        DefaultFunction::ByteStringToInteger => {
            let tipo = Type::function(vec![Type::bool(), Type::byte_array()], Type::int());

            (tipo, 2)
        }
        DefaultFunction::AndByteString => {
            let tipo = Type::function(
                vec![Type::bool(), Type::byte_array(), Type::byte_array()],
                Type::byte_array(),
            );

            (tipo, 3)
        }
        DefaultFunction::OrByteString => {
            let tipo = Type::function(
                vec![Type::bool(), Type::byte_array(), Type::byte_array()],
                Type::byte_array(),
            );

            (tipo, 3)
        }
        DefaultFunction::XorByteString => {
            let tipo = Type::function(
                vec![Type::bool(), Type::byte_array(), Type::byte_array()],
                Type::byte_array(),
            );

            (tipo, 3)
        }
        DefaultFunction::ComplementByteString => {
            let tipo = Type::function(vec![Type::byte_array()], Type::byte_array());

            (tipo, 1)
        }
        DefaultFunction::ReadBit => {
            let tipo = Type::function(vec![Type::byte_array(), Type::int()], Type::bool());

            (tipo, 2)
        }
        DefaultFunction::WriteBits => {
            let tipo = Type::function(
                vec![Type::byte_array(), Type::list(Type::int()), Type::bool()],
                Type::byte_array(),
            );

            (tipo, 3)
        }
        DefaultFunction::ReplicateByte => {
            let tipo = Type::function(vec![Type::int(), Type::int()], Type::byte_array());

            (tipo, 2)
        }
        DefaultFunction::ShiftByteString => {
            let tipo = Type::function(vec![Type::byte_array(), Type::int()], Type::byte_array());

            (tipo, 2)
        }
        DefaultFunction::RotateByteString => {
            let tipo = Type::function(vec![Type::byte_array(), Type::int()], Type::byte_array());

            (tipo, 2)
        }
        DefaultFunction::CountSetBits => {
            let tipo = Type::function(vec![Type::byte_array()], Type::int());

            (tipo, 1)
        }
        DefaultFunction::FindFirstSetBit => {
            let tipo = Type::function(vec![Type::byte_array()], Type::int());

            (tipo, 1)
        }
        DefaultFunction::Ripemd_160 => {
            let tipo = Type::function(vec![Type::byte_array()], Type::byte_array());

            (tipo, 1)
        } // DefaultFunction::ExpModInteger => {
          //     let tipo = Type::function(vec![Type::int(), Type::int(), Type::int()], Type::int());

          //     (tipo, 3)
          // }
    };

    ValueConstructor::public(
        tipo,
        ValueConstructorVariant::ModuleFn {
            name: builtin.aiken_name(),
            field_map: None,
            module: "".to_string(),
            arity,
            location: Span::empty(),
            builtin: Some(builtin),
        },
    )
}

pub fn prelude_functions(
    id_gen: &IdGenerator,
    module_types: &HashMap<String, TypeInfo>,
) -> IndexMap<FunctionAccessKey, TypedFunction> {
    let mut functions = IndexMap::new();

    let unconstr_index_body = TypedExpr::Call {
        location: Span::empty(),
        tipo: Type::int(),
        fun: TypedExpr::local_var(
            CONSTR_INDEX_EXPOSER,
            Type::function(vec![Type::data()], Type::int()),
            Span::empty(),
        )
        .into(),
        args: vec![CallArg {
            label: None,
            location: Span::empty(),
            value: TypedExpr::Var {
                location: Span::empty(),
                constructor: ValueConstructor {
                    public: true,
                    tipo: Type::data(),
                    variant: ValueConstructorVariant::LocalVariable {
                        location: Span::empty(),
                    },
                },
                name: "constr".to_string(),
            },
        }],
    };

    let unconstr_index_func = Function {
        arguments: vec![TypedArg {
            arg_name: ArgName::Named {
                name: "constr".to_string(),
                label: "constr".to_string(),
                location: Span::empty(),
            },
            is_validator_param: false,
            doc: None,
            location: Span::empty(),
            annotation: None,
            tipo: Type::data(),
        }],
        on_test_failure: OnTestFailure::FailImmediately,
        doc: Some(
            indoc::indoc! {
                r#"
                /// Access the index of a constr typed as Data. Fails if the Data object is not a constr.
                "#
            }.to_string()
        ),
        location: Span::empty(),
        name: "unconstr_index".to_string(),
        public: true,
        return_annotation: None,
        return_type: Type::int(),
        end_position: 0,
        body: unconstr_index_body,
    };

    functions.insert(
        FunctionAccessKey {
            module_name: BUILTIN.to_string(),
            function_name: "unconstr_index".to_string(),
        },
        unconstr_index_func,
    );

    let unconstr_fields_body = TypedExpr::Call {
        location: Span::empty(),
        tipo: Type::list(Type::data()),
        fun: TypedExpr::local_var(
            CONSTR_FIELDS_EXPOSER,
            Type::function(vec![Type::data()], Type::list(Type::data())),
            Span::empty(),
        )
        .into(),
        args: vec![CallArg {
            label: None,
            location: Span::empty(),
            value: TypedExpr::Var {
                location: Span::empty(),
                constructor: ValueConstructor {
                    public: true,
                    tipo: Type::data(),
                    variant: ValueConstructorVariant::LocalVariable {
                        location: Span::empty(),
                    },
                },
                name: "constr".to_string(),
            },
        }],
    };

    let unconstr_fields_func = Function {
        arguments: vec![TypedArg {
            arg_name: ArgName::Named {
                name: "constr".to_string(),
                label: "constr".to_string(),
                location: Span::empty(),
            },
            is_validator_param: false,
            doc: None,
            location: Span::empty(),
            annotation: None,
            tipo: Type::data(),
        }],
        on_test_failure: OnTestFailure::FailImmediately,
        doc: Some(
            indoc::indoc! {
                r#"
                /// Access the fields of a constr typed as Data. Fails if the Data object is not a constr.
                "#
            }.to_string()
        ),
        location: Span::empty(),
        name: "unconstr_fields".to_string(),
        public: true,
        return_annotation: None,
        return_type: Type::list(Type::data()),
        end_position: 0,
        body: unconstr_fields_body,
    };

    functions.insert(
        FunctionAccessKey {
            module_name: BUILTIN.to_string(),
            function_name: "unconstr_fields".to_string(),
        },
        unconstr_fields_func,
    );

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "as_data".to_string(),
        },
        Function {
            arguments: vec![TypedArg {
                arg_name: ArgName::Named {
                    name: "data".to_string(),
                    label: "data".to_string(),
                    location: Span::empty(),
                },
                is_validator_param: false,
                location: Span::empty(),
                annotation: None,
                doc: None,
                tipo: Type::data(),
            }],
            on_test_failure: OnTestFailure::FailImmediately,
            body: TypedExpr::Var {
                location: Span::empty(),
                constructor: ValueConstructor {
                    public: true,
                    tipo: Type::data(),
                    variant: ValueConstructorVariant::LocalVariable {
                        location: Span::empty(),
                    },
                },
                name: "data".to_string(),
            },
            doc: Some(
                indoc::indoc! {
                    r#"
                    A function for explicitly upcasting any serialisable type into `Data`.
                    "#
                }
                .to_string(),
            ),
            location: Span::empty(),
            name: "as_data".to_string(),
            public: true,
            return_annotation: None,
            return_type: Type::data(),
            end_position: 0,
        },
    );

    // /// Negate the argument. Useful for map/fold and pipelines.
    // pub fn not(self: Bool) -> Bool {
    //   !self
    // }
    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "not".to_string(),
        },
        Function {
            arguments: vec![TypedArg {
                arg_name: ArgName::Named {
                    name: "self".to_string(),
                    label: "self".to_string(),
                    location: Span::empty(),
                },
                is_validator_param: false,
                doc: None,
                location: Span::empty(),
                annotation: None,
                tipo: Type::bool(),
            }],
            on_test_failure: OnTestFailure::FailImmediately,
            doc: Some(
                indoc::indoc! {
                    r#"
                    /// Like `!`, but as a function. Handy for chaining using the pipe operator `|>` or to pass as a function.
                    "#
                }.to_string()
            ),
            location: Span::empty(),
            name: "not".to_string(),
            public: true,
            return_annotation: None,
            return_type: Type::bool(),
            end_position: 0,
            body: TypedExpr::UnOp {
                location: Span::empty(),
                tipo: Type::bool(),
                op: UnOp::Not,
                value: Box::new(TypedExpr::Var {
                    location: Span::empty(),
                    constructor: ValueConstructor {
                        public: true,
                        tipo: Type::bool(),
                        variant: ValueConstructorVariant::LocalVariable {
                            location: Span::empty(),
                        },
                    },
                    name: "self".to_string(),
                }),
            },
        },
    );

    // /// A function that returns its argument. Handy as a default behavior sometimes.
    // pub fn identity(a: a) -> a {
    //   a
    // }
    let a_var = Type::generic_var(id_gen.next());

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "identity".to_string(),
        },
        Function {
            arguments: vec![TypedArg {
                arg_name: ArgName::Named {
                    name: "a".to_string(),
                    label: "a".to_string(),
                    location: Span::empty(),
                },
                is_validator_param: false,
                location: Span::empty(),
                annotation: None,
                doc: None,
                tipo: a_var.clone(),
            }],
            on_test_failure: OnTestFailure::FailImmediately,
            body: TypedExpr::Var {
                location: Span::empty(),
                constructor: ValueConstructor {
                    public: true,
                    tipo: a_var.clone(),
                    variant: ValueConstructorVariant::LocalVariable {
                        location: Span::empty(),
                    },
                },
                name: "a".to_string(),
            },
            doc: Some(
                indoc::indoc! {
                    r#"
                    A function that returns its argument. Handy as a default behavior sometimes.
                    "#
                }
                .to_string(),
            ),
            location: Span::empty(),
            name: "identity".to_string(),
            public: true,
            return_annotation: None,
            return_type: a_var,
            end_position: 0,
        },
    );

    // /// A function that always return its first argument. Handy in folds and maps.
    // pub fn always(a: a, b _b: b) -> a {
    //   a
    // }
    let a_var = Type::generic_var(id_gen.next());
    let b_var = Type::generic_var(id_gen.next());

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "always".to_string(),
        },
        Function {
            on_test_failure: OnTestFailure::FailImmediately,
            arguments: vec![
                TypedArg {
                    arg_name: ArgName::Named {
                        name: "a".to_string(),
                        label: "a".to_string(),
                        location: Span::empty(),
                    },
                    is_validator_param: false,
                    location: Span::empty(),
                    annotation: None,
                    doc: None,
                    tipo: a_var.clone(),
                },
                TypedArg {
                    arg_name: ArgName::Discarded {
                        name: "_b".to_string(),
                        label: "_b".to_string(),
                        location: Span::empty(),
                    },
                    is_validator_param: false,
                    location: Span::empty(),
                    annotation: None,
                    doc: None,
                    tipo: b_var,
                },
            ],
            body: TypedExpr::Var {
                location: Span::empty(),
                constructor: ValueConstructor {
                    public: true,
                    tipo: a_var.clone(),
                    variant: ValueConstructorVariant::LocalVariable {
                        location: Span::empty(),
                    },
                },
                name: "a".to_string(),
            },
            doc: Some(
                indoc::indoc! {
                    r#"
                    A function that always return its first argument. Handy in folds and maps.

                    ```aiken
                    let always_14 = always(14, _)
                    always_14(42) == 14
                    always_14(1337) == 14
                    always_14(0) == 14
                    ```
                    "#
                }
                .to_string(),
            ),
            location: Span::empty(),
            name: "always".to_string(),
            public: true,
            return_annotation: None,
            return_type: a_var,
            end_position: 0,
        },
    );

    // /// A function that absorbs any expression and returns true. Useful to write failing test
    // /// scenarios regardless of the output of a function.
    // pub fn tautology(a: a) -> Bool {
    //   a == a
    // }
    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "tautology".to_string(),
        },
        aiken_fn!(
            &module_types,
            &id_gen,
            r#"
                pub fn tautology(a: a) -> Bool {
                  a == a
                }
            "#
        ),
    );

    // /// A function that absorbs any expression and returns true. Useful to write failing test
    // // scenarios regardless of the output of a function.
    // pub fn tautology(a: a) -> Bool {
    //   a == a
    // }
    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "tautology".to_string(),
        },
        aiken_fn!(
            &module_types,
            &id_gen,
            r#"
                pub fn tautology(a: a) -> Bool {
                  a == a
                }
            "#
        ),
    );

    // /// A function that flips the arguments of a function.
    // pub fn flip(f: fn(a, b) -> c) -> fn(b, a) -> c {
    //   fn(b, a) { f(a, b) }
    // }
    let a_var = Type::generic_var(id_gen.next());
    let b_var = Type::generic_var(id_gen.next());
    let c_var = Type::generic_var(id_gen.next());

    let input_type = Type::function(vec![a_var.clone(), b_var.clone()], c_var.clone());
    let return_type = Type::function(vec![b_var.clone(), a_var.clone()], c_var.clone());

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "flip".to_string(),
        },
        Function {
            on_test_failure: OnTestFailure::FailImmediately,
            arguments: vec![TypedArg {
                arg_name: ArgName::Named {
                    name: "f".to_string(),
                    label: "f".to_string(),
                    location: Span::empty(),
                },
                is_validator_param: false,
                location: Span::empty(),
                annotation: None,
                doc: None,
                tipo: input_type.clone(),
            }],
            body: TypedExpr::Fn {
                location: Span::empty(),
                tipo: return_type.clone(),
                is_capture: false,
                args: vec![
                    TypedArg {
                        arg_name: ArgName::Named {
                            name: "b".to_string(),
                            label: "b".to_string(),
                            location: Span::empty(),
                        },
                        is_validator_param: false,
                        location: Span::empty(),
                        annotation: None,
                        doc: None,
                        tipo: b_var.clone(),
                    },
                    TypedArg {
                        arg_name: ArgName::Named {
                            name: "a".to_string(),
                            label: "a".to_string(),
                            location: Span::empty(),
                        },
                        is_validator_param: false,
                        location: Span::empty(),
                        annotation: None,
                        doc: None,
                        tipo: a_var.clone(),
                    },
                ],
                body: Box::new(TypedExpr::Call {
                    location: Span::empty(),
                    tipo: c_var,
                    fun: Box::new(TypedExpr::Var {
                        location: Span::empty(),
                        constructor: ValueConstructor {
                            public: true,
                            tipo: input_type,
                            variant: ValueConstructorVariant::LocalVariable {
                                location: Span::empty(),
                            },
                        },
                        name: "f".to_string(),
                    }),
                    args: vec![
                        CallArg {
                            label: None,
                            location: Span::empty(),
                            value: TypedExpr::Var {
                                location: Span::empty(),
                                constructor: ValueConstructor {
                                    public: true,
                                    tipo: a_var,
                                    variant: ValueConstructorVariant::LocalVariable {
                                        location: Span::empty(),
                                    },
                                },
                                name: "a".to_string(),
                            },
                        },
                        CallArg {
                            label: None,
                            location: Span::empty(),
                            value: TypedExpr::Var {
                                location: Span::empty(),
                                constructor: ValueConstructor {
                                    public: true,
                                    tipo: b_var,
                                    variant: ValueConstructorVariant::LocalVariable {
                                        location: Span::empty(),
                                    },
                                },
                                name: "b".to_string(),
                            },
                        },
                    ],
                }),
                return_annotation: None,
            },
            doc: Some(
                indoc::indoc! {
                    r#"
                    A function that flips the arguments of a function.

                    ```aiken
                    pub fn titleize(left: String, right: String) {}

                    titleize("Hello", "World") // "Hello, World!"

                    flip(titleize)("Hello", "World") // "World, Hello!"
                    ```
                    "#
                }
                .to_string(),
            ),
            location: Span::empty(),
            name: "flip".to_string(),
            public: true,
            return_annotation: None,
            return_type,
            end_position: 0,
        },
    );

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "enumerate".to_string(),
        },
        aiken_fn!(
            &module_types,
            &id_gen,
            r#"
                fn enumerate(
                  self: List<a>,
                  zero: b,
                  with: fn(a, b) -> b,
                  last: fn(a, b) -> b,
                ) -> b {
                  when self is {
                    [] -> zero
                    [x] -> last(x, zero)
                    [x, ..xs] -> with(x, enumerate(xs, zero, with, last))
                  }
                }
            "#
        ),
    );

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "encode_base16".to_string(),
        },
        aiken_fn!(
            &module_types,
            &id_gen,
            r#"
                use aiken/builtin

                fn encode_base16(bytes: ByteArray, ix: Int, builder: ByteArray) -> ByteArray {
                  if ix < 0 {
                    builder
                  } else {
                    let byte = builtin.index_bytearray(bytes, ix)
                    let msb = byte / 16
                    let lsb = byte % 16
                    let builder =
                      builtin.cons_bytearray(
                        msb + if msb < 10 {
                          48
                        } else {
                          55
                        },
                        builtin.cons_bytearray(
                          lsb + if lsb < 10 {
                            48
                          } else {
                            55
                          },
                          builder,
                        ),
                      )
                    encode_base16(bytes, ix - 1, builder)
                  }
                }
            "#
        ),
    );

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "do_from_int".to_string(),
        },
        aiken_fn!(
            &module_types,
            &id_gen,
            r#"
                use aiken/builtin

                fn do_from_int(i: Int, digits: ByteArray) -> ByteArray {
                  if i <= 0 {
                    digits
                  } else {
                    do_from_int(
                      builtin.quotient_integer(i, 10),
                      builtin.cons_bytearray(builtin.remainder_integer(i, 10) + 48, digits),
                    )
                  }
                }
            "#
        ),
    );

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "from_int".to_string(),
        },
        aiken_fn!(
            &module_types,
            &id_gen,
            r#"
                use aiken/builtin

                /// Encode an integer into UTF-8.
                fn from_int(i: Int, digits: ByteArray) -> ByteArray {
                  if i == 0 {
                    builtin.append_bytearray(#"30", digits)
                  } else if i < 0 {
                    builtin.append_bytearray(#"2d", from_int(-i, digits))
                  } else {
                    do_from_int(
                      builtin.quotient_integer(i, 10),
                      builtin.cons_bytearray(builtin.remainder_integer(i, 10) + 48, digits),
                    )
                  }
                }
            "#
        ),
    );

    functions.insert(
        FunctionAccessKey {
            module_name: "".to_string(),
            function_name: "diagnostic".to_string(),
        },
        aiken_fn!(
            &module_types,
            &id_gen,
            r#"
              use aiken/builtin

              fn diagnostic(self: Data, builder: ByteArray) -> ByteArray {
                builtin.choose_data(
                  self,
                  {
                    let Pair(constr, fields) = builtin.un_constr_data(self)

                    let builder =
                      when fields is {
                        [] -> builtin.append_bytearray(#"5b5d29", builder)
                        _ -> {
                          let bytes =
                            enumerate(
                              fields,
                              builtin.append_bytearray(#"5d29", builder),
                              fn(e: Data, st: ByteArray) {
                                diagnostic(e, builtin.append_bytearray(#"2c20", st))
                              },
                              fn(e: Data, st: ByteArray) { diagnostic(e, st) },
                            )
                          builtin.append_bytearray(#"5b5f20", bytes)
                        }
                      }

                    let constr_tag =
                      if constr < 7 {
                        121 + constr
                      } else if constr < 128 {
                        1280 + constr - 7
                      } else {
                        fail @"What are you doing? No I mean, seriously."
                      }

                    builder
                      |> builtin.append_bytearray(#"28", _)
                      |> from_int(constr_tag, _)
                  },
                  {
                    let elems = builtin.un_map_data(self)
                    when elems is {
                      [] -> builtin.append_bytearray(#"7b7d", builder)
                      _ -> {
                        let bytes =
                          enumerate(
                            elems,
                            builtin.append_bytearray(#"207d", builder),
                            fn(e: Pair<Data, Data>, st: ByteArray) {
                              let value = diagnostic(e.2nd, builtin.append_bytearray(#"2c20", st))
                              diagnostic(e.1st, builtin.append_bytearray(#"3a20", value))
                            },
                            fn(e: Pair<Data, Data>, st: ByteArray) {
                              let value = diagnostic(e.2nd, st)
                              diagnostic(e.1st, builtin.append_bytearray(#"3a20", value))
                            },
                          )
                        builtin.append_bytearray(#"7b5f20", bytes)
                      }
                    }
                  },
                  {
                    let elems = builtin.un_list_data(self)
                    when elems is {
                      [] -> builtin.append_bytearray(#"5b5d", builder)
                      _ -> {
                        let bytes =
                          enumerate(
                            elems,
                            builtin.append_bytearray(#"5d", builder),
                            fn(e: Data, st: ByteArray) {
                              diagnostic(e, builtin.append_bytearray(#"2c20", st))
                            },
                            fn(e: Data, st: ByteArray) { diagnostic(e, st) },
                          )
                        builtin.append_bytearray(#"5b5f20", bytes)
                      }
                    }
                  },
                  self
                    |> builtin.un_i_data
                    |> from_int(builder),
                  {
                    let bytes = builtin.un_b_data(self)
                    bytes
                      |> encode_base16(
                          builtin.length_of_bytearray(bytes) - 1,
                          builtin.append_bytearray(#"27", builder),
                        )
                      |> builtin.append_bytearray(#"6827", _)
                  },
                )
              }
            "#
        ),
    );

    functions
}

pub fn prelude_data_types(id_gen: &IdGenerator) -> IndexMap<DataTypeKey, TypedDataType> {
    let mut data_types = IndexMap::new();

    // Data
    let data_data_type = TypedDataType::data();
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::DATA.to_string(),
        },
        data_data_type,
    );

    // Void
    let void_data_type = TypedDataType::void();
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::VOID.to_string(),
        },
        void_data_type,
    );

    // Ordering
    let ordering_data_type = TypedDataType::ordering();
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::ORDERING.to_string(),
        },
        ordering_data_type,
    );

    // Bool
    let bool_data_type = TypedDataType::bool();
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::BOOL.to_string(),
        },
        bool_data_type,
    );

    // Option
    let option_data_type = TypedDataType::option(Type::generic_var(id_gen.next()));
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::OPTION.to_string(),
        },
        option_data_type,
    );

    // Pair
    let pair_data_type = TypedDataType::pair(
        Type::generic_var(id_gen.next()),
        Type::generic_var(id_gen.next()),
    );
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::PAIR.to_string(),
        },
        pair_data_type,
    );

    // Never
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::NEVER.to_string(),
        },
        TypedDataType::never(),
    );

    // PRNG
    let prng_data_type = TypedDataType::prng();
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::PRNG.to_string(),
        },
        prng_data_type,
    );

    // __ScriptPurpose
    let script_purpose_data_type = TypedDataType::script_purpose();
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::SCRIPT_PURPOSE.to_string(),
        },
        script_purpose_data_type,
    );

    // __ScriptContext
    let script_context_data_type = TypedDataType::script_context();
    data_types.insert(
        DataTypeKey {
            module_name: "".to_string(),
            defined_type: well_known::SCRIPT_CONTEXT.to_string(),
        },
        script_context_data_type,
    );

    data_types
}

// ----------------------------------------------------------------------------
// TypedDataTypes
//
// TODO: Rewrite in terms of ValueConstructor to avoid duplication and ensure
// consistency with prelude definitions.

impl TypedDataType {
    pub fn data() -> Self {
        DataType::known_enum(well_known::DATA, &[])
    }

    pub fn void() -> Self {
        DataType::known_enum(well_known::VOID, well_known::VOID_CONSTRUCTORS)
    }

    pub fn bool() -> Self {
        DataType::known_enum(well_known::BOOL, well_known::BOOL_CONSTRUCTORS)
    }

    pub fn script_purpose() -> Self {
        DataType::known_enum(
            well_known::SCRIPT_PURPOSE,
            well_known::SCRIPT_PURPOSE_CONSTRUCTORS,
        )
    }

    pub fn script_context() -> Self {
        DataType::known_enum(
            well_known::SCRIPT_CONTEXT,
            well_known::SCRIPT_CONTEXT_CONSTRUCTORS,
        )
    }

    pub fn prng() -> Self {
        let bytearray_arg = |label: &str| RecordConstructorArg {
            label: Some(label.to_string()),
            doc: None,
            annotation: Annotation::bytearray(Span::empty()),
            location: Span::empty(),
            tipo: Type::byte_array(),
        };

        let int_arg = |label: &str| RecordConstructorArg {
            label: Some(label.to_string()),
            doc: None,
            annotation: Annotation::int(Span::empty()),
            location: Span::empty(),
            tipo: Type::int(),
        };

        DataType::known_data_type(
            well_known::PRNG,
            &[
                RecordConstructor::known_record(
                    well_known::PRNG_CONSTRUCTORS[0],
                    &[bytearray_arg("seed"), bytearray_arg("choices")],
                ),
                RecordConstructor::known_record(
                    well_known::PRNG_CONSTRUCTORS[1],
                    &[int_arg("cursor"), bytearray_arg("choices")],
                ),
            ],
        )
    }

    pub fn ordering() -> Self {
        DataType::known_enum(well_known::ORDERING, well_known::ORDERING_CONSTRUCTORS)
    }

    pub fn option(tipo: Rc<Type>) -> Self {
        DataType {
            decorators: vec![],
            constructors: vec![
                RecordConstructor {
                    decorators: vec![],
                    location: Span::empty(),
                    name: well_known::OPTION_CONSTRUCTORS[0].to_string(),
                    arguments: vec![RecordConstructorArg {
                        label: None,
                        annotation: Annotation::Var {
                            location: Span::empty(),
                            name: "a".to_string(),
                        },
                        location: Span::empty(),
                        tipo: tipo.clone(),
                        doc: None,
                    }],
                    doc: None,
                    sugar: false,
                },
                RecordConstructor {
                    decorators: vec![],
                    location: Span::empty(),
                    name: well_known::OPTION_CONSTRUCTORS[1].to_string(),
                    arguments: vec![],
                    doc: None,
                    sugar: false,
                },
            ],
            doc: None,
            location: Span::empty(),
            name: well_known::OPTION.to_string(),
            opaque: false,
            parameters: vec!["a".to_string()],
            public: true,
            typed_parameters: vec![tipo],
        }
    }

    pub fn pair(left: Rc<Type>, right: Rc<Type>) -> Self {
        DataType {
            decorators: vec![],
            constructors: vec![RecordConstructor {
                decorators: vec![],
                location: Span::empty(),
                name: well_known::PAIR.to_string(),
                arguments: vec![
                    RecordConstructorArg {
                        label: None,
                        annotation: Annotation::Var {
                            location: Span::empty(),
                            name: "left".to_string(),
                        },
                        location: Span::empty(),
                        tipo: left.clone(),
                        doc: None,
                    },
                    RecordConstructorArg {
                        label: None,
                        annotation: Annotation::Var {
                            location: Span::empty(),
                            name: "right".to_string(),
                        },
                        location: Span::empty(),
                        tipo: right.clone(),
                        doc: None,
                    },
                ],
                doc: None,
                sugar: false,
            }],
            doc: None,
            location: Span::empty(),
            name: well_known::PAIR.to_string(),
            opaque: false,
            parameters: vec!["left".to_string(), "right".to_string()],
            public: true,
            typed_parameters: vec![left, right],
        }
    }

    pub fn never() -> Self {
        DataType::known_enum(well_known::NEVER, well_known::NEVER_CONSTRUCTORS)
    }
}
