package bndtools.jareditor.internal;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Manifest;
import java.util.zip.ZipException;

import aQute.bnd.header.Attrs;
import aQute.bnd.header.Parameters;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.Domain;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.Resource;
import aQute.bnd.osgi.Verifier;
import aQute.lib.collections.SortedList;
import aQute.lib.io.IO;
import aQute.libg.generics.Create;
import bndtools.jareditor.internal.utils.CollectionUtil;

public class Printer extends Processor {

    final static int MANIFEST = 2;
    final static int LIST = 4;

    final static int IMPEXP = 16;
    final static int USES = 32;
    final static int USEDBY = 64;
    final static int COMPONENT = 128;
    final static int METATYPE = 256;
    final static int VERIFY = 1;

    PrintStream out = System.out;
    OutputStreamWriter or = new OutputStreamWriter(out, Constants.DEFAULT_CHARSET);

    private static final String EOL = String.format("%n");

    public void setOut(PrintStream out) {
        this.out = out;
        this.or = new OutputStreamWriter(out, Constants.DEFAULT_CHARSET);
    }

    public void doPrint(String string, int options) throws Exception {
        int optionsi = options;
        File file = new File(string);
        if (!file.exists())
            error("File to print not found: " + string);
        else {
            if (optionsi == 0)
                optionsi = VERIFY | MANIFEST | IMPEXP | USES;
            doPrint(file, optionsi);
        }
    }

    private void doPrint(File file, int options) throws ZipException, IOException, Exception {
        try (Jar jar = new Jar(file.getName(), file)) {
            if ((options & VERIFY) != 0) {
                Verifier verifier = new Verifier(jar);
                verifier.setPedantic(isPedantic());
                verifier.verify();
                getInfo(verifier);
            }
            if ((options & MANIFEST) != 0) {
                Manifest manifest = jar.getManifest();
                if (manifest == null)
                    warning("JAR has no manifest " + file);
                else {
                    out.println("[MANIFEST " + jar.getName() + "]");
                    printManifest(manifest);
                }
                out.println();
            }
            if ((options & IMPEXP) != 0) {
                out.println("[IMPEXP]");
                Manifest m = jar.getManifest();

                if (m != null) {
                    Domain domain = Domain.domain(m);
                    Parameters imports = domain.getImportPackage();
                    Parameters exports = domain.getExportPackage();
                    for (String p : exports.keySet()) {
                        if (imports.containsKey(p)) {
                            Attrs attrs = imports.get(p);
                            if (attrs.containsKey(VERSION_ATTRIBUTE)) {
                                exports.get(p).put("imported-as", attrs.get(VERSION_ATTRIBUTE));
                            }
                        }
                    }
                    print("Import-Package", new TreeMap<String,Attrs>(imports));
                    print("Export-Package", new TreeMap<String,Attrs>(exports));
                } else
                    warning("File has no manifest");
            }

            if ((options & (USES | USEDBY)) != 0) {
                out.println();
                try (Analyzer analyzer = new Analyzer()) {
                    analyzer.setPedantic(isPedantic());
                    analyzer.setJar(jar);
                    analyzer.analyze();
                    if ((options & USES) != 0) {
                        out.println("[USES]");
                        printMultiMap(analyzer.getUses());
                        out.println();
                    }
                    if ((options & USEDBY) != 0) {
                        out.println("[USEDBY]");
                        Map<PackageRef,Set<PackageRef>> usedBy = CollectionUtil.invertMapOfCollection(analyzer.getUses());
                        printMultiMap(usedBy);
                    }
                    analyzer.setJar((Jar) null);
                }
                out.println();
            }

            if ((options & COMPONENT) != 0) {
                printComponents(jar);
                out.println();
            }

            if ((options & METATYPE) != 0) {
                printMetatype(jar);
                out.println();
            }

            if ((options & LIST) != 0) {
                out.println("[LIST]");
                for (Map.Entry<String,Map<String,Resource>> entry : jar.getDirectories().entrySet()) {
                    String name = entry.getKey();
                    Map<String,Resource> contents = entry.getValue();
                    out.println(name);
                    if (contents != null) {
                        for (String element : contents.keySet()) {
                            int n = element.lastIndexOf('/');
                            if (n > 0)
                                element = element.substring(n + 1);
                            out.print("  ");
                            out.print(element);
                            String path = element;
                            if (name.length() != 0)
                                path = name + "/" + element;
                            Resource r = contents.get(path);
                            if (r != null) {
                                String extra = r.getExtra();
                                if (extra != null) {
                                    out.print(" extra='" + escapeUnicode(extra) + "'");
                                }
                            }
                            out.println();
                        }
                    }
                }
                out.println();
            }
        }
    }

    /**
     * @param manifest
     */
    private void printManifest(Manifest manifest) {
        SortedSet<String> sorted = new TreeSet<String>();
        for (Object element : manifest.getMainAttributes().keySet()) {
            sorted.add(element.toString());
        }
        for (String key : sorted) {
            Object value = manifest.getMainAttributes().getValue(key);
            format(out, "%-40s %-40s%s", new Object[] {
                    key, value, EOL
            });
        }
    }

