module token_bridge::bridge_state {
    use std::option::{Self, Option};
    use std::ascii::{Self};

    use sui::object::{Self, UID};
    use sui::vec_map::{Self, VecMap};
    use sui::tx_context::{TxContext};
    use sui::coin::{Self, Coin, TreasuryCap, CoinMetadata};
    use sui::transfer::{Self};
    use sui::tx_context::{Self};
    use sui::sui::SUI;

    use token_bridge::string32;
    use token_bridge::dynamic_set;
    use token_bridge::asset_meta::{Self, AssetMeta};

    use wormhole::external_address::{Self, ExternalAddress};
    use wormhole::myu16::{U16};
    use wormhole::wormhole::{Self};
    use wormhole::state::{Self as wormhole_state, State as WormholeState};
    use wormhole::emitter::{EmitterCapability};
    use wormhole::set::{Self, Set};

    const E_IS_NOT_WRAPPED_ASSET: u64 = 0;
    const E_IS_NOT_REGISTERED_NATIVE_ASSET: u64 = 1;
    const E_COIN_TYPE_HAS_NO_REGISTERED_INTEGER_ADDRESS: u64 = 2;
    const E_COIN_TYPE_HAS_REGISTERED_INTEGER_ADDRESS: u64 = 3;
    const E_ORIGIN_CHAIN_MISMATCH: u64 = 4;
    const E_ORIGIN_ADDRESS_MISMATCH: u64 = 5;
    const E_IS_WRAPPED_ASSET: u64 = 6;

    friend token_bridge::vaa;
    friend token_bridge::register_chain;
    friend token_bridge::wrapped;
    friend token_bridge::complete_transfer;
    friend token_bridge::transfer_tokens;
    friend token_bridge::attest_token;
    #[test_only]
    friend token_bridge::bridge_state_test;
    #[test_only]
    friend token_bridge::complete_transfer_test;
    #[test_only]
    friend token_bridge::token_bridge_vaa_test;

    /// Capability for creating a bridge state object, granted to sender when this
    /// module is deployed
    struct DeployerCapability has key, store {id: UID}

    /// WrappedAssetInfo<CoinType> stores all the metadata about a wrapped asset
    struct WrappedAssetInfo<phantom CoinType> has key, store {
        id: UID,
        token_chain: U16,
        token_address: ExternalAddress,
        treasury_cap: TreasuryCap<CoinType>,
    }

    struct NativeAssetInfo<phantom CoinType> has key, store {
        id: UID,
        // Even though we can look up token_chain at any time from wormhole state,
        // it can be more efficient to store it here locally so we don't have to do lookups.
        custody: Coin<CoinType>,
        asset_meta: AssetMeta,
    }

    /// OriginInfo is a non-Sui object that stores info about a tokens native token
    /// chain and address
    struct OriginInfo<phantom CoinType> has store, copy, drop {
        token_chain: U16,
        token_address: ExternalAddress,
    }

    public fun get_token_chain_from_origin_info<CoinType>(origin_info: &OriginInfo<CoinType>): U16 {
        return origin_info.token_chain
    }

    public fun get_token_address_from_origin_info<CoinType>(origin_info: &OriginInfo<CoinType>): ExternalAddress {
        return origin_info.token_address
    }

    public fun get_origin_info_from_wrapped_asset_info<CoinType>(wrapped_asset_info: &WrappedAssetInfo<CoinType>): OriginInfo<CoinType> {
        OriginInfo { token_chain: wrapped_asset_info.token_chain, token_address: wrapped_asset_info.token_address }
    }

    public fun get_origin_info_from_native_asset_info<CoinType>(native_asset_info: &NativeAssetInfo<CoinType>): OriginInfo<CoinType> {
        let asset_meta = &native_asset_info.asset_meta;
        let token_chain = asset_meta::get_token_chain(asset_meta);
        let token_address = asset_meta::get_token_address(asset_meta);
        OriginInfo { token_chain, token_address }
    }

