/* UtilLib - Copyright (c) 2007-2015 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.instruction;

import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import net.mapeadores.util.text.StringUtils;


/**
 *
 * @author Vincent Calame
 */
public final class InstructionParser {

    private final static int WAITING_FROM_ARGUMENT_END_STEP = 3;
    private final static int WAITING_FROM_KEY_STEP = 4;
    private final static int WAITING_FROM_VALUE_STEP = 5;
    private final static int WAITING_FROM_EQUAL_STEP = 6;
    private final static int KEY_STEP = 7;
    private final static int QUOTED_KEY_STEP = 8;
    private final static int VALUE_STEP = 9;
    private final static int QUOTED_VALUE_STEP = 10;
    private final List<Argument> argumentList = new ArrayList<Argument>();
    private final InstructionErrorHandler errorHandler;
    private ArgumentParser currentArgumentParser;
    private int currentLineNumber = 0;


    private InstructionParser(InstructionErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    public static Instruction parse(String s, InstructionErrorHandler errorHandler) {
        if (errorHandler == null) {
            errorHandler = InstructionUtils.DEFAULT_ERROR_HANDLER;
        }
        InstructionParser parser = new InstructionParser(errorHandler);
        return parser.parseInstruction(s);
    }

    private Instruction parseInstruction(String s) {
        currentArgumentParser = new ArgumentParser();
        try {
            StringReader stringReader = new StringReader(s);
            LineNumberReader bufreader = new LineNumberReader(stringReader);
            String line;
            while ((line = bufreader.readLine()) != null) {
                try {
                    parseLine(line, bufreader.getLineNumber());
                } catch (IllegalArgumentException ise) {
                    break;
                }
            }
        } catch (IOException ioe) {
        }
        Argument arg = currentArgumentParser.toArgument();
        if (arg != null) {
            argumentList.add(arg);
        }
        currentArgumentParser = null;
        if (argumentList.isEmpty()) {
            return null;
        } else {
            return InstructionUtils.toInstruction(argumentList);
        }
    }

    private void parseLine(String line, int lineNumber) {
        currentLineNumber = lineNumber;
        int length = line.length();
        for (int i = 0; i < length; i++) {
            i = currentArgumentParser.parseCharAt(line, i);
        }
    }

    private int getCurrentLineNumber() {
        return currentLineNumber;
    }

    private void endArgument() {
        Argument arg = currentArgumentParser.toArgument();
        if (arg != null) {
            argumentList.add(arg);
        }
        currentArgumentParser = new ArgumentParser();
    }


    private class ArgumentParser {

        private final StringBuilder keyBuffer = new StringBuilder();
        private final StringBuilder valueBuffer = new StringBuilder();
        private boolean previousWhite = false;
        private int step = WAITING_FROM_KEY_STEP;

        private ArgumentParser() {
        }

        private Argument toArgument() {
            String key = keyBuffer.toString();
            if (key.length() == 0) {
                return null;
            }
            return InstructionUtils.toArgument(key, valueBuffer.toString());
        }

        private void testWhiteChar(char carac) {
            switch (step) {
                case WAITING_FROM_ARGUMENT_END_STEP:
                case WAITING_FROM_KEY_STEP:
                case WAITING_FROM_EQUAL_STEP:
                    break;
                case KEY_STEP:
                    step = WAITING_FROM_EQUAL_STEP;
                    break;
                case QUOTED_KEY_STEP:
                    keyBuffer.append(carac);
                    break;
                case VALUE_STEP:
                    previousWhite = true;
                    break;
                case QUOTED_VALUE_STEP:
                    valueBuffer.append(carac);
                    break;
            }

        }

        private boolean testEnd(char carac) {
            if ((carac == ',') || (carac == ';')) {
                endArgument();
                return true;
            }
            return false;
        }

        private int parseCharAt(String line, int index) {
            char carac = line.charAt(index);
            if (Character.isWhitespace(carac)) {
                testWhiteChar(carac);
                return index;
            }
            if ((step != QUOTED_KEY_STEP) && (step != QUOTED_VALUE_STEP)) {
                if (testEnd(carac)) {
                    return index;
                }
            }
            switch (step) {
                case WAITING_FROM_ARGUMENT_END_STEP:
                    valueBuffer.append(carac);
                    errorHandler.invalidEndCharacterError(valueBuffer.toString(), getCurrentLineNumber(), index);
                    throw new IllegalArgumentException();
                case WAITING_FROM_KEY_STEP:
                    if (carac == '\"') {
                        step = QUOTED_KEY_STEP;
                    } else if (testAsciiChar(carac, index, keyBuffer)) {
                        step = KEY_STEP;
                    }
                    break;
                case WAITING_FROM_EQUAL_STEP:
                    if (carac == '=') {
                        step = WAITING_FROM_VALUE_STEP;
                    } else {
                        keyBuffer.append(carac);
                        errorHandler.invalidSeparatorCharacterError(keyBuffer.toString(), getCurrentLineNumber(), index);
                        throw new IllegalArgumentException();
                    }
                    break;
                case WAITING_FROM_VALUE_STEP:
                    if (carac == '\"') {
                        step = QUOTED_VALUE_STEP;
                    } else if (testAsciiChar(carac, index, valueBuffer)) {
                        step = VALUE_STEP;
                    }
                    break;
                case KEY_STEP:
                    if (carac == '=') {
                        step = WAITING_FROM_VALUE_STEP;
                    } else {
                        testAsciiChar(carac, index, keyBuffer);
                    }
                    break;
                case QUOTED_KEY_STEP:
                    if (carac == '\"') {
                        step = WAITING_FROM_EQUAL_STEP;
                    } else {
                        index = parseQuoteCharAt(carac, line, index, keyBuffer);
                    }
                    break;
                case VALUE_STEP:
                    if (previousWhite) {
                        valueBuffer.append(' ');
                    }
                    testAsciiChar(carac, index, valueBuffer);
                    break;
                case QUOTED_VALUE_STEP:
                    if (carac == '\"') {
                        step = WAITING_FROM_ARGUMENT_END_STEP;
                    } else {
                        index = parseQuoteCharAt(carac, line, index, valueBuffer);
                    }
                    break;
            }
            previousWhite = false;
            return index;
        }

        private boolean testAsciiChar(char carac, int index, StringBuilder buf) {
            if (testEnd(carac)) {
                return false;
            } else {
                if (StringUtils.isValidAsciiChar(carac)) {
                    buf.append(carac);
                    return true;
                } else {
                    buf.append(carac);
                    errorHandler.invalidAsciiCharacterError(buf.toString(), getCurrentLineNumber(), index);
                    throw new IllegalArgumentException();
                }
            }
        }

        private int parseQuoteCharAt(char carac, String line, int index, StringBuilder buf) {
            if (carac == '\\') {
                if (index < (line.length() - 1)) {
                    char next = line.charAt(index + 1);
                    switch (next) {
                        case 'n':
                            buf.append("\n");
                            break;
                        case 't':
                            buf.append("\t");
                            break;
                        case 'r':
                            buf.append("\r");
                            break;
                        default:
                            buf.append(next);
                    }
                    index = index + 1;
                }
            } else {
                buf.append(carac);
            }
            return index;
        }

    }

}