    private void print(String msg, Map< ? , ? extends Map< ? , ? >> ports) {
        if (ports.isEmpty())
            return;
        out.println(msg);
        for (Entry< ? , ? extends Map< ? , ? >> entry : ports.entrySet()) {
            Object key = entry.getKey();
            Map< ? , ? > clause = Create.copy((Map< ? , ? >) entry.getValue());
            clause.remove("uses:");
            format(out, "  %-38s %s%s", key.toString().trim(), clause.isEmpty() ? "" : clause.toString(), EOL);
        }
    }

    private <T extends Comparable< ? super T>> void printMultiMap(Map<T, ? extends Collection<T>> map) {
        SortedList<T> keys = new SortedList<T>(map.keySet());
        for (Object key : keys) {
            String name = key.toString();

            SortedList<T> values = new SortedList<T>(map.get(key));
            String list = vertical(41, values);
            format(out, "%-40s %s", name, list);
        }
    }

    private static String vertical(int padding, Collection< ? > used) {
        StringBuilder sb = new StringBuilder();
        String del = "";
        for (Object s : used) {
            String name = s.toString();
            sb.append(del);
            sb.append(name);
            sb.append(EOL);
            del = pad(padding);
        }
        if (sb.length() == 0)
            sb.append(EOL);
        return sb.toString();
    }

    private static String pad(int i) {
        StringBuilder sb = new StringBuilder();
        int ii = i;
        while (ii-- > 0)
            sb.append(' ');
        return sb.toString();
    }

    private static void format(PrintStream out, String string, Object... objects) {
        if (objects == null || objects.length == 0)
            return;

        StringBuffer sb = new StringBuffer();
        int index = 0;
        for (int i = 0; i < string.length(); i++) {
            char c = string.charAt(i);
            switch (c) {
            case '%' :
                String s = objects[index++] + "";
                int width = 0;
                int justify = -1;

                i++;

                c = string.charAt(i++);
                switch (c) {
                case '-' :
                    justify = -1;
                    break;
                case '+' :
                    justify = 1;
                    break;
                case '|' :
                    justify = 0;
                    break;
                default :
                    --i;
                }
                c = string.charAt(i++);
                while (c >= '0' && c <= '9') {
                    width *= 10;
                    width += c - '0';
                    c = string.charAt(i++);
                }
                --i;
                if (c != 's') {
                    throw new IllegalArgumentException("Invalid sprintf format:  " + string);
                }

                if (s.length() > width)
                    sb.append(s);
                else {
                    switch (justify) {
                    case -1 :
                    default :
                        sb.append(s);
                        for (int j = 0; j < width - s.length(); j++)
                            sb.append(" ");
                        break;

                    case 1 :
                        for (int j = 0; j < width - s.length(); j++)
                            sb.append(" ");
                        sb.append(s);
                        break;

                    case 0 :
                        int spaces = (width - s.length()) / 2;
                        for (int j = 0; j < spaces; j++)
                            sb.append(" ");
                        sb.append(s);
                        for (int j = 0; j < width - s.length() - spaces; j++)
                            sb.append(" ");
                        break;
                    }
                }
                break;

            default :
                sb.append(c);
            }
        }
        out.print(sb);
    }

    private static final String escapeUnicode(String s) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c >= ' ' && c <= '~' && c != '\\')
                sb.append(c);
            else {
                sb.append("\\u");
                sb.append(nibble(c >> 12));
                sb.append(nibble(c >> 8));
                sb.append(nibble(c >> 4));
                sb.append(nibble(c));
            }
        }
        return sb.toString();
    }

    private static final char nibble(int i) {
        return "0123456789ABCDEF".charAt(i & 0xF);
    }

    /**
     * Print the components in this JAR.
     *
     * @param jar
     */
    private void printComponents(Jar jar) throws Exception {
        out.println("[COMPONENTS]");
        Manifest manifest = jar.getManifest();
        if (manifest == null) {
            out.println("No manifest");
            return;
        }

        String componentHeader = manifest.getMainAttributes().getValue(Constants.SERVICE_COMPONENT);
        Parameters clauses = new Parameters(componentHeader);
        boolean printed = false;
        for (String path : clauses.keySet()) {
            printed = true;
            out.println(path);

            Resource r = jar.getResource(path);
            if (r != null) {
                IO.copy(IO.reader(r.openInputStream(), Constants.DEFAULT_CHARSET), or);
            } else {
                out.println("  - no resource");
                warning("No Resource found for service component: " + path);
            }
        }
        if (printed) {
            out.println();
        }
    }

    /**
     * Print the metatypes in this JAR.
     *
     * @param jar
     */
    private void printMetatype(Jar jar) throws Exception {
        out.println("[METATYPE]");
        Manifest manifest = jar.getManifest();
        if (manifest == null) {
            out.println("No manifest");
            return;
        }

        Map<String,Resource> map = jar.getDirectories().get("OSGI-INF/metatype");
        if (map != null) {
            for (Map.Entry<String,Resource> entry : map.entrySet()) {
                out.println(entry.getKey());
                IO.copy(IO.reader(entry.getValue().openInputStream(), Constants.DEFAULT_CHARSET), or);
                out.println();
            }
            out.println();
        }
    }

}
