/**
 * @author Tres Finocchiaro
 *
 * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
 *
 * LGPL 2.1 This is free software.  This software and source code are released under
 * the "LGPL 2.1 License".  A copy of this license should be distributed with
 * this software. http://www.gnu.org/licenses/lgpl-2.1.html
 */

package qz.printer;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.printer.info.CachedPrintServiceLookup;
import qz.printer.info.NativePrinter;
import qz.printer.info.NativePrinterMap;
import qz.utils.SystemUtilities;

import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.attribute.ResolutionSyntax;
import javax.print.attribute.standard.*;
import java.util.*;

public class PrintServiceMatcher {
    private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class);

    // PrintService is slow in CUPS, use a cache instead per JDK-7001133
    // TODO: Include JDK version test for caching when JDK-7001133 is fixed upstream
    private static final boolean useCache = SystemUtilities.isUnix();

    public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) {
        NativePrinterMap printers = NativePrinterMap.getInstance();
        printers.putAll(true, lookupPrintServices());
        if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); }
        if (!silent) { log.debug("Found {} printers", printers.size()); }
        return printers;
    }

    private static PrintService[] lookupPrintServices() {
        return useCache ? CachedPrintServiceLookup.lookupPrintServices() :
                PrintServiceLookup.lookupPrintServices(null, null);
    }

    private static PrintService lookupDefaultPrintService() {
        return useCache ? CachedPrintServiceLookup.lookupDefaultPrintService() :
                PrintServiceLookup.lookupDefaultPrintService();
    }

    public static NativePrinterMap getNativePrinterList(boolean silent) {
        return getNativePrinterList(silent, false);
    }

    public static NativePrinterMap getNativePrinterList() {
        return getNativePrinterList(false);
    }

    public static NativePrinter getDefaultPrinter() {
        PrintService defaultService = lookupDefaultPrintService();

        if(defaultService == null) {
            return null;
        }

        NativePrinterMap printers = NativePrinterMap.getInstance();
        if (!printers.contains(defaultService)) {
            printers.putAll(false, defaultService);
        }

        return printers.get(defaultService);
    }

    public static String findPrinterName(String query) throws JSONException {
        NativePrinter printer = PrintServiceMatcher.matchPrinter(query);

        if (printer != null) {
            return printer.getPrintService().value().getName();
        } else {
            return null;
        }
    }

    /**
     * Finds {@code PrintService} by looking at any matches to {@code printerSearch}.
     *
     * @param printerSearch Search query to compare against service names.
     */
    public static NativePrinter matchPrinter(String printerSearch, boolean silent) {
        NativePrinter exact = null;
        NativePrinter begins = null;
        NativePrinter partial = null;

        if (!silent) { log.debug("Searching for PrintService matching {}", printerSearch); }

        // Fix for https://github.com/qzind/tray/issues/931
        // This is more than an optimization, removal will lead to a regression
        NativePrinter defaultPrinter = getDefaultPrinter();
        if (defaultPrinter != null && printerSearch.equals(defaultPrinter.getName())) {
            if (!silent) { log.debug("Matched default printer, skipping further search"); }
            return defaultPrinter;
        }

        printerSearch = printerSearch.toLowerCase(Locale.ENGLISH);

        // Search services for matches
        for(NativePrinter printer : getNativePrinterList(silent).values()) {
            if (printer.getName() == null) {
                continue;
            }
            String printerName = printer.getName().toLowerCase(Locale.ENGLISH);
            if (printerName.equals(printerSearch)) {
                exact = printer;
                break;
            }
            if (printerName.startsWith(printerSearch)) {
                begins = printer;
                continue;
            }
            if (printerName.contains(printerSearch)) {
                partial = printer;
                continue;
            }

            if (SystemUtilities.isMac()) {
                // 1.9 compat: fallback for old style names
                PrinterName name = printer.getLegacyName();
                if (name == null || name.getValue() == null) { continue; }
                printerName = name.getValue().toLowerCase(Locale.ENGLISH);
                if (printerName.equals(printerSearch)) {
                    exact = printer;
                    continue;
                }
                if (printerName.startsWith(printerSearch)) {
                    begins = printer;
                    continue;
                }
                if (printerName.contains(printerSearch)) {
                    partial = printer;
                }
            }
        }

        // Return closest match
        NativePrinter use = null;
        if (exact != null) {
            use = exact;
        } else if (begins != null) {
            use = begins;
        } else if (partial != null) {
            use = partial;
        }

        if (use != null) {
            if(!silent) log.debug("Found match: {}", use.getPrintService().value().getName());
        } else {
            log.warn("Printer not found: {}", printerSearch);
        }

        return use;
    }

    public static NativePrinter matchPrinter(String printerSearch) {
        return matchPrinter(printerSearch, false);
    }

    public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException {
        JSONArray list = new JSONArray();

        PrintService defaultService = lookupDefaultPrintService();

        boolean mediaTrayCrawled = false;

        for(NativePrinter printer : getNativePrinterList().values()) {
            PrintService ps = printer.getPrintService().value();
            JSONObject jsonService = new JSONObject();
            jsonService.put("name", ps.getName());

            if (includeDetails) {
                jsonService.put("driver", printer.getDriver().value());
                jsonService.put("connection", printer.getConnection());
                jsonService.put("default", ps == defaultService);

                if (!mediaTrayCrawled) {
                    log.info("Gathering printer MediaTray information...");
                    mediaTrayCrawled = true;
                }

                HashSet<String> uniqueSizes = new HashSet<>(); // prevents duplicates
                JSONArray trays = new JSONArray();
                JSONArray sizes = new JSONArray();

                for(Media m : (Media[])ps.getSupportedAttributeValues(Media.class, null, null)) {
                    if (m instanceof MediaTray) { trays.put(m.toString()); }
                    if (m instanceof MediaSizeName) {
                        if(uniqueSizes.add(m.toString())) {
                            MediaSize mediaSize = MediaSize.getMediaSizeForName((MediaSizeName)m);
                            if(mediaSize == null) {
                                continue;
                            }

                            JSONObject size = new JSONObject();
                            size.put("name", m.toString());

                            JSONObject in = new JSONObject();
                            in.put("width", mediaSize.getX(MediaPrintableArea.INCH));
                            in.put("height", mediaSize.getY(MediaPrintableArea.INCH));
                            size.put("in", in);

                            JSONObject mm = new JSONObject();
                            mm.put("width", mediaSize.getX(MediaPrintableArea.MM));
                            mm.put("height", mediaSize.getY(MediaPrintableArea.MM));
                            size.put("mm", mm);

                            sizes.put(size);
                        }

                    }
                }

                if(trays.length() > 0) {
                    jsonService.put("trays", trays);
                }
                if(sizes.length() > 0) {
                    jsonService.put("sizes", sizes);
                }

                PrinterResolution res = printer.getResolution().value();
                int density = -1; if (res != null) { density = res.getFeedResolution(ResolutionSyntax.DPI); }
                jsonService.put("density", density);
            }

            list.put(jsonService);
        }

        return list;
    }

}