    public(friend) fun create_wrapped_asset_info<CoinType>(
        token_chain: U16,
        token_address: ExternalAddress,
        treasury_cap: TreasuryCap<CoinType>,
        ctx: &mut TxContext
    ): WrappedAssetInfo<CoinType> {
        return WrappedAssetInfo {
            id: object::new(ctx),
            token_chain,
            token_address,
            treasury_cap
        }
    }

    // Integer label for coin types registered with Wormhole

    struct NativeIdRegistry has key, store {
        id: UID,
        index: u64, // next index to use
    }

    fun next_native_id(registry: &mut NativeIdRegistry): ExternalAddress {
        use wormhole::serialize::serialize_u64;

        let cur_index = registry.index;
        registry.index = cur_index + 1;
        let bytes = std::vector::empty<u8>();
        serialize_u64(&mut bytes, cur_index);
        external_address::from_bytes(bytes)
    }

    // Treasury caps, token stores, consumed VAAs, registered emitters, etc.
    // are stored as dynamic fields of bridge state.
    struct BridgeState has key, store {
        id: UID,

        /// Set of consumed VAA hashes
        consumed_vaas: Set<vector<u8>>,

        /// Token bridge owned emitter capability
        emitter_cap: EmitterCapability,

        /// Mapping of bridge contracts on other chains
        registered_emitters: VecMap<U16, ExternalAddress>,

        native_id_registry: NativeIdRegistry,
    }

    fun init(ctx: &mut TxContext) {
        transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
    }

    #[test_only]
    public fun test_init(ctx: &mut TxContext) {
        transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
    }

    // converts owned state object into a shared object, so that anyone can get a reference to &mut State
    // and pass it into various functions
    public entry fun init_and_share_state(
        deployer: DeployerCapability,
        emitter_cap: EmitterCapability,
        ctx: &mut TxContext
    ) {
        let DeployerCapability{ id } = deployer;
        object::delete(id);
        let state = BridgeState {
            id: object::new(ctx),
            consumed_vaas: set::new(ctx),
            emitter_cap,
            registered_emitters: vec_map::empty(),
            native_id_registry: NativeIdRegistry {
                id: object::new(ctx),
                index: 1,
            }
        };

        // permanently shares state
        transfer::share_object(state);
    }

    public(friend) fun deposit<CoinType>(
        bridge_state: &mut BridgeState,
        coin: Coin<CoinType>,
    ) {
        // TODO: create custom errors for each dynamic_set::borrow_mut
        let native_asset = dynamic_set::borrow_mut<NativeAssetInfo<CoinType>>(&mut bridge_state.id);
        coin::join<CoinType>(&mut native_asset.custody, coin);
    }

    public(friend) fun withdraw<CoinType>(
        _verified_coin_witness: VerifiedCoinType<CoinType>,
        bridge_state: &mut BridgeState,
        value: u64,
        ctx: &mut TxContext
    ): Coin<CoinType> {
        let native_asset = dynamic_set::borrow_mut<NativeAssetInfo<CoinType>>(&mut bridge_state.id);
        coin::split<CoinType>(&mut native_asset.custody, value, ctx)
    }

    public(friend) fun mint<CoinType>(
        _verified_coin_witness: VerifiedCoinType<CoinType>,
        bridge_state: &mut BridgeState,
        value: u64,
        ctx: &mut TxContext,
    ): Coin<CoinType> {
        let wrapped_info = dynamic_set::borrow_mut<WrappedAssetInfo<CoinType>>(&mut bridge_state.id);
        coin::mint<CoinType>(&mut wrapped_info.treasury_cap, value, ctx)
    }

    public(friend) fun burn<CoinType>(
        bridge_state: &mut BridgeState,
        coin: Coin<CoinType>,
    ) {
        let wrapped_info = dynamic_set::borrow_mut<WrappedAssetInfo<CoinType>>(&mut bridge_state.id);
        coin::burn<CoinType>(&mut wrapped_info.treasury_cap, coin);
    }

