import type { Parsed, StatementNode } from "./parser";
import dbg from "debug";
const log = dbg("app");

const builtin = ["number", "string", "boolean"];

export class IRError extends Error {
   constructor(public statement: StatementNode, message: string) {
      super("Error building IR: " + message);
   }
}

export interface TypeFieldDefinition {
   name: string;
   type: string;
   array: boolean;
   map?: string;
}

export interface TypeDefinition {
   name: string;
   depends: string[];
   fields: TypeFieldDefinition[];
}

export interface EnumValueDefinition {
   name: string;
   value: number;
}

export interface EnumDefinition {
   name: string;
   values: EnumValueDefinition[];
}

export type ServiceFunctionDecorators = {
   description: string;
   parameters: {
      name:string;
      description: string;
   }[];
   returns: string;
}

export interface ServiceFunctionParamsDefinition {
   name: string;
   inputs: { type: string; name: string, array: boolean }[];
   return: { type: string, array: boolean } | undefined;
   decorators: ServiceFunctionDecorators
}
export type ServiceFunctionDefinition = ServiceFunctionParamsDefinition;

export interface ServiceDefinition {
   name: string;
   depends: string[];
   functions: ServiceFunctionDefinition[];
}

export type Step = [
   "type" | "enum" | "service",
   TypeDefinition | EnumDefinition | ServiceDefinition
];

export type IR = Step[];

export default function get_ir(parsed: Parsed): IR {
   log("Generatie IR from parse output");
   let defined: string[] = [];
   let types: string[] = [];
   let enums: string[] = [];

   // Verifiy and generating steps

   let steps: Step[] = [];

   parsed.forEach((statement) => {
      log("Working on statement of type %s", statement.type);
      if (statement.type == "import")
         throw new IRError(
            statement,
            "Import statements are invalid at this step!"
         );

      if (statement.type === "type") {
         if (defined.indexOf(statement.name) >= 0) {
            throw new IRError(
               statement,
               `Type ${statement.name} already defined!`
            );
         }

         let depends: string[] = [];
         const fields = statement.fields.map<TypeFieldDefinition>((field) => {
            if (field.type !== "type_field") {
               throw new IRError(field, "Invalid statement!");
            }

            if (defined.indexOf(field.fieldtype) < 0) {
               if (builtin.indexOf(field.fieldtype) < 0) {
                  throw new IRError(
                     field,
                     `Type ${field.fieldtype} is not defined!`
                  );
               }
            } else {
               if (depends.indexOf(field.fieldtype) < 0)
                  depends.push(field.fieldtype);
            }

            if (
               field.map &&
               field.map !== "number" &&
               field.map !== "string"
            ) {
               throw new IRError(
                  field,
                  `Type ${field.map} is not valid as map key!`
               );
            }

            return {
               name: field.name,
               type: field.fieldtype,
               array: field.array,
               map: field.map,
            };
         });
         steps.push([
            statement.type,
            {
               name: statement.name,
               depends,
               fields,
            },
         ]);
         defined.push(statement.name);
         types.push(statement.name);
      } else if (statement.type === "enum") {
         if (defined.indexOf(statement.name) >= 0) {
            throw new IRError(
               statement,
               `Type ${statement.name} already defined!`
            );
         }

         let last = -1;
         let values = statement.values.map<EnumValueDefinition>((valueS) => {
            let value = last + 1;
            if (valueS.value) {
               if (valueS.value <= last) {
                  throw new IRError(
                     statement,
                     "Enum value must be larger than the previous one!"
                  );
               } else {
                  value = valueS.value;
               }
            }

            last = value;

            return {
               name: valueS.name,
               value,
            };
         });
         steps.push([
            "enum",
            {
               name: statement.name,
               values,
            } as EnumDefinition,
         ]);
         defined.push(statement.name);
         enums.push(statement.name);
      } else if (statement.type === "service") {
         if (defined.indexOf(statement.name) >= 0) {
            throw new IRError(
               statement,
               `Type ${statement.name} already defined!`
            );
         }

         let depends: string[] = [];
         let alreadyFoundFunctions = new Set<string>();
         let functions = statement.functions.map((fnc) => {
            if (alreadyFoundFunctions.has(fnc.name))
               throw new IRError(
                  fnc,
                  `Function with name ${fnc.name} already defined!`
               );
            alreadyFoundFunctions.add(fnc.name);
            if (fnc.return_type) {
               if (defined.indexOf(fnc.return_type.type) >= 0) {
                  if (!depends.some((a) => a === fnc.return_type.type))
                     depends.push(fnc.return_type.type);
               } else {
                  if (fnc.return_type.type !== "void" && builtin.indexOf(fnc.return_type.type) < 0) {
                     throw new IRError(
                        fnc,
                        `Type ${fnc.return_type.type} is not defined`
                     );
                  }
               }
            }

            for (const input of fnc.inputs) {
               if (defined.indexOf(input.type) >= 0) {
                  if (!depends.some((a) => a === input.type))
                     depends.push(input.type);
               } else {
                  if (builtin.indexOf(input.type) < 0) {
                     throw new IRError(
                        fnc,
                        `Type ${input.type} is not defined`
                     );
                  }
               }
            }

            let decorators = {} as ServiceFunctionDecorators;

            fnc.decorators.forEach((values, key)=>{
               for(const val of values) {
                  switch(key) {
                     case "Description":
                        if(decorators.description)
                           throw new IRError(fnc, `Decorator 'Description' can only be used once!`);
                        if(val.length != 1)
                           throw new IRError(fnc, `Decorator 'Description' requires exactly one parameter!`);
                        decorators.description = val[0];
                        break;
                     case "Returns":
                           if(decorators.returns)
                              throw new IRError(fnc, `Decorator 'Returns' can only be used once!`);
                           if(val.length != 1)
                              throw new IRError(fnc, `Decorator 'Returns' requires exactly one parameter!`);
                           decorators.returns = val[0];
                           break;
                     case "Param":
                        if(!decorators.parameters)
                           decorators.parameters = [];
                        if(val.length != 2)
                           throw new IRError(fnc, `Decorator 'Param' requires exactly two parameters!`);
                        const [name, description] = val;
                        if(!fnc.inputs.find(e=>e.name == name))
                           throw new IRError(fnc, `Decorator 'Param' requires the first param to equal the name of a function parameter!`);
                        if(decorators.parameters.find(e=>e.name == name))
                           throw new IRError(fnc, `Decorator 'Param' has already been set for the parameter ${name}!`);

                        decorators.parameters.push({
                           name,
                           description,
                        })
                        break;
                     default:
                        throw new IRError(fnc, `Decorator ${key} is not a valid decorator!`);
                  }
               }
            })


            return {
               name: fnc.name,
               inputs: fnc.inputs,
               return: fnc.return_type,
               decorators
            } as ServiceFunctionDefinition;
         });

         steps.push([
            "service",
            {
               name: statement.name,
               depends,
               functions,
            } as ServiceDefinition,
         ]);
      } else {
         throw new IRError(statement, "Invalid statement!");
      }
   });

   return steps;
}
