#include "pgf.h"
#include "data.h"
#include "gu/mem.h"
#include "gu/string.h"

typedef struct PgfCFHypos PgfCFHypos;
struct PgfCFHypos {
	PgfType* ty;
	PgfCFHypos* next;
};

typedef struct PgfContext PgfContext;
struct PgfContext {
	PgfCId var;
	PgfType* ty;
	PgfContext* next;
};

typedef struct {
	PgfCFHypos* hypos;
	PgfCId cat;
} PgfCFType;

typedef struct {
	PgfAbstr* abstr;
	GuExn* exn;
	GuPool* pool;
	GuPool* tmp_pool;
} PgfTypeChecker;

static PgfCFType null_cf_type = { .hypos = NULL, .cat = NULL };

static PgfCFType 
pgf_ty2cfty(PgfTypeChecker* checker, PgfType* ty)
{
	PgfCFHypos* hypos = NULL;
	size_t n_hypos = gu_seq_length(ty->hypos);
	while (n_hypos-- > 0) {
		PgfHypo* hypo = gu_seq_index(ty->hypos, PgfHypo, n_hypos);

		PgfCFHypos* new_hypos = gu_new(PgfCFHypos, checker->tmp_pool);
		new_hypos->ty   = hypo->type;
		new_hypos->next = hypos;
		hypos = new_hypos;
	}
	return ((PgfCFType) { .hypos = hypos, .cat = ty->cid });
}

static PgfType*
pgf_cfty2ty(PgfTypeChecker* checker, PgfCFType cf_ty)
{
	PgfCFHypos* hypos;

	size_t n_hypos = 0;
	hypos = cf_ty.hypos;
	while (hypos != NULL) {
		n_hypos++;
		hypos = hypos->next;
	}

	PgfType* ty = gu_new(PgfType, checker->pool);
	ty->hypos   = gu_new_seq(PgfHypo, n_hypos, checker->pool);
	ty->cid     = cf_ty.cat;
	ty->n_exprs = 0;

	size_t i = 0;
	hypos = cf_ty.hypos;
	while (hypos != NULL) {
		PgfHypo* hypo = gu_seq_index(ty->hypos, PgfHypo, i++);
		hypo->bind_type = PGF_BIND_TYPE_EXPLICIT;
		hypo->cid       = "_";
		hypo->type      = pgf_cfty2ty(checker, pgf_ty2cfty(checker, hypos->ty));
		hypos = hypos->next;
	}

	return ty;
}

static PgfPrintContext*
pgf_tc_mk_print_context(PgfTypeChecker* checker, PgfContext* ctxt)
{
	PgfPrintContext* pctxt = NULL;
	PgfPrintContext** pprev = &pctxt;

	while (ctxt != NULL) {
		PgfPrintContext* new_pctxt =
			gu_new(PgfPrintContext, checker->tmp_pool);
		new_pctxt->name = ctxt->var;
		new_pctxt->next = NULL;

		*pprev = new_pctxt;
		pprev  = &new_pctxt->next;

		ctxt = ctxt->next;
	}

	return pctxt;
}

static void
pgf_tc_err_cannot_infer(PgfTypeChecker* checker, PgfContext* ctxt, PgfExpr e)
{
	GuStringBuf* sbuf = gu_string_buf(checker->tmp_pool);
    GuOut* out = gu_string_buf_out(sbuf);
    GuExn* err = gu_exn(checker->tmp_pool);

	gu_puts("Cannot infer the type of expression ", out, err);
	pgf_print_expr(e, pgf_tc_mk_print_context(checker, ctxt), 0, out, err);

	GuExnData* exn = gu_raise(checker->exn, PgfTypeError);
	exn->data = (void*)
		gu_string_buf_freeze(sbuf, exn->pool);
}

static void
pgf_tc_err_exp_fun_type_1(PgfTypeChecker* checker, PgfContext* ctxt, 
                          PgfExpr e, PgfType* ty)
{
	GuStringBuf* sbuf = gu_string_buf(checker->tmp_pool);
    GuOut* out = gu_string_buf_out(sbuf);
    GuExn* err = gu_exn(checker->tmp_pool);

	PgfPrintContext* pctxt = pgf_tc_mk_print_context(checker, ctxt);
	gu_puts("The expression ", out, err);
	pgf_print_expr(e, pctxt, 0, out, err);
	gu_puts(" is of function type but ", out, err);
	pgf_print_type(ty, pctxt, 0, out, err);
	gu_puts(" is expected", out, err);

	GuExnData* exn = gu_raise(checker->exn, PgfTypeError);
	exn->data = (void*)
		gu_string_buf_freeze(sbuf, exn->pool);
}

