import { format } from "https://deno.land/std@0.146.0/datetime/mod.ts";

import { Type } from "./type_utils.ts";
import * as TypeUtils from "./type_utils.ts";

import { capitalize, escapeXML } from "./utils.ts";

import * as SWC from "./swc.ts";
import * as Errors from "./errors.ts";
import * as Statements from "./statements.ts";
import { BlockStatement, NumericLiteral, Statement } from "./swc.ts";

export interface Ident { type: Type; defined: boolean; }
export interface Assignment { name: string; value: string }
export type Idents = Map<string, Ident>;
export type ProgramAttributes = Partial<{
  [index: string]: string;
  name: string;
  authors: string;
  about: string;
  saved: string;
  created: string;
  edited: string;
}>
export type ProgramMetadata = Partial<{
  [index: string]: string | number;
  username: string;
  hostname: string;
  editversion: number;
  editMysteryNumber: number;
}>;
export interface TransformOptions {
  [index: string]: number | boolean | ProgramAttributes | ProgramMetadata;
  showSourceStatements: boolean;
  attr: ProgramAttributes;
  meta: ProgramMetadata;
};
export interface TransformState {
  input: string;
  idents: Map<string, Ident>;
  indent: string;
  opts: TransformOptions;
}

export function transform (
  input: string,
  ast: SWC.Module | SWC.Program,
  options: Partial<TransformOptions>,
  customTemplate?: string
): string {
  const state: TransformState = {
    input: input,
    indent: "    ".repeat(3),
    idents: new Map(),
    opts: {
      showSourceStatements: options.showSourceStatements ?? false,
      attr: { ...options.attr },
      meta: { ...options.meta },
    }
  };

  const body = transformBlock(ast.body, state);

  const meta: ProgramMetadata = {
    username: state.opts.meta.username ?? "ezfprg",
    hostname: state.opts.meta.hostname ?? "deno",
    editVersion: state.opts.meta.editVersion ?? 1,
    editMysteryNumber: state.opts.meta.editMysteryNumber ?? 2980,
  };

  const now = format(new Date(), "yyyy-MM-dd hh:mm:ss a");
  const created = `${meta.username};${meta.hostname};${now.replace(" ", ";")}`;
  const edited = `${created};${meta.editVersion};${meta.editMysteryNumber}`;

  const attr: ProgramAttributes = {
    name: state.opts.attr.name ?? "Untitled",
    authors: state.opts.attr.authors ?? "ezfprg",
    about: state.opts.attr.about ?? "Converted from TypeScript by ezfprg",
    saved: state.opts.attr.saved ?? now,
    created: btoa(created),
    edited: btoa(edited),
  };

  let template = customTemplate 
    ?? Deno.readTextFileSync("./assets/template.fprg");

  for (const [k, v] of Object.entries(attr)) {
    template = template.replace(`[[${k}]]`, v!);
  } 

  return template.replace("[[body]]", () => body);
}

export function transformBlock (
  block: Array<SWC.Statement> | Array<SWC.ModuleItem>,
  state: TransformState,
) {
  let output = "";

  for (const stmt of block) {
    if (state.opts.showSourceStatements) {
      const rawStmt = state.input.slice(
        stmt.span.start - 1,
        stmt.span.end - 1
      );
      output += state.indent + Statements.comment(rawStmt);
    }

    switch (stmt.type) {
      case "VariableDeclaration":
        output += transformVariableDecl(stmt, state);
        break;
      case "ExpressionStatement": {
        switch (stmt.expression.type) {
          case "AssignmentExpression":
            output += transformAssignmentExpr(stmt.expression, state);
            break;
          case "CallExpression":
            output += transformBlockCallExpr(stmt.expression, state);
            break;
        }
      } break;
      case "IfStatement":
        output += transformIfStmt(stmt, state);
        break;
      case "WhileStatement":
        output += transformWhileStmt(stmt, state);
        break;
      case "ForStatement":
        output += transformForStmt(stmt, state);
        break;
    }
  }

  return output;
}

