/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.plugins.postgres.util;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.rhq.core.util.jdbc.JDBCUtil;

/**
 * Represents a PostgreSQL configuration file (i.e. postgresql.conf) - provides methods for reading and updating
 * configuration parameters.
 *
 * @author Greg Hinkle
 * @author Ian Springer
 */
public class PostgresqlConfFile {
    private Log log = LogFactory.getLog(PostgresqlConfFile.class);

    private Map<String, String> properties = new HashMap<String, String>();

    private File configurationFile;

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    public PostgresqlConfFile(File configurationFile) throws IOException {
        this.configurationFile = configurationFile;

        // declared outside the try block, so they can be safely closed in finally block
        BufferedReader r = null;
        try {
            r = new BufferedReader(new FileReader(configurationFile));
            Pattern p = Pattern.compile("^" // start match at the beginning of lines
                + "\\s*" // optional whitespace
                + "(\\w+)" // a word, captured
                + "\\s*" // optional whitespace
                + "\\=" // '='
                + "\\s*" // optional whitespace
                + "(" // then capture either
                + "(?:\\w+)" // a word; non-captured, but captured by outer parentheses
                + "|" // or
                + "(?:\\'[^\\']*\\')" // a sequence of non-apostrophe characters surrounded by two apostrophes;
                // also non-captured, but captured by outer parenthesis
                + ")" + ".*" // ignored the rest of the line
                + "$"); // end match at the end of lines
            String line;
            while ((line = r.readLine()) != null) {
                Matcher m = p.matcher(line);
                if (m.matches()) {
                    String val = m.group(2);
                    if (val.startsWith("'") && val.endsWith("'")) {
                        val = val.substring(1, val.length() - 1);
                    }

                    this.properties.put(m.group(1), val);
                }
            }
        } finally {
            JDBCUtil.safeClose(r);
        }
    }

    @Nullable
    public String getProperty(String paramName) {
        return this.properties.get(paramName);
    }

    @NotNull
    public List<String> getPropertyList(String propertyName) {
        String prop = getProperty(propertyName);
        
        if (prop == null) return Collections.emptyList();
        
        return Arrays.asList(stripQuotes(prop).split(","));
    }
    
    @NotNull
    public static String stripQuotes(@NotNull String value) {
        if (value.startsWith("'") && value.endsWith("'")) {
            return value.substring(1, value.length() - 1);
        }
        
        return value;
    }
    
    @Nullable
    public String getPort() {
        return getProperty("port");
    }

    public void setProperties(Map<String, String> parameters) {
        // declared outside the try block, so they can be safely closed in finally block
        BufferedReader r = null;
        BufferedOutputStream bos = null;

        try {
            r = new BufferedReader(new FileReader(configurationFile));
            StringBuilder newBuffer = new StringBuilder();

            Pattern p = Pattern.compile("^" // start match at the beginning of lines
                + "\\s*" // optional whitespace
                + "(\\#?)" // an optional '#' sign, signifying a commented line, captured
                + "\\s*" // optional whitespace
                + "(\\w+)" // a word, captured
                + "\\s*" // optional whitespace
                + "\\=" // '='
                + "\\s*" // optional whitespace
                + "(" // then capture either
                + "(?:\\w+)" // a word; non-captured, but captured by outer parentheses
                + "|" // or
                + "(?:\\'[^\\']*\\')" // a sequence of non-apostrophe characters surrounded by two apostrophes;
                // also non-captured, but captured by outer parenthesis
                + ")" + "(.*)" // capture the rest of the characters on the line
                + "$"); // end match at the end of lines

            String line;
            // update existing properties

            while ((line = r.readLine()) != null) {
                Matcher m = p.matcher(line);
                if (m.matches()) {
                    boolean isCommented = m.group(1).equals("#");
                    String paramName = m.group(2);
                    String oldParamValue = m.group(3);
                    String tail = m.group(4);
                    if (oldParamValue.startsWith("'") && oldParamValue.endsWith("'")) {
                        //noinspection UnusedAssignment
                        oldParamValue = oldParamValue.substring(1, oldParamValue.length() - 1);
                    }

                    if (parameters.containsKey(paramName)) {
                        String paramValue = parameters.get(paramName);
                        if (paramValue == null) {
                            // A null value means the parameter should not be specified, so
                            // comment out the original line if it wasn't already commented out.
                            if (!isCommented) {
                                newBuffer.append('#');
                            }

                            newBuffer.append(line).append(LINE_SEPARATOR);
                        } else {
                            newBuffer.append(paramName).append(" = ").append(paramValue);
                            if (tail != null) {
                                newBuffer.append(tail);
                            }

                            newBuffer.append(LINE_SEPARATOR);
                        }

                        parameters.remove(paramName);
                    } else {
                        newBuffer.append(line).append(LINE_SEPARATOR);
                    }
                } else {
                    newBuffer.append(line).append(LINE_SEPARATOR);
                }
            }

            // add previously non-existent, non-null params at the end
            for (String paramName : parameters.keySet()) {
                String paramValue = parameters.get(paramName);
                if (paramValue != null) {
                    newBuffer.append(paramName).append(" = ").append(paramValue).append(LINE_SEPARATOR);
                }
            }

            backupFile(configurationFile);

            bos = new BufferedOutputStream(new FileOutputStream(configurationFile));
            bos.write(newBuffer.toString().getBytes());
        } catch (FileNotFoundException e) {
            log.warn("Unable to find Postgres configuration file", e);
        } catch (IOException e) {
            log.warn("Unable to update Postgres configuration file", e);
        } finally {
            JDBCUtil.safeClose(r);
            JDBCUtil.safeClose(bos);
        }
    }

    private static void backupFile(File file) throws IOException {
        String backupName = file.getName();
        File dir = file.getParentFile();
        int i = 0;
        while (new File(dir, backupName + ".bak." + i).exists()) {
            i++;
        }

        backupName += ".bak." + i;
        copyFile(file, new File(dir, backupName));
    }

    private static void copyFile(File in, File out) throws IOException {
        FileInputStream fis = new FileInputStream(in);
        FileOutputStream fos = new FileOutputStream(out);
        byte[] buf = new byte[1024];
        int i;
        try {
            while ((i = fis.read(buf)) != -1) {
                fos.write(buf, 0, i);
            }
        } finally {
            /*
             * even if this method declares to throw an exception, we should still clean up after ourselves
             */
            JDBCUtil.safeClose(fis);
            JDBCUtil.safeClose(fos);
        }
    }

    @Override
    public String toString() {
        return this.configurationFile.toString();
    }
}