// storage type

type threshold is nat
type max_proposal is nat
type max_message_size is nat
type state_hash is bytes
type addr_set is set (address)
type message_store is map (bytes, addr_set)
type proposal_counters is map (address, nat)

type storage is
  record [
    state_hash           : state_hash;
    threshold            : threshold;
    max_proposal         : max_proposal;
    max_message_size     : max_message_size;
    authorized_addresses : addr_set;
    message_store        : message_store;
    proposal_counters    : proposal_counters
  ]

// I/O types

type message is bytes -> list (operation)
type send_pt is message
type withdraw_pt is message
type default_pt is unit

type return is list (operation) * storage

type parameter is
  Send     of send_pt
| Withdraw of withdraw_pt
| Default  of default_pt

function send (const param : send_pt; const s : storage) : return is
  block {
    // check sender against the authorized addresses

    if not Set.mem (Tezos.sender, s.authorized_addresses)
    then failwith("Unauthorized address")
    else skip;

    // check message size against the stored limit

    var message : message := param;
    const packed_msg : bytes = Bytes.pack (message);
    if Bytes.length (packed_msg) > s.max_message_size
    then failwith ("Message size exceed maximum limit")
    else skip;

    (* compute the new set of addresses associated with the message and
       update counters *)

    var new_store : addr_set := set [];

    case map_get (packed_msg, s.message_store) of
      Some (voters) ->
        block {
          (* The message is already stored.
             Increment the counter only if the sender is not already
             associated with the message. *)
          if Set.mem (Tezos.sender, voters)
          then skip
          else s.proposal_counters[Tezos.sender] :=
                 get_force (Tezos.sender, s.proposal_counters) + 1n;
                 new_store := Set.add (Tezos.sender,voters)
        }
    | None ->
        block {
          // the message has never been received before
          s.proposal_counters[sender] :=
             get_force (Tezos.sender, s.proposal_counters) + 1n;
             new_store := set [Tezos.sender]
        }
    end;

    // check sender counters against the maximum number of proposal

    var sender_proposal_counter : nat :=
      get_force (Tezos.sender, s.proposal_counters);

    if sender_proposal_counter > s.max_proposal
    then failwith ("Maximum number of proposal reached")
    else skip;

    // check the threshold

    var ret_ops : list (operation) := nil;

    if Set.cardinal (new_store) >= s.threshold then {
      remove packed_msg from map s.message_store;
      ret_ops := message (s.state_hash);
      // update the state hash
      s.state_hash := Crypto.sha256 (Bytes.concat (s.state_hash, packed_msg));
      // decrement the counters
      for addr -> ctr in map s.proposal_counters block {
        if Set.mem (addr, new_store) then
          s.proposal_counters[addr] := abs (ctr - 1n)
        else skip
      }
    } else s.message_store[packed_msg] := new_store
  } with (ret_ops, s)

function withdraw (const param : withdraw_pt; const s : storage) : return is
  block {
    var message : message := param;
    const packed_msg : bytes = Bytes.pack (message);

    case s.message_store[packed_msg] of
      Some (voters) ->
        block {
          // The message is stored
          const new_set : addr_set = Set.remove (Tezos.sender, voters);

          (* Decrement the counter only if the sender was already
             associated with the message *)

          if Set.cardinal (voters) =/= Set.cardinal (new_set)
          then s.proposal_counters[Tezos.sender] :=
                 abs (get_force (Tezos.sender, s.proposal_counters) - 1n)
          else skip;

          (* If the message is left without any associated addresses,
             remove the corresponding message_store field *)

          if Set.cardinal (new_set) = 0n
          then remove packed_msg from map s.message_store
          else s.message_store[packed_msg] := new_set
        }
    | None -> skip
    end // The message is not stored, ignore.
  } with ((nil : list (operation)), s)

function default (const p : default_pt; const s : storage) : return is
    ((nil : list (operation)), s)

function main (const param : parameter; const s : storage) : return  is
  case param of
    (* Propagate message p if the number of authorized addresses having
       voted for the same message p equals the threshold. *)
    | Send (p) -> send (p, s)

    (* Withraw vote for message p *)
    | Withdraw (p) -> withdraw (p, s)

    (* Use this action to transfer tez to the contract *)
    | Default (p) -> default (p, s)
  end
