include "List.aes"
include "Pair.aes"
namespace Worker =
  type package_code = string
  record package = {
    daily_cap : int,
    price : int}
  record aggregated_package = {
    daily_cap : int,
    count : int}
  record worker = {
    daily_cap : int,
    can_withdraw_payout : bool,
    packages : map(package_code, aggregated_package),
    joined_pool_tmst : int}
  record transfer_packs =
    {
     worker : address,
     packages_to_move : list(string * int),
     new_address : address
     }
  datatype approvable_action = Transfer(transfer_packs)
  function new_package(price : int, cap : int) : package =
    {daily_cap = cap,
     price = price}
  function claim(ps : list(package_code * (package * int)), joined_tmst : int) : worker =
    let daily_cap = daily_cap_from_packs_list(ps) 
    let packs : map(package_code, aggregated_package) =
      List.foldl(
        (accum, t) =>
          let pack_id = Pair.fst(t)
          let (pack, cnt) = Pair.snd(t)
          let val =
            switch(Map.lookup(pack_id, accum))
              None => {daily_cap = pack.daily_cap, count = cnt}
              Some(v) => v{count = v.count + cnt}
          accum{[pack_id] = val},
        {},
        ps)
    {daily_cap = daily_cap,
     can_withdraw_payout = false,
     packages = packs,
     joined_pool_tmst = joined_tmst}
  function split_packages(w : worker, split : transfer_packs) : worker * worker =
    let (packages_left, packages_collected) =
      List.foldl(
        (accum, p) =>
          let accum_left = Pair.fst(accum)
          let accum_collected = Pair.snd(accum)
          let code = Pair.fst(p)
          let count = Pair.snd(p)
          switch(Map.lookup(code, accum_left))
            None => abort("Does not own enough packages")
            Some(owned_packs) =>
              let left_packs = owned_packs.count - count 
              require(left_packs > -1, "Does not own enough packages")
              (accum_left{[code] = owned_packs{count = left_packs}}, (code, owned_packs{count = count}) :: accum_collected),
        (w.packages, []),
        split.packages_to_move)
    let daily_cap_delta = List.sum(List.map((t) => Pair.snd(t).daily_cap * Pair.snd(t).count, packages_collected))
    let new_w = {daily_cap = daily_cap_delta,
                 can_withdraw_payout = false,
                 packages = Map.from_list(packages_collected),
                 joined_pool_tmst = Chain.block_height}
    require(Map.size(new_w.packages) == List.length(packages_collected), "Do not split counts of the same package code")
    (w{daily_cap = w.daily_cap - daily_cap_delta, packages = packages_left}, new_w)
  function merge_workers(w1 : worker, w2: worker) =
    let packages =
      List.foldl(
        (accum, t) =>
          let code = Pair.fst(t)
          let aggr_pack = Pair.snd(t)
          let updated_pack =
            switch(Map.lookup(code, accum))
              None => aggr_pack
              Some(p) => p{count = p.count + aggr_pack.count}
          accum{[code] = updated_pack},
        w1.packages,
        Map.to_list(w2.packages))
    // if one of them is allowed to withdraw, so is the resulting new account
    let oldest_tmst =
      switch(w1.joined_pool_tmst < w2.joined_pool_tmst)
        true => w1.joined_pool_tmst
        false => w2.joined_pool_tmst
    let can_withdraw_payout = w1.can_withdraw_payout || w2.can_withdraw_payout
    let daily_cap = daily_cap_from_packages(packages) 
    {daily_cap = daily_cap,
     can_withdraw_payout = can_withdraw_payout,
     packages = packages,
     joined_pool_tmst = oldest_tmst}
  function daily_cap_from_packs_list(ps : list(package_code * (package * int))) =
    List.sum(List.map((t) => Pair.fst(Pair.snd(t)).daily_cap * Pair.snd(Pair.snd(t)), ps))
                
  function daily_cap_from_packages(ps : map(package_code, aggregated_package)) =
    List.sum(List.map((t) => Pair.snd(t).daily_cap * Pair.snd(t).count, Map.to_list(ps)))
contract interface Data =
  stateful entrypoint set_hive : (address) => unit
  stateful entrypoint add : (address, Worker.worker) => unit
  stateful entrypoint remove : (address) => unit
  payable stateful entrypoint give_rewards : (list(address * int)) => unit
  entrypoint balance : (address) => int
  entrypoint assert_is_payable : (address) => unit
  stateful entrypoint payout : () => unit
  stateful entrypoint payout_without_payable_check : (address) => unit
  entrypoint all_balances : () => list(address * int)
  entrypoint all_daily_caps : () => list(address * int)
  entrypoint all : () => list(address)
  entrypoint all_detailed : () => map(address, Worker.worker)
  entrypoint member : (address) => bool
  entrypoint get : (address) => Worker.worker
  stateful entrypoint rename : (address, address) => unit
  stateful entrypoint make_payable : (address) => unit
  stateful entrypoint make_non_payable : (address) => unit
  stateful entrypoint split_packages : (Worker.transfer_packs) => unit
  entrypoint all_balances_and_daily_caps : () => list(address * int * int)
