/* UtilLib - Copyright (c) 2013-2024 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.mapeadores.util.logicaloperation;

import java.util.ArrayList;
import java.util.List;
import net.mapeadores.util.exceptions.SwitchException;


/**
 *
 * @author Vincent Calame
 */
class InformatiqueParser {

    private final static short WAINTING_FOR_OPERANDE = 1;
    private final static short ON_SIMPLE_OPERANDE = 2;
    private final static short WAITING_FOR_OPERATOR = 3;
    private final String operationString;
    private final SubOperandBuilder rootBuilder;
    private int currentIndex;
    private int remainingLength;
    private short state;
    private boolean currentAcceptMode;
    private SubOperandBuilder currentSubOperandBuilder;


    InformatiqueParser(String operationString) {
        this.operationString = operationString;
        this.currentIndex = 0;
        this.remainingLength = operationString.length() - 1;
        this.state = WAINTING_FOR_OPERANDE;
        this.currentAcceptMode = true;
        rootBuilder = new SubOperandBuilder(null, true);
        currentSubOperandBuilder = rootBuilder;
    }

    Operand toOperand() {
        Operand result = rootBuilder.toOperand(false);
        if (result == null) {
            throw new RuntimeOperandException(LogicalOperationConstants.EMPTY_ERRORKEY, 0);
        }
        return result;
    }

    void parse() {
        while (remainingLength >= 0) {
            test();
        }
        while (!currentSubOperandBuilder.isRoot()) {
            flushCurrentSubOperand();
        }
    }

    private void test() {
        switch (state) {
            case WAINTING_FOR_OPERANDE:
                testWaitingForOperand();
                break;
            case ON_SIMPLE_OPERANDE:
                testOnSimpleOperande();
                break;
            case WAITING_FOR_OPERATOR:
                testWaitingForOperator();
                break;
            default:
                throw new SwitchException("Unknown state = " + state);
        }
    }

    private void testWaitingForOperand() {
        char carac = operationString.charAt(currentIndex);
        if (Character.isWhitespace(carac)) {
        } else if (carac == '(') {
            SubOperandBuilder newOperandBuilder = new SubOperandBuilder(currentSubOperandBuilder, currentAcceptMode);
            currentSubOperandBuilder.add(newOperandBuilder);
            currentSubOperandBuilder = newOperandBuilder;
            changeState(WAINTING_FOR_OPERANDE);
        } else if (carac == ')') {
            flushCurrentSubOperand();
            changeState(WAITING_FOR_OPERATOR);
        } else if (carac == '!') {
            currentAcceptMode = !currentAcceptMode;
        } else if ((carac == '&') || (carac == '|')) {
            if ((remainingLength == 0) || (operationString.charAt(currentIndex + 1) != carac)) {
                throw new RuntimeOperandException(LogicalOperationConstants.MISSING_OPERATORSECONDARYCHAR_ERRORKEY, currentIndex);
            } else {
                throw new RuntimeOperandException(LogicalOperationConstants.UNEXPECTEDOPERATOR_ERRORKEY, currentIndex);
            }
        } else {
            currentSubOperandBuilder.newSimpleOperand(carac, currentAcceptMode);
            changeState(ON_SIMPLE_OPERANDE);
        }
        next();
    }

