/**
 * 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.aries.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import org.apache.aries.util.internal.MessageUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;

public class FragmentBuilder {
    private List<String> importPackages = new ArrayList<String>();
    private List<String> exportPackages = new ArrayList<String>();
    private Bundle hostBundle;
    private String nameExtension;
    private String bundleNameExtension;
    private String fragmentName;
    private Map<String, byte[]> files = new HashMap<String, byte[]>();

    public FragmentBuilder(Bundle host) {
        this(host, ".fragment", "Fragment");
    }
    
    public FragmentBuilder(Bundle host, String symbolicNameSuffix, String bundleNameSuffix) {
        hostBundle = host;
        nameExtension = symbolicNameSuffix;
        bundleNameExtension = bundleNameSuffix;

        // make sure we have an initial '.'
        if (!nameExtension.startsWith(".")) {
            nameExtension = "." + nameExtension;
        }
    }

    public void setName(String name) {
        fragmentName = name;
    }

    public void addImports(String... imports) {
        importPackages.addAll(Arrays.asList(imports));
    }

    public void addExports(String... imports) {
        exportPackages.addAll(Arrays.asList(imports));
    }

    public void addImportsFromExports(Bundle exportBundle) {
        String exportString = (String) exportBundle.getHeaders().get(Constants.EXPORT_PACKAGE);

        if (exportString != null) {
            String exportVersion = exportBundle.getVersion().toString();
            String bundleConstraint = Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE
                    + "=\"" + exportBundle.getSymbolicName() + "\"";
            String bundleVersionConstraint = Constants.BUNDLE_VERSION_ATTRIBUTE
                    + "=\"[" + exportVersion + "," + exportVersion + "]\"";

            List<String> exports = parseDelimitedString(exportString, ",", true);            
            for (String export : exports) {
                importPackages.add(convertExportToImport(export, bundleConstraint, bundleVersionConstraint));
            }
        }
    }

    /**
     * Filter out directives in the export statement
     * 
     * @param exportStatement
     * @return
     */
    private String convertExportToImport(String exportStatement,
                                         String bundleConstraint, 
                                         String bundleVersionConstraint) {
        StringBuffer result = new StringBuffer();

        for (String fragment : exportStatement.split("\\s*;\\s*")) {
            int pos = fragment.indexOf('=');

            // similar to fragment.contains(":=") but looks for the first '='
            // and checks whether this is part of ':='
            // in this way we will not be fooled by attributes like
            // a="something:=strange"
            if (!(pos > 0 && fragment.charAt(pos - 1) == ':')) {
                result.append(fragment);
                result.append(';');
            }
        }

        result.append(bundleConstraint);
        result.append(';');
        result.append(bundleVersionConstraint);

        return result.toString();
    }

    public void addFile(String path, byte[] content) {
        files.put(path, content);
    }

    public Bundle install(BundleContext ctx) throws IOException, BundleException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        JarOutputStream jos = null;

        try {
            jos = new JarOutputStream(baos, makeManifest());
            addFileContent(jos);
        } finally {
            if (jos != null)
                jos.close();
            baos.close();
        }

        byte[] inMemoryJar = baos.toByteArray();
        ByteArrayInputStream bais = new ByteArrayInputStream(inMemoryJar);

        return ctx.installBundle(getFragmentSymbolicName(), bais);
    }

    private void addFileContent(JarOutputStream jos) throws IOException {
        for (Map.Entry<String, byte[]> entry : files.entrySet()) {
            jos.putNextEntry(new JarEntry(entry.getKey()));
            jos.write(entry.getValue());
        }
    }

    public String getFragmentSymbolicName() {
        return hostBundle.getSymbolicName() + nameExtension;
    }

    public String getFragmentBundleName() {
        if (fragmentName != null) {
            return fragmentName;
        } else {
            String bundleName = (String) hostBundle.getHeaders().get(Constants.BUNDLE_NAME);
            if (bundleName != null && bundleNameExtension != null) {
                return bundleName.trim() + " " + bundleNameExtension.trim();
            }
        }
        return null;
    }
    
    private Manifest makeManifest() {
        String commonVersion = hostBundle.getVersion().toString();
        String fragmentHost = hostBundle.getSymbolicName() + ";"
                + Constants.BUNDLE_VERSION_ATTRIBUTE + "=\"" + commonVersion
                + "\"";

        Manifest m = new Manifest();
        Attributes manifestAttributes = m.getMainAttributes();
        manifestAttributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
        manifestAttributes.putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
        manifestAttributes.putValue(Constants.BUNDLE_SYMBOLICNAME, getFragmentSymbolicName());   
        
        String bundleName = getFragmentBundleName();        
        if (bundleName != null) {
            manifestAttributes.putValue(Constants.BUNDLE_NAME, bundleName);
        }
            
        manifestAttributes.putValue(Constants.BUNDLE_VERSION, commonVersion);
        manifestAttributes.putValue(Constants.BUNDLE_VENDOR, "Apache");
        manifestAttributes.putValue(Constants.FRAGMENT_HOST, fragmentHost);

        addImportsAndExports(manifestAttributes);

        return m;
    }

    private void addImportsAndExports(Attributes attrs) {
        if (!importPackages.isEmpty()) {
            attrs.putValue(Constants.IMPORT_PACKAGE, joinStrings(importPackages, ','));
        }

        if (!exportPackages.isEmpty()) {
            attrs.putValue(Constants.EXPORT_PACKAGE, joinStrings(exportPackages, ','));
        }
    }

    private String joinStrings(List<String> strs, char separator) {
        StringBuilder result = new StringBuilder();
        boolean first = true;
        for (String str : strs) {
            if (first)
                first = false;
            else
                result.append(separator);

            result.append(str);
        }

        return result.toString();
    }
    
    private static List<String> parseDelimitedString(String value, String delim, boolean includeQuotes) {   
        if (value == null) {       
            value = "";
        }

        List<String> list = new ArrayList<String>();

        int CHAR = 1;
        int DELIMITER = 2;
        int STARTQUOTE = 4;
        int ENDQUOTE = 8;

        StringBuffer sb = new StringBuffer();

        int expecting = (CHAR | DELIMITER | STARTQUOTE);

        for (int i = 0; i < value.length(); i++) {        
            char c = value.charAt(i);

            boolean isDelimiter = (delim.indexOf(c) >= 0);
            boolean isQuote = (c == '"');

            if (isDelimiter && ((expecting & DELIMITER) > 0)) {            
                list.add(sb.toString().trim());
                sb.delete(0, sb.length());
                expecting = (CHAR | DELIMITER | STARTQUOTE);
            } else if (isQuote && ((expecting & STARTQUOTE) > 0)) { 
                if (includeQuotes) {
                    sb.append(c);
                }
                expecting = CHAR | ENDQUOTE;
            } else if (isQuote && ((expecting & ENDQUOTE) > 0)) {    
                if (includeQuotes) {
                    sb.append(c);
                }
                expecting = (CHAR | STARTQUOTE | DELIMITER);
            } else if ((expecting & CHAR) > 0) {            
                sb.append(c);
            } else {
                throw new IllegalArgumentException(MessageUtil.getMessage("UTIL0012E", value));
            }
        }

        if (sb.length() > 0) {        
            list.add(sb.toString().trim());
        }

        return list;
    }
}
