import type { Token } from "./tokenizer";

export interface DefinitionNode {
   type: string;
   location: {
      file: string;
      idx: number;
   };
}

export interface ImportStatement extends DefinitionNode {
   type: "import";
   path: string;
}

export interface TypeFieldStatement extends DefinitionNode {
   type: "type_field";
   name: string;
   fieldtype: string;
   array: boolean;
   map?: string;
}

export interface EnumValueStatement extends DefinitionNode {
   type: "enum_value";
   name: string;
   value?: number;
}

export interface EnumStatement extends DefinitionNode {
   type: "enum";
   name: string;
   values: EnumValueStatement[];
}

export interface TypeStatement extends DefinitionNode {
   type: "type";
   name: string;
   fields: TypeFieldStatement[];
}

export interface IServiceFunctionInput {
   name: string;
   type: string;
   array: boolean;
}

export interface IServiceFunctionReturn {
   type: string;
   array: boolean;
}


export type Decorators = Map<string, string[][]>;

export interface ServiceFunctionStatement extends DefinitionNode {
   type: "service_function";
   inputs: IServiceFunctionInput[];
   name: string;
   return_type: IServiceFunctionReturn | undefined; // Makes it a notification
   decorators: Decorators;
}

export interface ServiceStatement extends DefinitionNode {
   type: "service";
   name: string;
   functions: ServiceFunctionStatement[];
}

export type RootStatementNode =
   | ImportStatement
   | TypeStatement
   | ServiceStatement
   | EnumStatement;
export type StatementNode =
   | RootStatementNode
   | TypeFieldStatement
   | ServiceFunctionStatement
   | EnumValueStatement;

export type Parsed = RootStatementNode[];

export class ParserError extends Error {
   token: Token;
   constructor(message: string, token: Token) {
      super(message);
      this.token = token;
   }
}