    public(friend) fun publish_message(
        wormhole_state: &mut WormholeState,
        bridge_state: &mut BridgeState,
        nonce: u64,
        payload: vector<u8>,
        message_fee: Coin<SUI>,
    ): u64 {
        wormhole::publish_message(
            &mut bridge_state.emitter_cap,
            wormhole_state,
            nonce,
            payload,
            message_fee,
        )
    }

    /// getters

    public fun vaa_is_consumed(state: &BridgeState, hash: vector<u8>): bool {
        set::contains(&state.consumed_vaas, hash)
    }

    public fun get_registered_emitter(state: &BridgeState, chain_id: &U16): Option<ExternalAddress> {
        if (vec_map::contains(&state.registered_emitters, chain_id)) {
            option::some(*vec_map::get(&state.registered_emitters, chain_id))
        } else {
            option::none()
        }
    }

    public fun is_wrapped_asset<CoinType>(bridge_state: &BridgeState): bool {
        dynamic_set::exists_<WrappedAssetInfo<CoinType>>(&bridge_state.id)
    }

    public fun is_registered_native_asset<CoinType>(bridge_state: &BridgeState): bool {
        dynamic_set::exists_<NativeAssetInfo<CoinType>>(&bridge_state.id)
    }

    /// Returns the origin information for a CoinType
    public fun origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
        if (is_wrapped_asset<CoinType>(bridge_state)) {
            get_wrapped_asset_origin_info<CoinType>(bridge_state)
        } else {
            get_registered_native_asset_origin_info(bridge_state)
        }
    }

    /// A value of type `VerifiedCoinType<T>` witnesses the fact that the type
    /// `T` has been verified to correspond to a particular chain id and token
    /// address (may be either a wrapped or native asset).
    /// The verification is performed by `verify_coin_type`.
    ///
    /// This is important because the coin type is an input to several
    /// functions, and is thus untrusted. Most coin-related functionality
    /// requires passing in a coin type generic argument.
    /// When transferring tokens *out*, that type instantiation determines the
    /// token bridge's behaviour, and thus we just take whatever was supplied.
    /// When transferring tokens *in*, it's the transfer VAA that determines
    /// which coin should be used via the origin chain and origin address
    /// fields.
    ///
    /// For technical reasons, the latter case still requires a type argument to
    /// be passed in (since Move does not support existential types, so we must
    /// rely on old school universal quantification). We must thus verify that
    /// the supplied type corresponds to the origin info in the VAA.
    ///
    /// Accordingly, the `mint` and `withdraw` operations are gated by this
    /// witness type, since these two operations require a VAA to supply the
    /// token information. This ensures that those two functions can't be called
    /// without first verifying the `CoinType`.
    struct VerifiedCoinType<phantom CoinType> has copy, drop {}

    /// See the documentation for `VerifiedCoinType` above.
    public fun verify_coin_type<CoinType>(
        bridge_state: &BridgeState,
        token_chain: U16,
        token_address: ExternalAddress
    ): VerifiedCoinType<CoinType> {
        let coin_origin = origin_info<CoinType>(bridge_state);
        assert!(coin_origin.token_chain == token_chain, E_ORIGIN_CHAIN_MISMATCH);
        assert!(coin_origin.token_address == token_address, E_ORIGIN_ADDRESS_MISMATCH);
        VerifiedCoinType {}
    }

    public fun get_wrapped_asset_origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
        assert!(is_wrapped_asset<CoinType>(bridge_state), E_IS_NOT_WRAPPED_ASSET);
        let wrapped_asset_info = dynamic_set::borrow<WrappedAssetInfo<CoinType>>(&bridge_state.id);
        get_origin_info_from_wrapped_asset_info(wrapped_asset_info)
    }

    public fun get_registered_native_asset_origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
        let native_asset_info = dynamic_set::borrow<NativeAssetInfo<CoinType>>(&bridge_state.id);
        get_origin_info_from_native_asset_info(native_asset_info)
    }

    /// setters

    public(friend) fun set_registered_emitter(state: &mut BridgeState, chain_id: U16, emitter: ExternalAddress) {
        if (vec_map::contains<U16, ExternalAddress>(&mut state.registered_emitters, &chain_id)){
            vec_map::remove<U16, ExternalAddress>(&mut state.registered_emitters, &chain_id);
        };
        vec_map::insert<U16, ExternalAddress>(&mut state.registered_emitters, chain_id, emitter);
    }

    /// dynamic ops

    public(friend) fun store_consumed_vaa(bridge_state: &mut BridgeState, vaa: vector<u8>) {
        set::add(&mut bridge_state.consumed_vaas, vaa);
    }

    public(friend) fun register_wrapped_asset<CoinType>(bridge_state: &mut BridgeState, wrapped_asset_info: WrappedAssetInfo<CoinType>) {
        dynamic_set::add<WrappedAssetInfo<CoinType>>(&mut bridge_state.id, wrapped_asset_info);
    }

    public(friend) fun register_native_asset<CoinType>(
        wormhole_state: &WormholeState,
        bridge_state: &mut BridgeState,
        coin_meta: &CoinMetadata<CoinType>,
        ctx: &mut TxContext
    ): AssetMeta {
        assert!(!is_wrapped_asset<CoinType>(bridge_state), E_IS_WRAPPED_ASSET); // TODO - test
        let asset_meta = asset_meta::create(
            next_native_id(&mut bridge_state.native_id_registry),
            wormhole_state::get_chain_id(wormhole_state), // TODO: should we just hardcode this?
            coin::get_decimals<CoinType>(coin_meta), // decimals
            string32::from_bytes(ascii::into_bytes(coin::get_symbol<CoinType>(coin_meta))), // symbol
            string32::from_string(&coin::get_name<CoinType>(coin_meta)) // name
        );
        let native_asset_info = NativeAssetInfo<CoinType> {
            id: object::new(ctx),
            custody: coin::zero(ctx),
            asset_meta,
        };
        dynamic_set::add<NativeAssetInfo<CoinType>>(&mut bridge_state.id, native_asset_info);
        asset_meta
    }

}

