/*******************************************************************************
 * Copyright (c) 2013, 2018 Red Hat, Inc.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Neil Guzman - ruby implementation (B#350066)
 *******************************************************************************/
package org.eclipse.linuxtools.internal.rpmstubby.parser;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.linuxtools.internal.rpmstubby.StubbyLog;

/**
 * Class to parse a Ruby gemspec to grab specfile properties.
 */
public class RubyGemParser {

    private IFile file;
    private String gemVariable;
    private Map<String, List<String>> mSetupDefinitions;
    private Map<String, ArrayList<String>> mSetupDependencies;

    private static final String SETUP_START = "^gem[:\\.]{1,2}specification[:\\.]{1,2}new(\\s+)?do(\\s+)?\\|(\\s+)?(\\w+)(\\s+)?\\|";
    private static final String STRING = "(?:\\\"([^\\\"]+)\\\"|'([^']+)'|(?:%q|%Q)(?:([^\\w])([^/3].+)[^\\w]))"; // (%q|%Q) "value" | 'value' | {value}
    private static final String LIST = "(?!%q|%Q)(?:%w|%W)?(?:([\\W])(.+)[\\W])";                                 // (%w|%W) [value, value2] | {"value", "value2"}
    private static final String STRING_LIST = "(?:\\\"([^\\\"]+)\\\"|'([^']+)')(?:[, ])??";                       // "test", "test2" | 'test' 'test2'
    private static final String GENERIC_LIST = "(?:\\S+)(?:\\s+)??";                                              // test, test2 | test test2

    private static final String REPLACE_ME = "(%REPLACE_ME)";
    private String simpleDefineRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?=(?!=)(\\s+)?(.*)";         // gem.variable = ...
    private String genericDefineRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?(.*)";                     // gem.variable...
    private String simpleFunctionRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?(?:\\((.*)\\))(.*)?";     // gem.variable(...)

    /**
     * Initialize
     *
     * @param file
     *            The gemspec file
     * @throws IOException Could not read from file
     * @throws CoreException File is not valid
     */
    public RubyGemParser(IFile file) throws IOException, CoreException {
        mSetupDefinitions = new HashMap<>();
        mSetupDependencies = new HashMap<>();
        if (file.getContents().available() <= 0) {
            return;
        }
        this.file = file;
        gemVariable = "";
        parse();
    }

    /**
     * Parse the Ruby gemspec file
     */
    public void parse() {
        List<String> rawSetupDefinitions = new ArrayList<>();
        String line = "";
        long startPos;
        long endPos;
        try (RandomAccessFile raf = new RandomAccessFile(file.getRawLocation()
                .makeAbsolute().toFile(), "r")) {
            startPos = findStart(raf);
            endPos = findEnd(raf, startPos);

            raf.seek(startPos);
            while ((line = raf.readLine()) != null
                    && raf.getFilePointer() < endPos) {
                rawSetupDefinitions.add(line);
            }

            List<String> lSetupDefinitions = prepareOptions(rawSetupDefinitions);

            for (String str : lSetupDefinitions) {
                parseLine(str);
            }
        } catch (IOException e) {
            StubbyLog.logError(e);
        }
    }

    /**
     * Get the values taken from parsing the file as a list object
     *
     * @param key
     *            The gemspec option to get the values for
     * @return The values of the gemspec option
     */
    public List<String> getValueList(String key) {
        List<String> rc = new ArrayList<>();

        if (mSetupDependencies.containsKey(key)) {
            if (!mSetupDependencies.get(key).isEmpty()) {
                for (String element : mSetupDependencies.get(key)) {
                    rc.add(element);
                }
            }
        } else if (mSetupDefinitions.containsKey(key)) {
            if (!mSetupDefinitions.get(key).isEmpty()) {
                for (String element : mSetupDefinitions.get(key)) {
                    rc.add(element);
                }
            }
        }

        return rc;
    }

    /**
     * Parses a line to figure out what type of line it is
     *
     * @param str
     *            The line to parse
     */
    private void parseLine(String str) {
        if (str.matches(simpleDefineRx)) {
            parseSimpleDefine(str);
        } else if (str.matches(simpleFunctionRx)) {
            parseSimpleFunction(str);
        } else if (str.matches(genericDefineRx)) {
            parseGenericOption(str);
        }
    }