export default function parse(tokens: Token[], file: string): Parsed {
   const tokenIterator = tokens[Symbol.iterator]();
   let currentToken: Token = tokenIterator.next().value;
   let nextToken: Token = tokenIterator.next().value;

   const eatToken = (value?: string) => {
      if (value && value !== currentToken.value) {
         throw new ParserError(
            `Unexpected token value, expected '${value}', received '${currentToken.value}'`,
            currentToken
         );
      }
      let idx = currentToken.startIdx;
      currentToken = nextToken;
      nextToken = tokenIterator.next().value;
      return idx;
   };

   const eatText = (): [string, number] => {
      checkTypes("text");
      let val = currentToken.value;
      let idx = currentToken.startIdx;
      eatToken();
      return [val, idx];
   };
   const eatNumber = (): number => {
      checkTypes("number");
      let val = Number(currentToken.value);
      if (Number.isNaN(val)) {
         throw new ParserError(
            `Value cannot be parsed as number! ${currentToken.value}`,
            currentToken
         );
      }
      eatToken();
      return val;
   };

   

   const checkTypes = (...types: string[]) => {
      if (types.indexOf(currentToken.type) < 0) {
         throw new ParserError(
            `Unexpected token value, expected ${types.join(" | ")}, received '${
               currentToken.value
            }'`,
            currentToken
         );
      }
   };

   // const parseUnionField = (): UnionFieldStatement => {
   //    let idx = currentToken.startIdx;
   //    let name = currentToken.value;
   //    eatToken();
   //    eatToken(":");
   //    let [type] = eatText();
   //    eatToken("=");
   //    let label = eatNumber();
   //    eatToken(";");

   //    return {
   //       type: "union_field",
   //       name,
   //       label,
   //       fieldtype: type,
   //       location: { file, idx },
   //    };
   // };

   const parseTypeField = (): TypeFieldStatement => {
      const idx = currentToken.startIdx;
      let name = currentToken.value;
      eatToken();
      eatToken(":");

      let array = false;
      let type: string;
      let mapKey: string | undefined = undefined;

      if (currentToken.type === "curly_open") {
         eatToken("{");
         [mapKey] = eatText();
         eatToken(",");
         [type] = eatText();
         eatToken("}");
      } else {
         [type] = eatText();
         if (currentToken.type === "array") {
            array = true;
            eatToken("[]");
         }
      }

      eatToken(";");
      return {
         type: "type_field",
         name,
         fieldtype: type,
         array,
         map: mapKey,
         location: { file, idx },
      };
   };

   const parseTypeStatement = (): TypeStatement => {
      const idx = eatToken("type");
      let [name] = eatText();
      eatToken("{");
      let fields: TypeFieldStatement[] = [];
      while (currentToken.type === "text" || currentToken.type === "keyword") {
         //Keywords can also be field names
         fields.push(parseTypeField());
      }

      eatToken("}");

      return {
         type: "type",
         name,
         fields,
         location: { file, idx },
      };
   };

   // const parseUnionStatement = (): UnionStatement => {
   //    const idx = eatToken("union");
   //    let [name] = eatText();
   //    eatToken("{");
   //    let fields: UnionFieldStatement[] = [];
   //    while (currentToken.type === "text") {
   //       fields.push(parseUnionField());
   //    }

   //    eatToken("}");

   //    return {
   //       type: "union",
   //       name,
   //       fields,
   //       location: { file, idx },
   //    };
   // };

   const parseImportStatement = (): ImportStatement => {
      const idx = eatToken("import");
      checkTypes("text", "string");
      let path = currentToken.value;
      if (currentToken.type === "string") {
         path = path.substring(1, path.length - 1);
      }

      eatToken();
      eatToken(";");
      return {
         type: "import",
         path,
         location: { file, idx },
      };
   };

   const parseEnumValue = (): EnumValueStatement => {
      let [name, idx] = eatText();
      let value = undefined;
      if (currentToken.type === "equals") {
         eatToken("=");
         value = eatNumber();
      }
      return {
         type: "enum_value",
         name,
         value,
         location: { file, idx },
      };
   };

   const parseEnumStatement = (): EnumStatement => {
      let idx = eatToken("enum");
      let [name] = eatText();
      eatToken("{");
      let values: EnumValueStatement[] = [];
      let next = currentToken.type === "text";
      while (next) {
         values.push(parseEnumValue());
         if (currentToken.type === "comma") {
            eatToken(",");
            next = true;
         } else {
            next = false;
         }
      }
      eatToken("}");

      return {
         type: "enum",
         name: name,
         values: values,
         location: { file, idx },
      };
   };

   const parseFunctionDecorator = (decorators: Decorators = new Map) => {
      const idx = eatToken("@");
      const [decorator] = eatText();
      eatToken("(");
      let args: string[] = [];
      let first = true;
      while(currentToken.value !== ")") {
         if(first) {
            first= false;
         } else {
            eatToken(",");
         }
         checkTypes("string");
         // if(currentToken.type == "number") {
         //    let arg = Number(currentToken.value);
         //    if (Number.isNaN(arg)) {
         //       throw new ParserError(
         //          `Value cannot be parsed as number! ${currentToken.value}`,
         //          currentToken
         //       );
         //    }
         // }
         args.push(currentToken.value.slice(1, -1));
         eatToken();

      }
      eatToken(")");

      let dec = decorators.get(decorator) || [];
      dec.push(args);
      decorators.set(decorator, dec);

      return decorators;
   }

   const parseServiceFunction = (
      decorators: Decorators,
      notification?: boolean,
   ): ServiceFunctionStatement => {
      const [name, idx] = eatText();

      eatToken("(");

      let input_streaming: string | undefined = undefined;
      let inputs = [];

      if (currentToken.value !== ")") {
         while (true) {
            const [name] = eatText();
            eatToken(":");
            const [type] = eatText();
            let array = false;
            if (currentToken.type === "array") {
               array = true;
               eatToken("[]");
            }
            inputs.push({ name, type, array });
            if (currentToken.value !== ",") break;
            eatToken(",");
         }
      }

      eatToken(")");

      let return_type: IServiceFunctionReturn | undefined = undefined;
      if (!notification) {
         eatToken(":");

         let [type] = eatText();
         let array = false;
         if (currentToken.type === "array") {
            array = true;
            eatToken("[]");
         }
         return_type = {
            type,
            array
         }
      }

      eatToken(";");

      return {
         type: "service_function",
         name,
         location: {
            file,
            idx,
         },
         inputs,
         return_type,
         decorators
      };
   };

   const parseServiceStatement = (): ServiceStatement => {
      let idx = eatToken("service");
      let [name] = eatText();
      eatToken("{");
      let functions: ServiceFunctionStatement[] = [];

      while (currentToken.type !== "curly_close") {
         let decorators:Decorators = new Map;
         while(currentToken.type == "at") {
            parseFunctionDecorator(decorators);
         }


         let notification = false;
         if (currentToken.value == "notification") {
            eatText();
            notification = true;
         }
         functions.push(parseServiceFunction(decorators, notification));
      }
      eatToken("}");

      return {
         type: "service",
         name: name,
         functions,
         location: { file, idx },
      };
   };

   const parseStatement = () => {
      if (currentToken.type === "keyword") {
         switch (currentToken.value) {
            case "type":
               return parseTypeStatement();
            // case "union":
            //    return parseUnionStatement();
            case "import":
               return parseImportStatement();
            case "enum":
               return parseEnumStatement();
            case "service":
               return parseServiceStatement();
            default:
               throw new ParserError(
                  `Unknown keyword ${currentToken.value}`,
                  currentToken
               );
         }
      } else {
         throw new ParserError(
            `Invalid statement! ${currentToken.value}`,
            currentToken
         );
      }
   };

   const nodes: RootStatementNode[] = [];
   while (currentToken) {
      nodes.push(parseStatement());
   }

   return nodes;
}
