public class SessionTypes {
    public static final class Eps { }
    public static final class Send<A, R> { }
    public static final class Recv<A, R> { }
    public static final class Choose<R, S> { }
    public static final class Offer<R, S> { }

    public static final class Dual<R, S> {
        private Dual() { }

        public static Dual<Eps, Eps> eps = new Dual<Eps, Eps>();

        public <A> Dual<Send<A, R>, Recv<A, S>> send() {
            return new Dual<Send<A, R>, Recv<A, S>>();
        }

        public <A> Dual<Recv<A, R>, Send<A, S>> recv() {
            return new Dual<Recv<A, R>, Send<A, S>>();
        }

        public <R2, S2> Dual<Choose<R, R2>, Offer<S, S2>>
        choose(Dual<R2, S2> right) {
            right.nullCheck();
            return new Dual<Choose<R, R2>, Offer<S, S2>>();
        }

        public <R2, S2> Dual<Offer<R, R2>, Choose<S, S2>>
        offer(Dual<R2, S2> right) {
            right.nullCheck();
            return new Dual<Offer<R, R2>, Choose<S, S2>>();
        }

        public Dual<S, R> flip() {
            return new Dual<S, R>();
        }

        public void nullCheck() { }
    }

    public static final class Unit { }

    public interface Func<A, B> {
        B call(A a);
    }

    public static final class Session<S, T, A> {
        private static interface Thunk<A> {
            A force();
        }

        private final Thunk<A> action;
        private Session(Thunk<A> thunk) { action = thunk; }

        public Session(final A a) {
            action = new Thunk<A>() {
                public A force() { return a; }
            };
        }

        public <U, B> Session<S, U, B>
        bind(final Func<A, Session<T, U, B>> func) {
            return new Session<S, U, B>(new Thunk<B>() {
                public B force() {
                    return func.call(action.force()).action.force();
                }
            });
        }

        public <U, B> Session<S, U, B>
        bind(final Session<T, U, B> next) {
            return new Session<S, U, B>(new Thunk<B>() {
                public B force() {
                    action.force();
                    return next.action.force();
                }
            });
        }

        public static <A, S> Session<Send<A, S>, S, Unit>
        send(final A a) {
            return new Session<Send<A, S>, S, Unit>(new Thunk<Unit>() {
                public Unit force() {
                    return new Unit();
                }
            });
        }

        public static <A, S> Session<Recv<A, S>, S, A>
        recv() {
            return new Session<Recv<A, S>, S, A>(new Thunk<A>() {
                public A force() {
                    throw new Error();
                }
            });
        }

        public static <A, S> Session<Recv<A, S>, S, A>
        mock_recv(final A a) {
            return new Session<Recv<A, S>, S, A>(new Thunk<A>() {
                public A force() {
                    return a;
                }
            });
        }

        public static <R, S> Session<Choose<R, S>, R, Unit> sel1() {
            return new Session<Choose<R, S>, R, Unit>(new Thunk<Unit>() {
                public Unit force() {
                    return new Unit();
                }
            });
        }

        public static <R, S> Session<Choose<R, S>, S, Unit> sel2() {
            return new Session<Choose<R, S>, S, Unit>(new Thunk<Unit>() {
                public Unit force() {
                    return new Unit();
                }
            });
        }

        public static <R, S, U, A> Session<Offer<R, S>, U, A>
        offer(final Session<R, U, A> left,
               final Session<S, U, A> right) {
            return new Session<Offer<R, S>, U, A>(new Thunk<A>() {
                public A force() {
                    return left.action.force();
                }
            });
        }

        public static Session<Eps, Unit, Unit> close =
            new Session<Eps, Unit, Unit>(new Thunk<Unit>() {
                public Unit force() {
                    return new Unit();
                }
            });

        public static <R, S, A> A
        run(Dual<R, S> witness, Session<R, Unit, Unit> r,
                                Session<S, Unit, A> s) {
            witness.nullCheck();
            return s.action.force();
        }
    }

    public static void main(String[] args) {
        Session<Send<String, Eps>, Unit, Unit> a =
          Session
            .<String, Eps>send("meh!")
            .bind(Session.close);

        Session<Recv<String, Eps>, Unit, String> b =
          Session
            .<String, Eps>mock_recv("hello")
            .bind (new Func<String, Session<Eps, Unit, String>>() {
                public Session<Eps, Unit, String> call(String s) {
                    return Session
                      .close
                      .bind(new Session<Unit, Unit, String>(s));
                }
            });

        String result = Session.run(Dual.eps.<String>send(), a, b);
        System.out.println(result);

        // How about a null witness?  Should raise a
        // NullPointerException instead of running the session:
        Session.run(null, a, b);
    }
}

