package org.kravemir.lightvalue;

import org.kravemir.lightvalue.model.ClassDef;
import org.kravemir.lightvalue.model.ModelFile;
import org.kravemir.lightvalue.model.PropertyDef;

import java.util.*;

public class ModelParser {

    private final Queue<Token> tokens;

    public ModelParser(Collection<Token> tokens) {
        this.tokens = new ArrayDeque<>(tokens);
    }

    public ModelFile parseFile() {
        ModelFile file = new ModelFile();

        consumeToken(Token.Type.PACKAGE);
        file.setFilePackage(parseIdentifierPath());
        consumeToken(Token.Type.SEMICOLON);

        parseImports(file);

        parseFileStatements(file);

        return file;
    }

    private void parseImports(ModelFile file) {
        while(isNext(Token.Type.IMPORT)) {
            consumeToken();
            file.addImport(parseIdentifierPath());
            consumeToken(Token.Type.SEMICOLON);
        }
    }

    private void parseFileStatements(ModelFile file) {
        while(!tokens.isEmpty()) {
            parseFileStatement(file);
        }
    }

    private void parseFileStatement(ModelFile file) {
        file.addClass(parseClassDefinition());
    }

    private ClassDef parseClassDefinition() {
        ClassDef classDef = new ClassDef();

        consumeToken(Token.Type.CLASS);
        classDef.setName(consumeToken(Token.Type.IDENTIFIER).getValue());
        parseClassStatements(classDef);

        return classDef;
    }

    private void parseClassStatements(ClassDef classDef) {
        consumeToken(Token.Type.LCURLY);
        while(!tokens.isEmpty()) {
            switch (nextType()) {
                case CLASS:
                    ClassDef subClassDef = parseClassDefinition();
                    classDef.addSubClass(subClassDef);
                    break;
                case IDENTIFIER:
                case REPEATED:
                case MAP:
                    parseClassProperty(classDef);
                    break;
                case RCURLY:
                    consumeToken();
                    return;
                default:
                    throw new RuntimeException(String.format("Unexpected token %s, expected class statement", nextType()));
            }
        }
    }

    private void parseClassProperty(ClassDef classDef) {
        String name;
        String type1 = null, type2 = null;
        boolean repeated = false;
        boolean map = false;

        switch (nextType()) {
            case REPEATED:
                repeated = true;
                consumeToken();
                type1 = consumeToken(Token.Type.IDENTIFIER).getValue();
                break;
            case MAP:
                map = true;
                consumeToken();
                consumeToken(Token.Type.LESS);
                type1 = consumeToken(Token.Type.IDENTIFIER).getValue();
                consumeToken(Token.Type.COMMA);
                type2 = consumeToken(Token.Type.IDENTIFIER).getValue();
                consumeToken(Token.Type.MORE);
                break;
            case IDENTIFIER:
                type1 = consumeToken().getValue();
                break;
        }

        name = consumeToken(Token.Type.IDENTIFIER).getValue();
        consumeToken(Token.Type.SEMICOLON);

        classDef.addProperty(new PropertyDef(
                name, type1, type2, repeated, map
        ));
    }

    private String parseIdentifierPath() {
        List<String> identifiers = new ArrayList<>();

        identifiers.add(consumeToken(Token.Type.IDENTIFIER).getValue());
        while(isNext(Token.Type.DOT)) {
            consumeToken();
            identifiers.add(consumeToken(Token.Type.IDENTIFIER).getValue());
        }

        return String.join(".", identifiers);
    }

    private Token.Type nextType() {
        return tokens.peek().getType();
    }

    private boolean isNext(Token.Type tokenType) {
        return Objects.equals(nextType(), tokenType);
    }

    private Token consumeToken() {
        return tokens.poll();
    }

    private Token consumeToken(Token.Type expectedTokenType) {
        if(isNext(expectedTokenType)) {
            return consumeToken();
        } else {
            throw new RuntimeException(String.format(
                    "Unexpected token %s at %d, expected %s",
                    tokens.peek().getType(), tokens.peek().getLine(), expectedTokenType
            ));
        }
    }
}