    private void testOnSimpleOperande() {
        char carac = operationString.charAt(currentIndex);
        if ((carac == '&') || (carac == '|')) {
            if ((remainingLength == 0) || (operationString.charAt(currentIndex + 1) != carac)) {
                throw new RuntimeOperandException(LogicalOperationConstants.MISSING_OPERATORSECONDARYCHAR_ERRORKEY, currentIndex);
            } else {
                boolean test = currentSubOperandBuilder.checkOperator(toOperator(carac));
                if (!test) {
                    throw new RuntimeOperandException(LogicalOperationConstants.DIFFERENTOPERATOR_ERRORKEY, currentIndex);
                }
                changeState(WAINTING_FOR_OPERANDE);
                next();
            }
        } else if (carac == ')') {
            flushCurrentSubOperand();
            changeState(WAITING_FOR_OPERATOR);
        } else if (carac == '(') {
            throw new RuntimeOperandException(LogicalOperationConstants.OPENINGPARENTHESIS_ERRORKEY, currentIndex);
        } else if ((carac == '\\') && (remainingLength > 0)) {
            char nextChar = operationString.charAt(currentIndex + 1);
            switch (nextChar) {
                case ')':
                case '(':
                case '\\':
                case '&':
                case '|':
                    currentSubOperandBuilder.appendToSimpleOperand(nextChar);
                    next();
                    break;
                default:
                    currentSubOperandBuilder.appendToSimpleOperand(carac);
            }
        } else {
            currentSubOperandBuilder.appendToSimpleOperand(carac);
        }
        next();
    }

    private void testWaitingForOperator() {
        char carac = operationString.charAt(currentIndex);
        if (Character.isWhitespace(carac)) {
        } else if ((carac == '&') || (carac == '|')) {
            if ((remainingLength == 0) || (operationString.charAt(currentIndex + 1) != carac)) {
                throw new RuntimeOperandException(LogicalOperationConstants.MISSING_OPERATORSECONDARYCHAR_ERRORKEY, currentIndex);
            } else {
                boolean test = currentSubOperandBuilder.checkOperator(toOperator(carac));
                if (!test) {
                    throw new RuntimeOperandException(LogicalOperationConstants.DIFFERENTOPERATOR_ERRORKEY, currentIndex);
                }
                changeState(WAINTING_FOR_OPERANDE);
                next();
            }
        } else if (carac == ')') {
            flushCurrentSubOperand();
        } else {
            throw new RuntimeOperandException(LogicalOperationConstants.MISSINGOPERATOR_ERRORKEY, currentIndex);
        }
        next();
    }

    private void flushCurrentSubOperand() {
        if (currentSubOperandBuilder.isRoot()) {
            throw new RuntimeOperandException(LogicalOperationConstants.TOOMANYCLOSINGPARENTHESIS_ERRORKEY, currentIndex);
        }
        currentSubOperandBuilder = currentSubOperandBuilder.getParentBuilder();
    }

    private void changeState(short newState) {
        state = newState;
        currentAcceptMode = true;
    }

    private void next() {
        currentIndex++;
        remainingLength--;
    }

    private static short toOperator(char carac) {
        switch (carac) {
            case '&':
                return LogicalOperationConstants.INTERSECTION_OPERATOR;
            case '|':
                return LogicalOperationConstants.UNION_OPERATOR;
            default:
                throw new SwitchException("Unknown operator character = " + carac);
        }
    }


    private static abstract class OperandBuilder {

        protected OperandBuilder() {
        }

        protected abstract Operand toOperand(boolean inverse);

    }


    private static class SimpleOperandBuilder extends OperandBuilder {

        private boolean acceptMode;
        private final StringBuilder buf = new StringBuilder();
        private boolean withWhiteSpace;
        private boolean maybeScope = true;
        private int lastColonIndex = -1;

        private SimpleOperandBuilder(boolean acceptMode) {
            this.acceptMode = acceptMode;
        }

        private void append(char c) {
            if (Character.isWhitespace(c)) {
                if (buf.length() > 0) {
                    maybeScope = false;
                    withWhiteSpace = true;
                }
            } else {
                if (withWhiteSpace) {
                    buf.append(' ');
                    withWhiteSpace = false;
                }
                buf.append(c);
                if (maybeScope) {
                    if (c == ':') {
                        int colonIndex = buf.length() - 1;
                        if (colonIndex == 0) {
                            maybeScope = false;
                        } else {
                            lastColonIndex = colonIndex;
                        }
                    } else if (!testScopeChar(c)) {
                        maybeScope = false;
                    }
                }
            }
        }