    /**
     * Parses a string to figure its value
     *
     * @param str
     *            The string parse
     * @return A list of objects that was found
     */
    private static List<String> parseValue(String str) {
        List<String> rc = new ArrayList<>();
        String temp = str.trim();
        Pattern pattern = null;
        Matcher variableMatcher = null;

        if (temp.matches(STRING)) {
            pattern = Pattern.compile(STRING,
                    Pattern.CASE_INSENSITIVE);
            variableMatcher = pattern.matcher(temp);
            // "" matches group 1
            if (temp.startsWith("\"") && variableMatcher.matches()) {
                rc.add(variableMatcher.group(1));
            } // '' matches group 2
            else if (temp.startsWith("'") && variableMatcher.matches()) {
                rc.add(variableMatcher.group(2));
            } // %q|%Q match
            else if ((temp.startsWith("%q") || temp.startsWith("%Q"))
                    && variableMatcher.matches()) {
                rc.add(variableMatcher.group(4));
            }
        } else if (temp.matches(LIST)) {
            pattern = Pattern.compile(LIST,
                    Pattern.CASE_INSENSITIVE);
            variableMatcher = pattern.matcher(temp);
            if (variableMatcher.matches()) {
                rc.addAll(parseList(variableMatcher.group(2)));
            }
        }

        return rc;
    }

    /**
     * Parse the long string into a list
     *
     * @param str
     *            The string to parse into a list
     * @return A list containing the found values
     */
    private static List<String> parseList(String str) {
        List<String> rc = new ArrayList<>();
        String temp = str.trim();
        Pattern pattern = isPatternFoundList(str);

        if (pattern == null) {
            return rc;
        }

        Matcher variableMatcher = pattern.matcher(temp);

        if (variableMatcher != null) {
            while (variableMatcher.find()) {
                rc.add(variableMatcher.group());
            }
        }
        return rc;
    }

    /**
     * Check if the string is a list of string values e.g. ["test", "test2"] OR
     * ['test' 'test2']
     *
     * @param str
     *            The string containing the list
     * @return The pattern of the string
     */
    private static Pattern isPatternFoundList(String str) {
        Pattern rc = Pattern.compile("");
        Pattern pattern = Pattern.compile(STRING_LIST,
                Pattern.CASE_INSENSITIVE);
        Matcher variableMatcher = pattern.matcher(str);

        if (variableMatcher.find()) {
            rc = pattern;
        } else {
            rc = isGenericFoundList(str);
        }

        return rc;
    }

    /**
     * Check if the string is a list of generic values (non-strings) e.g. [test,
     * test2] OR [test test2]
     *
     * @param str
     *            The string containing the list
     * @return The pattern of the string
     */
    private static Pattern isGenericFoundList(String str) {
        Pattern rc = Pattern.compile("");
        Pattern pattern = Pattern.compile(GENERIC_LIST,
                Pattern.CASE_INSENSITIVE);
        Matcher variableMatcher = pattern.matcher(str);

        if (variableMatcher.find()) {
            rc = pattern;
        }

        return rc;
    }

    /**
     * Parse and grab the value of a simple define. The value taken is anything
     * after "gem.variable = ..."
     *
     * @param str
     *            The simple define to parse
     */
    private void parseSimpleDefine(String str) {
        String temp = str.trim();
        Pattern pattern = Pattern.compile(simpleDefineRx,
                Pattern.CASE_INSENSITIVE);
        Matcher variableMatcher = pattern.matcher(temp);

        if (variableMatcher.find()) {
            String optionName = variableMatcher.group(2);
            List<String> value = parseValue(variableMatcher
                    .group(5));
            if (!value.isEmpty()) {
                mSetupDefinitions.put(optionName, value);
            }
        }
    }

    /**
     * Parse and grab the value of a simple function. The value taken is
     * anything in "gem.variable(...)"
     *
     * @param str
     *            The function to parse
     */
    private void parseSimpleFunction(String str) {
        String temp = str.trim();
        Pattern pattern = Pattern.compile(simpleFunctionRx,
                Pattern.CASE_INSENSITIVE);
        Matcher variableMatcher = pattern.matcher(temp);

        if (variableMatcher.find()) {
            String functionName = variableMatcher.group(2);
            ArrayList<String> dependencies = new ArrayList<>();
            dependencies.add(variableMatcher.group(4));
            if (!mSetupDependencies.containsKey(functionName)) {
                mSetupDependencies.put(functionName, dependencies);
            } else {
                if (!mSetupDependencies.get(functionName).containsAll(
                        dependencies)) {
                    mSetupDependencies.get(functionName).addAll(dependencies);
                }
            }
        }
    }