static void
pgf_tc_err_exp_fun_type_2(PgfTypeChecker* checker, PgfContext* ctxt, 
                          PgfExpr e, PgfType* ty)
{
	GuStringBuf* sbuf = gu_string_buf(checker->tmp_pool);
    GuOut* out = gu_string_buf_out(sbuf);
    GuExn* err = gu_exn(checker->tmp_pool);

	PgfPrintContext* pctxt = pgf_tc_mk_print_context(checker, ctxt);
	gu_puts("A function type is expected for the expression ", out, err);
	pgf_print_expr(e, pctxt, 0, out, err);
	gu_puts(" instead of type ", out, err);
	pgf_print_type(ty, pctxt, 0, out, err);

	GuExnData* exn = gu_raise(checker->exn, PgfTypeError);
	exn->data = (void*)
		gu_string_buf_freeze(sbuf, exn->pool);
}

static void
pgf_tc_err_n_args(PgfTypeChecker* checker, 
                  PgfAbsCat* abs_cat, size_t n_args)
{
	GuExnData* exn = gu_raise(checker->exn, PgfTypeError);
	exn->data = (void*)
		gu_format_string(exn->pool, 
		                 "The category %s has %d indices but %d are given",
		                 abs_cat->name,
		                 gu_seq_length(abs_cat->context),
		                 n_args);
}

static void
pgf_tc_err_type_mismatch(PgfTypeChecker* checker,
                         PgfContext* ctxt,
                         PgfExpr e, PgfCFType ty1, PgfCFType ty2)
{
	GuStringBuf* sbuf = gu_string_buf(checker->tmp_pool);
    GuOut* out = gu_string_buf_out(sbuf);
    GuExn* err = gu_exn(checker->tmp_pool);

	PgfPrintContext* pctxt = pgf_tc_mk_print_context(checker, ctxt);
	gu_puts("The expected type of the expression ", out, err);
	pgf_print_expr(e, pctxt, 0, out, err);
	gu_puts(" is ", out, err);
	pgf_print_type(pgf_cfty2ty(checker, ty1), pctxt, 0, out, err);
	gu_puts(" but ", out, err);
	pgf_print_type(pgf_cfty2ty(checker, ty2), pctxt, 0, out, err);
	gu_puts(" is infered", out, err);

	GuExnData* exn = gu_raise(checker->exn, PgfTypeError);
	exn->data = (void*)
		gu_string_buf_freeze(sbuf, exn->pool);
}

static bool
pgf_unify_types(PgfTypeChecker* checker, PgfCFType ty1, PgfCFType ty2)
{
	if (strcmp(ty1.cat, ty2.cat) != 0)
		return false;
		
	while (ty1.hypos != NULL && ty2.hypos != NULL) {
		if (!pgf_unify_types(checker,
		                     pgf_ty2cfty(checker, ty1.hypos->ty),
		                     pgf_ty2cfty(checker, ty2.hypos->ty)))
			return false;

		ty1.hypos = ty1.hypos->next;
		ty2.hypos = ty2.hypos->next;
	}
	
	if (ty1.hypos != NULL || ty2.hypos != NULL)
		return false;

	return true;
}

static PgfCFType
pgf_inf_expr(PgfTypeChecker* checker, PgfContext* ctxt, PgfExpr* pe);