        @Override
        protected Operand toOperand(boolean inverse) {
            if (inverse) {
                acceptMode = !acceptMode;
            }
            if (buf.length() == 0) {
                return null;
            }
            String operandString = buf.toString();
            String scope, body;
            if (lastColonIndex > 0) {
                scope = operandString.substring(0, lastColonIndex);
                body = operandString.substring(lastColonIndex + 1).trim();
            } else {
                scope = "";
                body = operandString;
            }
            return LogicalOperationUtils.toSimpleOperand(acceptMode, operandString, scope, body);
        }

    }


    private static class SubOperandBuilder extends OperandBuilder {

        private final boolean acceptMode;
        private short operator = 0;
        private final SubOperandBuilder parentBuilder;
        private SimpleOperandBuilder currentSimpleOperandBuilder;
        private final List<OperandBuilder> builderList = new ArrayList<OperandBuilder>();

        private SubOperandBuilder(SubOperandBuilder parentBuilder, boolean acceptMode) {
            this.acceptMode = acceptMode;
            this.parentBuilder = parentBuilder;
        }

        private boolean checkOperator(short newOperator) {
            if (this.operator == 0) {
                this.operator = newOperator;
                return true;
            }
            return (this.operator == newOperator);
        }

        private void add(SubOperandBuilder subOperandBuilder) {
            builderList.add(subOperandBuilder);
        }

        private SubOperandBuilder getParentBuilder() {
            return parentBuilder;
        }

        private void newSimpleOperand(char carac, boolean acceptMode) {
            SimpleOperandBuilder simpleOperandBuilder = new SimpleOperandBuilder(acceptMode);
            simpleOperandBuilder.append(carac);
            currentSimpleOperandBuilder = simpleOperandBuilder;
            builderList.add(simpleOperandBuilder);
        }

        private void appendToSimpleOperand(char carac) {
            currentSimpleOperandBuilder.append(carac);
        }

        @Override
        protected Operand toOperand(boolean inverse) {
            if (!acceptMode) {
                inverse = !inverse;
            }
            int size = builderList.size();
            if (size == 0) {
                return null;
            }
            if (size == 1) {
                return builderList.get(0).toOperand(inverse);
            }
            if (operator == 0) {
                operator = LogicalOperationConstants.UNION_OPERATOR;
            }
            if (inverse) {
                operator = LogicalOperationUtils.inverseOperator(operator);
            }
            List<Operand> resultList = new ArrayList<Operand>();
            for (int i = 0; i < size; i++) {
                Operand operand = builderList.get(i).toOperand(inverse);
                if (operand != null) {
                    if (operand instanceof SubOperand) {
                        SubOperand subOperand = (SubOperand) operand;
                        if (subOperand.getOperator() == operator) {
                            int childLength = subOperand.size();
                            for (int j = 0; j < childLength; j++) {
                                resultList.add(subOperand.get(j));
                            }
                        } else {
                            resultList.add(operand);
                        }
                    } else {
                        resultList.add(operand);
                    }
                }
            }
            int size2 = resultList.size();
            if (size2 == 0) {
                return null;
            }
            if (size2 == 1) {
                return resultList.get(1);
            }
            return LogicalOperationUtils.toSubOperand(resultList.toArray(new Operand[size2]), operator);
        }

        private boolean isRoot() {
            return (parentBuilder == null);
        }

    }

    private static boolean testScopeChar(char carac) {
        if (carac == '_') {
            return true;
        }
        if ((carac >= 'a') && (carac <= 'z')) {
            return true;
        }
        if ((carac >= 'A') && (carac <= 'Z')) {
            return true;
        }
        if ((carac >= '0') && (carac <= '9')) {
            return true;
        }
        switch (carac) {
            case '_':
            case '-':
            case '.':
            case ',':
            case '!':
                return true;
            default:
                return false;
        }
    }

}
