// A protocol compiler for Rust.

use to_str::ToStr;

use dvec::DVec;

use ast::ident;
use util::interner;
use print::pprust;
use pprust::{item_to_str, ty_to_str};
use ext::base::{mk_ctxt, ext_ctxt};
use parse::*;
use proto::*;

use ast_builder::{append_types, path, empty_span};

// Transitional reexports so qquote can find the paths it is looking for
mod syntax {
    #[legacy_exports];
    pub use ext;
    pub use parse;
}

trait gen_send {
    fn gen_send(cx: ext_ctxt, try: bool) -> @ast::item;
}

trait to_type_decls {
    fn to_type_decls(cx: ext_ctxt) -> ~[@ast::item];
    fn to_endpoint_decls(cx: ext_ctxt, dir: direction) -> ~[@ast::item];
}

trait gen_init {
    fn gen_init(cx: ext_ctxt) -> @ast::item;
    fn compile(cx: ext_ctxt) -> @ast::item;
}

impl message: gen_send {
    fn gen_send(cx: ext_ctxt, try: bool) -> @ast::item {
        debug!("pipec: gen_send");
        match self {
          message(_id, span, tys, this,
                  Some({state: next, tys: next_tys})) => {
            debug!("pipec: next state exists");
            let next = this.proto.get_state(next);
            assert next_tys.len() == next.ty_params.len();
            let arg_names = tys.mapi(|i, _ty| cx.ident_of(~"x_"+i.to_str()));

            let args_ast = (arg_names, tys).map(
                |n, t| cx.arg(*n, *t)
            );

            let pipe_ty = cx.ty_path_ast_builder(
                path(~[this.data_name()], span)
                .add_tys(cx.ty_vars(this.ty_params)));
            let args_ast = vec::append(
                ~[cx.arg(cx.ident_of(~"pipe"),
                              pipe_ty)],
                args_ast);

            let mut body = ~"{\n";

            if this.proto.is_bounded() {
                let (sp, rp) = match (this.dir, next.dir) {
                  (send, send) => (~"move c", ~"move s"),
                  (send, recv) => (~"s", ~"c"),
                  (recv, send) => (~"s", ~"c"),
                  (recv, recv) => (~"move c", ~"move s")
                };

                body += ~"let b = pipe.reuse_buffer();\n";
                body += fmt!("let %s = pipes::SendPacketBuffered(\
                              ptr::addr_of(&(b.buffer.data.%s)));\n",
                             sp, next.name);
                body += fmt!("let %s = pipes::RecvPacketBuffered(\
                              ptr::addr_of(&(b.buffer.data.%s)));\n",
                             rp, next.name);
            }
            else {
                let pat = match (this.dir, next.dir) {
                  (send, send) => "(move c, move s)",
                  (send, recv) => "(s, c)",
                  (recv, send) => "(s, c)",
                  (recv, recv) => "(move c, move s)"
                };

                body += fmt!("let %s = pipes::entangle();\n", pat);
            }
            body += fmt!("let message = %s::%s(%s);\n",
                         this.proto.name,
                         self.name(),
                         str::connect(vec::append_one(
                           arg_names.map(|x| ~"move " + cx.str_of(*x)),
                             ~"move s"), ~", "));

            if !try {
                body += fmt!("pipes::send(move pipe, move message);\n");
                // return the new channel
                body += ~"move c }";
            }
            else {
                body += fmt!("if pipes::send(move pipe, move message) {\n \
                                  pipes::rt::make_some(move c) \
                              } else { pipes::rt::make_none() } }");
            }

            let body = cx.parse_expr(body);

            let mut rty = cx.ty_path_ast_builder(path(~[next.data_name()],
                                                      span)
                                                 .add_tys(next_tys));
            if try {
                rty = cx.ty_option(rty);
            }

            let name = cx.ident_of(if try { ~"try_" + self.name()
                                          } else { self.name() } );

            cx.item_fn_poly(name,
                            args_ast,
                            rty,
                            self.get_params(),
                            cx.expr_block(body))
          }

            message(_id, span, tys, this, None) => {
                debug!("pipec: no next state");
                let arg_names = tys.mapi(|i, _ty| (~"x_" + i.to_str()));

                let args_ast = (arg_names, tys).map(
                    |n, t| cx.arg(cx.ident_of(*n), *t)
                );

                let args_ast = vec::append(
                    ~[cx.arg(cx.ident_of(~"pipe"),
                                  cx.ty_path_ast_builder(
                                      path(~[this.data_name()], span)
                                      .add_tys(cx.ty_vars(this.ty_params))))],
                    args_ast);

                let message_args = if arg_names.len() == 0 {
                    ~""
                }
                else {
                    ~"(" + str::connect(arg_names.map(|x| ~"move " + *x),
                                        ~", ") + ~")"
                };

                let mut body = ~"{ ";
                body += fmt!("let message = %s::%s%s;\n",
                             this.proto.name,
                             self.name(),
                             message_args);

                if !try {
                    body += fmt!("pipes::send(move pipe, move message);\n");
                    body += ~" }";
                } else {
                    body += fmt!("if pipes::send(move pipe, move message) { \
                                      pipes::rt::make_some(()) \
                                  } else { pipes::rt::make_none() } }");
                }

                let body = cx.parse_expr(body);

                let name = if try {
                    ~"try_" + self.name()
                }
                else { self.name() };

                cx.item_fn_poly(cx.ident_of(name),
                                args_ast,
                                if try {
                                    cx.ty_option(cx.ty_nil_ast_builder())
                                } else {
                                    cx.ty_nil_ast_builder()
                                },
                                self.get_params(),
                                cx.expr_block(body))
            }
          }
        }

