
/* Contract type */
contract VotingType =
  type state
  function init            : list(string) => state

  function giveRightToVote : address => unit
  function delegate        : address => unit
  function vote            : int => unit
  function winnerName      : unit => string
  function currentTally    : unit => list(string * int)

/* Contract implementation */
contract Voting =

  // Types
  record proposal =
    { name : string
    , voteCount : uint
    }

  datatype vote = NotVoted | Voted(int) | Delegated(address)

  record voter =
    { weight : int
    , vote : vote
    }

  record state =
    { chairPerson : address
    , voters : map(address, voter)
    , proposals : list(proposal)
    }

  // Initialization
  function init(proposalNames: list(string)): state =
    { chairPerson = caller(),
      voters      = Map.empty,
      proposals   = MyList.map((name) => {name = name, voteCount = 0}, proposalNames) }

  function initVoter() = { weight = 1, vote = NotVoted}

  function giveRightToVote(voter: address) =
    require(caller() == state.chairPerson)
    require(!Map.mem(voter, state.voters))
    put(state{ voters = Map.add(voter, initVoter(), state.voters) })

  function delegateChain(delegate: address) =
    require(delegate != caller()) /* Delegation loop! */
    let voter = Map.find(delegate, state.voters)
    switch(voter.vote)
      Delegated(d) => delegateChain(d)
      _            => delegate

  function addVote(candidate, weight) =
    let proposal = MyList.nth(state.proposals, candidate)
    proposal{ voteCount = proposal.voteCount + weight }

  function delegateVote(delegateTo: address, weight: uint) =
    let voter = Map.find(delegateTo, state.voters)
    switch(voter.vote)
      Voted(vote)  => addVote(vote, weight)
      Delegated(_) => abort("impossible")   // impossible
      NotVoted     => voter{ weight = voter.weight + weight }

  function delegate(delegateTo: address) =
    require(delegateTo != caller())
    let voter = Map.find(caller(), state.voters)
    require(voter.vote == NotVoted)
    let finalDelegate = delegateChain(delegateTo)
    let voter' = voter{ vote = Delegated(finalDelegate) }
    delegateVote(finalDelegate, voter.weight)

  function vote(candidate: uint) =
    let voter = Map.find(caller(), state.voters)
    require(voter.vote == NotVoted)
    let voter' = voter{ vote = Voted(candidate) }
    addVote(candidate, voter.weight)

  function winningProposal'(current, rest) =
    switch(rest)
      []      => current
      p :: ps => winningProposal'(if (p.voteCount > current.voteCount) p else current, ps)

  // const
  function winningProposal() : proposal =
    switch(state.proposals)
      []      => abort("none")
      p :: ps => winningProposal'(p, ps)

  // const
  function winnerName() = winningProposal().name

  // const
  function currentTally() =
    MyList.map((p) => (p.name, p.voteCount), state.proposals)