export function transformNewScope (
  body: BlockStatement,
  state: TransformState,
): string {
  if (body.stmts.length === 0) return "";

  const oldIndent = state.indent.toString();
  const oldIdents: Idents = new Map(state.idents);

  state.indent = state.indent + "        ";

  const out = transformBlock(body.stmts, state);

  state.indent = oldIndent;
  state.idents = oldIdents;

  return out;
}

export function transformExpr (
  expr: SWC.Literal | SWC.Expression,
  idents: Map<string, Ident>,
): [string, Type] {
  switch (expr.type) {
    case "NumericLiteral": {
      // @ts-ignore - ts thinks that raw does not exist in a NumericLiteral
      const val = "raw" in expr ? expr.raw : expr.value.toString();
      return [val, val.includes(".") ? "float" : "int"];
    }
    case "StringLiteral":
      return [`"${expr.value}"`, "string"];
    case "BooleanLiteral":
      return [expr.value.toString(), "boolean"];
    case "Identifier":
    case "CallExpression": {
      let name: string;
      if (expr.type === "Identifier") {
        name = expr.value;
      } else if ("value" in expr.callee) {
        name = expr.callee.value.toString();
      } else {
        throw new Errors.InvalidExpression(expr.callee.type, expr.span.start);
      }

      const matchedIdent = idents.get(name);
      if (!matchedIdent || !matchedIdent.defined) {
        throw new Errors.UndefinedReference(name, expr.span.start);
      }

      return [name, matchedIdent.type];
    }
    case "ParenthesisExpression": {
      const [_expr, exprType] = transformExpr(expr.expression, idents);
      return [`(${_expr})`, exprType];
    }
    case "UnaryExpression": {
      const [_expr, exprType] = transformExpr(expr.argument, idents);
      return [`${expr.operator}${_expr}`, exprType];
    }
    case "BinaryExpression": {
      const [left, lType] = transformExpr(expr.left, idents);
      const [right, rType] = transformExpr(expr.right, idents);

      if (!TypeUtils.validateBinaryOpTypes(lType, rType, expr.operator)) {
        throw new Errors.InvalidBinaryOp(
          lType,
          rType,
          expr.operator,
          expr.span.start
        );
      }

      const op = [lType, rType].includes("string") && expr.operator === "+"
        ? "&"
        : expr.operator;
      const type = TypeUtils.castValidBinaryOpTypes(lType, rType, op);

      return [`${left} ${op} ${right}`, type];
    }
    default:
      // @ts-ignore: fuck you ts
      throw new Errors.InvalidExpression(expr.type, expr.span.start);
  }
}

