/**
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.kerby.kerberos.kerb.common;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * A parser to parse krb5.conf format file.
 */
public class Krb5Parser {
    private File krb5conf;
    /**
     * The variable items regards section name as a key of Map, and
     * contents of a section as a value with Object type.
     * In specific, the value is a recursive Map type, which can be
     * in the form of both Map<String, String> and Map<String, Object>,
     * depending on contents of the section.
     */
    private Map<String, Object> items;

    public Krb5Parser(File confFile) {
        krb5conf = confFile;
        items = null;
    }

    /**
     * Load the krb5.conf into a member variable, which is a Map.
     * @throws IOException e
     */
    public void load() throws IOException {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(krb5conf.toPath()),
            StandardCharsets.UTF_8))) {
            items = new IdentityHashMap<>();

            String originLine = br.readLine();
            while (originLine != null) {
                String line = originLine.trim();
            /*parse through comments*/
                if (line.isEmpty() || isComment(line)) {
                    originLine = br.readLine();
                } else if (line.startsWith("[")) {
                    insertSections(line, br, items);
                    originLine = br.readLine();
                } else if (line.startsWith("include")) {
                    String[] splited = line.trim().split("\\s+");
                    if (splited.length == 2) {
                        items.put(splited[0], splited[1]);
                    } else {
                        throw new RuntimeException("Unable to parse:" + originLine);
                    }
                    originLine = br.readLine();
                } else {
                    throw new RuntimeException("Unable to parse:" + originLine);
                }
            }
        }
    }

    /**
     * Get the whole map.
     * @return member variable items.
     */
    public Map<String, Object> getItems() {
        return items;
    }



    /**
     * Get all the names of sections in a list.
     * @return a list of section names.
     */
    public List<String> getSections() {
        return new ArrayList<String>(items.keySet());
    }

    /**
     * Get the contents of a section given the section name.
     * @param sectionName the name of a section
     * @param keys the keys list
     * @return a Map of section contents
     */
    public Object getSection(String sectionName, String... keys) {
        Object value = null;
        for (Map.Entry<String, Object> item : items.entrySet()) {
            if (item.getKey().equals(sectionName)) {
                value = item.getValue();
                if (keys.length == 0) {
                    return value;
                }
                Map<String, Object> map = (Map) item.getValue();
                for (Map.Entry<String, Object> entry : map.entrySet()) {
                    if (entry.getKey().equals(keys[0])) {
                        value = entry.getValue();
                    }
                }
            }
        }

        for (int i = 1; i < keys.length; i++) {
            Map<String, Object> map = (Map) value;
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                if (entry.getKey().equals(keys[i])) {
                    value = entry.getValue();
                }
            }
        }
        return value;
    }

    /**
     * Print all the meaningful contents of the krb5.conf on the console.
     * Comments are ignored.
     * Hierarchy is considered.
     * Attention that the order of sections and the order inside a section
     * will be different to the original file, due to the use of HashMap.
     */
    public void dump() {
        printSection(items);
    }

    private void insertSections(String line, BufferedReader br, Map<String, Object> items) throws IOException {
        while (line.startsWith("[")) {
            int pos = line.indexOf("]");
            if (pos < 2 || pos < line.length() - 1) {
                throw new IOException("Illegal section name: " + line);
            }
            String sectionName = line.substring(1, pos);
            Map<String, Object> entries = new IdentityHashMap<>();
            line = br.readLine();
            if (line == null) {
                break;
            }
            while (!line.isEmpty() && isComment(line)) {
                line = br.readLine();
                if (line == null) {
                    break;
                }
            }
            if (line != null) {
                line = line.trim();
                line = insertEntries(line, br, entries);
                items.put(sectionName, entries);
            }
            /*line has been modified after the recursive.*/
            if (line == null) {
                /*the end of file*/
                break;
            }
        }
    }

    /**
     * recursively go through the key-value pairs of a section
     * */
    private String insertEntries(String line, BufferedReader br, Map<String, Object> entries) throws IOException {
        if (line == null) {
            return line;
        }
        if (line.startsWith("[")) {
            return line;
        }

        if (line.startsWith("}")) {
            line = br.readLine();
            if (line != null) {
                line = line.trim();
            }
            return line;
        }
        if (line.isEmpty() || isComment(line)) {
            line = br.readLine();
            if (line != null) {
                line = line.trim();
                line = insertEntries(line, br, entries);
            }
            return line;
        }
        /*some special cases above*/
        String[] kv = line.split("=", 2);
        kv[0] = kv[0].trim();
        kv[1] = kv[1].trim();

        if (kv[1].startsWith("{")) {
            Map<String, Object> meValue = new IdentityHashMap<>();
            line = br.readLine();
            while (line != null && (isComment(line.trim()) || line.trim().isEmpty())) {
                line = br.readLine();
            }
            if (line != null) {
                line = line.trim();
                if (!line.contains("=") && !isComment(line) && !line.isEmpty()) {
                    // multi-line value
                    List<String> values = new ArrayList<>();
                    while (line != null && !line.contains("}")) {
                        line = line.trim();
                        if (!line.isEmpty() && !isComment(line)) {
                            values.add(line.trim());
                        }
                        line = br.readLine();
                    }
                    entries.put(kv[0], values);
                    line = br.readLine();
                    if (line != null) {
                        line = line.trim();
                    }
                } else {
                    line = insertEntries(line, br, meValue);
                    entries.put(kv[0], meValue);
                }
                line = insertEntries(line, br, entries);
            }
        }   else {
            entries.put(kv[0], kv[1]);
            line = br.readLine();
            if (line != null) {
                line = line.trim();
                line = insertEntries(line, br, entries);
            }
        }
        return line;
    }

    private void printSection(Map<String, Object> map) {
        Iterator iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            String key = (String) entry.getKey();
            Object value = entry.getValue();
            System.out.println("[" + key + "]");

            if (value instanceof Map) {
                int count = 0;
                printEntry((Map) value, count);
            }   else {
                throw new RuntimeException("Unable to print contents of [" + key + "]");
            }
        }
    }

    private void printEntry(Map<String, Object> map, int count) {
        Iterator iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            String key = (String) entry.getKey();
            Object value = entry.getValue();
            for (int i = 0; i < count; i++) {
                System.out.print("\t");
            }
            if (value instanceof String) {
                System.out.println(key + " = " + (String) value);
            }
            if (value instanceof Map) {
                System.out.println(key + " = {");
                printEntry((Map) value, count + 1);
                for (int i = 0; i < count; i++) {
                    System.out.print("\t");
                }
                System.out.println("}");
            }
        }
    }

    private boolean isComment(String line) {
        return line.charAt(0) == '#' || line.charAt(0) == ';';
    }
}
