/*******************************************************************************
 * 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 - python implementation (B#350065)
 *******************************************************************************/
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.Map.Entry;
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 Python setup.py to grab specfile properties
 *
 */
public class PythonEggParser {

    private Map<String, String> variables;
    private Map<String, String> setupOptions;
    private IFile file;

    /**
     * Initialize and then parse the file
     *
     * @param file The Python setup.py file
     * @throws CoreException Throws CoreException
     * @throws IOException  Throws IOException
     */
    public PythonEggParser(IFile file) throws IOException, CoreException {
        setupOptions = new HashMap<>();
        variables = new HashMap<>();
        // end if file is empty or cannot get its contents
        if (file.getContents().available() <= 0) {
            return;
        }
        this.file = file;
        parse();
    }

    /**
     * Parse the contents of the Python setup.py file and grab
     * variables and meta-data from the setup(...) function
     *
     */
    public void parse() {
        String line = "";
        String setupLine = "";
        List<String> vars = new ArrayList<>();
        int offset = 0;
        try {
            RandomAccessFile raf = new RandomAccessFile(file.getRawLocation().makeAbsolute().toFile(), "r");

            // end if cannot find setup(
            long bytesToSkip = findStartSetup(raf);
            if (bytesToSkip == -1) {
                return;
            }

            // end if the end of setup cannot be found
            long stop = findEndSetup(raf, bytesToSkip);
            if (stop == -1) {
                return;
            }

            raf.seek(0);
            while ((line = raf.readLine()) != null) {
                if (!line.trim().startsWith("#")) {
                    if (isLineSimpleDefinition(line)) {
                        vars.add(line);
                    } else if (!vars.isEmpty() && vars.get(vars.size()-1).trim().endsWith("\\") && isLineContinuation(line)){
                        offset = vars.get(vars.size()-1).lastIndexOf('\\');
                        vars.set(vars.size() -1, vars.get(vars.size() - 1).substring(0, offset));
                        vars.set(vars.size() - 1, vars.get(vars.size() - 1)
                                .concat(line.trim()));
                    }
                }
            }

            raf.seek(bytesToSkip);
            while ((line = raf.readLine()) != null && raf.getFilePointer() <= stop) {
                line = line.trim();
                if (!line.startsWith("#")) {
                    if (setupLine.equals("")) {
                        setupLine = line.trim();
                    } else {
                        setupLine = setupLine.concat(line.trim());
                    }
                }
            }

            List<String> list = prepareSetupOptions(setupLine);

            for (String str : vars) {
                variables.putAll(parseLine(str));
            }

            for (String str : list) {
                setupOptions.putAll(parseLine(str));
            }

            resolveVariables(variables, setupOptions);

            raf.close();
        } catch (IOException e) {
            StubbyLog.logError(e);
        }
    }

    /**
     * Check to see if the string passed in is a function
     *
     * @param str The string to check
     * @return True if the string matches the function regex
     */
    public boolean checkFunction(String str) {
        boolean rc = false;
        Pattern pattern = Pattern.compile("\\s*\\w*\\s*?\\(.*\\)\\s*");
        Matcher variableMatcher = pattern.matcher(str);

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

        return rc;
    }

    /**
     * Get the value of the variable
     *
     * @param key The variable to get the value of
     * @return The value of the variable
     */
    public String getValue(String key) {
        String rc = "";
        Pattern pattern = Pattern.compile("\\s*\\((.+)\\)\\s*");
        Matcher variableMatcher = null;

        if (setupOptions.containsKey(key)) {
            rc = setupOptions.get(key).replaceAll("('|\")", "").trim();
            variableMatcher = pattern.matcher(rc);
            if (variableMatcher.matches()) {
                rc = variableMatcher.group(1);
            }
        }

        return rc;
    }

    /**
     * Get the list of strings for a key.
     * Use with classifiers, platforms, install_requires, etc.
     *
     * @param key The variable to get the value of
     * @return The value of the variable
     */
    public List<String> getValueList(String key) {
        List<String> rc = new ArrayList<>();
        Pattern pattern = Pattern.compile("^\\[(.*)\\]");

        if (setupOptions.containsKey(key)) {
            Matcher variableMatcher = pattern.matcher(setupOptions.get(key).trim());
            if (variableMatcher.find()) {
                String[] temp = variableMatcher.group(1).replaceAll("('|\")", "").split(",");
                for (String str : temp) {
                    if (!str.isEmpty() && !str.trim().startsWith("#")) {
                        rc.add(str.trim());
                    }
                }
            }
        }

        return rc;
    }