    fn to_ty(cx: ext_ctxt) -> @ast::ty {
        cx.ty_path_ast_builder(path(~[cx.ident_of(self.name())], self.span())
          .add_tys(cx.ty_vars(self.get_params())))
    }
}

impl state: to_type_decls {
    fn to_type_decls(cx: ext_ctxt) -> ~[@ast::item] {
        debug!("pipec: to_type_decls");
        // This compiles into two different type declarations. Say the
        // state is called ping. This will generate both `ping` and
        // `ping_message`. The first contains data that the user cares
        // about. The second is the same thing, but extended with a
        // next packet pointer, which is used under the covers.

        let name = self.data_name();

        let mut items_msg = ~[];

        for self.messages.each |m| {
            let message(name, span, tys, this, next) = *m;

            let tys = match next {
              Some({state: next, tys: next_tys}) => {
                let next = this.proto.get_state(next);
                let next_name = cx.str_of(next.data_name());

                let dir = match this.dir {
                  send => ~"server",
                  recv => ~"client"
                };

                vec::append_one(tys,
                                cx.ty_path_ast_builder(
                                    path(~[cx.ident_of(dir),
                                           cx.ident_of(next_name)], span)
                                    .add_tys(next_tys)))
              }
              None => tys
            };

            let v = cx.variant(cx.ident_of(name), span, tys);

            items_msg.push(v);
        }

        ~[cx.item_enum_poly(name,
                            self.span,
                            ast::enum_def({ variants: items_msg,
                                            common: None }),
                            self.ty_params)]
    }

