/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.idegen;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.common.io.LineProcessor;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Logger;

/**
 * Parses the make files and finds the appropriate section a given module.
 */
public class MakeFileParser {

    private static final Logger logger = Logger.getLogger(MakeFileParser.class.getName());
    public static final String VALUE_DELIMITER = "|";

    private File makeFile;
    private HashMap<String, String> values;

    /**
     * Create a parser for a given make file and module name.
     * <p>
     * A make file may contain multiple modules.
     *
     * @param makeFile The make file to parse.
     */
    public MakeFileParser(File makeFile) {
        this.makeFile = Preconditions.checkNotNull(makeFile);
    }

    public Iterable<String> getValues(String key) {
        String str = values.get(key);
        if (str == null) {
            return null;
        }
        return Splitter.on(VALUE_DELIMITER).trimResults().omitEmptyStrings().split(str);
    }

    /**
     * Extracts the relevant portion of the make file and converts into key value pairs. <p> Since
     * each make file may contain multiple build targets (modules), this method will determine which
     * part is the correct portion for the given module name.
     */
    public void parse() throws IOException {
        values = Maps.newHashMap();
        logger.info("Parsing " + makeFile.getCanonicalPath());

        Files.readLines(makeFile, Charset.forName("UTF-8"), new MakeFileLineProcessor());
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(this).add("values", values).toString();
    }

    private class MakeFileLineProcessor implements LineProcessor<Object> {

        private StringBuilder lineBuffer;

        // Keep a list of LOCAL_ variables to clear when CLEAR_VARS is encountered.
        private HashSet<String> localVars = Sets.newHashSet();

        @Override
        public boolean processLine(String line) throws IOException {
            String trimmed = line.trim();
            // Skip comments.
            if (!trimmed.isEmpty() && trimmed.charAt(0) == '#') {
                return true;
            }
            appendPartialLine(trimmed);

            if (!trimmed.isEmpty() && trimmed.charAt(trimmed.length() - 1) == '\\') {
                // This is a partial line.  Do not process yet.
                return true;
            }

            String completeLine = lineBuffer.toString().trim();
            // Reset the line buffer.
            lineBuffer = null;

            if (Strings.isNullOrEmpty(completeLine)) {
                return true;
            }

            processKeyValuePairs(completeLine);
            return true;
        }

        private void processKeyValuePairs(String line) {
            if (line.contains("=")) {
                String[] arr;
                if (line.contains(":")) {
                    arr = line.split(":=");
                } else {
                    arr = line.split("\\+=");
                }
                if (arr.length > 2) {
                    logger.info("Malformed line " + line);
                } else {
                    // Store the key in case the line continues
                    String key = arr[0].trim();
                    if (arr.length == 2) {
                        // There may be multiple values on one line.
                        List<String> valuesArr = tokenizeValue(arr[1]);
                        for (String value : valuesArr) {
                            appendValue(key, value);
                        }

                    }
                }
            } else {
                //logger.info("Skipping line " + line);
            }
        }

        private void appendPartialLine(String line) {
            if (lineBuffer == null) {
                lineBuffer = new StringBuilder();
            } else {
                lineBuffer.append(" ");
            }
            if (line.endsWith("\\")) {
                lineBuffer.append(line.substring(0, line.length() - 1).trim());
            } else {
                lineBuffer.append(line);
            }
        }

        private List<String> tokenizeValue(String rawValue) {
            String value = rawValue.trim();
            ArrayList<String> result = Lists.newArrayList();
            if (value.isEmpty()) {
                return result;
            }

            // Value may contain function calls such as "$(call all-java-files-under)" or refer
            // to variables such as "$(my_var)"
            value = findVariables(value);

            String[] tokens = value.split(" ");
            Collections.addAll(result, tokens);
            return result;
        }

        private String findVariables(String value) {

            int variableStart = value.indexOf("$(");
            // Keep going until we substituted all variables.
            while (variableStart > -1) {
                StringBuilder sb = new StringBuilder();
                sb.append(value.substring(0, variableStart));

                // variable found
                int variableEnd = findClosingParen(value, variableStart);
                if (variableEnd > variableStart) {
                    String result = substituteVariables(value.substring(variableStart + 2, variableEnd));
                    sb.append(result);
                } else {
                    throw new IllegalArgumentException(
                            "Malformed variable reference in make file: " + value);
                }
                if (variableEnd + 1 < value.length()) {
                    sb.append(value.substring(variableEnd + 1));
                }
                value = sb.toString();
                variableStart = value.indexOf("$(");
            }
            return value;
        }

        private int findClosingParen(String value, int startIndex) {
            int openParenCount = 0;
            for (int i = startIndex; i < value.length(); i++) {
                char ch = value.charAt(i);
                if (ch == ')') {
                    openParenCount--;
                    if (openParenCount == 0) {
                        return i;
                    }
                } else if (ch == '(') {
                    openParenCount++;
                }
            }
            return -1;
        }

        /**
         * Look for and handle $(...) variables.
         */
        private String substituteVariables(String rawValue) {
            if (rawValue.isEmpty()) {
                return rawValue;
            }
            String value = rawValue;
            if (value.startsWith("call all-java-files-under")) {
                // Ignore the call and function, keep the args.
                value = value.substring(25).trim();
            } else if (value.startsWith("call")) {
                value = value.substring(4).trim();
            }

            // Check for single variable
            if (value.indexOf(' ') == -1) {
                // Substitute.
                value = values.get(value);
                if (value == null) {
                    value = "";
                }
                return value;
            } else {
                return findVariables(value);
            }
        }

        @Override
        public Object getResult() {
            return null;
        }

        /**
         * Add a value to the hash map. If the key already exists, will append instead of
         * over-writing the existing value.
         *
         * @param key The hashmap key
         * @param newValue The value to append.
         */
        private void appendValue(String key, String newValue) {
            String value = values.get(key);
            if (value == null) {
                values.put(key, newValue);
            } else {
                values.put(key, value + VALUE_DELIMITER + newValue);
            }
        }
    }

    public static void main(String[] args) {
        MakeFileParser parser = new MakeFileParser(new File(args[0]));
        try {
            parser.parse();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(parser.toString());
    }
}