    /**
     * Prepare the setup options by returning each comma delimited option
     *
     * @param setupLine The single string containing all the setup options
     * @return A list of setup options
     */
    private static List<String> prepareSetupOptions(String setupLine) {
        List<String> rc = new ArrayList<>();
        // match the setup(...) pattern
        Pattern pattern = Pattern.compile("\\bsetup\\b(\\s+)?\\((.*)\\)");
        Matcher variableMatcher = pattern.matcher(setupLine);

        if (variableMatcher.find()) {
            setupLine = variableMatcher.group(2);
        }

        String[] tempList = setupLine.split("(?=,)");

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

        return rc;
    }

    /**
     * Resolves the setup option variables if they are referencing a
     * define from outside the setup() function.
     *
     * @param variables The variables outside the setup() function
     * @param options The options to be resolved within the setup() function
     */
    private static void resolveVariables(Map<String, String> variables, Map<String, String> options) {
        for (Entry<String, String> entry : options.entrySet()) {
            if (variables.containsKey(entry.getValue())) {
                options.put(entry.getKey(), variables.get(entry.getValue()));
            }
        }
    }

    /**
     * Check to see if the line in setup option is a new key->value pair
     *
     * @param line Line to check
     * @return True if the line contains a key->value
     */
    private static boolean isOptionLineKeyValuePair(String line) {
        boolean rc = false;
        Pattern pattern = Pattern.compile("(\\w+)(\\s+)?=[^=].*");
        Matcher variableMatcher = pattern.matcher(line.toLowerCase());

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

        return rc;
    }

    /**
     * Check to see if the line is a simple variable declaration (var=value)
     *
     * @param line Line to check
     * @return True if it is a simple variable declaration
     */
    private static boolean isLineSimpleDefinition(String line) {
        boolean rc = false;
        Pattern pattern = Pattern.compile("^(\\w+)(\\s+)?=(\\s+)?");
        Matcher variableMatcher = pattern.matcher(line.toLowerCase());

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

        return rc;
    }

    /**
     * Check to see if the line is a continuation from the previous.
     * It is a continuation from the previous if it starts
     * with a ' or "
     *
     * @param line Line to check
     * @return True if the line is a continuation
     */
    private static boolean isLineContinuation(String line) {
        boolean rc = false;
        Pattern pattern = Pattern.compile(".*[\'\"](/s+)?$");
        Matcher variableMatcher = pattern.matcher(line.toLowerCase());

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

        return rc;
    }

    /**
     * Parse the line and split it into a key->value pair
     *
     * @param line The line to be parsed
     * @return The map containing the key->value pair
     */
    private static Map<String, String> parseLine(String line) {
        Map<String, String> rc = new HashMap<>();
        Pattern pattern = Pattern.compile("(\\s+)?(\\w+)(\\s+)?=(\\s+)?(.*)");
        Matcher variableMatcher = pattern.matcher(line);

        if (variableMatcher.find()) {
            String value = variableMatcher.group(5);
            if (value.charAt(value.length()-1) == ',') {
                value = value.substring(0, value.length()-1);
            }
            rc.put(variableMatcher.group(2), value);
        }

        return rc;
    }

    /**
     * Find the offset of when setup(...) starts
     *
     * @param reader The file reader
     * @return The position of the start of setup(...
     * @throws IOException
     */
    private static long findStartSetup(RandomAccessFile reader) throws IOException {
        long rc = -1;
        long previous = 0;
        Pattern pattern = Pattern.compile("^\\bsetup\\b(\\s+)?(\\()?");
        Matcher variableMatcher = null;
        String line = "";

        reader.seek(0);
        while ((line = reader.readLine()) != null && rc == -1) {
            variableMatcher = pattern.matcher(line.toLowerCase());
            if (variableMatcher.find()) {
                // get the previous line's file pointer location
                rc = previous;
            }
            previous = reader.getFilePointer();
        }

        return rc;
    }

    /**
     * Find the offset of when setup(...) ends based on the
     * position AFTER the closing bracket of setup()
     *
     * @param reader The file reader
     * @param startPosition The position of the start of setup(...
     * @return The position of the end of setup ...)
     * @throws IOException
     */
    private static long findEndSetup(RandomAccessFile reader, long startPosition) throws IOException {
        int bracketCounter = 0;
        boolean flag = false;
        boolean stop = false;
        String line = "";

        reader.seek(startPosition);
        while ((line = reader.readLine()) != null && stop == false) {
            for (char x : line.toCharArray()) {
                if (x == '(') {
                    bracketCounter++;
                } else if (x == ')') {
                    bracketCounter--;
                }
                if (flag && bracketCounter == 0) {
                    stop = true;
                }
                // prevent ending prematurely
                if (bracketCounter != 0) {
                    flag = true;
                }
            }
        }

        return reader.getFilePointer();
    }
}