export function transformBlockCallExpr (
  expr: SWC.CallExpression,
  state: TransformState,
): string {
  let name: string;

  if ("value" in expr.callee) {
    name = expr.callee.value.toString();
  } else {
    throw new Errors.InvalidExpression(
      expr.callee.type,
      expr.span.start
    );
  }

  switch (name) {
    case "println": {
      if (expr.arguments.length < 1) {
        throw new Errors.InvalidArgumentCount(
          name,
          expr.arguments.length,
          1,
          expr.span.start
        );
      }

      const [newExpr, _] = transformExpr(
        expr.arguments[0].expression,
        state.idents,
      );

      return state.indent + Statements.output(newExpr, true);
    }
    case "print": {
      if (expr.arguments.length < 1) {
        throw new Errors.InvalidArgumentCount(
          name,
          expr.arguments.length,
          1,
          expr.span.start
        );
      }

      const [newExpr, _] = transformExpr(
        expr.arguments[0].expression,
        state.idents,
      );

      return state.indent + Statements.output(newExpr, false);
    }
    case "input": {
      if (expr.arguments.length !== 1) {
        throw new Errors.InvalidArgumentCount(
          name,
          expr.arguments.length,
          1,
          expr.span.start
        );
      }

      if (expr.arguments[0].expression.type !== "Identifier") {
        throw new Errors.InvalidSyntaxType(
          expr.arguments[0].expression.type,
          "<Identifier>",
          expr.span.start
        );
      }

      const variable = expr.arguments[0].expression.value.toString();
      const matchedIdent = state.idents.get(variable);

      if (!matchedIdent) {
        throw new Errors.UndefinedReference(variable, expr.span.start);
      }

      state.idents.set(variable, { ...matchedIdent, defined: true });
      return state.indent + Statements.input(variable);
    }
    case "attr":
    case "meta": {
      if (expr.arguments.length !== 2) {
        throw new Errors.InvalidArgumentCount(
          name,
          expr.arguments.length,
          2,
          expr.span.start
        );
      }
      
      for (const arg of expr.arguments) {
        if (arg.expression.type !== "StringLiteral") {
          throw new Errors.InvalidSyntaxType(
            expr.arguments[0].expression.type,
            "<StringLiteral>",
            expr.span.start
          );
        }
      }

      const args = expr.arguments
        .map((a) => a.expression as SWC.StringLiteral);

      const key: string = args[0].value;

      if (TypeUtils.validKeys[name].includes(key)) {
        if (state.opts[name][key] === undefined) {
          state.opts[name][key] = args[1].value;
        }
      } else {
        throw new Errors.InvalidAttrOrMeta(key, name, expr.span.start);
      }

      return "";
    }
    case "comment": {
      if (expr.arguments.length !== 1) {
        throw new Errors.InvalidArgumentCount(
          name,
          expr.arguments.length,
          1,
          expr.span.start
        );
      }

      if (expr.arguments[0].expression.type !== "StringLiteral") {
        throw new Errors.InvalidSyntaxType(
          expr.arguments[0].expression.type,
          "<StringLiteral>",
          expr.span.start
        );
      }

      const comment = expr.arguments[0].expression.value.toString();
      return state.indent + Statements.comment(comment);
    }
    case "insist": {
      if (expr.arguments.length !== 1) {
        throw new Errors.InvalidArgumentCount(
          name,
          expr.arguments.length,
          1,
          expr.span.start
        );
      }

      if (expr.arguments[0].expression.type !== "Identifier") {
        throw new Errors.InvalidSyntaxType(
          expr.arguments[0].expression.type,
          "<Identifier>",
          expr.span.start
        );
      }

      const ident = expr.arguments[0].expression.value;
      const matchedIdent = state.idents.get(ident);

      if (!matchedIdent) {
        throw new Errors.UndefinedReference(ident, expr.span.start);
      }

      state.idents.set(ident, { ...matchedIdent, defined: true });
      return "";
    }
    default:
      throw new Errors.UndefinedReference(name, expr.span.start);
  }
}

export function transformAssignmentExpr (
  expr: SWC.AssignmentExpression,
  state: TransformState
): string {
  if (expr.left.type !== "Identifier") {
    throw new Errors.InvalidAssignmentTarget(expr.span.start);
  }

  const name = expr.left.value;
  const matchedIdent = state.idents.get(name);

  if (!matchedIdent) {
    throw new Errors.UndefinedReference(name, expr.span.start);
  }

  const type = matchedIdent.type;
  const [newExpr, exprType] = transformExpr(expr.right, state.idents);
  
  if (!TypeUtils.validateAssignmentTypes(type, exprType)) {
    throw new Errors.InvalidType(type, exprType, expr.span.start);
  }
  
  if (!matchedIdent.defined) {
    state.idents.set(name, { ...matchedIdent, defined: true });
  }

  return state.indent + Statements.assign(name, newExpr);
}