    /**
     * Parse and grab the value of a generic option. The value taken is anything
     * after "gem.variable..."
     *
     * @param str
     *            The option to parse
     */
    private void parseGenericOption(String str) {
        String temp = str.trim();
        Pattern pattern = Pattern.compile(genericDefineRx,
                Pattern.CASE_INSENSITIVE);
        Matcher variableMatcher = pattern.matcher(temp);

        if (variableMatcher.find()) {
            String functionName = variableMatcher.group(2);
            ArrayList<String> dependencies = new ArrayList<>();
            dependencies.add(variableMatcher.group(4));
            if (!mSetupDependencies.containsKey(functionName)) {
                mSetupDependencies.put(functionName, dependencies);
            } else {
                if (!mSetupDependencies.get(functionName).containsAll(
                        dependencies)) {
                    mSetupDependencies.get(functionName).addAll(dependencies);
                }
            }
        }
    }

    /**
     * Prepare the options within the specification by taking multiple lines and
     * concatenating them to the option it belongs to
     *
     * @param list
     *            The list of options in their raw form
     * @return A refined list of options with a single line for each option
     */
    private List<String> prepareOptions(List<String> list) {
        List<String> rc = new ArrayList<>();
        String temp = "";

        for (String str : list) {
            temp = str.trim();
            if (isLineValidOption(temp)) {
                rc.add(str);
            } else if (!temp.startsWith("#") && !rc.isEmpty()) {
                rc.set(rc.size() - 1, rc.get(rc.size() - 1).concat(str));
            }
        }

        return rc;
    }

    /**
     * Check to see if the line being read is a valid option within the
     * specification
     *
     * @param line
     *            The line to check
     * @return True if the option within the specification is valid
     */
    private boolean isLineValidOption(String line) {
        boolean rc = false;
        Pattern pattern = Pattern.compile(genericDefineRx,
                Pattern.CASE_INSENSITIVE);
        Matcher variableMatcher = pattern.matcher(line);

        if (variableMatcher.matches()) {
            rc = true;
        }

        return rc;
    }

    /**
     * Find the offset of when the specification starts and also store the
     * gemVariable used in the specification
     *
     * @param reader
     *            The file reader
     * @return The position of the start of the specification
     * @throws IOException
     */
    private long findStart(RandomAccessFile raf) throws IOException {
        long rc = -1;
        Pattern pattern = Pattern
                .compile(
                        SETUP_START,
                        Pattern.CASE_INSENSITIVE);
        Matcher variableMatcher = null;
        String line = "";

        raf.seek(0);
        while ((line = raf.readLine()) != null && rc == -1) {
            variableMatcher = pattern.matcher(line.trim());
            if (variableMatcher.matches()) {
                setGemVariable(variableMatcher.group(4));
                rc = raf.getFilePointer();
            }
        }

        return rc;
    }

    /**
     * Find the offset of when specification ends based on the position before
     * the end statement
     *
     * @param reader
     *            The file reader
     * @param startPosition
     *            The position of the start of the specification
     * @return The position of the end of the specification
     * @throws IOException
     */
    private static long findEnd(RandomAccessFile raf, long startPos)
            throws IOException {
        long rc = -1;
        Pattern pattern = Pattern.compile("^end", Pattern.CASE_INSENSITIVE);
        Matcher variableMatcher = null;
        String line = "";

        raf.seek(startPos);
        while ((line = raf.readLine()) != null && rc == -1) {
            variableMatcher = pattern.matcher(line.trim());
            if (variableMatcher.matches()) {
                rc = raf.getFilePointer();
            }
        }

        return rc;
    }

    /**
     * Sets the gemVariable taken from Gem::Specification.new do |gemVariable|
     *
     * @param str
     *            The variable to set the gemVariable as
     */
    private void setGemVariable(String str) {
        if (gemVariable.isEmpty()) {
            gemVariable = str.trim();
            simpleDefineRx = simpleDefineRx.replace(REPLACE_ME, gemVariable);
            simpleFunctionRx = simpleFunctionRx.replace(REPLACE_ME, gemVariable);
            genericDefineRx = genericDefineRx.replace(REPLACE_ME, gemVariable);
        }
    }
}