    fn to_endpoint_decls(cx: ext_ctxt, dir: direction) -> ~[@ast::item] {
        debug!("pipec: to_endpoint_decls");
        let dir = match dir {
          send => (*self).dir,
          recv => (*self).dir.reverse()
        };
        let mut items = ~[];
        for self.messages.each |m| {
            if dir == send {
                items.push(m.gen_send(cx, true));
                items.push(m.gen_send(cx, false));
            }
        }

        if !self.proto.is_bounded() {
            items.push(
                cx.item_ty_poly(
                    self.data_name(),
                    self.span,
                    cx.ty_path_ast_builder(
                        path(~[cx.ident_of(~"pipes"),
                               cx.ident_of(dir.to_str() + ~"Packet")],
                             empty_span())
                        .add_ty(cx.ty_path_ast_builder(
                            path(~[cx.ident_of(self.proto.name),
                                   self.data_name()],
                                 empty_span())
                            .add_tys(cx.ty_vars(self.ty_params))))),
                    self.ty_params));
        }
        else {
            items.push(
                cx.item_ty_poly(
                    self.data_name(),
                    self.span,
                    cx.ty_path_ast_builder(
                        path(~[cx.ident_of(~"pipes"),
                               cx.ident_of(dir.to_str()
                                           + ~"PacketBuffered")],
                             empty_span())
                        .add_tys(~[cx.ty_path_ast_builder(
                            path(~[cx.ident_of(self.proto.name),
                                   self.data_name()],
                                 empty_span())
                            .add_tys(cx.ty_vars(self.ty_params))),
                                   self.proto.buffer_ty_path(cx)])),
                    self.ty_params));
        };
        items
    }
}

impl protocol: gen_init {
    fn gen_init(cx: ext_ctxt) -> @ast::item {
        let ext_cx = cx;

        debug!("gen_init");
        let start_state = self.states[0];

        let body = if !self.is_bounded() {
            match start_state.dir {
              send => #ast { pipes::entangle() },
              recv => {
                #ast {{
                    let (s, c) = pipes::entangle();
                    (move c, move s)
                }}
              }
            }
        }
        else {
            let body = self.gen_init_bounded(ext_cx);
            match start_state.dir {
              send => body,
              recv => {
                #ast {{
                    let (s, c) = $(body);
                    (move c, move s)
                }}
              }
            }
        };

        cx.parse_item(fmt!("pub fn init%s() -> (client::%s, server::%s)\
                            { use pipes::HasBuffer; %s }",
                           start_state.ty_params.to_source(cx),
                           start_state.to_ty(cx).to_source(cx),
                           start_state.to_ty(cx).to_source(cx),
                           body.to_source(cx)))
    }

    fn gen_buffer_init(ext_cx: ext_ctxt) -> @ast::expr {
        ext_cx.rec(self.states.map_to_vec(|s| {
            let fty = s.to_ty(ext_cx);
            ext_cx.field_imm(ext_cx.ident_of(s.name),
                             #ast { pipes::mk_packet::<$(fty)>() })
        }))
    }

    fn gen_init_bounded(ext_cx: ext_ctxt) -> @ast::expr {
        debug!("gen_init_bounded");
        let buffer_fields = self.gen_buffer_init(ext_cx);

        let buffer = #ast {
            ~{header: pipes::BufferHeader(),
              data: $(buffer_fields)}
        };

        let entangle_body = ext_cx.block_expr(
            ext_cx.block(
                self.states.map_to_vec(
                    |s| ext_cx.parse_stmt(
                        fmt!("data.%s.set_buffer_(buffer)",
                             s.name))),
                ext_cx.parse_expr(
                    fmt!("ptr::addr_of(&(data.%s))",
                         self.states[0].name))));

        #ast {{
            let buffer = $(buffer);
            do pipes::entangle_buffer(move buffer) |buffer, data| {
                $(entangle_body)
            }
        }}
    }

    fn buffer_ty_path(cx: ext_ctxt) -> @ast::ty {
        let mut params: ~[ast::ty_param] = ~[];
        for (copy self.states).each |s| {
            for s.ty_params.each |tp| {
                match params.find(|tpp| tp.ident == tpp.ident) {
                  None => params.push(*tp),
                  _ => ()
                }
            }
        }

        cx.ty_path_ast_builder(path(~[cx.ident_of(~"__Buffer")], self.span)
                               .add_tys(cx.ty_vars(params)))
    }

    fn gen_buffer_type(cx: ext_ctxt) -> @ast::item {
        let ext_cx = cx;
        let mut params: ~[ast::ty_param] = ~[];
        let fields = do (copy self.states).map_to_vec |s| {
            for s.ty_params.each |tp| {
                match params.find(|tpp| tp.ident == tpp.ident) {
                  None => params.push(*tp),
                  _ => ()
                }
            }
            let ty = s.to_ty(cx);
            let fty = #ast[ty] {
                pipes::Packet<$(ty)>
            };
            cx.ty_field_imm(cx.ident_of(s.name), fty)
        };

        cx.item_ty_poly(
            cx.ident_of(~"__Buffer"),
            cx.empty_span(),
            cx.ty_rec(fields),
            params)
    }

    fn compile(cx: ext_ctxt) -> @ast::item {
        let mut items = ~[self.gen_init(cx)];
        let mut client_states = ~[];
        let mut server_states = ~[];

        // :(
        for (copy self.states).each |s| {
            items += s.to_type_decls(cx);

            client_states += s.to_endpoint_decls(cx, send);
            server_states += s.to_endpoint_decls(cx, recv);
        }

        if self.is_bounded() {
            items.push(self.gen_buffer_type(cx))
        }

        items.push(cx.item_mod(cx.ident_of(~"client"),
                               self.span,
                               client_states));
        items.push(cx.item_mod(cx.ident_of(~"server"),
                               self.span,
                               server_states));

        cx.item_mod(cx.ident_of(self.name), self.span, items)
    }
}