contract interface Hive =
  entrypoint leader : () => address
  stateful entrypoint enroll : (address, Worker.worker) => unit
  entrypoint member : (address) => bool
  stateful entrypoint remove : (address) => unit
  entrypoint get : (address) => Worker.worker
  stateful entrypoint set_locked : (bool) => unit
  stateful entrypoint set_leader : (address) => unit
  entrypoint can_be_destroyed : () => bool
  entrypoint info : () => address * string * address * string * string * string * string * list(string)
  stateful entrypoint make_payable : (address) => unit
  stateful entrypoint make_non_payable : (address) => unit
  entrypoint assert_worker_is_payable : (address) => unit
  stateful entrypoint force_payout : (address) => unit
  stateful entrypoint change_worker_address : (address, address) => unit
  entrypoint balance : (address) => int
  stateful entrypoint set_data_ct : (Data) => unit
  stateful entrypoint move_data_and_coins_to_new_hive : (Hive) => Data
  payable entrypoint receive_coins : () => unit 
  stateful entrypoint evacuate_coins : (int, address) => unit
  stateful entrypoint split_packages : (Worker.transfer_packs) => unit
include "List.aes"
include "Pair.aes"
main contract HiveData:Data =
  record state =
    { 
      workers : map(address, Worker.worker),
      hive_ct : address,
      balances : map(address, int)
    }
  entrypoint init(hive : address) =
    {workers = {},
     hive_ct = hive,
     balances = {}}
  
  stateful entrypoint set_hive(hive_ct : address) =
    assert_caller_is_hive_contract()
    put(state{hive_ct = hive_ct})
  stateful entrypoint add(worker_address : address, worker : Worker.worker) =
    assert_caller_is_hive_contract()
    switch(Map.lookup(worker_address, state.workers))
      None =>
        put(state{workers[worker_address] = worker})
      Some(old_worker) =>
        put(state{workers[worker_address] = Worker.merge_workers(worker, old_worker)})
  /* deletes a worker from the hive if present. Currently the accumulated coins remain in
     hive. */
  stateful entrypoint remove(worker_address : address) =
    assert_caller_is_hive_contract()
    put(state{workers = Map.delete(worker_address, state.workers)})
  payable stateful entrypoint give_rewards(rewards : list(address * int )) =
    assert_caller_is_hive_contract()
    List.foreach(rewards, give_reward)
  entrypoint balance(worker_address : address) =
    switch(Map.lookup(worker_address, state.balances))
      None => 0 
      Some(balance) => balance
  entrypoint assert_is_payable(worker_addr : address) =
    let worker = get(worker_addr)
    require(worker.can_withdraw_payout, "Not allowed to withdraw")
  stateful entrypoint payout() =
    assert_is_payable(Call.origin)
    payout_without_payable_check(Call.origin)
  stateful entrypoint payout_without_payable_check(worker_addr) =
    assert_caller_is_hive_contract()
    switch(balance(worker_addr))
      0 => abort("No balance")
      balance =>
        Chain.spend(Call.origin, balance)
        put(state{balances = Map.delete(Call.origin, state.balances)})
  entrypoint all_balances() : list(address * int) =
    Map.to_list(state.balances)
  entrypoint all_daily_caps() : list(address * int) =
    List.map((kv) =>
                let worker = Pair.snd(kv)
                (Pair.fst(kv), worker.daily_cap),
        Map.to_list(state.workers))
  entrypoint all_balances_and_daily_caps() : list(address * int * int) =
    List.map((kv) =>
                let worker = Pair.snd(kv)
                let balance = balance(Pair.fst(kv))
                (Pair.fst(kv), balance, worker.daily_cap),
        Map.to_list(state.workers))
  entrypoint all() : list(address) =
    map_keys(state.workers)
  entrypoint all_detailed() : map(address, Worker.worker) =
    state.workers
  entrypoint member(worker_address : address) : bool =
    Map.member(worker_address, state.workers)
  stateful entrypoint rename(old_addr : address, new_addr : address) =
    assert_caller_is_hive_contract() 
    put(state{workers = rename_key(old_addr, new_addr, state.workers),
              balances = rename_key(old_addr, new_addr, state.balances)})
  entrypoint get(worker_address : address) =
    switch(Map.lookup(worker_address, state.workers))
      None => abort("Unknown worker")
      Some(worker) => worker
  function assert_caller_is_hive_contract() =
    require(Call.caller == state.hive_ct, "Call it through the hive contract")
  stateful function give_reward((worker_address : address, reward : int)) =
    assert_caller_is_hive_contract()
    let old_balance = balance(worker_address)
    put(state{balances[worker_address] = old_balance + reward})
  stateful entrypoint make_payable(worker_address : address) =
    assert_caller_is_hive_contract()
    let worker = get(worker_address)
    let validated_worker = worker{can_withdraw_payout = true}
    put(state{workers[worker_address] = validated_worker})
  stateful entrypoint make_non_payable(worker_address : address) =
    assert_caller_is_hive_contract()
    let worker = get(worker_address)
    let unvalidated_worker = worker{can_withdraw_payout = false}
    put(state{workers[worker_address] = unvalidated_worker})
  stateful entrypoint split_packages(split : Worker.transfer_packs) =
    assert_caller_is_hive_contract()
    let worker = get(split.worker)
    let (worker_updated, new_worker) = Worker.split_packages(worker, split)
    let new_worker1 =
      switch(Map.lookup(split.new_address, state.workers))
        None => new_worker
        Some(old_new_worker) => Worker.merge_workers(new_worker, old_new_worker)
    put(state{workers = state.workers{[split.worker] = worker_updated, [split.new_address] = new_worker1}})
  function map_keys(map) =
    List.map((kv) => Pair.fst(kv), Map.to_list(map))
  function rename_key(old_key, new_key, map) =
    switch(Map.lookup(old_key, map))
      None => map
      Some(value) =>
        let map1 = Map.delete(old_key, map)
        map1{[new_key] = value}