export function transformVariableDecl (
  stmt: SWC.VariableDeclaration,
  state: TransformState,
) {
  let output = "";

  const declarations: Record<Type, string[]> = {
    int: [],
    float: [],
    string: [],
    boolean: [],
  };

  const assignments = new Array<Assignment>();

  for (const decl of stmt.declarations) {
    if (decl.id.type !== "Identifier") {
      throw new Errors.InvalidAssignmentTarget(decl.span.start);
    }

    if (state.idents.has(decl.id.value)) {
      throw new Errors.AlreadyDefined(decl.id.value, decl.id.span.start);
    }

    let type: Type | undefined;

    if (decl.id.typeAnnotation) {
      const annot = decl.id.typeAnnotation.typeAnnotation;

      if ("kind" in annot) {
        type = annot.kind as Type;
      } else if ("typeName" in annot) {
        // @ts-ignore: who the fuck asked + L + ratio
        type = annot.typeName.value;
      } else {
        throw new Errors.InvalidTypeAnnotation(
          annot.type,
          decl.id.span.start
        );
      }

      if (!TypeUtils.typeNames.includes(type!)) {
        throw new Errors.InvalidType(
          "int | float | string | boolean",
          type!,
          stmt.span.start
        );
      }
    }

    if (decl.init) {
      const [expr, exprType] = transformExpr(decl.init, state.idents);

      if (type && !TypeUtils.validateAssignmentTypes(type!, exprType)) {
        throw new Errors.InvalidType(type, exprType, decl.span.start);
      } else if (!type) {
        type = exprType;
      }
      assignments.push({ name: decl.id.value, value: expr });
    }

    if (!type) {
      throw new Errors.MissingType(decl.id.value, stmt.span.start);
    }

    declarations[type].push(decl.id.value);
    state.idents.set(
      decl.id.value,
      { type: type, defined: decl.init !== null }
    );
  }

  TypeUtils.typeNames.forEach((type) => {
    const names = declarations[type];
    if (names.length > 0) {
      output += state.indent + Statements.declare(
        names.join(", "),
        type as Type
      );
    }
  });

  assignments.forEach((a) => {
    output += state.indent + Statements.assign(a.name, a.value);
  });

  return output;
}

export function transformIfStmt (
  stmt: SWC.IfStatement,
  state: TransformState
): string {
  const [expr, exprType] = transformExpr(stmt.test, state.idents);

  if (exprType !== "boolean") {
    throw new Errors.InvalidType("boolean", exprType, stmt.span.start);
  }

  let arm1: string;
  if (stmt.consequent && stmt.consequent.type === "BlockStatement") {
    arm1 = transformNewScope(stmt.consequent, state);
  } else {
    throw new Errors.InvalidSyntaxType(
      "BlockStatement",
      stmt.consequent.type,
      stmt.span.start
    );
  }

  let arm2 = "";

  if (stmt.alternate) {
    if (stmt.alternate.type === "BlockStatement") {
      arm2 = transformNewScope(stmt.alternate, state);
    } else if (stmt.alternate.type === "IfStatement") {
      const oldIndent = state.indent;
      state.indent += "    ";
      arm2 = transformIfStmt(stmt.alternate, state);
      state.indent = oldIndent;
    } else {
      throw new Errors.InvalidSyntaxType(
        "BlockStatement",
        stmt.alternate.type,
        stmt.span.start
      );
    }
  }

  return Statements._if(state.indent, expr, arm1, arm2);
}

export function transformWhileStmt (
  stmt: SWC.WhileStatement,
  state: TransformState
): string { 
  const [expr, exprType] = transformExpr(stmt.test, state.idents);

  if (exprType !== "boolean") {
    throw new Errors.InvalidType("boolean", exprType, stmt.span.start);
  }

  let body: string;

  if (stmt.body && stmt.body.type === "BlockStatement") {
    body = transformNewScope(stmt.body, state);
  } else {
    throw new Errors.InvalidSyntaxType(
      "BlockStatement",
      stmt.body.type,
      stmt.span.start
    );
  }

  return Statements._while(state.indent, expr, body);
}