trait to_source {
    // Takes a thing and generates a string containing rust code for it.
    fn to_source(cx: ext_ctxt) -> ~str;
}

impl @ast::item: to_source {
    fn to_source(cx: ext_ctxt) -> ~str {
        item_to_str(self, cx.parse_sess().interner)
    }
}

impl ~[@ast::item]: to_source {
    fn to_source(cx: ext_ctxt) -> ~str {
        str::connect(self.map(|i| i.to_source(cx)), ~"\n\n")
    }
}

impl @ast::ty: to_source {
    fn to_source(cx: ext_ctxt) -> ~str {
        ty_to_str(self, cx.parse_sess().interner)
    }
}

impl ~[@ast::ty]: to_source {
    fn to_source(cx: ext_ctxt) -> ~str {
        str::connect(self.map(|i| i.to_source(cx)), ~", ")
    }
}

impl ~[ast::ty_param]: to_source {
    fn to_source(cx: ext_ctxt) -> ~str {
        pprust::typarams_to_str(self, cx.parse_sess().interner)
    }
}

impl @ast::expr: to_source {
    fn to_source(cx: ext_ctxt) -> ~str {
        pprust::expr_to_str(self, cx.parse_sess().interner)
    }
}

trait ext_ctxt_parse_utils {
    fn parse_item(s: ~str) -> @ast::item;
    fn parse_expr(s: ~str) -> @ast::expr;
    fn parse_stmt(s: ~str) -> @ast::stmt;
}

impl ext_ctxt: ext_ctxt_parse_utils {
    fn parse_item(s: ~str) -> @ast::item {
        let res = parse::parse_item_from_source_str(
            ~"***protocol expansion***",
            @(copy s),
            self.cfg(),
            ~[],
            self.parse_sess());
        match res {
          Some(ast) => ast,
          None => {
            error!("Parse error with ```\n%s\n```", s);
            fail
          }
        }
    }

    fn parse_stmt(s: ~str) -> @ast::stmt {
        parse::parse_stmt_from_source_str(
            ~"***protocol expansion***",
            @(copy s),
            self.cfg(),
            ~[],
            self.parse_sess())
    }

    fn parse_expr(s: ~str) -> @ast::expr {
        parse::parse_expr_from_source_str(
            ~"***protocol expansion***",
            @(copy s),
            self.cfg(),
            self.parse_sess())
    }
}