#[test_only]
module token_bridge::bridge_state_test{
    use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_from_address, take_shared, return_shared};
    use sui::coin::{CoinMetadata};

    use wormhole::state::{State};
    use wormhole::test_state::{init_wormhole_state};
    use wormhole::wormhole::{Self};
    use wormhole::external_address::{Self};

    use token_bridge::bridge_state::{Self as state, BridgeState, DeployerCapability};
    use token_bridge::native_coin_witness::{Self, NATIVE_COIN_WITNESS};
    use token_bridge::native_coin_witness_v2::{Self, NATIVE_COIN_WITNESS_V2};

    fun scenario(): Scenario { test_scenario::begin(@0x123233) }
    fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }

    #[test]
    fun test_state_setters() {
        test_state_setters_(scenario())
    }

    #[test]
    fun test_coin_type_addressing(){
        test_coin_type_addressing_(scenario())
    }

    #[test]
    #[expected_failure(abort_code = 0, location=0000000000000000000000000000000000000002::dynamic_field)]
    fun test_coin_type_addressing_failure_case(){
        test_coin_type_addressing_failure_case_(scenario())
    }

    public fun set_up_wormhole_core_and_token_bridges(admin: address, test: Scenario): Scenario {
        // init and share wormhole core bridge
        test =  init_wormhole_state(test, admin);

        // call init for token bridge to get deployer cap
        next_tx(&mut test, admin); {
            state::test_init(ctx(&mut test));
        };

        // register for emitter cap and init_and_share token bridge
        next_tx(&mut test, admin); {
            let wormhole_state = take_shared<State>(&test);
            let my_emitter = wormhole::register_emitter(&mut wormhole_state, ctx(&mut test));
            let deployer = take_from_address<DeployerCapability>(&test, admin);
            state::init_and_share_state(deployer, my_emitter, ctx(&mut test));
            return_shared<State>(wormhole_state);
        };

        next_tx(&mut test, admin); {
            let bridge_state = take_shared<BridgeState>(&test);
            return_shared<BridgeState>(bridge_state);
        };

        return test
    }

    fun test_state_setters_(test: Scenario) {
        let (admin, _, _) = people();

        test = set_up_wormhole_core_and_token_bridges(admin, test);

        //test BridgeState setter and getter functions
        next_tx(&mut test, admin); {
            let state = take_shared<BridgeState>(&test);

            // test store consumed vaa
            state::store_consumed_vaa(&mut state, x"1234");
            assert!(state::vaa_is_consumed(&state, x"1234"), 0);

            // TODO - test store coin store
            // TODO - test store treasury cap

            return_shared<BridgeState>(state);
        };
        test_scenario::end(test);
    }

    fun test_coin_type_addressing_(test: Scenario) {
        let (admin, _, _) = people();

        test = set_up_wormhole_core_and_token_bridges(admin, test);

        //test coin type addressing
        next_tx(&mut test, admin); {
            native_coin_witness::test_init(ctx(&mut test));
            native_coin_witness_v2::test_init(ctx(&mut test));
        };
        next_tx(&mut test, admin); {
            let wormhole_state = take_shared<State>(&test);
            let bridge_state = take_shared<BridgeState>(&test);
            let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
            state::register_native_asset<NATIVE_COIN_WITNESS>(
                &mut wormhole_state,
                &mut bridge_state,
                &coin_meta,
                ctx(&mut test)
            );
            let origin_info = state::origin_info<NATIVE_COIN_WITNESS>(&bridge_state);
            let address = state::get_token_address_from_origin_info(&origin_info);
            assert!(address == external_address::from_bytes(x"01"), 0);

            let coin_meta_v2 = take_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(&test);
            state::register_native_asset<NATIVE_COIN_WITNESS_V2>(
                &mut wormhole_state,
                &mut bridge_state,
                &coin_meta_v2,
                ctx(&mut test)
            );
            let origin_info = state::origin_info<NATIVE_COIN_WITNESS_V2>(&bridge_state);
            let address = state::get_token_address_from_origin_info(&origin_info);
            assert!(address == external_address::from_bytes(x"02"), 0);

            return_shared<State>(wormhole_state);
            return_shared<BridgeState>(bridge_state);
            return_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(coin_meta_v2);
            return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
        };
        test_scenario::end(test);
    }


    fun test_coin_type_addressing_failure_case_(test: Scenario) {
        let (admin, _, _) = people();

        test = set_up_wormhole_core_and_token_bridges(admin, test);

        //test coin type addressing
        next_tx(&mut test, admin); {
            native_coin_witness::test_init(ctx(&mut test));
            native_coin_witness_v2::test_init(ctx(&mut test));
        };
        next_tx(&mut test, admin); {
            let wormhole_state = take_shared<State>(&test);
            let bridge_state = take_shared<BridgeState>(&test);
            let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
            state::register_native_asset<NATIVE_COIN_WITNESS>(
                &mut wormhole_state,
                &mut bridge_state,
                &coin_meta,
                ctx(&mut test)
            );
            let origin_info = state::origin_info<NATIVE_COIN_WITNESS>(&bridge_state);
            let address = state::get_token_address_from_origin_info(&origin_info);
            assert!(address == external_address::from_bytes(x"01"), 0);

            // aborts because trying to re-register native coin
            state::register_native_asset<NATIVE_COIN_WITNESS>(
                &mut wormhole_state,
                &mut bridge_state,
                &coin_meta,
                ctx(&mut test)
            );

            return_shared<State>(wormhole_state);
            return_shared<BridgeState>(bridge_state);
            return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
        };
        test_scenario::end(test);
    }
}