static void
pgf_tc_expr(PgfTypeChecker* checker,
            PgfContext* ctxt, PgfExpr* pe, PgfCFType ty)
{
	GuVariantInfo i = gu_variant_open(*pe);
	switch (i.tag) {
	case PGF_EXPR_ABS: {
		PgfExprAbs* eabs = i.data;

		if (ty.hypos == NULL) {
			pgf_tc_err_exp_fun_type_1(checker, ctxt, *pe, pgf_cfty2ty(checker, ty));
			return;
		}

		PgfContext* new_ctxt = gu_new(PgfContext, checker->tmp_pool);
		new_ctxt->var  = eabs->id;
		new_ctxt->ty   = ty.hypos->ty;
		new_ctxt->next = ctxt;

		PgfExprAbs* new_eabs =
			gu_new_variant(PGF_EXPR_ABS,
			               PgfExprAbs,
			               pe, checker->pool);
		new_eabs->bind_type = eabs->bind_type;
		new_eabs->id        = gu_string_copy(eabs->id, checker->pool);
		new_eabs->body      = eabs->body;

		PgfCFType new_ty = { .hypos = ty.hypos->next, .cat = ty.cat };
		pgf_tc_expr(checker, new_ctxt, &new_eabs->body, new_ty);
		break;
	}
	case PGF_EXPR_LIT:
	case PGF_EXPR_APP:
	case PGF_EXPR_FUN:
	case PGF_EXPR_VAR:
	case PGF_EXPR_TYPED: {
		PgfCFType inf_ty =
			pgf_inf_expr(checker, ctxt, pe);
		if (!gu_ok(checker->exn))
			return;
		if (!pgf_unify_types(checker, ty, inf_ty)) {
			pgf_tc_err_type_mismatch(checker, ctxt, *pe, ty, inf_ty);
			return;
		}
		break;
	}
	case PGF_EXPR_META: {
		PgfExprMeta* emeta = i.data;
		
		PgfExprMeta* new_emeta =
			gu_new_variant(PGF_EXPR_META,
			               PgfExprMeta,
			               pe, checker->pool);
		new_emeta->id = emeta->id;
		break;
	}
	case PGF_EXPR_IMPL_ARG: {
		PgfExprImplArg* eimpl = i.data;
		
		PgfExprImplArg* new_eimpl =
			gu_new_variant(PGF_EXPR_IMPL_ARG,
			               PgfExprImplArg,
			               pe, checker->pool);
		new_eimpl->expr = eimpl->expr;
		pgf_tc_expr(checker, ctxt, &new_eimpl->expr, ty);
		break;
	}
	default:
		gu_impossible();
	}
}

static void
pgf_tc_type(PgfTypeChecker* checker, PgfType** pty)
{
	PgfType* ty = *pty;

	PgfAbsCat* abs_cat =
		gu_seq_binsearch(checker->abstr->cats, pgf_abscat_order, PgfAbsCat, ty->cid);
	if (abs_cat == NULL) {
		GuExnData* exn = gu_raise(checker->exn, PgfExn);
		exn->data = (void*)
			gu_format_string(exn->pool,
			                 "Unknown category \"%s\"", ty->cid);
		return;
	}

	PgfContext* ctxt = NULL;

	size_t n_hypos = gu_seq_length(ty->hypos);
	PgfHypos* new_hypos = gu_new_seq(PgfHypo, n_hypos, checker->pool);
	for (size_t i = 0; i < n_hypos; i++) {
		PgfHypo* hypo     = gu_seq_index(ty->hypos, PgfHypo, i);
		PgfHypo* new_hypo = gu_seq_index(new_hypos, PgfHypo, i);

		new_hypo->bind_type = hypo->bind_type;
		new_hypo->cid       = gu_string_copy(hypo->cid, checker->pool);
		new_hypo->type      = hypo->type;

		pgf_tc_type(checker, &new_hypo->type);
		if (!gu_ok(checker->exn))
			return;
			
		PgfContext* new_ctxt = gu_new(PgfContext, checker->tmp_pool);
		new_ctxt->var  = hypo->cid;
		new_ctxt->ty   = hypo->type;
		new_ctxt->next = ctxt;
		ctxt = new_ctxt;
	}

	PgfType *new_ty =
		gu_new_flex(checker->pool, PgfType, exprs, ty->n_exprs);
	new_ty->hypos   = new_hypos;
	new_ty->cid     = gu_string_copy(ty->cid, checker->pool);
	new_ty->n_exprs = ty->n_exprs;

	if (gu_seq_length(abs_cat->context) != ty->n_exprs) {
		pgf_tc_err_n_args(checker, abs_cat, ty->n_exprs);
		return;
	}

	for (size_t i = 0; i < ty->n_exprs; i++) {
		PgfHypo* hypo = gu_seq_index(abs_cat->context, PgfHypo, i);

		new_ty->exprs[i] = ty->exprs[i];
		pgf_tc_expr(checker, ctxt, &new_ty->exprs[i], pgf_ty2cfty(checker, hypo->type));
		if (!gu_ok(checker->exn))
			return;
	}

	*pty = new_ty;
}