export function transformForStmt (
  stmt: SWC.ForStatement,
  state: TransformState
): string {
  let output = "";
  let variable: string;
  let start: number;

  if (!stmt.init) {
    throw new Errors.ForMissingInit(stmt.span.start);
  } else if (stmt.init.type === "VariableDeclaration") {
    const decl = stmt.init.declarations[0];
    if (decl.id.type !== "Identifier") {
      throw new Errors.InvalidSyntaxType(
        "Identifier",
        decl.id.type,
        stmt.init.span.start
      );
    } else if (!decl.init || decl.init.type !== "NumericLiteral") {
      throw new Errors.InvalidSyntaxType(
        "NumericLiteral",
        decl.init?.type ?? "Nothing",
        stmt.init.span.start
      );
    }

    variable = (decl.id as SWC.Identifier).value;
    start = (decl.init as SWC.NumericLiteral).value;

    output += state.indent + transformVariableDecl(stmt.init, state);
  } else if (stmt.init.type === "AssignmentExpression") {
    if (stmt.init.left.type !== "Identifier") {
      throw new Errors.InvalidSyntaxType(
        "Identifier",
        stmt.init.left.type,
        stmt.init.span.start
      );
    } else if (stmt.init.right.type !== "NumericLiteral") {
      throw new Errors.InvalidSyntaxType(
        "NumericLiteral",
        stmt.init?.type,
        stmt.init.span.start
      );
    }

    variable = stmt.init.left.value;
    start = stmt.init.right.value;
  } else {
    throw new Errors.InvalidSyntaxType(
      "VariableDeclaration | AssignmentExpression",
      stmt.init.type,
      stmt.span.start
    );
  }

  if (!stmt.test) {
    throw new SyntaxError(
    );
  } else if (stmt.test.type !== "NumericLiteral") {
    throw new Errors.InvalidSyntaxType(
      "NumericLiteral",
      stmt.test.type,
      stmt.span.start
    );
  }

  const end: number = stmt.test.value;
  let step: number;
  let direction: "inc" | "dec";

  if (!stmt.update) {
    throw new Errors.ForMissingEnd(stmt.span.start);
  } else if (stmt.update.type === "UpdateExpression") {
    if (stmt.update.argument.type !== "Identifier") {
      throw new Errors.InvalidSyntaxType(
        "Identifier",
        stmt.update.argument.type,
        stmt.update.span.start
      );
    } else if (stmt.update.argument.value !== variable) {
      throw new Errors.ForVariableMismatch(
        variable,
        stmt.update.argument.value,
        stmt.update.span.start
      );
    } else if (stmt.update.operator === "++") {
      step = 1;
      direction = "inc";
    } else if (stmt.update.operator === "--") {
      step = 1;
      direction = "dec";
    } else {
      throw new Errors.ForInvalidOperator(
        stmt.update.operator,
        stmt.update.span.start
      );
    }
  } else if (stmt.update.type === "AssignmentExpression") {
    if (stmt.update.left.type !== "Identifier") {
      throw new Errors.InvalidSyntaxType(
        "Identifier",
        stmt.update.left.type,
        stmt.update.span.start
      );
    } else if (stmt.update.left.value !== variable) {
      throw new Errors.ForVariableMismatch(
        variable,
        stmt.update.left.value,
        stmt.update.span.start
      );
    }

    if (stmt.update.operator === "+=") {
      direction = "inc";
    } else if (stmt.update.operator === "-=") {
      direction = "dec";
    } else {
      throw new Errors.ForInvalidOperator(
        stmt.update.operator,
        stmt.update.span.start
      );
    }

    if (stmt.update.right.type !== "NumericLiteral") {
      throw new Errors.InvalidSyntaxType(
        "NumericLiteral",
        stmt.update.right.type,
        stmt.update.span.start
      );
    }

    step = stmt.update.right.value;
  } else {
    throw new Errors.InvalidSyntaxType(
      "UpdateExpression | AssignmentExpression",
      stmt.update.type,
      stmt.span.start
    );
  }

  let body: string;
  if (stmt.body && stmt.body.type === "BlockStatement") {
    body = transformNewScope(stmt.body, state);
  } else {
    throw new Errors.InvalidSyntaxType(
      "BlockStatement",
      stmt.body.type,
      stmt.span.start
    );
  }

  output += Statements._for(
    state.indent,
    variable,
    start,
    end,
    direction,
    step,
    body
  );
  return output;
}