/* FichothequeLib_Tools - Copyright (c) 2014-2022 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.fichotheque.tools.parsers;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import net.fichotheque.corpus.fiche.AttConsumer;
import net.fichotheque.corpus.fiche.Atts;


/**
 *
 * @author Vincent Calame
 */
public class AttsParser {

    private final static short WAITING_FOR_NAME = 1;
    private final static short ON_NAME = 2;
    private final static short WAITING_FOR_VALUE = 3;
    private final static short ON_VALUE = 4;
    private final static short ON_QUOTE = 5;
    private final String source;
    private final int startIndex;
    private final List<String> nameList = new ArrayList<String>();
    private final List<String> valueList = new ArrayList<String>();
    private final char[] nameBuffer;
    private final char[] valueBuffer;
    private short state = WAITING_FOR_NAME;
    private int namePointer = 0;
    private int valuePointer = 0;
    private boolean withDataPrefix = false;
    private String currentName;
    private boolean escaped;
    private int closeIndex;
    private int currentIndex;
    private int automaticCount = 0;

    public AttsParser(String source, int startIndex) {
        this.source = source;
        this.startIndex = startIndex;
        nameBuffer = new char[source.length() - startIndex];
        valueBuffer = new char[source.length() - startIndex];
    }

    public void addAtt(String name, String value) {
        nameList.add(name);
        valueList.add(value);
        automaticCount++;
    }

    public void run() throws ParseException {
        int length = source.length();
        boolean continueParsing = true;
        for (int i = startIndex; i < length; i++) {
            currentIndex = i;
            char carac = source.charAt(i);
            continueParsing = checkChar(carac);
            if (!continueParsing) {
                closeIndex = i;
                break;
            }
        }
        if (continueParsing) {
            throw new ParseException("Missing closing )", currentIndex);
        }
    }

    public int getCloseIndex() {
        return closeIndex;
    }

    public Atts getAtts() {
        boolean onlyAutomatic = false;
        int size = nameList.size();
        if ((size > 0) && (size == automaticCount)) {
            onlyAutomatic = true;
        }
        return new InternalAtts(nameList, valueList, onlyAutomatic);
    }

    private boolean checkChar(char carac) throws ParseException {
        boolean continueParsing = true;
        switch (state) {
            case WAITING_FOR_NAME:
                continueParsing = checkWaitingForName(carac);
                break;
            case ON_NAME:
                continueParsing = checkOnName(carac);
                break;
            case WAITING_FOR_VALUE:
                continueParsing = checkWaitingForValue(carac);
                break;
            case ON_VALUE:
                continueParsing = checkOnValue(carac);
                break;
            case ON_QUOTE:
                continueParsing = checkOnQuote(carac);
                break;
        }
        return continueParsing;
    }

    private boolean checkWaitingForName(char carac) throws ParseException {
        switch (carac) {
            case ')':
                return false;
            case ' ':
                return true;
        }
        if (isValideFisrtNameChar(carac)) {
            if (carac == '-') {
                withDataPrefix = true;
            } else {
                withDataPrefix = false;
            }
            bufName(carac);
            state = ON_NAME;
            return true;
        } else {
            throw new ParseException("Invalid name first character: " + carac, currentIndex);
        }
    }

    private boolean checkOnName(char carac) throws ParseException {
        if (carac == '=') {
            initName();
            state = WAITING_FOR_VALUE;
            return true;
        } else if (isValidNameChar(carac)) {
            bufName(carac);
            return true;
        } else {
            throw new ParseException("Invalid name character: " + carac, currentIndex);
        }
    }

    private boolean checkWaitingForValue(char carac) throws ParseException {
        switch (carac) {
            case ')':
                addAtt();
                return false;
            case '"':
                state = ON_QUOTE;
                return true;
            case ' ':
                addAtt();
                state = WAITING_FOR_NAME;
                return true;
            default:
                bufValue(carac);
                state = ON_VALUE;
                return true;
        }
    }

    private boolean checkOnValue(char carac) throws ParseException {
        switch (carac) {
            case ')':
                addAtt();
                return false;
            case '"':
                throw new ParseException("\" character inside value", currentIndex);
            case ' ':
                addAtt();
                state = WAITING_FOR_NAME;
                return true;
            default:
                bufValue(carac);
                return true;
        }
    }

    private boolean checkOnQuote(char carac) {
        switch (carac) {
            case '"':
                if (escaped) {
                    escaped = false;
                    bufValue('"');
                } else {
                    addAtt();
                    state = WAITING_FOR_NAME;
                }
                return true;
            case '\\':
                if (escaped) {
                    escaped = false;
                    bufValue('\\');
                } else {
                    escaped = true;
                }
                return true;
            default:
                if (escaped) {
                    escaped = false;
                    bufValue('\\');
                }
                bufValue(carac);
                return true;
        }
    }

    private void bufName(char carac) {
        nameBuffer[namePointer] = carac;
        namePointer++;
    }

    private void bufValue(char carac) {
        valueBuffer[valuePointer] = carac;
        valuePointer++;
    }

    private void initName() {
        currentName = new String(nameBuffer, 0, namePointer);
        if (withDataPrefix) {
            currentName = "data" + currentName;
        }
        namePointer = 0;
        withDataPrefix = false;
    }

    private void addAtt() {
        String attValue = new String(valueBuffer, 0, valuePointer);
        valuePointer = 0;
        switch (currentName) {
            case "c":
                nameList.add("class");
                valueList.add(attValue);
                break;
            case "cm":
            case "fbe":
                addClassAtt(attValue, currentName);
                break;
            default:
                nameList.add(currentName);
                valueList.add(attValue);
        }
    }

    private void addClassAtt(String attValue, String prefix) {
        if (!attValue.isEmpty()) {
            nameList.add("class");
            valueList.add(prefix + "-" + attValue);
        }

    }

    public static boolean isValideFisrtNameChar(char carac) {
        if ((carac >= 'a') && (carac <= 'z')) {
            return true;
        }
        if ((carac >= 'A') && (carac <= 'Z')) {
            return true;
        }
        switch (carac) {
            case '_':
            case '-':
                return true;
            default:
                return false;
        }
    }

    public static boolean isValidNameChar(char carac) {
        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 '.':
                return true;
            default:
                return false;
        }
    }

    public static void populate(AttConsumer attConsumer, List<Atts> attsList) {
        if (attsList != null) {
            Atts atts = attsList.get(0);
            int attLength = atts.size();
            for (int i = 0; i < attLength; i++) {
                attConsumer.putAtt(atts.getName(i), atts.getValue(i));
            }
        }
    }

    public static void populate(AttConsumer attConsumer, List<Atts> attsList, int index) {
        if (attsList != null) {
            if (index < attsList.size()) {
                Atts atts = attsList.get(index);
                int attLength = atts.size();
                for (int i = 0; i < attLength; i++) {
                    attConsumer.putAtt(atts.getName(i), atts.getValue(i));
                }
            }
        }
    }


    private static class InternalAtts implements Atts {

        private final List<String> nameList;
        private final List<String> valueList;
        private final boolean onlyAutomatic;

        private InternalAtts(List<String> nameList, List<String> valueList, boolean onlyAutomatic) {
            this.nameList = nameList;
            this.valueList = valueList;
            this.onlyAutomatic = onlyAutomatic;
        }

        @Override
        public int size() {
            return nameList.size();
        }

        @Override
        public String getName(int index) {
            return nameList.get(index);
        }

        @Override
        public String getValue(int index) {
            return valueList.get(index);
        }

        @Override
        public boolean hasOnlyAutomaticAtts() {
            return onlyAutomatic;
        }

    }

}