static PgfCFType
pgf_inf_expr(PgfTypeChecker* checker, PgfContext* ctxt, PgfExpr* pe)
{
	GuVariantInfo i = gu_variant_open(*pe);
	switch (i.tag) {
	case PGF_EXPR_ABS: {
		pgf_tc_err_cannot_infer(checker, ctxt, *pe);
		return null_cf_type;
	}
	case PGF_EXPR_APP: {
		PgfExprApp* eapp = i.data;

		PgfExprApp* new_eapp =
			gu_new_variant(PGF_EXPR_APP,
			               PgfExprApp,
			               pe, checker->pool);
		new_eapp->fun = eapp->fun;
		new_eapp->arg = eapp->arg;

		PgfCFType fun_ty =
			pgf_inf_expr(checker, ctxt, &new_eapp->fun);
		if (!gu_ok(checker->exn))
			return null_cf_type;
		if (fun_ty.hypos == NULL) {
			pgf_tc_err_exp_fun_type_2(checker,
			                          ctxt,
                                      eapp->fun, pgf_cfty2ty(checker, fun_ty));
			return null_cf_type;
		}

		pgf_tc_expr(checker, ctxt, &new_eapp->arg, pgf_ty2cfty(checker, fun_ty.hypos->ty));
		return ((PgfCFType) { .hypos = fun_ty.hypos->next,
		                      .cat   = fun_ty.cat
		                    });
	}
	case PGF_EXPR_LIT: {
		PgfExprLit* elit = i.data;

		PgfExprLit* new_elit =
			gu_new_variant(PGF_EXPR_LIT,
			               PgfExprLit,
			               pe, checker->pool);

		GuVariantInfo i = gu_variant_open(elit->lit);
		switch (i.tag) {
		case PGF_LITERAL_STR: {
			PgfLiteralStr* lstr = i.data;

			PgfLiteralStr* new_lstr =
				gu_new_flex_variant(PGF_LITERAL_STR,
				                    PgfLiteralStr,
				                    val, strlen(lstr->val)+1,
				                    &new_elit->lit, checker->pool);
			strcpy(new_lstr->val, lstr->val);

			return ((PgfCFType) { .hypos = NULL, .cat = "String" });
		}
		case PGF_LITERAL_INT: {
			PgfLiteralInt* lint = i.data;

			PgfLiteralInt* new_lint =
				gu_new_variant(PGF_LITERAL_INT,
				               PgfLiteralInt,
				               &new_elit->lit, checker->pool);
			new_lint->val = lint->val;

			return ((PgfCFType) { .hypos = NULL, .cat = "Int" });
		}
		case PGF_LITERAL_FLT: {
			PgfLiteralFlt* lflt = i.data;

			PgfLiteralFlt* new_lflt =
				gu_new_variant(PGF_LITERAL_FLT,
				               PgfLiteralFlt,
				               &new_elit->lit, checker->pool);
			new_lflt->val = lflt->val;

			return ((PgfCFType) { .hypos = NULL, .cat = "Float" });
		}
		default:
			gu_impossible();
		}
		break;
	}
	case PGF_EXPR_META: {
		pgf_tc_err_cannot_infer(checker, ctxt, *pe);
		return null_cf_type;
	}
	case PGF_EXPR_FUN: {
		PgfExprFun* efun = i.data;

		PgfType* ty = NULL;

		int var = 0;
		PgfContext* var_ctxt = ctxt;
		while (var_ctxt != NULL) {
			if (strcmp(var_ctxt->var, efun->fun) == 0) {
				PgfExprVar* new_evar =
					gu_new_variant(PGF_EXPR_VAR,
				                   PgfExprVar,
					               pe, checker->pool);
				new_evar->var = var;
				ty = var_ctxt->ty;
				break;
			}

			var++;
			var_ctxt = var_ctxt->next;
		}

		if (ty == NULL) {
			PgfAbsFun* abs_fun =
				gu_seq_binsearch(checker->abstr->funs, pgf_absfun_order, PgfAbsFun, efun->fun);
			if (abs_fun == NULL) {
				GuExnData* exn = gu_raise(checker->exn, PgfExn);
				exn->data = (void*)
					gu_format_string(exn->pool, 
									 "Unknown function \"%s\"", efun->fun);
				return null_cf_type;
			} else {
				PgfExprFun *new_efun =
					gu_new_flex_variant(PGF_EXPR_FUN,
					                    PgfExprFun,
					                    fun, strlen(efun->fun)+1,
					                    pe, checker->pool);
				strcpy(new_efun->fun, efun->fun);
				ty = abs_fun->type;
			}
		}

		return pgf_ty2cfty(checker, ty);
	}
	case PGF_EXPR_VAR: {
		PgfExprVar* evar = i.data;

		int var = evar->var;
		PgfContext* var_ctxt = ctxt;
		assert(var_ctxt != NULL);
		while (var > 0) {
			var--;
			var_ctxt = var_ctxt->next;
			assert(var_ctxt != NULL);
		}

		return pgf_ty2cfty(checker, var_ctxt->ty);
	}
	case PGF_EXPR_TYPED: {
		PgfExprTyped* etyped = i.data;
		
		PgfExprTyped* new_etyped =
			gu_new_variant(PGF_EXPR_TYPED,
			               PgfExprTyped,
			               pe, checker->pool);
		new_etyped->expr = etyped->expr;
		new_etyped->type = etyped->type;

		pgf_tc_type(checker, &new_etyped->type);
		if (!gu_ok(checker->exn))
			return null_cf_type;

		PgfCFType cf_ty = pgf_ty2cfty(checker, new_etyped->type);

		pgf_tc_expr(checker, ctxt, &new_etyped->expr, cf_ty);
		return cf_ty;
	}
	case PGF_EXPR_IMPL_ARG: {
		PgfExprImplArg* eimpl = i.data;
		
		PgfExprImplArg* new_eimpl =
			gu_new_variant(PGF_EXPR_IMPL_ARG,
			               PgfExprImplArg,
			               pe, checker->pool);
		new_eimpl->expr = eimpl->expr;
		return pgf_inf_expr(checker, ctxt, &new_eimpl->expr);
	}
	default:
		gu_impossible();
	}

	return null_cf_type;
}

void
pgf_check_expr(PgfPGF* gr, PgfExpr* pe, PgfType* ty,
               GuExn* exn, GuPool* pool)
{
	GuPool* tmp_pool = gu_new_pool();
	
	PgfTypeChecker* checker = gu_new(PgfTypeChecker, tmp_pool);
	checker->abstr    = &gr->abstract;
	checker->exn      = exn;
	checker->pool     = pool;
	checker->tmp_pool = tmp_pool;
	
	pgf_tc_expr(checker, NULL, pe, pgf_ty2cfty(checker, ty));
	
	gu_pool_free(tmp_pool);
}

PgfType*
pgf_infer_expr(PgfPGF* gr, PgfExpr* pe,
               GuExn* exn, GuPool* pool)
{
	GuPool* tmp_pool = gu_new_pool();
	
	PgfTypeChecker* checker = gu_new(PgfTypeChecker, tmp_pool);
	checker->abstr    = &gr->abstract;
	checker->exn      = exn;
	checker->pool     = pool;
	checker->tmp_pool = tmp_pool;

	PgfCFType cf_ty = pgf_inf_expr(checker, NULL, pe);
	
	PgfType* ty = NULL;
	if (gu_ok(exn)) {
		ty = pgf_cfty2ty(checker, cf_ty);
	}

	gu_pool_free(tmp_pool);

	return ty;
}

void
pgf_check_type(PgfPGF* gr, PgfType** pty, 
               GuExn* exn, GuPool* pool)
{
	GuPool* tmp_pool = gu_new_pool();
	
	PgfTypeChecker* checker = gu_new(PgfTypeChecker, tmp_pool);
	checker->abstr    = &gr->abstract;
	checker->exn      = exn;
	checker->pool     = pool;
	checker->tmp_pool = tmp_pool;

	pgf_tc_type(checker, pty);

	gu_pool_free(tmp_pool);
}
